Compare commits

..

12 Commits

Author SHA1 Message Date
Daniel Schmidt
bdfd8e319f fix: view breaking if 1 hp is in damage screen 2024-09-02 22:02:05 +02:00
Daniel Schmidt
f2bd471d3e feat: rebalance weapon 2024-09-02 21:57:49 +02:00
Daniel Schmidt
7e7afb4dce feat: add REST and LONG_REST card 2024-09-02 12:29:35 +02:00
Daniel Schmidt
a3ae4898d8 feat: add NANOBOT_SWARM 2024-09-02 12:23:17 +02:00
Daniel Schmidt
747101d9ca feat: run_tests script 2024-09-02 12:13:59 +02:00
Daniel Schmidt
4bce308ef5 feat: some new artifacts and cards 2024-09-02 12:13:53 +02:00
Daniel Schmidt
efbc74077a fix: save being broken after a while 2024-09-02 12:13:16 +02:00
Daniel Schmidt
1604f876a8 feat: optimize file loading in WASM 2024-08-31 09:20:37 +02:00
Daniel Schmidt
1c4ecf2a26 fix: fix tutorial for wasm 2024-08-30 16:30:32 +02:00
Daniel Schmidt
54179ae189 feat: CI release to itch.io 2024-08-30 16:19:15 +02:00
Daniel Schmidt
b03a6520cf feat: basic tutorial 2024-08-30 12:20:03 +02:00
Daniel Schmidt
43d44d5751 chore: update go version in dockerfile 2024-08-28 19:50:16 +02:00
28 changed files with 708 additions and 99 deletions

View File

@ -43,6 +43,7 @@ jobs:
ldflags: "-X 'github.com/BigJk/end_of_eden/internal/git.Tag=${{ github.ref_name }}' -X 'github.com/BigJk/end_of_eden/internal/git.CommitHash=${{ github.sha }}'"
goos: linux
goarch: amd64
compress_assets: zip
release-linux-gl-amd64:
permissions: write-all
name: release linux/amd64 gl
@ -63,6 +64,7 @@ jobs:
ldflags: "-X 'github.com/BigJk/end_of_eden/internal/git.Tag=${{ github.ref_name }}' -X 'github.com/BigJk/end_of_eden/internal/git.CommitHash=${{ github.sha }}'"
goos: linux
goarch: amd64
compress_assets: zip
release-windows-term-amd64:
permissions: write-all
name: release windows/amd64 term
@ -196,3 +198,46 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip
release-itch:
permissions: write-all
name: release itch
runs-on: ubuntu-latest
needs:
[
release-windows-term-amd64,
release-windows-gl-amd64,
release-macos-term-amd64,
release-macos-gl-amd64,
release-macos-term-arm64,
release-macos-gl-arm64,
release-linux-term-amd64,
release-linux-gl-amd64,
release-wasm,
]
steps:
- name: Download Release
uses: robinraju/release-downloader@v1
with:
repository: "BigJk/end_of_eden"
tag: ${{ github.ref_name }}
fileName: "*.zip"
- name: Install Butler
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
rm butler.zip
./butler -V
- name: Push to Itch.io
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
run: |
./butler push end_of_eden_term-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-gl-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-gl-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-term-arm64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-gl-arm64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-gl-amd64 --userversion ${{ github.ref_name }}
./butler push eoe.wasm-${{ github.ref_name }}-js-wasm.zip BigJk/end-of-eden:web --userversion ${{ github.ref_name }}

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS build-stage
FROM golang:1.23 AS build-stage
WORKDIR /build
RUN mkdir /app
@ -26,4 +26,4 @@ RUN apt-get install -y libasound2-dev
EXPOSE 8273
EXPOSE 8272
CMD ["/app/end_of_eden"]
CMD ["/app/end_of_eden"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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 = ""

View File

@ -0,0 +1,21 @@
register_enemy("NANOBOT_SWARM", {
name = l("enemies.NANOBOT_SWARM.name", "Nanobot Swarm"),
description = l("enemies.NANOBOT_SWARM.description", "A growing swarm of nanobots."),
look = ".*#.-",
color = "#9b5de5",
initial_hp = 15,
max_hp = 100,
gold = 50,
intend = function(ctx)
return "Deal " ..
highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, ctx.round + 1)) .. " damage. Heal " .. highlight(1) .. " HP."
end,
callbacks = {
on_turn = function(ctx)
deal_damage(ctx.guid, PLAYER_ID, ctx.round + 1)
heal(ctx.guid, ctx.guid, 1)
return nil
end
}
})

View File

@ -16,9 +16,11 @@ register_artifact("ARM_MOUNTED_GUN", {
register_card("ARM_MOUNTED_GUN", {
name = l("cards.ARM_MOUNTED_GUN.name", "Arm Mounted Gun"),
description = l("cards.ARM_MOUNTED_GUN.description", "Exhaust. Use your arm mounted gun to deal 15 (+3 for each upgrade) damage."),
description = l("cards.ARM_MOUNTED_GUN.description",
"Exhaust. Use your arm mounted gun to deal 15 (+3 for each upgrade) damage."),
state = function(ctx)
return string.format(l("cards.ARM_MOUNTED_GUN.state", "Use your arm mounted gun to deal %s damage."), highlight(7 + ctx.level * 3))
return string.format(l("cards.ARM_MOUNTED_GUN.state", "%s. Use your arm mounted gun to deal %s damage."),
highlight("Exhaust"), highlight(7 + ctx.level * 3))
end,
tags = { "ATK", "R", "T", "ARM" },
max_level = 1,
@ -33,7 +35,7 @@ register_card("ARM_MOUNTED_GUN", {
return nil
end
},
test = function ()
test = function()
return assert_cast_damage("ARM_MOUNTED_GUN", 7)
end
})
})

View File

@ -1,4 +1,5 @@
local hand_warning = "**Important:** If you already carry a artifact in your hand, you will have to drop it and related cards to pick up the new one."
local hand_warning =
"**Important:** If you already carry a artifact in your hand, you will have to drop it and related cards to pick up the new one."
HAND_WEAPONS = {
{
@ -53,9 +54,11 @@ HAND_WEAPONS_ARTIFACT_IDS = fun.iter(HAND_WEAPONS):map(function(w) return w.id e
for _, weapon in pairs(HAND_WEAPONS) do
register_card(weapon.id, {
name = l("cards." .. weapon.id .. ".name", weapon.name),
description = l("cards." .. weapon.id .. ".description", string.format("Use to deal %s (+3 for each upgrade) damage.", weapon.base_damage)),
description = l("cards." .. weapon.id .. ".description",
string.format("Use to deal %s (+1 for each upgrade) damage.", weapon.base_damage)),
state = function(ctx)
return string.format(l("cards." .. weapon.id .. ".state", "Use to deal %s damage."), highlight(weapon.base_damage + ctx.level * 3))
return string.format(l("cards." .. weapon.id .. ".state", "Use to deal %s damage."),
highlight(weapon.base_damage + ctx.level * 1))
end,
tags = weapon.tags,
max_level = 3,
@ -121,7 +124,8 @@ for _, weapon in pairs(HAND_WEAPONS) do
}
})
add_found_artifact_event(weapon.id, weapon.image, string.format("%s\n\n%s", weapon.description, hand_warning), registered.card[weapon.id].description, weapon.event_tags)
add_found_artifact_event(weapon.id, weapon.image, string.format("%s\n\n%s", weapon.description, hand_warning),
registered.card[weapon.id].description, weapon.event_tags)
end
---hand_weapon_event returns a random hand weapon event weighted by price.
@ -130,4 +134,4 @@ function hand_weapon_event()
local ids = fun.iter(HAND_WEAPONS):map(function(w) return w.id end):totable()
local prices = fun.iter(HAND_WEAPONS):map(function(w) return 500 - w.price end):totable()
return choose_weighted(ids, prices)
end
end

View File

@ -0,0 +1,25 @@
register_artifact("BIO_RECYCLER", {
name = "Bio Recycler",
description = "Heal 1 on kill.",
tags = { "_ACT_0" },
price = 200,
order = 0,
callbacks = {
on_actor_die = function(ctx)
if ctx.source == PLAYER_ID then
heal(PLAYER_ID, PLAYER_ID, 1)
end
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
deal_damage(dummy, PLAYER_ID, 1, true)
local hp_before = get_player().hp
deal_damage(PLAYER_ID, dummy, 100)
local hp_after = get_player().hp
if hp_after - hp_before ~= 1 then
return "Expected 1 HP heal, got " .. (hp_after - hp_before)
end
end
})

View File

@ -0,0 +1,22 @@
register_artifact("GOLD_SCRAPPER", {
name = "Gold Scrapper",
description = "Gain 15 gold on kill.",
tags = { "_ACT_0" },
price = 200,
order = 0,
callbacks = {
on_actor_die = function(ctx)
if ctx.source == PLAYER_ID then
give_player_gold(15)
end
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
deal_damage(PLAYER_ID, dummy, 100)
if get_player().gold ~= 15 then
return "Expected 15 gold, got " .. get_player().gold
end
end
})

View File

@ -0,0 +1,23 @@
register_artifact("HEADBUT_HELMET", {
name = "Headbut Helmet",
description = "Gain 1 Knock Out card.",
tags = { "_ACT_0" },
price = 200,
order = 0,
callbacks = {
on_pick_up = function(ctx)
give_card("KNOCK_OUT", PLAYER_ID)
return nil
end
},
test = function()
local cards = get_cards(PLAYER_ID)
for _, card_guid in ipairs(cards) do
local card = get_card_instance(card_guid)
if card.type_id == "KNOCK_OUT" then
return nil
end
end
return "Expected to find KNOCK_OUT card, but did not."
end
})

View File

@ -0,0 +1,20 @@
register_card("LONG_REST", {
name = l("cards.LONG_REST.name", "Long Rest"),
description = l("cards.REST.description", "Heal for 4 (+1 per level)."),
state = function(ctx)
return string.format(l("cards.REST.state", "Take a short rest. Heal for %s."), highlight(4 + ctx.level))
end,
tags = { "HEAL" },
max_level = 2,
color = COLOR_GREEN,
need_target = false,
does_exhaust = true,
point_cost = 3,
price = 300,
callbacks = {
on_cast = function(ctx)
heal(ctx.caster, ctx.caster, 4 + ctx.level)
return nil
end
},
})

View File

@ -0,0 +1,52 @@
register_card("NULLIFY", {
name = l("cards.NULLIFY.name", "Nullify"),
description = string.format(
l("cards.NULLIFY.description", "%s\n\nDeploy a temporary damage nullifier. %s all damage this round."),
highlight("Exhaust"), highlight("Negates")
),
tags = { "DEF", "_ACT_0" },
max_level = 0,
color = COLOR_BLUE,
need_target = false,
does_exhaust = true,
point_cost = 3,
price = 200,
callbacks = {
on_cast = function(ctx)
give_status_effect("NULLIFY", ctx.caster, 1 + ctx.level)
return nil
end
}
})
register_status_effect("NULLIFY", {
name = l("status_effects.NULLIFY.name", "Nullify Field"),
description = l("status_effects.NULLIFY.description", "Negates all damage this round."),
look = "NF",
foreground = COLOR_BLUE,
can_stack = false,
decay = DECAY_ALL,
rounds = 1,
order = 100,
callbacks = {
on_damage_calc = function(ctx)
if ctx.target == ctx.owner then
return 0
end
return ctx.damage
end,
},
test = function()
return assert_chain({
function() return assert_status_effect_count(1) end,
function() return assert_status_effect("NULLIFY", 1) end,
function()
local dummy = add_actor_by_enemy("DUMMY")
local damage = deal_damage(dummy, PLAYER_ID, 100)
if damage ~= 0 then
return "Expected 0 damage, got " .. damage
end
end
})
end
})

View File

@ -0,0 +1,19 @@
register_card("REST", {
name = l("cards.REST.name", "Short Rest"),
description = l("cards.REST.description", "Heal for 1 (+1 per level)."),
state = function(ctx)
return string.format(l("cards.REST.state", "Take a short rest. Heal for %s."), highlight(1 + ctx.level))
end,
tags = { "HEAL" },
max_level = 3,
color = COLOR_GREEN,
need_target = false,
point_cost = 1,
price = 120,
callbacks = {
on_cast = function(ctx)
heal(ctx.caster, ctx.caster, 1 + ctx.level)
return nil
end
},
})

View File

@ -25,6 +25,32 @@ It seems to be eating the metal from the walls. It looks at you and after a few
}
})
register_event("NANOBOT_SWARM", {
name = "Is this a swarm of...",
description = [[!!nanobot_swarm.jpg
You are walking through the facility hoping to find a way out. After a few turns you hear a strange noise. You look around and come across a swarm of nanobots.
**It continues to grow and it looks like it's going to attack you.**
]],
tags = { "_ACT_0_FIGHT" },
choices = {
{
description = "Fight!",
callback = function()
add_actor_by_enemy("NANOBOT_SWARM")
if random() < 0.25 then
add_actor_by_enemy("NANOBOT_SWARM")
end
if random() < 0.05 then
add_actor_by_enemy("REPAIR_DRONE")
end
return GAME_STATE_FIGHT
end
}
}
})
register_event("CLEAN_BOT", {
name = "Corpse. Clean. Engage.",
description = [[!!clean_bot.jpg

View File

@ -92,9 +92,9 @@ You found a chest with a strange symbol on it. The chest is protected by a stran
})
register_event("GAIN_GOLD_ACT_0", {
name = "",
name = "Old Gold Cache",
description = [[
...
You find an old chest filled with gold. You can either take it or leave.
]],
tags = { "_ACT_0" },
choices = {

View File

@ -19,42 +19,29 @@ register_story_teller("_ACT_0", {
possible = find_events_by_tags({ "_ACT_0_FIGHT" })
end
print(#get_event_history())
print("[ACT_0 ST] history:", get_event_history())
-- filter out events by id that have already been played
possible = fun.iter(possible):filter(function(event)
return event == "MERCHANT" or not table.contains(history, event.id)
return event.id == "MERCHANT" or not table.contains(history, event.id)
end):totable()
print("[ACT_0 ST] possible:", fun.iter(possible):map(function(e) return e.id end):totable())
-- fallback for now
if #possible == 0 then
possible = find_events_by_tags({ "_ACT_0_FIGHT" })
end
local choosen = possible[random_int(0, #possible)]
local choosen_id = random_int(0, #possible);
print("[ACT_0 ST] choosen_id:", choosen_id)
local choosen = possible[1 + choosen_id]
if choosen ~= nil then
print("[ACT_0 ST] choosen:", choosen.id)
set_event(choosen.id)
end
-- if we cleared a stage, give the player a random artifact
local last_stage_count = fetch("last_stage_count")
local current_stage_count = get_stages_cleared()
if last_stage_count ~= current_stage_count then
local gets_random_artifact = random() < 0.25
if gets_random_artifact then
local player_artifacts = fun.iter(get_actor(PLAYER_ID).artifacts):map(function(id)
return get_artifact(id).id
end):totable()
local artifacts = find_artifacts_by_tags({ "_ACT_0" })
if #artifacts > 0 then
local artifact = choose_weighted_by_price(artifacts)
if not table.contains(player_artifacts, artifact) then
give_artifact(PLAYER_ID, artifact)
end
end
end
end
return GAME_STATE_EVENT
end

View File

@ -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
})

View File

@ -197,6 +197,65 @@
term.onData((data) => bubbletea_write(data));
}
function ensureAllFiles() {
// Wait for WASM to be loaded
if (!globalThis.version) {
setTimeout(ensureAllFiles, 100);
return;
}
// Clear cache if version mismatch
const lastVersion = globalThis.settings.getString("lastCachedVersion");
if (!lastVersion || lastVersion === "" || globalThis.version !== lastVersion) {
console.log("Clearing file cache due to version mismatch");
const keys = Object.keys(window.localStorage);
for (const key of keys) {
if (key.indexOf("assets") !== -1) {
window.localStorage.removeItem(key);
}
}
}
// Cache files if they don't exist
fetch("./assets/file_index.json")
.then((index) => index.json())
.then((index) => {
const promises = [];
for (const file of index) {
if (!file.isFile) {
continue;
}
// only ensure files that end in .lua or .text
if (!file.path.endsWith(".lua") && !file.path.endsWith(".txt")) {
continue;
}
if (window.fsRead(file.path)) {
continue;
}
promises.push(
fetch(file.path)
.then((response) => response.text())
.then((text) => {
window.fsWrite(file.path, text);
}),
);
}
if (promises.length === 0) {
return;
}
Promise.all(promises).then(() => {
console.log("All files loaded");
globalThis.settings.set("lastCachedVersion", globalThis.version);
});
});
}
function init() {
const go = new Go();
WebAssembly.instantiateStreaming(fetch("./eoe.wasm"), go.importObject).then((result) => {
@ -206,10 +265,11 @@
});
// Init terminal. This should be done after bubbletea is initialized. For now, I use a timeout.
document.fonts.load((globalThis.settings.getInt("font_size") ?? 14) + 'px "IosevkaTermNerdFontMono"').then(() => initTerminal());
document.fonts.load((globalThis.settings.getInt("font_size") ?? 12) + 'px "IosevkaTermNerdFontMono"').then(() => initTerminal());
});
}
ensureAllFiles();
init();
</script>
</body>

View File

@ -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: |

View File

@ -53,6 +53,12 @@ Represents the fight game state.
</details>
<details> <summary><b><code>GAME_STATE_GAMEOVER</code></b> </summary> <br/>
Represents the game over game state.
</details>
<details> <summary><b><code>GAME_STATE_MERCHANT</code></b> </summary> <br/>
Represents the merchant game state.

View File

@ -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.")

View File

@ -157,6 +157,8 @@ func NewSession(options ...func(s *Session)) *Session {
session.SetOnLuaError(nil)
session.luaState, session.luaDocs = SessionAdapter(session)
session.resources = NewResourcesManager(session.luaState, session.luaDocs, session.log)
session.resources.MarkBaseGame()
for i := range options {
if options[i] == nil {
@ -165,8 +167,6 @@ func NewSession(options ...func(s *Session)) *Session {
options[i](session)
}
session.resources = NewResourcesManager(session.luaState, session.luaDocs, session.log)
session.resources.MarkBaseGame()
session.loadMods(session.loadedMods)
session.log.Println("Session started!")
@ -212,6 +212,15 @@ func WithMods(mods []string) func(s *Session) {
}
}
// WithLuaString sets the lua code that should be executed.
func WithLuaString(lua string) func(s *Session) {
return func(s *Session) {
if err := s.luaState.DoString(lua); err != nil {
s.logLuaError("WithLuaString", "", err)
}
}
}
// WithSeed sets the seed for the random number generator.
func WithSeed(seed uint64) func(s *Session) {
return func(s *Session) {
@ -272,20 +281,25 @@ func (s *Session) LuaErrors() chan LuaError {
// ToSavedState creates a saved state of the session that can be serialized with Gob.
func (s *Session) ToSavedState() SavedState {
return SavedState{
State: s.state,
Seed: s.seed,
Rand: s.randSrc,
Actors: s.actors,
Instances: s.instances,
StagesCleared: s.stagesCleared,
CurrentEvent: s.currentEvent,
CurrentFight: s.currentFight,
PointsPerRound: s.pointsPerRound,
Merchant: s.merchant,
EventHistory: s.eventHistory,
StateCheckpoints: s.stateCheckpoints,
CtxData: s.ctxData,
LoadedMods: s.loadedMods,
State: s.state,
Seed: s.seed,
Rand: s.randSrc,
Actors: s.actors,
Instances: s.instances,
StagesCleared: s.stagesCleared,
CurrentEvent: s.currentEvent,
CurrentFight: s.currentFight,
PointsPerRound: s.pointsPerRound,
Merchant: s.merchant,
EventHistory: s.eventHistory,
StateCheckpoints: lo.Map(s.stateCheckpoints, func(item StateCheckpoint, index int) StateCheckpoint {
return StateCheckpoint{
Session: nil,
Events: item.Events,
}
}),
CtxData: s.ctxData,
LoadedMods: s.loadedMods,
}
}
@ -376,6 +390,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)
@ -491,6 +517,14 @@ func (s *Session) SetEvent(id string) {
if _, ok := s.resources.Events[id]; ok {
s.eventHistory = append(s.eventHistory, id)
_, _ = s.resources.Events[id].OnEnter.Call(CreateContext("type_id", id))
} else {
s.log.Println("Event not found:", id)
s.currentEvent = ""
// If we can't find the event, we just go to the next state
if s.state == GameStateEvent {
s.SetGameState(GameStateRandom)
}
}
}
@ -820,8 +854,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)
}

View File

@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/samber/lo"
"io"
"net/http"
"os"
@ -16,6 +15,9 @@ import (
"sort"
"strings"
"syscall/js"
"github.com/BigJk/end_of_eden/internal/git"
"github.com/samber/lo"
)
type noOpWriteCloser struct{}
@ -51,6 +53,8 @@ func init() {
}
return nil
}))
js.Global().Set("version", git.Tag)
}
func ReadDir(path string) ([]FileInfo, error) {

3
run_tests.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
EOE_TESTER_WORKING_DIR=$(pwd) go test ./cmd/internal/tester -v

View File

@ -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
}
}
//

View File

@ -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},

View File

@ -147,6 +147,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 := fs.ReadFile("./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.WithLuaString(string(tutorialLua)),
))),
)
case ChoiceAbout:
audio.Play("btn_menu")

View File

@ -2,62 +2,63 @@ package ui
import (
"fmt"
"github.com/samber/lo"
"strconv"
"strings"
"github.com/samber/lo"
)
// Numbers is a slice of strings that represent the numbers 0-9 in a 5x5 grid.
var Numbers = []string{
`
`
`,
`
`,
`
`
`,
`
`,
`
`
`,
`
`
`,
`
`
`,
`
`
`,
`
`
`,
`
`
`,
`
`
`,
}