Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdfd8e319f | ||
|
|
f2bd471d3e | ||
|
|
7e7afb4dce | ||
|
|
a3ae4898d8 | ||
|
|
747101d9ca | ||
|
|
4bce308ef5 | ||
|
|
efbc74077a | ||
|
|
1604f876a8 | ||
|
|
1c4ecf2a26 | ||
|
|
54179ae189 | ||
|
|
b03a6520cf | ||
|
|
43d44d5751 | ||
|
|
24ad4eb61a | ||
|
|
2b085f0c99 | ||
|
|
275460002e | ||
|
|
98da619522 | ||
|
|
7f85f3c3b0 | ||
|
|
11402099d0 | ||
|
|
389e5ea18e | ||
|
|
c2c991f6b7 | ||
|
|
dc5b148bea | ||
|
|
5bc167d3c9 | ||
|
|
a61af0c2d7 | ||
|
|
f992cab2e3 | ||
|
|
feff00155c | ||
|
|
475190c95f | ||
|
|
312c3db924 |
69
.github/workflows/release.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: wangyoucao577/go-release-action@v1
|
||||
with:
|
||||
goversion: "1.21.6"
|
||||
goversion: "1.23.0"
|
||||
md5sum: FALSE
|
||||
compress_assets: "zip"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@ -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
|
||||
@ -106,15 +108,15 @@ jobs:
|
||||
- name: Fetch Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: "^1.23"
|
||||
- name: Build
|
||||
run: |
|
||||
go build -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 }}'" -o end_of_eden -tags ebitenginesinglethread ./cmd/game
|
||||
export BIN=end_of_eden_term-$(basename ${GITHUB_REF})-macos-amd64
|
||||
mkdir $BIN
|
||||
cp ./end_of_eden $BIN/end_of_eden
|
||||
cp -r ./assets $BIN/assets/
|
||||
zip -r $BIN.zip $BIN
|
||||
go build -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 }}'" -o end_of_eden -tags ebitenginesinglethread ./cmd/game
|
||||
export BIN=end_of_eden_term-$(basename ${GITHUB_REF})-macos-amd64
|
||||
mkdir $BIN
|
||||
cp ./end_of_eden $BIN/end_of_eden
|
||||
cp -r ./assets $BIN/assets/
|
||||
zip -r $BIN.zip $BIN
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@ -133,7 +135,7 @@ jobs:
|
||||
- name: Fetch Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: "^1.23"
|
||||
- name: Build
|
||||
run: |
|
||||
go build -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 }}'" -o end_of_eden -tags ebitenginesinglethread ./cmd/game_win
|
||||
@ -156,7 +158,7 @@ jobs:
|
||||
- name: Fetch Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: "^1.23"
|
||||
- name: Build
|
||||
run: |
|
||||
go build -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 }}'" -o end_of_eden -tags ebitenginesinglethread ./cmd/game
|
||||
@ -183,7 +185,7 @@ jobs:
|
||||
- name: Fetch Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: "^1.23"
|
||||
- name: Build
|
||||
run: |
|
||||
go build -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 }}'" -o end_of_eden -tags ebitenginesinglethread ./cmd/game_win
|
||||
@ -195,4 +197,47 @@ jobs:
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip
|
||||
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 }}
|
||||
|
||||
@ -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"]
|
||||
|
||||
BIN
assets/images/cyber_slime.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 20 KiB |
BIN
assets/images/electro_barrier.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/laser_drone.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/images/nanobot_swarm.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/plasma_golem.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/symbiotic_parasite.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/upgrade_station.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
@ -12,13 +12,13 @@ end
|
||||
---highlight_warn some value with warning colors
|
||||
---@param val any
|
||||
function highlight_warn(val)
|
||||
return text_underline(text_bold(_escape_color("38;5;161") .. "[" .. tostring(val) .. "]" .. string.char(27) .. "[0m"))
|
||||
return text_underline(text_bold(_escape_color("38;5;161") .. "[" .. tostring(val) .. "]" .. string.char(27) .. "[0m"))
|
||||
end
|
||||
|
||||
---highlight_success some value with success colors
|
||||
---@param val any
|
||||
function highlight_success(val)
|
||||
return text_underline(text_bold(_escape_color("38;5;119") .. "[" .. tostring(val) .. "]" .. string.char(27) .. "[0m"))
|
||||
return text_underline(text_bold(_escape_color("38;5;119") .. "[" .. tostring(val) .. "]" .. string.char(27) .. "[0m"))
|
||||
end
|
||||
|
||||
---choose_weighted chooses an item from a list of choices, with a weight for each item.
|
||||
@ -33,7 +33,7 @@ function choose_weighted(choices, weights)
|
||||
total_weight = total_weight + weight
|
||||
end
|
||||
|
||||
local random = math.random() * total_weight
|
||||
local random = random() * total_weight
|
||||
for i, weight in ipairs(weights) do
|
||||
random = random - weight
|
||||
if random <= 0 then
|
||||
@ -83,27 +83,39 @@ end
|
||||
---@param tags string[]
|
||||
---@return artifact[]
|
||||
function find_artifacts_by_tags(tags)
|
||||
return find_by_tags(registered.artifact, tags)
|
||||
local found = find_by_tags(registered.artifact, tags)
|
||||
table.sort(found, function(a, b) return a.id:upper() < b.id:upper() end)
|
||||
return found
|
||||
end
|
||||
|
||||
---find_cards_by_tags find all cards with the given tags.
|
||||
---@param tags string[]
|
||||
---@return card[]
|
||||
function find_cards_by_tags(tags)
|
||||
return find_by_tags(registered.card, tags)
|
||||
local found = find_by_tags(registered.card, tags)
|
||||
table.sort(found, function(a, b) return a.id:upper() < b.id:upper() end)
|
||||
return found
|
||||
end
|
||||
|
||||
---find_events_by_tags find all events with the given tags.
|
||||
---@param tags string[]
|
||||
---@return event[]
|
||||
function find_events_by_tags(tags)
|
||||
return find_by_tags(registered.event, tags)
|
||||
local found = find_by_tags(registered.event, tags)
|
||||
--table.sort(found, function(a, b) return a.id:upper() < b.id:upper() end)
|
||||
return found
|
||||
end
|
||||
|
||||
---choose_weighted_by_price choose a random item from the given list, weighted by price.
|
||||
---@param items artifact|card
|
||||
---@return string
|
||||
function choose_weighted_by_price(items)
|
||||
table.sort(items, function(a, b)
|
||||
if a.id == nil then
|
||||
return a.type_id < b.type_id
|
||||
end
|
||||
return a.id < b.id
|
||||
end)
|
||||
return choose_weighted(
|
||||
fun.iter(items):map(function(item) return item.id or item.type_id end):totable(),
|
||||
fun.iter(items):map(function(item) return item.price end):totable()
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
@ -41,6 +44,16 @@ function fetch(key) end
|
||||
---@return guid
|
||||
function guid() end
|
||||
|
||||
--- Returns a random number between 0 and 1. Prefer this function over math.random(), as this is seeded for the session.
|
||||
---@return number
|
||||
function random() end
|
||||
|
||||
--- Returns a random number between min and max. Prefer this function over math.random(), as this is seeded for the session.
|
||||
---@param min number
|
||||
---@param max number
|
||||
---@return number
|
||||
function random_int(min, max) end
|
||||
|
||||
--- Stores a persistent value for this run that will be restored after a save load. Can store any lua basic value or table.
|
||||
---@param key string
|
||||
---@param value any
|
||||
|
||||
@ -6,7 +6,7 @@ function cast_random(guid, target)
|
||||
if #cards == 0 then
|
||||
print("can't cast_random with zero cards available!")
|
||||
else
|
||||
cast_card(cards[math.random(#cards)], target)
|
||||
cast_card(cards[random_int(0, #cards)], target)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
49
assets/scripts/enemies/cyber_slime.lua
Normal file
@ -0,0 +1,49 @@
|
||||
register_enemy("CYBER_SLIME_MINION", {
|
||||
name = "Cyber Slime Offspring",
|
||||
description = "A smaller version of the Cyber Slime.",
|
||||
look = [[ o ]],
|
||||
color = "#00ff00",
|
||||
initial_hp = 4,
|
||||
max_hp = 4,
|
||||
gold = 10,
|
||||
intend = function(ctx)
|
||||
return "Deal " .. highlight(1) .. " damage"
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
deal_damage(ctx.guid, PLAYER_ID, 1)
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_enemy("CYBER_SLIME", {
|
||||
name = "Cyber Slime",
|
||||
description = "A cybernetic slime that splits into smaller slimes when defeated.",
|
||||
look = [[ (O) ]],
|
||||
color = "#00ff00",
|
||||
initial_hp = 10,
|
||||
max_hp = 10,
|
||||
gold = 50,
|
||||
intend = function(ctx)
|
||||
return "Deal " .. highlight(2) .. " damage"
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
deal_damage(ctx.guid, PLAYER_ID, 2)
|
||||
return nil
|
||||
end,
|
||||
on_actor_die = function(ctx)
|
||||
if get_actor(ctx.target).type_id ~= "CYBER_SLIME" then
|
||||
return nil
|
||||
end
|
||||
|
||||
add_actor_by_enemy("CYBER_SLIME_MINION")
|
||||
add_actor_by_enemy("CYBER_SLIME_MINION")
|
||||
if random() < 0.25 then
|
||||
add_actor_by_enemy("CYBER_SLIME_MINION")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
21
assets/scripts/enemies/nanobot_swarm.lua
Normal 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
|
||||
}
|
||||
})
|
||||
40
assets/scripts/enemies/repair_drone.lua
Normal file
@ -0,0 +1,40 @@
|
||||
REPAIR_DRONE_HEAL = 2
|
||||
|
||||
register_enemy("REPAIR_DRONE", {
|
||||
name = "Repair Drone",
|
||||
description = "A drone designed to repair and support other machines.",
|
||||
look = [[]rr[]],
|
||||
color = "#00ff00",
|
||||
initial_hp = 10,
|
||||
max_hp = 10,
|
||||
gold = 50,
|
||||
intend = function(ctx)
|
||||
local opponents = get_opponent_guids(PLAYER_ID)
|
||||
|
||||
-- Check if any opponent needs healing
|
||||
for _, opponent_guid in ipairs(opponents) do
|
||||
local opponent = get_actor(opponent_guid)
|
||||
if opponent_guid ~= ctx.guid and opponent.hp < opponent.max_hp then
|
||||
return "Heal " .. highlight(REPAIR_DRONE_HEAL) .. " HP to an ally"
|
||||
end
|
||||
end
|
||||
|
||||
return "Standby..."
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
local opponents = get_opponent_guids(PLAYER_ID)
|
||||
|
||||
-- Check if any opponent needs healing
|
||||
for _, opponent_guid in ipairs(opponents) do
|
||||
local opponent = get_actor(opponent_guid)
|
||||
if opponent_guid ~= ctx.guid and opponent.hp < opponent.max_hp then
|
||||
heal(ctx.guid, opponent_guid, REPAIR_DRONE_HEAL)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
@ -16,7 +16,4 @@ register_card("DEBUG_INSTA_KILL", {
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function()
|
||||
return assert_cast_damage("DEBUG_INSTA_KILL", 10000)
|
||||
end
|
||||
})
|
||||
|
||||
36
assets/scripts/equipment/consumeable/adrenaline_shot.lua
Normal file
@ -0,0 +1,36 @@
|
||||
register_card("ADRENALINE_SHOT", {
|
||||
name = "Adrenaline Shot",
|
||||
description = "Gain 2 additional action points for the next 3 turns.",
|
||||
tags = { "BUFF", "_ACT_0" },
|
||||
max_level = 0,
|
||||
color = COLOR_RED,
|
||||
need_target = false,
|
||||
does_consume = true,
|
||||
point_cost = 0,
|
||||
price = 400,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
give_status_effect("ADRENALINE_SHOT", ctx.caster, 3)
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_status_effect("ADRENALINE_SHOT", {
|
||||
name = "Adrenaline Shot",
|
||||
description = "Gain 2 additional action points.",
|
||||
look = "AS",
|
||||
foreground = COLOR_RED,
|
||||
can_stack = false,
|
||||
decay = DECAY_ONE,
|
||||
rounds = 3,
|
||||
order = 100,
|
||||
callbacks = {
|
||||
on_player_turn = function(ctx)
|
||||
if ctx.owner == PLAYER_ID then
|
||||
player_give_action_points(2)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
39
assets/scripts/equipment/consumeable/smoke_bomb.lua
Normal file
@ -0,0 +1,39 @@
|
||||
register_card("SMOKE_BOMB", {
|
||||
name = "Smoke Bomb",
|
||||
description = "Reduces the accuracy of all enemies for 1 turn.",
|
||||
tags = { "CC", "_ACT_0" },
|
||||
max_level = 0,
|
||||
color = COLOR_GRAY,
|
||||
need_target = false,
|
||||
does_consume = true,
|
||||
point_cost = 0,
|
||||
price = 150,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
local enemies = get_opponent_guids(PLAYER_ID)
|
||||
for _, enemy in ipairs(enemies) do
|
||||
give_status_effect("SMOKE_BOMB", enemy, 1)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_status_effect("SMOKE_BOMB", {
|
||||
name = "Smoke Bomb",
|
||||
description = "Reduces accuracy by 50%.",
|
||||
look = "SB",
|
||||
foreground = COLOR_GRAY,
|
||||
can_stack = false,
|
||||
decay = DECAY_ONE,
|
||||
rounds = 1,
|
||||
order = 100,
|
||||
callbacks = {
|
||||
on_damage_calc = function(ctx)
|
||||
if ctx.source == ctx.owner then
|
||||
return ctx.damage * 0.5
|
||||
end
|
||||
return ctx.damage
|
||||
end
|
||||
}
|
||||
})
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
25
assets/scripts/equipment/permanents/bio_recycler.lua
Normal 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
|
||||
})
|
||||
22
assets/scripts/equipment/permanents/gold_scrapper.lua
Normal 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
|
||||
})
|
||||
23
assets/scripts/equipment/permanents/headbut_helmet.lua
Normal 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
|
||||
})
|
||||
20
assets/scripts/equipment/permanents/long_rest.lua
Normal 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
|
||||
},
|
||||
})
|
||||
52
assets/scripts/equipment/permanents/nullify.lua
Normal 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
|
||||
})
|
||||
16
assets/scripts/equipment/permanents/reflective_armor.lua
Normal file
@ -0,0 +1,16 @@
|
||||
register_artifact("REFLECTIVE_ARMOR", {
|
||||
name = "Reflective Armor",
|
||||
description = "Reflects 25% of the damage back to the attacker.",
|
||||
tags = { "ARMOR", "_ACT_0" },
|
||||
price = 300,
|
||||
order = 0,
|
||||
callbacks = {
|
||||
on_damage_calc = function(ctx)
|
||||
if ctx.target == ctx.owner then
|
||||
local reflected_damage = ctx.damage * 0.25
|
||||
deal_damage(ctx.target, ctx.source, reflected_damage, true)
|
||||
end
|
||||
return ctx.damage
|
||||
end
|
||||
}
|
||||
})
|
||||
19
assets/scripts/equipment/permanents/rest.lua
Normal 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
|
||||
},
|
||||
})
|
||||
@ -13,9 +13,38 @@ It seems to be eating the metal from the walls. It looks at you and after a few
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("RUST_MITE")
|
||||
if math.random() < 0.25 then
|
||||
if random() < 0.25 then
|
||||
add_actor_by_enemy("RUST_MITE")
|
||||
end
|
||||
if random() < 0.15 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@ -38,9 +67,12 @@ It looks at you and says "Corpse. Clean. Engage.".
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("CLEAN_BOT")
|
||||
if math.random() < 0.25 then
|
||||
if random() < 0.25 then
|
||||
add_actor_by_enemy("CLEAN_BOT")
|
||||
end
|
||||
if random() < 0.15 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
@ -60,9 +92,12 @@ It seems to be waiting for its prey to come closer and there is no way around it
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("CYBER_SPIDER")
|
||||
if math.random() < 0.25 then
|
||||
if random() < 0.25 then
|
||||
add_actor_by_enemy("CYBER_SPIDER")
|
||||
end
|
||||
if random() < 0.15 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
@ -72,7 +107,9 @@ It seems to be waiting for its prey to come closer and there is no way around it
|
||||
register_event("LASER_DRONE", {
|
||||
name = "A menacing drone appears...",
|
||||
description =
|
||||
[[As you explore the facility, you hear a high-pitched whirring sound. A drone equipped with a powerful laser cannon appears in front of you.
|
||||
[[!!laser_drone.jpg
|
||||
|
||||
As you explore the facility, you hear a high-pitched whirring sound. A drone equipped with a powerful laser cannon appears in front of you.
|
||||
|
||||
**It looks ready to attack!**
|
||||
]],
|
||||
@ -82,9 +119,12 @@ register_event("LASER_DRONE", {
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("LASER_DRONE")
|
||||
if math.random() < 0.10 then
|
||||
if random() < 0.10 then
|
||||
add_actor_by_enemy("LASER_DRONE")
|
||||
end
|
||||
if random() < 0.15 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
@ -94,7 +134,9 @@ register_event("LASER_DRONE", {
|
||||
register_event("PLASMA_GOLEM", {
|
||||
name = "A glowing figure emerges...",
|
||||
description =
|
||||
[[As you delve deeper into the facility, you notice a bright glow emanating from a nearby chamber. A massive golem made of pure plasma energy steps into view.
|
||||
[[!!plasma_golem.jpg
|
||||
|
||||
As you delve deeper into the facility, you notice a bright glow emanating from a nearby chamber. A massive golem made of pure plasma energy steps into view.
|
||||
|
||||
**It looks ready to unleash its power!**
|
||||
]],
|
||||
@ -104,6 +146,33 @@ register_event("PLASMA_GOLEM", {
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("PLASMA_GOLEM")
|
||||
if random() < 0.05 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
register_event("CYBER_SLIME", {
|
||||
name = "A strange cybernetic slime appears...",
|
||||
description =
|
||||
[[!!cyber_slime.jpg
|
||||
|
||||
As you explore the facility, you come across a strange cybernetic slime. It seems to be pulsating with energy and looks hostile.
|
||||
|
||||
**Prepare for a fight!**
|
||||
]],
|
||||
tags = { "_ACT_0_FIGHT" },
|
||||
choices = {
|
||||
{
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("CYBER_SLIME")
|
||||
if random() < 0.10 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
|
||||
@ -31,19 +31,20 @@ The merchant is always looking for a good deal, and they're not above haggling w
|
||||
register_event("RANDOM_ARTIFACT_ACT_0", {
|
||||
name = "Random Artifact",
|
||||
description = [[!!artifact_chest.jpg
|
||||
|
||||
You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
{
|
||||
description = "Random Artifact " ..
|
||||
highlight_success("Gain 1 Artifact") .. " " .. highlight_warn("Take 5 damage"),
|
||||
highlight_success("Gain 1 Artifact") .. " " .. highlight_warn("Take 2 damage"),
|
||||
callback = function()
|
||||
local possible = find_artifacts_by_tags({ "_ACT_0" })
|
||||
local choosen = choose_weighted_by_price(possible)
|
||||
if choosen then
|
||||
give_artifact(choosen, PLAYER_ID)
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 5, true)
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 2, true)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@ -60,13 +61,14 @@ You found a chest with a strange symbol on it. The chest is protected by a stran
|
||||
register_event("RANDOM_CONSUMEABLE_ACT_0", {
|
||||
name = "Random Consumeable",
|
||||
description = [[!!artifact_chest.jpg
|
||||
|
||||
You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
{
|
||||
description = "Random Artifact " ..
|
||||
highlight_success("Gain 1 Consumeable") .. " " .. highlight_warn("Take 2 damage"),
|
||||
highlight_success("Gain 1 Consumeable") .. " " .. highlight_warn("Take 2 damage"),
|
||||
callback = function()
|
||||
local possible = fun.iter(find_cards_by_tags({ "_ACT_0" }))
|
||||
:filter(function(card)
|
||||
@ -90,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 = {
|
||||
@ -112,28 +114,6 @@ register_event("GAIN_GOLD_ACT_0", {
|
||||
}
|
||||
})
|
||||
|
||||
register_event("GAIN_GOLD_ACT_0", {
|
||||
name = "",
|
||||
description = [[
|
||||
...
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
{
|
||||
description = "Take it! " .. highlight_success("Gain 20 Gold"),
|
||||
callback = function()
|
||||
give_player_gold(20)
|
||||
return nil
|
||||
end
|
||||
},
|
||||
{
|
||||
description = "Leave!",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
register_event("GOLD_TO_HP_ACT_0", {
|
||||
name = "Old Vending Machine",
|
||||
@ -164,7 +144,8 @@ You find an old vending machine, it seems to be still working. You can either pa
|
||||
|
||||
register_event("MAX_LIFE_ACT_0", {
|
||||
name = "Symbiotic Parasite",
|
||||
description = [[
|
||||
description = [[!!symbiotic_parasite.jpg
|
||||
|
||||
You find a strange creature, it seems to be a symbiotic parasite. It offers to increase your max HP by 5. You can either accept or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
@ -187,21 +168,22 @@ You find a strange creature, it seems to be a symbiotic parasite. It offers to i
|
||||
|
||||
register_event("GAMBLE_1_ACT_0", {
|
||||
name = "Electro Barrier",
|
||||
description = [[
|
||||
description = [[!!electro_barrier.jpg
|
||||
|
||||
You find a room with a strange device in the middle. It seems to be some kind of electro barrier protecting a storage container. You can either try to disable the barrier or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
{
|
||||
description = "50% " ..
|
||||
highlight_success("Gain Artifact & Consumeable") .. " 50% " .. highlight_warn("Take 5 damage"),
|
||||
highlight_success("Gain Artifact & Consumeable") .. " 50% " .. highlight_warn("Take 2 damage"),
|
||||
callback = function()
|
||||
local possible_artifacts = find_artifacts_by_tags({ "_ACT_0" })
|
||||
local possible_consumeables = fun.iter(find_cards_by_tags({ "_ACT_0" }))
|
||||
:filter(function(card)
|
||||
return card.does_consume
|
||||
end):totable()
|
||||
if math.random() < 0.5 then
|
||||
if random() < 0.5 then
|
||||
local choosen = choose_weighted_by_price(possible_artifacts)
|
||||
if choosen then
|
||||
give_artifact(choosen, PLAYER_ID)
|
||||
@ -211,7 +193,7 @@ You find a room with a strange device in the middle. It seems to be some kind of
|
||||
give_card(choosen, PLAYER_ID)
|
||||
end
|
||||
else
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 5, true)
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 2, true)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@ -227,14 +209,15 @@ You find a room with a strange device in the middle. It seems to be some kind of
|
||||
|
||||
register_event("UPRAGDE_CARD_ACT_0", {
|
||||
name = "Upgrade Station",
|
||||
description = [[
|
||||
description = [[!!upgrade_station.jpg
|
||||
|
||||
You find a old automatic workstation. You are able to get it working again. You can either upgrade a random card or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
{
|
||||
description = "Upgrade a card " ..
|
||||
highlight_success("Upgrade a card") .. " " .. highlight_warn("Take 5 damage"),
|
||||
highlight_success("Upgrade a card") .. " " .. highlight_warn("Take 2 damage"),
|
||||
callback = function()
|
||||
local cards = fun.iter(get_cards(PLAYER_ID))
|
||||
:filter(function(guid)
|
||||
@ -249,9 +232,9 @@ You find a old automatic workstation. You are able to get it working again. You
|
||||
return nil
|
||||
end
|
||||
|
||||
local choosen = cards[math.random(#cards)]
|
||||
local choosen = cards[random_int(0, #cards)]
|
||||
upgrade_card(choosen)
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 5, true)
|
||||
deal_damage(PLAYER_ID, PLAYER_ID, 2, true)
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -82,7 +82,7 @@ local string_gen = function(param, state)
|
||||
return state, r
|
||||
end
|
||||
|
||||
local ipairs_gen = ipairs({}) -- get the generating function from ipairs
|
||||
local ipairs_gen = ipairs({}) -- get the generating function from ipairs
|
||||
|
||||
local pairs_gen = pairs({ a = 0 }) -- get the generating function from pairs
|
||||
local map_gen = function(tab, key)
|
||||
@ -262,11 +262,11 @@ end
|
||||
exports.ones = ones
|
||||
|
||||
local rands_gen = function(param_x, _state_x)
|
||||
return 0, math.random(param_x[1], param_x[2])
|
||||
return 0, random_int(param_x[1], param_x[2])
|
||||
end
|
||||
|
||||
local rands_nil_gen = function(_param_x, _state_x)
|
||||
return 0, math.random()
|
||||
return 0, random()
|
||||
end
|
||||
|
||||
local rands = function(n, m)
|
||||
|
||||
@ -19,39 +19,30 @@ 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
|
||||
set_event(possible[math.random(#possible)].id)
|
||||
|
||||
-- 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 = math.random() < 0.25
|
||||
local choosen_id = random_int(0, #possible);
|
||||
print("[ACT_0 ST] choosen_id:", choosen_id)
|
||||
|
||||
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
|
||||
local choosen = possible[1 + choosen_id]
|
||||
if choosen ~= nil then
|
||||
print("[ACT_0 ST] choosen:", choosen.id)
|
||||
set_event(choosen.id)
|
||||
end
|
||||
|
||||
|
||||
return GAME_STATE_EVENT
|
||||
end
|
||||
})
|
||||
|
||||
225
assets/tutorial/tutorial.lua
Normal 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
|
||||
})
|
||||
@ -1,207 +1,276 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet">
|
||||
<script src="./wasm_exec.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'IosevkaTermNerdFontMono';
|
||||
src: url('assets/fonts/IosevkaTermNerdFontMono-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet" />
|
||||
<script src="./wasm_exec.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "IosevkaTermNerdFontMono";
|
||||
src: url("assets/fonts/IosevkaTermNerdFontMono-Bold.ttf") format("truetype");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IosevkaTermNerdFontMono';
|
||||
src: url('assets/fonts/IosevkaTermNerdFontMono-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IosevkaTermNerdFontMono";
|
||||
src: url("assets/fonts/IosevkaTermNerdFontMono-Regular.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IosevkaTermNerdFontMono';
|
||||
src: url('assets/fonts/IosevkaTermNerdFontMono-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IosevkaTermNerdFontMono";
|
||||
src: url("assets/fonts/IosevkaTermNerdFontMono-Italic.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'IosevkaTermNerdFontMono', monospace;
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "IosevkaTermNerdFontMono", monospace;
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
/* this is important */
|
||||
overflow: hidden;
|
||||
}
|
||||
.terminal-container {
|
||||
/* this is important */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* see : https://github.com/xtermjs/xterm.js/issues/3564#issuecomment-1004417440 */
|
||||
width: initial !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='loading' style='color: white; margin: 20px;'>
|
||||
<b style='color: red;'>Please click into this window once to allow audio!</b>
|
||||
<br><br>
|
||||
Game: Loading... This can take some time...
|
||||
</div>
|
||||
<div class="terminal-container" style="height: 100%; width: 100%;">
|
||||
<div id="terminal" style="height: 100%"></div>
|
||||
</div>
|
||||
<script>
|
||||
// Define variables to keep track of the audio elements
|
||||
let sound = new Audio();
|
||||
let music = new Audio();
|
||||
let currentMusicUrl = "";
|
||||
let userInteracted = false;
|
||||
.xterm .xterm-viewport {
|
||||
/* see : https://github.com/xtermjs/xterm.js/issues/3564#issuecomment-1004417440 */
|
||||
width: initial !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading" style="color: white; margin: 20px">
|
||||
<b style="color: red">Please click into this window once to allow audio!</b>
|
||||
<br /><br />
|
||||
Game: Loading... This can take some time...
|
||||
</div>
|
||||
<div class="terminal-container" style="height: 100%; width: 100%">
|
||||
<div id="terminal" style="height: 100%"></div>
|
||||
</div>
|
||||
<script>
|
||||
// Define variables to keep track of the audio elements
|
||||
let sound = new Audio();
|
||||
let music = new Audio();
|
||||
let currentMusicUrl = "";
|
||||
let userInteracted = false;
|
||||
|
||||
// Function to play a sound based on URL
|
||||
globalThis.playSound = (url) => {
|
||||
sound.src = url;
|
||||
sound.volume = 0.1;
|
||||
sound.play();
|
||||
}
|
||||
// Function to play a sound based on URL
|
||||
globalThis.playSound = (url) => {
|
||||
sound.src = url;
|
||||
sound.volume = 0.1;
|
||||
sound.play();
|
||||
};
|
||||
|
||||
// Function to loop a music song based on URL
|
||||
globalThis.loopMusic = (url) => {
|
||||
if (!userInteracted) {
|
||||
// If user hasn't interacted, set up a listener for user interaction
|
||||
document.addEventListener('click', function() {
|
||||
userInteracted = true;
|
||||
loopMusic(url); // Start playing the music after user interaction
|
||||
}, { once: true }); // Remove the listener after the first interaction
|
||||
return;
|
||||
}
|
||||
// Function to loop a music song based on URL
|
||||
globalThis.loopMusic = (url) => {
|
||||
if (!userInteracted) {
|
||||
// If user hasn't interacted, set up a listener for user interaction
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
userInteracted = true;
|
||||
loopMusic(url); // Start playing the music after user interaction
|
||||
},
|
||||
{ once: true },
|
||||
); // Remove the listener after the first interaction
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentMusicUrl === url) {
|
||||
return;
|
||||
} else {
|
||||
// If a new song is requested, stop the current song and set the new one
|
||||
music.src = url;
|
||||
music.volume = 0.1;
|
||||
currentMusicUrl = url;
|
||||
}
|
||||
if (currentMusicUrl === url) {
|
||||
return;
|
||||
} else {
|
||||
// If a new song is requested, stop the current song and set the new one
|
||||
music.src = url;
|
||||
music.volume = 0.1;
|
||||
currentMusicUrl = url;
|
||||
}
|
||||
|
||||
music.loop = true; // Loop the music
|
||||
music.play(); // Play the music
|
||||
}
|
||||
music.loop = true; // Loop the music
|
||||
music.play(); // Play the music
|
||||
};
|
||||
|
||||
globalThis.fsRead = (path) => {
|
||||
console.log("fsRead", path)
|
||||
return window.localStorage.getItem(path)
|
||||
}
|
||||
globalThis.fsRead = (path) => {
|
||||
console.log("fsRead", path);
|
||||
return window.localStorage.getItem(path);
|
||||
};
|
||||
|
||||
globalThis.fsWrite = (path, data) => {
|
||||
console.log("fsWrite", path)
|
||||
window.localStorage.setItem(path, data)
|
||||
}
|
||||
globalThis.fsWrite = (path, data) => {
|
||||
console.log("fsWrite", path);
|
||||
window.localStorage.setItem(path, data);
|
||||
};
|
||||
|
||||
globalThis.settings = {
|
||||
get(key, emptyValue) {
|
||||
console.log("get", key, emptyValue)
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(key))
|
||||
} catch (e) {
|
||||
return emptyValue !== undefined ? emptyValue : null
|
||||
}
|
||||
},
|
||||
getString(key) {
|
||||
return window.settings.get(key, "")
|
||||
},
|
||||
getInt(key) {
|
||||
return window.settings.get(key, 0)
|
||||
},
|
||||
getBool(key) {
|
||||
return window.settings.get(key, false)
|
||||
},
|
||||
getFloat(key) {
|
||||
return window.settings.get(key, 0.0)
|
||||
},
|
||||
getStrings(key) {
|
||||
return window.settings.get(key, [])
|
||||
},
|
||||
set(key, value) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value))
|
||||
},
|
||||
setDefault(key, value) {
|
||||
if (window.localStorage.getItem(key) !== null) {
|
||||
return
|
||||
}
|
||||
window.localStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
globalThis.settings.setDefault("font_size", 14);
|
||||
globalThis.settings = {
|
||||
get(key, emptyValue) {
|
||||
console.log("get", key, emptyValue);
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(key));
|
||||
} catch (e) {
|
||||
return emptyValue !== undefined ? emptyValue : null;
|
||||
}
|
||||
},
|
||||
getString(key) {
|
||||
return window.settings.get(key, "");
|
||||
},
|
||||
getInt(key) {
|
||||
return window.settings.get(key, 0);
|
||||
},
|
||||
getBool(key) {
|
||||
return window.settings.get(key, false);
|
||||
},
|
||||
getFloat(key) {
|
||||
return window.settings.get(key, 0.0);
|
||||
},
|
||||
getStrings(key) {
|
||||
return window.settings.get(key, []);
|
||||
},
|
||||
set(key, value) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
setDefault(key, value) {
|
||||
if (window.localStorage.getItem(key) !== null) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(key, value);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
globalThis.settings.setDefault("font_size", 12);
|
||||
|
||||
function initTerminal() {
|
||||
// Check if bubbletea is initialized
|
||||
if (globalThis.bubbletea_resize === undefined || globalThis.bubbletea_read === undefined || globalThis.bubbletea_write === undefined) {
|
||||
setTimeout(() => {
|
||||
console.log("waiting for bubbletea");
|
||||
initTerminal();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
function initTerminal() {
|
||||
// Check if bubbletea is initialized
|
||||
if (
|
||||
globalThis.bubbletea_resize === undefined ||
|
||||
globalThis.bubbletea_read === undefined ||
|
||||
globalThis.bubbletea_write === undefined
|
||||
) {
|
||||
setTimeout(() => {
|
||||
console.log("waiting for bubbletea");
|
||||
initTerminal();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove loading text
|
||||
document.getElementById('loading').remove();
|
||||
// Remove loading text
|
||||
document.getElementById("loading").remove();
|
||||
|
||||
const term = new Terminal({
|
||||
fontSize: globalThis.settings.getInt("font_size") ?? 14,
|
||||
fontFamily: 'IosevkaTermNerdFontMono',
|
||||
theme: {
|
||||
background: '#1a1a1a'
|
||||
}
|
||||
});
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.focus();
|
||||
const term = new Terminal({
|
||||
fontSize: globalThis.settings.getInt("font_size") ?? 12,
|
||||
fontFamily: "IosevkaTermNerdFontMono",
|
||||
theme: {
|
||||
background: "#1a1a1a",
|
||||
},
|
||||
});
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById("terminal"));
|
||||
term.focus();
|
||||
|
||||
// Register terminal resize
|
||||
fitAddon.fit();
|
||||
window.addEventListener('resize', () => (fitAddon.fit()));
|
||||
// Register terminal resize
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", () => fitAddon.fit());
|
||||
|
||||
// Initial resize
|
||||
bubbletea_resize(term.cols, term.rows)
|
||||
// Initial resize
|
||||
bubbletea_resize(term.cols, term.rows);
|
||||
|
||||
// Read from bubbletea and write to xterm
|
||||
setInterval(() => {
|
||||
const read = bubbletea_read();
|
||||
if (read && read.length > 0) {
|
||||
term.write(read);
|
||||
}
|
||||
}, 1000 / 30);
|
||||
// Read from bubbletea and write to xterm
|
||||
setInterval(() => {
|
||||
const read = bubbletea_read();
|
||||
if (read && read.length > 0) {
|
||||
term.write(read);
|
||||
}
|
||||
}, 1000 / 30);
|
||||
|
||||
// Resize on terminal resize
|
||||
term.onResize((size) => (bubbletea_resize(term.cols, term.rows)));
|
||||
// Resize on terminal resize
|
||||
term.onResize((size) => bubbletea_resize(term.cols, term.rows));
|
||||
|
||||
// Write xterm output to bubbletea
|
||||
term.onData((data) => (bubbletea_write(data)));
|
||||
}
|
||||
// Write xterm output to bubbletea
|
||||
term.onData((data) => bubbletea_write(data));
|
||||
}
|
||||
|
||||
function init() {
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("./eoe.wasm"), go.importObject).then((result) => {
|
||||
// Run wasm
|
||||
go.run(result.instance).then(() => {
|
||||
console.log("wasm finished");
|
||||
});
|
||||
function ensureAllFiles() {
|
||||
// Wait for WASM to be loaded
|
||||
if (!globalThis.version) {
|
||||
setTimeout(ensureAllFiles, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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()));
|
||||
})
|
||||
}
|
||||
// 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");
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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) => {
|
||||
// Run wasm
|
||||
go.run(result.instance).then(() => {
|
||||
console.log("wasm finished");
|
||||
});
|
||||
|
||||
// Init terminal. This should be done after bubbletea is initialized. For now, I use a timeout.
|
||||
document.fonts.load((globalThis.settings.getInt("font_size") ?? 12) + 'px "IosevkaTermNerdFontMono"').then(() => initTerminal());
|
||||
});
|
||||
}
|
||||
|
||||
ensureAllFiles();
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -78,6 +78,8 @@ func initSystems(hasAudio bool) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Setenv("EOE_IMG_TRUECOLOR", "1")
|
||||
|
||||
testArgs := testargs.New()
|
||||
flag.Parse()
|
||||
|
||||
|
||||
@ -9,27 +9,28 @@ Content that is dynamically generated at runtime is not included in this documen
|
||||
|
||||
| Type | Count |
|
||||
|----------------|-------|
|
||||
| Artifacts | 10 |
|
||||
| Cards | 18 |
|
||||
| Status Effects | 9 |
|
||||
| Enemies | 6 |
|
||||
| Events | 18 |
|
||||
| Artifacts | 11 |
|
||||
| Cards | 21 |
|
||||
| Status Effects | 11 |
|
||||
| Enemies | 9 |
|
||||
| Events | 19 |
|
||||
|
||||
|
||||
## Artifacts
|
||||
|
||||
| ID | Name | Description | Price | Tags | Test Present |
|
||||
|---------------------|-----------------|-----------------------------------------------------------------------------------------------------|-------|----------------|--------------------|
|
||||
| ``CROWBAR`` | Crowbar | A crowbar. It's a bit rusty, but it should still be useful! Can be used in your hand. | 80 | ATK, M, T, HND | :no_entry_sign: |
|
||||
| ``COMBAT_GLASSES`` | Combat Glasses | Whenever you play a **Ranged (R)** card, deal **1 additional damage** | 100 | _ACT_0 | :heavy_check_mark: |
|
||||
| ``COMBAT_GLOVES`` | Combat Gloves | Whenever you play a **Melee (M)** card, deal **1 additional damage** | 100 | _ACT_0 | :heavy_check_mark: |
|
||||
| ``PORTABLE_BUFFER`` | PRTBL Buffer | Start each turn with 1 **Block** | 100 | _ACT_0 | :no_entry_sign: |
|
||||
| ``SPEED_ENHANCER`` | Speed Enhancer | Start with a additional card at the beginning of combat. | 100 | _ACT_0 | :no_entry_sign: |
|
||||
| ``VIBRO_KNIFE`` | VIBRO Knife | A VIBRO knife. Uses ultrasonic vibrations to cut through almost anything. Can be used in your hand. | 180 | ATK, M, T, HND | :no_entry_sign: |
|
||||
| ``INTERVA_JUICER`` | Interval Juicer | **Heal 2** at the beginning of combat | 200 | _ACT_0 | :no_entry_sign: |
|
||||
| ``ARM_MOUNTED_GUN`` | Arm Mounted Gun | Weapon that is mounted on your arm. It is very powerful. | 250 | ARM | :no_entry_sign: |
|
||||
| ``LZR_PISTOL`` | LZR Pistol | A LZR pistol. Fires a concentrated beam of light. Can be used in your hand. | 280 | ATK, R, T, HND | :no_entry_sign: |
|
||||
| ``HAR_II`` | HAR-II | A HAR-II. A heavy assault rifle with a high rate of fire. Can be used in your hand. | 380 | ATK, R, T, HND | :no_entry_sign: |
|
||||
| ID | Name | Description | Price | Tags | Test Present |
|
||||
|----------------------|------------------|-----------------------------------------------------------------------------------------------------|-------|----------------|--------------------|
|
||||
| ``CROWBAR`` | Crowbar | A crowbar. It's a bit rusty, but it should still be useful! Can be used in your hand. | 80 | ATK, M, T, HND | :no_entry_sign: |
|
||||
| ``COMBAT_GLASSES`` | Combat Glasses | Whenever you play a **Ranged (R)** card, deal **1 additional damage** | 100 | _ACT_0 | :heavy_check_mark: |
|
||||
| ``COMBAT_GLOVES`` | Combat Gloves | Whenever you play a **Melee (M)** card, deal **1 additional damage** | 100 | _ACT_0 | :heavy_check_mark: |
|
||||
| ``PORTABLE_BUFFER`` | PRTBL Buffer | Start each turn with 1 **Block** | 100 | _ACT_0 | :no_entry_sign: |
|
||||
| ``SPEED_ENHANCER`` | Speed Enhancer | Start with a additional card at the beginning of combat. | 100 | _ACT_0 | :no_entry_sign: |
|
||||
| ``VIBRO_KNIFE`` | VIBRO Knife | A VIBRO knife. Uses ultrasonic vibrations to cut through almost anything. Can be used in your hand. | 180 | ATK, M, T, HND | :no_entry_sign: |
|
||||
| ``INTERVA_JUICER`` | Interval Juicer | **Heal 2** at the beginning of combat | 200 | _ACT_0 | :no_entry_sign: |
|
||||
| ``ARM_MOUNTED_GUN`` | Arm Mounted Gun | Weapon that is mounted on your arm. It is very powerful. | 250 | ARM | :no_entry_sign: |
|
||||
| ``LZR_PISTOL`` | LZR Pistol | A LZR pistol. Fires a concentrated beam of light. Can be used in your hand. | 280 | ATK, R, T, HND | :no_entry_sign: |
|
||||
| ``REFLECTIVE_ARMOR`` | Reflective Armor | Reflects 25% of the damage back to the attacker. | 300 | ARMOR, _ACT_0 | :no_entry_sign: |
|
||||
| ``HAR_II`` | HAR-II | A HAR-II. A heavy assault rifle with a high rate of fire. Can be used in your hand. | 380 | ATK, R, T, HND | :no_entry_sign: |
|
||||
|
||||
|
||||
## Cards
|
||||
@ -37,6 +38,7 @@ Content that is dynamically generated at runtime is not included in this documen
|
||||
| ID | Name | Description | Action Points | Exhaust | Consumable | Max Level | Price | Tags | Color | Used Callbacks | Test Present |
|
||||
|------------------------|--------------------|--------------------------------------------------------------------------------------------------------|---------------|--------------------|--------------------|-----------|-------|----------------|---------|----------------|--------------------|
|
||||
| ``ARM_MOUNTED_GUN`` | Arm Mounted Gun | Exhaust. Use your arm mounted gun to deal 15 (+3 for each upgrade) damage. | 3 | :heavy_check_mark: | :no_entry_sign: | 1 | -1 | ATK, R, T, ARM | #2f3e46 | ``OnCast`` | :heavy_check_mark: |
|
||||
| ``DEBUG_INSTA_KILL`` | DEBUG Insta Kill | ... | 0 | :no_entry_sign: | :no_entry_sign: | 1 | -1 | | #2f3e46 | ``OnCast`` | :no_entry_sign: |
|
||||
| ``KILL`` | Kill | Debug Card | 0 | :no_entry_sign: | :no_entry_sign: | 0 | -1 | | #2f3e46 | ``OnCast`` | :no_entry_sign: |
|
||||
| ``KNOCK_OUT`` | Knock Out | Inflicts **Knock Out** on the target, causing them to miss their next turn. | 2 | :no_entry_sign: | :no_entry_sign: | 0 | -1 | CC | #725e9c | ``OnCast`` | :no_entry_sign: |
|
||||
| ``MELEE_HIT`` | Melee Hit | Use your bare hands to deal 1 (+1 for each upgrade) damage. | 1 | :no_entry_sign: | :no_entry_sign: | 1 | -1 | ATK, M, HND | #2f3e46 | ``OnCast`` | :heavy_check_mark: |
|
||||
@ -50,10 +52,12 @@ Content that is dynamically generated at runtime is not included in this documen
|
||||
| ``FLASH_BANG`` | Flash Bang | **One-Time** - Inflicts **Blinded** on the target, causing them to deal less damage. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 150 | CC, _ACT_0 | #725e9c | ``OnCast`` | :no_entry_sign: |
|
||||
| ``FLASH_SHIELD`` | Flash Shield | **One-Time** - Deploy a temporary shield. **Negates** the next attack. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 150 | DEF, _ACT_0 | #219ebc | ``OnCast`` | :no_entry_sign: |
|
||||
| ``NANO_CHARGER`` | Nano Charger | **One-Time** - Supercharge your next attack. Deals **Double** damage. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 150 | BUFF, _ACT_0 | #c1121f | ``OnCast`` | :heavy_check_mark: |
|
||||
| ``SMOKE_BOMB`` | Smoke Bomb | Reduces the accuracy of all enemies for 1 turn. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 150 | CC, _ACT_0 | #2f3e46 | ``OnCast`` | :no_entry_sign: |
|
||||
| ``STIM_PACK`` | Stim Pack | **One-Time** - Restores **5** HP. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 150 | HEAL, _ACT_0 | #219ebc | ``OnCast`` | :heavy_check_mark: |
|
||||
| ``ENERGY_DRINK_2`` | ENRGY Drink X92 | **One-Time** - Gain 2 action points. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 250 | UTIL, _ACT_0 | #fb5607 | ``OnCast`` | :heavy_check_mark: |
|
||||
| ``ULTRA_FLASH_SHIELD`` | Ultra Flash Shield | **One-Time** - Deploy a temporary shield. **Negates** all attack this turn. | 3 | :no_entry_sign: | :heavy_check_mark: | 0 | 250 | DEF, _ACT_0 | #219ebc | ``OnCast`` | :no_entry_sign: |
|
||||
| ``ENERGY_DRINK_3`` | ENRGY Drink X93 | **One-Time** - Gain 3 action points. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 350 | UTIL, _ACT_0 | #fb5607 | ``OnCast`` | :heavy_check_mark: |
|
||||
| ``ADRENALINE_SHOT`` | Adrenaline Shot | Gain 2 additional action points for the next 3 turns. | 0 | :no_entry_sign: | :heavy_check_mark: | 0 | 400 | BUFF, _ACT_0 | #c1121f | ``OnCast`` | :no_entry_sign: |
|
||||
|
||||
|
||||
### Action Points
|
||||
@ -62,7 +66,7 @@ Content that is dynamically generated at runtime is not included in this documen
|
||||
pie
|
||||
title Action Points
|
||||
"3 AP" : 2
|
||||
"0 AP" : 9
|
||||
"0 AP" : 12
|
||||
"2 AP" : 1
|
||||
"1 AP" : 6
|
||||
```
|
||||
@ -74,9 +78,9 @@ title Action Points
|
||||
```mermaid
|
||||
pie
|
||||
title Card Types
|
||||
"Consume" : 11
|
||||
"Exhaust" : 1
|
||||
"Normal" : 8
|
||||
"Consume" : 9
|
||||
"Normal" : 9
|
||||
```
|
||||
|
||||
|
||||
@ -89,44 +93,50 @@ title Card Types
|
||||
| ``CHARGING`` | Charging | The drone is charging up for a powerful attack. | CHRG | #ff0000 | :no_entry_sign: | DecayNone | 1 | ``OnDamageCalc`` | :no_entry_sign: |
|
||||
| ``FLASH_BANG`` | Blinded | Causing **25%** less damage. | FL | #725e9c | :heavy_check_mark: | DecayOne | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
| ``KNOCK_OUT`` | Knock Out | Can't act | KO | #725e9c | :heavy_check_mark: | DecayOne | 1 | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``ADRENALINE_SHOT`` | Adrenaline Shot | Gain 2 additional action points. | AS | #c1121f | :no_entry_sign: | DecayOne | 3 | ``OnPlayerTurn`` | :no_entry_sign: |
|
||||
| ``BLOCK`` | Block | Decreases incoming damage for each stack | B | #219ebc | :heavy_check_mark: | DecayAll | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
| ``BOUNCE_SHIELD`` | Bounce Shield | Bounces back the next damage. Still takes damage. | BS | #219ebc | :no_entry_sign: | DecayAll | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
| ``FLASH_SHIELD`` | Flash Shield | Negates the next attack. | FS | #219ebc | :no_entry_sign: | DecayAll | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
| ``NANO_CHARGER`` | Nano Charge | Next attack deals **Double** damage. | NC | #c1121f | :no_entry_sign: | DecayAll | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
| ``SMOKE_BOMB`` | Smoke Bomb | Reduces accuracy by 50%. | SB | #2f3e46 | :no_entry_sign: | DecayOne | 1 | ``OnDamageCalc`` | :no_entry_sign: |
|
||||
| ``ULTRA_FLASH_SHIELD`` | Ultra Flash Shield | Negates all attacks. | UFS | #219ebc | :no_entry_sign: | DecayAll | 1 | ``OnDamageCalc`` | :heavy_check_mark: |
|
||||
|
||||
|
||||
## Enemies
|
||||
|
||||
| ID | Name | Description | Initial HP | Max HP | Color | Used Callbacks | Test Present |
|
||||
|------------------|--------------|------------------------------------------------|------------|--------|---------|------------------------------|-----------------|
|
||||
| ``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 | ``OnPlayerTurn``, ``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: |
|
||||
| ``PLASMA_GOLEM`` | Plasma Golem | A golem made of pure plasma energy. | 12 | 12 | #ff69b4 | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``RUST_MITE`` | Rust Mite | A small robot that eats metal. | 12 | 12 | #e6e65a | ``OnTurn`` | :no_entry_sign: |
|
||||
| ID | Name | Description | Initial HP | Max HP | Color | Used Callbacks | Test Present |
|
||||
|------------------------|-----------------------|-------------------------------------------------------------------|------------|--------|---------|------------------------------|-----------------|
|
||||
| ``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 | ``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: |
|
||||
| ``PLASMA_GOLEM`` | Plasma Golem | A golem made of pure plasma energy. | 12 | 12 | #ff69b4 | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``REPAIR_DRONE`` | Repair Drone | A drone designed to repair and support other machines. | 10 | 10 | #00ff00 | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``RUST_MITE`` | Rust Mite | A small robot that eats metal. | 12 | 12 | #e6e65a | ``OnTurn`` | :no_entry_sign: |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| ID | Name | Description | Tags | Choices | Test Present |
|
||||
|------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------|-----------------|
|
||||
| ``GAIN_GOLD_ACT_0`` | | ... - | _ACT_0 | <ul><li>``Take it! [Gain 20 Gold]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``PLASMA_GOLEM`` | A glowing figure emerges... | As you delve deeper into the facility, you notice a bright glow emanating from a nearby chamber. A massive golem made of pure plasma energy steps into view. - **It looks ready to unleash its power!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``LASER_DRONE`` | A menacing drone appears... | As you explore the facility, you hear a high-pitched whirring sound. A drone equipped with a powerful laser cannon appears in front of you. - **It looks ready to attack!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``MERCHANT`` | A strange figure | !!merchant.jpg - - The merchant is a tall, lanky figure draped in a long, tattered coat made of plant fibers and animal hides. Their face is hidden behind a mask made of twisted roots and vines, giving them an unsettling, almost alien appearance. - Despite their strange appearance, the merchant is a shrewd negotiator and a skilled trader. They carry with them a collection of bizarre and exotic items, including plant-based weapons, animal pelts, and strange, glowing artifacts that seem to pulse with an otherworldly energy. - The merchant is always looking for a good deal, and they're not above haggling with potential customers... | _ACT_0, _ACT_1, _ACT_2, _ACT_3 | <ul><li>``Trade``</li> <li>``Pass``</li></ul> | :no_entry_sign: |
|
||||
| ``CLEAN_BOT`` | Corpse. Clean. Engage. | !!clean_bot.jpg - While exploring the facility you hear a strange noise. Suddenly a strange robot appears from one of the corridors. - It seems to be cleaning up the area, but it's not working properly anymore and you can see small sparks coming out of it. - It looks at you and says "Corpse. Clean. Engage.". - **You're not sure what it means, but it doesn't seem to be friendly!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``GAMBLE_1_ACT_0`` | Electro Barrier | You find a room with a strange device in the middle. It seems to be some kind of electro barrier protecting a storage container. You can either try to disable the barrier or leave. - | _ACT_0 | <ul><li>``50% [Gain Artifact & Consumeable] 50% [Take 5 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``CROWBAR`` | Found: Crowbar | !!red_room.jpg - **You found something!** A crowbar. It's a bit rusty, but it should still be useful! - **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. | _ACT_0 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``HAR_II`` | Found: HAR-II | !!artifact_chest.jpg - **You found something!** A HAR-II. A heavy assault rifle with a high rate of fire. - **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. | _ACT_1 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``LZR_PISTOL`` | Found: LZR Pistol | !!artifact_chest.jpg - **You found something!** A LZR pistol. Fires a concentrated beam of light. - **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. | _ACT_1 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``VIBRO_KNIFE`` | Found: VIBRO Knife | !!artifact_chest.jpg - **You found something!** A VIBRO knife. Uses ultrasonic vibrations to cut through almost anything. - **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. | _ACT_0 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``GOLD_TO_HP_ACT_0`` | Old Vending Machine | You find an old vending machine, it seems to be still working. You can either pay 20 Gold to get 5 HP or leave. - | _ACT_0 | <ul><li>``Pay [20 Gold] [Gain 5 HP]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RANDOM_ARTIFACT_ACT_0`` | Random Artifact | !!artifact_chest.jpg - You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave. - | _ACT_0 | <ul><li>``Random Artifact [Gain 1 Artifact] [Take 5 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RANDOM_CONSUMEABLE_ACT_0`` | Random Consumeable | !!artifact_chest.jpg - You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave. - | _ACT_0 | <ul><li>``Random Artifact [Gain 1 Consumeable] [Take 2 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``MAX_LIFE_ACT_0`` | Symbiotic Parasite | You find a strange creature, it seems to be a symbiotic parasite. It offers to increase your max HP by 5. You can either accept or leave. - | _ACT_0 | <ul><li>``Accept it! [Gain 5 Max HP]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RUST_MITE`` | Tasty metals... | !!rust_mite.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 strange being. - It seems to be eating the metal from the walls. It looks at you and after a few seconds it rushes towards you. - **It seems to be hostile!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``UPRAGDE_CARD_ACT_0`` | Upgrade Station | You find a old automatic workstation. You are able to get it working again. You can either upgrade a random card or leave. - | _ACT_0 | <ul><li>``Upgrade a card [Upgrade a card] [Take 5 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``START`` | Waking up... | !!cryo_start.jpg - You wake up in a dimly lit room, the faint glow of a red emergency light casting an eerie hue over the surroundings. The air is musty and stale, the metallic scent of the cryo-chamber still lingering in your nostrils. You feel groggy and disoriented, your mind struggling to process what's happening. - As you try to sit up, you notice that your body is stiff and unresponsive. It takes a few moments for your muscles to warm up and regain their strength. Looking around, you see that the walls are made of a dull gray metal, covered in scratches and scuff marks. There's a faint humming sound coming from somewhere, indicating that the facility is still operational. - You try to remember how you ended up here, but your memories are hazy and fragmented. The last thing you recall is a blinding flash of light and a deafening boom. You must have been caught in one of the nuclear explosions that devastated the world. - As you struggle to gather your bearings, you notice a blinking panel on the wall, with the words *"Cryo Sleep Malfunction"* displayed in bold letters. It seems that the system has finally detected the error that caused your prolonged slumber and triggered your awakening. - **Shortly after you realize that you are not alone...** | | <ul><li>``Try to find a weapon. [Find melee weapon] [Take 4 damage]``</li> <li>``Gather your strength and attack it!``</li></ul> | :no_entry_sign: |
|
||||
| ``CYBER_SPIDER`` | What is this thing at the ceiling? | !!cyber_spider.jpg - You come around a corner and see a strange creature hanging from the ceiling. It looks like a spider, but it's made out of metal. - It seems to be waiting for its prey to come closer and there is no way around it. - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ID | Name | Description | Tags | Choices | Test Present |
|
||||
|------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------|-----------------|
|
||||
| ``GAIN_GOLD_ACT_0`` | | ... - | _ACT_0 | <ul><li>``Take it! [Gain 20 Gold]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``PLASMA_GOLEM`` | A glowing figure emerges... | !!plasma_golem.jpg - As you delve deeper into the facility, you notice a bright glow emanating from a nearby chamber. A massive golem made of pure plasma energy steps into view. - **It looks ready to unleash its power!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``LASER_DRONE`` | A menacing drone appears... | !!laser_drone.jpg - As you explore the facility, you hear a high-pitched whirring sound. A drone equipped with a powerful laser cannon appears in front of you. - **It looks ready to attack!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``CYBER_SLIME`` | A strange cybernetic slime appears... | !!cyber_slime.jpg - As you explore the facility, you come across a strange cybernetic slime. It seems to be pulsating with energy and looks hostile. - **Prepare for a fight!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``MERCHANT`` | A strange figure | !!merchant.jpg - - The merchant is a tall, lanky figure draped in a long, tattered coat made of plant fibers and animal hides. Their face is hidden behind a mask made of twisted roots and vines, giving them an unsettling, almost alien appearance. - Despite their strange appearance, the merchant is a shrewd negotiator and a skilled trader. They carry with them a collection of bizarre and exotic items, including plant-based weapons, animal pelts, and strange, glowing artifacts that seem to pulse with an otherworldly energy. - The merchant is always looking for a good deal, and they're not above haggling with potential customers... | _ACT_0, _ACT_1, _ACT_2, _ACT_3 | <ul><li>``Trade``</li> <li>``Pass``</li></ul> | :no_entry_sign: |
|
||||
| ``CLEAN_BOT`` | Corpse. Clean. Engage. | !!clean_bot.jpg - While exploring the facility you hear a strange noise. Suddenly a strange robot appears from one of the corridors. - It seems to be cleaning up the area, but it's not working properly anymore and you can see small sparks coming out of it. - It looks at you and says "Corpse. Clean. Engage.". - **You're not sure what it means, but it doesn't seem to be friendly!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``GAMBLE_1_ACT_0`` | Electro Barrier | !!electro_barrier.jpg - You find a room with a strange device in the middle. It seems to be some kind of electro barrier protecting a storage container. You can either try to disable the barrier or leave. - | _ACT_0 | <ul><li>``50% [Gain Artifact & Consumeable] 50% [Take 2 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``CROWBAR`` | Found: Crowbar | !!red_room.jpg - **You found something!** A crowbar. It's a bit rusty, but it should still be useful! - **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. | _ACT_0 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``HAR_II`` | Found: HAR-II | !!artifact_chest.jpg - **You found something!** A HAR-II. A heavy assault rifle with a high rate of fire. - **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. | _ACT_1 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``LZR_PISTOL`` | Found: LZR Pistol | !!artifact_chest.jpg - **You found something!** A LZR pistol. Fires a concentrated beam of light. - **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. | _ACT_1 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``VIBRO_KNIFE`` | Found: VIBRO Knife | !!artifact_chest.jpg - **You found something!** A VIBRO knife. Uses ultrasonic vibrations to cut through almost anything. - **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. | _ACT_0 | <ul><li>````</li> <li>``Leave...``</li></ul> | :no_entry_sign: |
|
||||
| ``GOLD_TO_HP_ACT_0`` | Old Vending Machine | You find an old vending machine, it seems to be still working. You can either pay 20 Gold to get 5 HP or leave. - | _ACT_0 | <ul><li>``Pay [20 Gold] [Gain 5 HP]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RANDOM_ARTIFACT_ACT_0`` | Random Artifact | !!artifact_chest.jpg - You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave. - | _ACT_0 | <ul><li>``Random Artifact [Gain 1 Artifact] [Take 2 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RANDOM_CONSUMEABLE_ACT_0`` | Random Consumeable | !!artifact_chest.jpg - You found a chest with a strange symbol on it. The chest is protected by a strange barrier. You can either open it and take some damage or leave. - | _ACT_0 | <ul><li>``Random Artifact [Gain 1 Consumeable] [Take 2 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``MAX_LIFE_ACT_0`` | Symbiotic Parasite | !!symbiotic_parasite.jpg - You find a strange creature, it seems to be a symbiotic parasite. It offers to increase your max HP by 5. You can either accept or leave. - | _ACT_0 | <ul><li>``Accept it! [Gain 5 Max HP]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``RUST_MITE`` | Tasty metals... | !!rust_mite.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 strange being. - It seems to be eating the metal from the walls. It looks at you and after a few seconds it rushes towards you. - **It seems to be hostile!** - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
| ``UPRAGDE_CARD_ACT_0`` | Upgrade Station | !!upgrade_station.jpg - You find a old automatic workstation. You are able to get it working again. You can either upgrade a random card or leave. - | _ACT_0 | <ul><li>``Upgrade a card [Upgrade a card] [Take 2 damage]``</li> <li>``Leave!``</li></ul> | :no_entry_sign: |
|
||||
| ``START`` | Waking up... | !!cryo_start.jpg - You wake up in a dimly lit room, the faint glow of a red emergency light casting an eerie hue over the surroundings. The air is musty and stale, the metallic scent of the cryo-chamber still lingering in your nostrils. You feel groggy and disoriented, your mind struggling to process what's happening. - As you try to sit up, you notice that your body is stiff and unresponsive. It takes a few moments for your muscles to warm up and regain their strength. Looking around, you see that the walls are made of a dull gray metal, covered in scratches and scuff marks. There's a faint humming sound coming from somewhere, indicating that the facility is still operational. - You try to remember how you ended up here, but your memories are hazy and fragmented. The last thing you recall is a blinding flash of light and a deafening boom. You must have been caught in one of the nuclear explosions that devastated the world. - As you struggle to gather your bearings, you notice a blinking panel on the wall, with the words *"Cryo Sleep Malfunction"* displayed in bold letters. It seems that the system has finally detected the error that caused your prolonged slumber and triggered your awakening. - **Shortly after you realize that you are not alone...** | | <ul><li>``Try to find a weapon. [Find melee weapon] [Take 4 damage]``</li> <li>``Gather your strength and attack it!``</li></ul> | :no_entry_sign: |
|
||||
| ``CYBER_SPIDER`` | What is this thing at the ceiling? | !!cyber_spider.jpg - You come around a corner and see a strange creature hanging from the ceiling. It looks like a spider, but it's made out of metal. - It seems to be waiting for its prey to come closer and there is no way around it. - | _ACT_0_FIGHT | <ul><li>``Fight!``</li></ul> | :no_entry_sign: |
|
||||
|
||||
@ -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.
|
||||
@ -108,6 +114,30 @@ guid() -> guid
|
||||
|
||||
</details>
|
||||
|
||||
<details> <summary><b><code>random</code></b> </summary> <br/>
|
||||
|
||||
Returns a random number between 0 and 1. Prefer this function over math.random(), as this is seeded for the session.
|
||||
|
||||
**Signature:**
|
||||
|
||||
```
|
||||
random() -> number
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details> <summary><b><code>random_int</code></b> </summary> <br/>
|
||||
|
||||
Returns a random number between min and max. Prefer this function over math.random(), as this is seeded for the session.
|
||||
|
||||
**Signature:**
|
||||
|
||||
```
|
||||
random_int(min : number, max : number) -> number
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details> <summary><b><code>store</code></b> </summary> <br/>
|
||||
|
||||
Stores a persistent value for this run that will be restored after a save load. Can store any lua basic value or table.
|
||||
|
||||
@ -2,6 +2,7 @@ package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@ -10,6 +11,11 @@ func init() {
|
||||
gob.Register(StateEventDamageData{})
|
||||
gob.Register(StateEventHealData{})
|
||||
gob.Register(StateEventMoneyData{})
|
||||
gob.Register(StateEventArtifactAddedData{})
|
||||
gob.Register(StateEventArtifactRemovedData{})
|
||||
gob.Register(StateEventCardAddedData{})
|
||||
gob.Register(StateEventCardRemovedData{})
|
||||
|
||||
gob.Register(StateCheckpoint{})
|
||||
gob.Register(StateCheckpointMarker{})
|
||||
}
|
||||
|
||||
16
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.")
|
||||
@ -88,6 +90,20 @@ fun = require "fun"
|
||||
|
||||
d.Category("Utility", "General game constants.", 1)
|
||||
|
||||
d.Function("random", "Returns a random number between 0 and 1. Prefer this function over math.random(), as this is seeded for the session.", "number")
|
||||
l.SetGlobal("random", l.NewFunction(func(state *lua.LState) int {
|
||||
state.Push(lua.LNumber(session.rand.Float64()))
|
||||
return 1
|
||||
}))
|
||||
|
||||
d.Function("random_int", "Returns a random number between min and max. Prefer this function over math.random(), as this is seeded for the session.", "number", "min : number", "max : number")
|
||||
l.SetGlobal("random_int", l.NewFunction(func(state *lua.LState) int {
|
||||
min := state.ToInt(1)
|
||||
max := state.ToInt(2)
|
||||
state.Push(lua.LNumber(session.rand.IntN(max-min) + min))
|
||||
return 1
|
||||
}))
|
||||
|
||||
d.Function("guid", "returns a new random guid.", "guid")
|
||||
l.SetGlobal("guid", l.NewFunction(func(state *lua.LState) int {
|
||||
state.Push(lua.LString(NewGuid("LUA")))
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package game
|
||||
|
||||
import "encoding/gob"
|
||||
import (
|
||||
"encoding/gob"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(SavedState{})
|
||||
@ -10,6 +13,8 @@ func init() {
|
||||
// runtime or other pointer.
|
||||
type SavedState struct {
|
||||
State GameState
|
||||
Seed uint64
|
||||
Rand *rand.PCG
|
||||
Actors map[string]Actor
|
||||
Instances map[string]any
|
||||
StagesCleared int
|
||||
|
||||
120
game/session.go
@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
@ -102,6 +102,9 @@ type Session struct {
|
||||
log *log.Logger
|
||||
luaState *lua.LState
|
||||
luaDocs *ludoc.Docs
|
||||
seed uint64
|
||||
randSrc *rand.PCG
|
||||
rand *rand.Rand
|
||||
resources *ResourcesManager
|
||||
|
||||
state GameState
|
||||
@ -128,9 +131,14 @@ type Session struct {
|
||||
|
||||
// NewSession creates a new game session.
|
||||
func NewSession(options ...func(s *Session)) *Session {
|
||||
seed := uint64(time.Now().UnixMilli())
|
||||
randSrc := rand.NewPCG(seed, 1337)
|
||||
session := &Session{
|
||||
log: log.New(io.Discard, "", 0),
|
||||
state: GameStateEvent,
|
||||
log: log.New(io.Discard, "", 0),
|
||||
seed: seed,
|
||||
randSrc: randSrc,
|
||||
rand: rand.New(randSrc),
|
||||
state: GameStateEvent,
|
||||
actors: map[string]Actor{
|
||||
PlayerActorID: NewActor(PlayerActorID),
|
||||
},
|
||||
@ -149,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 {
|
||||
@ -157,16 +167,17 @@ 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!")
|
||||
|
||||
session.log.Println("Seed:", session.seed)
|
||||
session.Log(LogTypeSuccess, fmt.Sprintf("Seed: %d", seed))
|
||||
|
||||
session.UpdatePlayer(func(actor *Actor) bool {
|
||||
actor.HP = 80
|
||||
actor.MaxHP = 80
|
||||
actor.Gold = 50 + rand.Intn(50)
|
||||
actor.HP = 100
|
||||
actor.MaxHP = 100
|
||||
actor.Gold = 0
|
||||
return true
|
||||
})
|
||||
|
||||
@ -201,6 +212,35 @@ 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) {
|
||||
s.seed = seed
|
||||
s.randSrc.Seed(seed, 1337)
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeedString sets the seed for the random number generator based on a string.
|
||||
func WithSeedString(seed string) func(s *Session) {
|
||||
return func(s *Session) {
|
||||
generatedSeed := uint64(0)
|
||||
for i, c := range seed {
|
||||
generatedSeed += uint64(c) + uint64(i)
|
||||
}
|
||||
s.seed = generatedSeed
|
||||
s.randSrc.Seed(generatedSeed, 1337)
|
||||
}
|
||||
}
|
||||
|
||||
// WithOnLuaError sets the function that will be called when a lua error happens.
|
||||
func WithOnLuaError(fn func(file string, line int, callback string, typeId string, err error)) func(s *Session) {
|
||||
return func(s *Session) {
|
||||
@ -241,18 +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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,6 +308,8 @@ func (s *Session) ToSavedState() SavedState {
|
||||
// should be loaded or the state could be corrupted.
|
||||
func (s *Session) LoadSavedState(save SavedState) {
|
||||
s.state = save.State
|
||||
s.seed = save.Seed
|
||||
s.randSrc = save.Rand
|
||||
s.actors = lo.MapValues(save.Actors, func(item Actor, key string) Actor {
|
||||
return item.Sanitize()
|
||||
})
|
||||
@ -341,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)
|
||||
@ -456,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -497,9 +566,11 @@ func (s *Session) SetupFight() {
|
||||
|
||||
// Save after each fight
|
||||
{
|
||||
s.Log(LogTypeSuccess, "Session saving...")
|
||||
|
||||
save, err := s.GobEncode()
|
||||
if err != nil {
|
||||
s.log.Println("Error saving file:", save)
|
||||
s.log.Println("Error saving file:", err)
|
||||
} else {
|
||||
if err := fs.WriteFile("./session.save", save); err != nil {
|
||||
s.log.Println("Error saving file:", save)
|
||||
@ -783,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)
|
||||
}
|
||||
|
||||
|
||||
4
go.mod
@ -1,8 +1,6 @@
|
||||
module github.com/BigJk/end_of_eden
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.6
|
||||
go 1.23.0
|
||||
|
||||
replace github.com/containerd/console => github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5
|
||||
|
||||
|
||||
@ -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
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
EOE_TESTER_WORKING_DIR=$(pwd) go test ./cmd/internal/tester -v
|
||||
@ -128,7 +128,7 @@ func Play(key string, volumeModifier ...float64) {
|
||||
return
|
||||
}
|
||||
|
||||
if settings.GetFloat("volume") == 0 {
|
||||
if settings.GetFloat("volume") == 0 || !settings.GetBool("audio") {
|
||||
return
|
||||
}
|
||||
|
||||
@ -154,7 +154,7 @@ func PlayMusic(key string) {
|
||||
return
|
||||
}
|
||||
|
||||
if settings.GetFloat("volume") == 0 {
|
||||
if settings.GetFloat("volume") == 0 || !settings.GetBool("audio") {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,12 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"github.com/BigJk/end_of_eden/internal/fs"
|
||||
"github.com/BigJk/end_of_eden/system/settings"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/BigJk/end_of_eden/internal/fs"
|
||||
"github.com/BigJk/end_of_eden/system/settings"
|
||||
)
|
||||
|
||||
// InitAudio initializes the audio system. Loads all audio files from the assets/audio folder.
|
||||
@ -27,7 +28,7 @@ func Play(key string, volumeModifier ...float64) {
|
||||
|
||||
// PlayMusic plays a music track. If the music track is not loaded, nothing will happen.
|
||||
func PlayMusic(key string) {
|
||||
if settings.GetFloat("volume") == 0 {
|
||||
if settings.GetFloat("volume") == 0 || !settings.GetBool("audio") {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,6 @@ package image
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/BigJk/end_of_eden/internal/fs"
|
||||
"github.com/BigJk/imeji"
|
||||
"github.com/BigJk/imeji/charmaps"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/muesli/termenv"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
@ -19,6 +14,12 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/BigJk/end_of_eden/internal/fs"
|
||||
"github.com/BigJk/imeji"
|
||||
"github.com/BigJk/imeji/charmaps"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
// TODO: Better decoupling in relation to session
|
||||
@ -49,24 +50,24 @@ func buildOption(options ...Option) (Options, []imeji.Option) {
|
||||
data.tag += os.Getenv("EOE_IMG_PATTERN")
|
||||
}
|
||||
|
||||
if runtime.GOOS == "js" {
|
||||
if runtime.GOOS == "js" || os.Getenv("EOE_IMG_TRUECOLOR") == "1" {
|
||||
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
|
||||
data.tag += "truecolor"
|
||||
}
|
||||
|
||||
switch termenv.DefaultOutput().Profile {
|
||||
case termenv.TrueColor:
|
||||
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
|
||||
data.tag += "truecolor"
|
||||
case termenv.ANSI:
|
||||
imejiOptions = append(imejiOptions, imeji.WithANSI())
|
||||
data.tag += "ansi"
|
||||
case termenv.ANSI256:
|
||||
imejiOptions = append(imejiOptions, imeji.WithANSI256())
|
||||
data.tag += "ansi256"
|
||||
default:
|
||||
// TODO: should this be the default fallback?
|
||||
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
|
||||
} else {
|
||||
switch termenv.DefaultOutput().Profile {
|
||||
case termenv.TrueColor:
|
||||
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
|
||||
data.tag += "truecolor"
|
||||
case termenv.ANSI:
|
||||
imejiOptions = append(imejiOptions, imeji.WithANSI())
|
||||
data.tag += "ansi"
|
||||
case termenv.ANSI256:
|
||||
imejiOptions = append(imejiOptions, imeji.WithANSI256())
|
||||
data.tag += "ansi256"
|
||||
default:
|
||||
// TODO: should this be the default fallback?
|
||||
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
|
||||
}
|
||||
}
|
||||
|
||||
// Build image options
|
||||
|
||||
@ -2,6 +2,8 @@ package gameview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BigJk/end_of_eden/game"
|
||||
"github.com/BigJk/end_of_eden/system/audio"
|
||||
"github.com/BigJk/end_of_eden/ui"
|
||||
@ -17,7 +19,6 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -139,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{
|
||||
@ -150,6 +152,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}))
|
||||
}
|
||||
}
|
||||
case "s":
|
||||
m.inPlayerView = !m.inPlayerView
|
||||
}
|
||||
}
|
||||
//
|
||||
@ -315,6 +319,8 @@ func (m Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.fightStatusTop(), m.merchant.View())
|
||||
case game.GameStateEvent:
|
||||
return lipgloss.Place(m.Size.Width, m.Size.Height, lipgloss.Center, lipgloss.Center, m.event.View(), lipgloss.WithWhitespaceChars(" "))
|
||||
case game.GameStateGameOver:
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Unknown State: %s", m.Session.GetGameState())
|
||||
@ -515,7 +521,7 @@ func (m Model) fightStatusBottom() string {
|
||||
lipgloss.Center,
|
||||
m.zones.Mark(ZoneEndTurn, style.HeaderStyle.Copy().Background(lo.Ternary(m.zones.Get(ZoneEndTurn).InBounds(m.LastMouse), style.BaseRed, style.BaseRedDarker)).Margin(0, 4, 0, 0).Render("End Turn")),
|
||||
style.RedDarkerText.Render(`▀ █▌█▌▪
|
||||
·██·
|
||||
·██·
|
||||
▪▐█·█▌`))),
|
||||
))
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package mainmenu
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/BigJk/end_of_eden/system/audio"
|
||||
"github.com/BigJk/end_of_eden/ui/style"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
@ -13,13 +15,15 @@ import (
|
||||
type Choice string
|
||||
|
||||
const (
|
||||
ChoiceWaiting = Choice("WAITING")
|
||||
ChoiceContinue = Choice("CONTINUE")
|
||||
ChoiceNewGame = Choice("NEW_GAME")
|
||||
ChoiceAbout = Choice("ABOUT")
|
||||
ChoiceSettings = Choice("SETTINGS")
|
||||
ChoiceMods = Choice("MODS")
|
||||
ChoiceExit = Choice("EXIT")
|
||||
ChoiceWaiting = Choice("WAITING")
|
||||
ChoiceContinue = Choice("CONTINUE")
|
||||
ChoiceTutorial = Choice("TUTORIAL")
|
||||
ChoiceNewGame = Choice("NEW_GAME")
|
||||
ChoiceNewGameSOD = Choice("NEW_GAME_SOD")
|
||||
ChoiceAbout = Choice("ABOUT")
|
||||
ChoiceSettings = Choice("SETTINGS")
|
||||
ChoiceMods = Choice("MODS")
|
||||
ChoiceExit = Choice("EXIT")
|
||||
)
|
||||
|
||||
type choiceItem struct {
|
||||
@ -42,13 +46,22 @@ 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},
|
||||
choiceItem{zones, "Settings", "Other settings won't let you survive...", ChoiceSettings},
|
||||
choiceItem{zones, "Mods", "Make the game even more fun!", ChoiceMods},
|
||||
choiceItem{zones, "Exit", "Got enough already?", ChoiceExit},
|
||||
}
|
||||
|
||||
// Hide exit on web
|
||||
if runtime.GOOS == "js" {
|
||||
choices = lo.Filter(choices, func(value list.Item, i int) bool {
|
||||
return value.(choiceItem).title != "Exit" || value.(choiceItem).title == "Mods"
|
||||
})
|
||||
}
|
||||
|
||||
if hideSettings {
|
||||
choices = lo.Filter(choices, func(value list.Item, i int) bool {
|
||||
return value.(choiceItem).key != ChoiceSettings
|
||||
|
||||
@ -2,6 +2,11 @@ package mainmenu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BigJk/end_of_eden/game"
|
||||
"github.com/BigJk/end_of_eden/internal/fs"
|
||||
"github.com/BigJk/end_of_eden/system/audio"
|
||||
@ -20,10 +25,6 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
"github.com/samber/lo"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
@ -119,6 +120,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
case ChoiceNewGameSOD:
|
||||
fallthrough
|
||||
case ChoiceNewGame:
|
||||
audio.Play("btn_menu")
|
||||
|
||||
@ -133,15 +136,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return fmt.Sprintf("./mods/%s/images/", item)
|
||||
})...)
|
||||
|
||||
isSOD := m.choices.selected == ChoiceNewGameSOD
|
||||
m.choices = m.choices.Clear()
|
||||
return m, tea.Sequence(
|
||||
cmd,
|
||||
root.Push(gameview.New(m, m.zones, game.NewSession(
|
||||
game.WithLogging(log.New(f, "SESSION ", log.Ldate|log.Ltime|log.Lshortfile)),
|
||||
game.WithMods(m.settings.GetStrings("mods")),
|
||||
lo.Ternary(isSOD, game.WithSeedString(time.Now().Format(time.DateOnly)), nil),
|
||||
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")
|
||||
|
||||
|
||||
@ -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{
|
||||
` ██████
|
||||
██ ████
|
||||
██ ██ ██
|
||||
████ ██
|
||||
` ██████
|
||||
██ ████
|
||||
██ ██ ██
|
||||
████ ██
|
||||
██████`,
|
||||
` ██
|
||||
███
|
||||
██
|
||||
██
|
||||
██ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
` ██
|
||||
███
|
||||
██
|
||||
██
|
||||
██ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
███████ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
██████ `,
|
||||
`██ ██
|
||||
██ ██
|
||||
███████
|
||||
██
|
||||
`██ ██
|
||||
██ ██
|
||||
███████
|
||||
██
|
||||
██`,
|
||||
`███████
|
||||
██
|
||||
███████
|
||||
██
|
||||
`███████
|
||||
██
|
||||
███████
|
||||
██
|
||||
███████`,
|
||||
` ██████
|
||||
██
|
||||
███████
|
||||
██ ██
|
||||
` ██████
|
||||
██
|
||||
███████
|
||||
██ ██
|
||||
██████`,
|
||||
`███████
|
||||
██
|
||||
██
|
||||
██
|
||||
`███████
|
||||
██
|
||||
██
|
||||
██
|
||||
██`,
|
||||
` █████
|
||||
██ ██
|
||||
█████
|
||||
██ ██
|
||||
` █████
|
||||
██ ██
|
||||
█████
|
||||
██ ██
|
||||
█████`,
|
||||
` █████
|
||||
██ ██
|
||||
██████
|
||||
██
|
||||
` █████
|
||||
██ ██
|
||||
██████
|
||||
██
|
||||
█████`,
|
||||
}
|
||||
|
||||
|
||||