diff --git a/assets/scripts/definitions/api.lua b/assets/scripts/definitions/api.lua
index 9fd2229..870365e 100644
--- a/assets/scripts/definitions/api.lua
+++ b/assets/scripts/definitions/api.lua
@@ -19,6 +19,9 @@ GAME_STATE_EVENT = ""
--- Represents the fight game state.
GAME_STATE_FIGHT = ""
+--- Represents the game over game state.
+GAME_STATE_GAMEOVER = ""
+
--- Represents the merchant game state.
GAME_STATE_MERCHANT = ""
diff --git a/assets/tutorial/tutorial.lua b/assets/tutorial/tutorial.lua
new file mode 100644
index 0000000..670660d
--- /dev/null
+++ b/assets/tutorial/tutorial.lua
@@ -0,0 +1,225 @@
+delete_base_game("event")
+
+register_enemy("TUTORIAL_DUMMY_1", {
+ name = l("enemies.TUTORIAL_DUMMY_1.name", "Dummy"),
+ description = l("enemies.TUTORIAL_DUMMY_1.description", "A dummy enemy for the tutorial"),
+ look = "D",
+ color = "#e6e65a",
+ initial_hp = 4,
+ max_hp = 4,
+ gold = 0,
+ intend = function(ctx)
+ return "Deal " .. highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, 1)) .. " damage"
+ end,
+ callbacks = {
+ on_turn = function(ctx)
+ deal_damage(ctx.guid, PLAYER_ID, 1)
+ return nil
+ end
+ }
+})
+
+register_enemy("TUTORIAL_DUMMY_2", {
+ name = l("enemies.TUTORIAL_DUMMY_2.name", "Dummy"),
+ description = l("enemies.TUTORIAL_DUMMY_2.description", "A dummy enemy for the tutorial"),
+ look = "D",
+ color = "#e6e65a",
+ initial_hp = 3,
+ max_hp = 3,
+ gold = 0,
+ intend = function(ctx)
+ return "Apply " .. highlight("Weakness")
+ end,
+ callbacks = {
+ on_turn = function(ctx)
+ give_status_effect("WEAKNESS", PLAYER_ID)
+ return nil
+ end
+ }
+})
+
+register_status_effect("WEAKNESS", {
+ name = "Weakness",
+ description = "Decreases damage dealt by 1",
+ look = "W",
+ foreground = COLOR_RED,
+ state = function(ctx)
+ return "Deals " .. highlight(1) .. " less damage"
+ end,
+ rounds = 2,
+ decay = DECAY_ONE,
+ can_stack = false,
+ callbacks = {
+ on_damage_calc = function(ctx)
+ if ctx.source == ctx.owner then
+ return ctx.damage - 1
+ end
+ return ctx.damage
+ end
+ }
+})
+
+register_card("MELEE_HIT", {
+ name = l("cards.MELEE_HIT.name", "Melee Hit"),
+ description = l("cards.MELEE_HIT.description", "Use your bare hands to deal 2 (+1 for each upgrade) damage."),
+ state = function(ctx)
+ return string.format(l("cards.MELEE_HIT.state", "Use your bare hands to deal %s damage."),
+ highlight(2 + ctx.level))
+ end,
+ tags = { "ATK", "M", "HND" },
+ max_level = 1,
+ color = COLOR_GRAY,
+ need_target = true,
+ point_cost = 1,
+ price = -1,
+ callbacks = {
+ on_cast = function(ctx)
+ deal_damage_card(ctx.caster, ctx.guid, ctx.target, 2 + ctx.level)
+ return nil
+ end
+ },
+ test = function()
+ return assert_cast_damage("MELEE_HIT", 2)
+ end
+})
+
+register_event("START", {
+ name = "Welcome!",
+ description = [[Welcome to *End of Eden*!
+
+This game is a roguelite deckbuilder where you explore a post-apocalyptic world, collect cards and artifacts and fight enemies. **Try to stay alive as long as possible!**
+
+**Lets start with some keyboard shortcuts**
+
+- *ESC* - Open the menu where you can see your cards, artifacts, ... or abort choices
+- *SPACE* - End your turn
+- *ARROW LEFT / ARROW RIGHT* - Select card or enemy to hit
+- *ENTER* - Confirm your choice
+- *X* - If you hover over a enemy and press X, you can see more infos about a enemy
+- *S* - Open player status
+
+You can also use the **mouse** to select cards, enemies and click buttons.
+
+**Cards**
+
+You have a deck of cards that you can use to attack, defend or apply status effects. You can see your cards in the bottom of the screen. All cards cost action points that reset on each turn. Use them wisely!
+
+**Combat**
+
+If you press Continue you will fight a dummy enemy. See if you are able to kill it!
+
+]],
+ choices = {
+ {
+ description = "Continue",
+ callback = function()
+ return nil
+ end
+ },
+ },
+ on_enter = function()
+ end,
+ on_end = function(ctx)
+ add_actor_by_enemy("TUTORIAL_DUMMY_1")
+ give_player_gold(500)
+ give_card("MELEE_HIT", PLAYER_ID)
+ set_event("TUTORIAL_1")
+ return GAME_STATE_FIGHT
+ end
+})
+
+register_event("TUTORIAL_1", {
+ name = "Status Effects",
+ description = [[*Awesome! You have defeated the dummy enemy!*
+
+Now you will face a enemy that will apply a *status effect* on you. Status effects can be positive or negative and can be applied by cards, enemies or other sources. Status effects that are applied to you are shown on the bottom of the screen. You can click on them or press *S* to see more information.
+
+If you press Continue you will fight some dummy enemies. See if you are able to kill them!
+]],
+ choices = {
+ {
+ description = "Continue",
+ callback = function()
+ return nil
+ end
+ },
+ },
+ on_enter = function()
+ end,
+ on_end = function(ctx)
+ add_actor_by_enemy("TUTORIAL_DUMMY_1")
+ add_actor_by_enemy("TUTORIAL_DUMMY_2")
+ set_event("TUTORIAL_2")
+ give_card("BLOCK", PLAYER_ID)
+ return GAME_STATE_FIGHT
+ end
+})
+
+register_event("TUTORIAL_2", {
+ name = "The Merchant",
+ description = [[*Awesome! You have defeated the dummy enemies!*
+
+Every now and then you will encounter a merchant. The merchant will offer you cards and artifacts that you can buy with gold. You can also remove or upgrade cards. Gold is earned by defeating enemies.
+
+If you press Continue you will meet the merchant. *Try to buy or upgrade something!*
+]],
+ choices = {
+ {
+ description = "Continue",
+ callback = function()
+ return nil
+ end
+ },
+ },
+ on_enter = function()
+ end,
+ on_end = function(ctx)
+ set_event("TUTORIAL_3")
+ return GAME_STATE_MERCHANT
+ end
+})
+
+register_event("TUTORIAL_3", {
+ name = "Finished!",
+ description = [[*Awesome! You have bought some stuff!*
+
+This is the end of the tutorial. You can now continue to explore the world and fight enemies. Good luck!
+]],
+ choices = {
+ {
+ description = "Continue",
+ callback = function()
+ return nil
+ end
+ },
+ },
+ on_enter = function()
+ end,
+ on_end = function(ctx)
+ add_actor_by_enemy("TUTORIAL_DUMMY_1")
+ add_actor_by_enemy("TUTORIAL_DUMMY_2")
+ add_actor_by_enemy("TUTORIAL_DUMMY_1")
+ set_event("TUTORIAL_4")
+ return GAME_STATE_FIGHT
+ end
+})
+
+
+register_event("TUTORIAL_4", {
+ name = "Be gone!",
+ description = [[*It is time to go...*]],
+ choices = {
+ {
+ description = "Continue",
+ callback = function()
+ return nil
+ end
+ },
+ },
+ on_enter = function()
+ end,
+ on_end = function(ctx)
+ deal_damage("TUTORIAL", PLAYER_ID, 1000, true)
+ return GAME_STATE_GAMEOVER
+ end
+})
diff --git a/docs/GAME_CONTENT_DOCS.md b/docs/GAME_CONTENT_DOCS.md
index 92d6180..fa58086 100644
--- a/docs/GAME_CONTENT_DOCS.md
+++ b/docs/GAME_CONTENT_DOCS.md
@@ -78,9 +78,9 @@ title Action Points
```mermaid
pie
title Card Types
+"Consume" : 11
"Exhaust" : 1
"Normal" : 9
-"Consume" : 11
```
@@ -108,7 +108,7 @@ title Card Types
|------------------------|-----------------------|-------------------------------------------------------------------|------------|--------|---------|------------------------------|-----------------|
| ``CYBER_SPIDER`` | CYBER Spider | It waits for its prey to come closer | 8 | 8 | #ff4d6d | ``OnTurn`` | :no_entry_sign: |
| ``CLEAN_BOT`` | Cleaning Bot | It never stopped cleaning... | 13 | 13 | #32a891 | ``OnTurn``, ``OnPlayerTurn`` | :no_entry_sign: |
-| ``CYBER_SLIME`` | Cyber Slime | A cybernetic slime that splits into smaller slimes when defeated. | 10 | 10 | #00ff00 | ``OnTurn``, ``OnActorDie`` | :no_entry_sign: |
+| ``CYBER_SLIME`` | Cyber Slime | A cybernetic slime that splits into smaller slimes when defeated. | 10 | 10 | #00ff00 | ``OnActorDie``, ``OnTurn`` | :no_entry_sign: |
| ``CYBER_SLIME_MINION`` | Cyber Slime Offspring | A smaller version of the Cyber Slime. | 4 | 4 | #00ff00 | ``OnTurn`` | :no_entry_sign: |
| ``DUMMY`` | Dummy | End me... | 100 | 100 | #deeb6a | ``OnTurn`` | :no_entry_sign: |
| ``LASER_DRONE`` | Laser Drone | A drone equipped with a powerful laser cannon. | 7 | 7 | #ff0000 | ``OnTurn`` | :no_entry_sign: |
diff --git a/docs/LUA_API_DOCS.md b/docs/LUA_API_DOCS.md
index 6fb5029..7786fd3 100644
--- a/docs/LUA_API_DOCS.md
+++ b/docs/LUA_API_DOCS.md
@@ -53,6 +53,12 @@ Represents the fight game state.
+ GAME_STATE_GAMEOVER
+
+Represents the game over game state.
+
+
+
GAME_STATE_MERCHANT
Represents the merchant game state.
diff --git a/game/lua.go b/game/lua.go
index 6d1eef4..64a96ca 100644
--- a/game/lua.go
+++ b/game/lua.go
@@ -70,11 +70,13 @@ fun = require "fun"
d.Global("GAME_STATE_EVENT", "Represents the event game state.")
d.Global("GAME_STATE_MERCHANT", "Represents the merchant game state.")
d.Global("GAME_STATE_RANDOM", "Represents the random game state in which the active story teller will decide what happens next.")
+ d.Global("GAME_STATE_GAMEOVER", "Represents the game over game state.")
l.SetGlobal("GAME_STATE_FIGHT", lua.LString(GameStateFight))
l.SetGlobal("GAME_STATE_EVENT", lua.LString(GameStateEvent))
l.SetGlobal("GAME_STATE_MERCHANT", lua.LString(GameStateMerchant))
l.SetGlobal("GAME_STATE_RANDOM", lua.LString(GameStateRandom))
+ l.SetGlobal("GAME_STATE_GAMEOVER", lua.LString(GameStateGameOver))
d.Global("DECAY_ONE", "Status effect decays by 1 stack per turn.")
d.Global("DECAY_ALL", "Status effect decays by all stacks per turn.")
diff --git a/game/session.go b/game/session.go
index f5e5a4c..68ccea1 100644
--- a/game/session.go
+++ b/game/session.go
@@ -376,6 +376,18 @@ func (s *Session) logLuaError(callback string, typeId string, err error) {
func (s *Session) loadMods(mods []string) {
for i := range mods {
+ // Load single lua files
+ if strings.HasSuffix(mods[i], ".lua") && filepath.IsAbs(mods[i]) {
+ luaBytes, err := fs.ReadFile(mods[i])
+ if err != nil {
+ panic(err)
+ }
+ if err := s.luaState.DoString(string(luaBytes)); err != nil {
+ s.logLuaError("ModLoader", "", err)
+ }
+ continue
+ }
+
mod, err := ModDescription(filepath.Join("./mods", mods[i]))
if err != nil {
log.Println("Error loading mod:", err)
@@ -820,8 +832,13 @@ func (s *Session) SetupMerchant() {
}
}
-// LeaveMerchant finishes the merchant state and lets the storyteller decide what to do next.
+// LeaveMerchant finishes the merchant state and lets the storyteller decide what to do next. If an event is still set we switch to it.
func (s *Session) LeaveMerchant() {
+ if s.currentEvent != "" {
+ s.SetGameState(GameStateEvent)
+ return
+ }
+
s.SetGameState(GameStateRandom)
}
diff --git a/ui/menus/gameview/gameview.go b/ui/menus/gameview/gameview.go
index 7554f8d..878e82d 100644
--- a/ui/menus/gameview/gameview.go
+++ b/ui/menus/gameview/gameview.go
@@ -140,7 +140,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// Show tooltip
- if msg.String() == "x" {
+ switch msg.String() {
+ case "x":
for i := 0; i < m.Session.GetOpponentCount(game.PlayerActorID); i++ {
if m.zones.Get(fmt.Sprintf("%s%d", ZoneEnemy, i)).InBounds(m.LastMouse) {
cmds = append(cmds, root.TooltipCreate(root.Tooltip{
@@ -151,6 +152,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}))
}
}
+ case "s":
+ m.inPlayerView = !m.inPlayerView
}
}
//
diff --git a/ui/menus/mainmenu/choices.go b/ui/menus/mainmenu/choices.go
index 1c32b81..f06f276 100644
--- a/ui/menus/mainmenu/choices.go
+++ b/ui/menus/mainmenu/choices.go
@@ -17,6 +17,7 @@ type Choice string
const (
ChoiceWaiting = Choice("WAITING")
ChoiceContinue = Choice("CONTINUE")
+ ChoiceTutorial = Choice("TUTORIAL")
ChoiceNewGame = Choice("NEW_GAME")
ChoiceNewGameSOD = Choice("NEW_GAME_SOD")
ChoiceAbout = Choice("ABOUT")
@@ -45,6 +46,7 @@ type ChoicesModel struct {
func NewChoicesModel(zones *zone.Manager, hideSettings bool) ChoicesModel {
choices := []list.Item{
choiceItem{zones, "Continue", "Ready to continue dying?", ChoiceContinue},
+ choiceItem{zones, "Tutorial", "Learn the basics.", ChoiceTutorial},
choiceItem{zones, "New Game", "Start a new try.", ChoiceNewGame},
choiceItem{zones, "New Game: Seed of the Day", "Start a new try with the daily seed.", ChoiceNewGameSOD},
choiceItem{zones, "About", "Want to know more?", ChoiceAbout},
diff --git a/ui/menus/mainmenu/mainmenu.go b/ui/menus/mainmenu/mainmenu.go
index 2752841..85ddff5 100644
--- a/ui/menus/mainmenu/mainmenu.go
+++ b/ui/menus/mainmenu/mainmenu.go
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
+ "path/filepath"
"strings"
"time"
@@ -147,6 +148,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
lo.Ternary(os.Getenv("EOE_DEBUG") == "1", game.WithDebugEnabled(8272), nil),
))),
)
+ case ChoiceTutorial:
+ audio.Play("btn_menu")
+
+ tutorialLua, err := filepath.Abs("./assets/tutorial/tutorial.lua")
+ if err != nil {
+ panic(err)
+ }
+
+ m.choices = m.choices.Clear()
+ return m, tea.Sequence(
+ cmd,
+ root.Push(gameview.New(m, m.zones, game.NewSession(
+ game.WithMods([]string{tutorialLua}),
+ ))),
+ )
case ChoiceAbout:
audio.Play("btn_menu")