Compare commits

...

37 Commits

Author SHA1 Message Date
Daniel Schmidt
bdfd8e319f fix: view breaking if 1 hp is in damage screen 2024-09-02 22:02:05 +02:00
Daniel Schmidt
f2bd471d3e feat: rebalance weapon 2024-09-02 21:57:49 +02:00
Daniel Schmidt
7e7afb4dce feat: add REST and LONG_REST card 2024-09-02 12:29:35 +02:00
Daniel Schmidt
a3ae4898d8 feat: add NANOBOT_SWARM 2024-09-02 12:23:17 +02:00
Daniel Schmidt
747101d9ca feat: run_tests script 2024-09-02 12:13:59 +02:00
Daniel Schmidt
4bce308ef5 feat: some new artifacts and cards 2024-09-02 12:13:53 +02:00
Daniel Schmidt
efbc74077a fix: save being broken after a while 2024-09-02 12:13:16 +02:00
Daniel Schmidt
1604f876a8 feat: optimize file loading in WASM 2024-08-31 09:20:37 +02:00
Daniel Schmidt
1c4ecf2a26 fix: fix tutorial for wasm 2024-08-30 16:30:32 +02:00
Daniel Schmidt
54179ae189 feat: CI release to itch.io 2024-08-30 16:19:15 +02:00
Daniel Schmidt
b03a6520cf feat: basic tutorial 2024-08-30 12:20:03 +02:00
Daniel Schmidt
43d44d5751 chore: update go version in dockerfile 2024-08-28 19:50:16 +02:00
Daniel Schmidt
24ad4eb61a chore: bump go versions 2024-08-28 07:54:15 +02:00
Daniel Schmidt
2b085f0c99 chore: updated docs 2024-08-27 23:14:27 +02:00
Daniel Schmidt
275460002e feat: correct seeding 2024-08-27 23:14:00 +02:00
Daniel Schmidt
98da619522 fix: remove unknown state for gameover 2024-08-27 21:45:21 +02:00
Daniel Schmidt
7f85f3c3b0 fix: saving of the game 2024-08-27 21:40:16 +02:00
Daniel Schmidt
11402099d0 fix: correctly use the "audio" setting flag 2024-08-27 21:25:30 +02:00
Daniel Schmidt
389e5ea18e feat: some more event images 2024-08-27 21:25:12 +02:00
Daniel Schmidt
c2c991f6b7 fix: CYBER_SLIME splitting when other enemy dies 2024-08-27 21:24:42 +02:00
Daniel Schmidt
dc5b148bea chore: updated docs 2024-08-26 20:54:52 +02:00
Daniel Schmidt
5bc167d3c9 feat: add 3 equipments 2024-08-26 20:54:19 +02:00
Daniel Schmidt
a61af0c2d7 feat: add REPAIR_DRONE enemy 2024-08-26 20:43:48 +02:00
Daniel Schmidt
f992cab2e3 feat: add CYBER_SLIME enemy 2024-08-26 20:31:29 +02:00
Daniel Schmidt
feff00155c fix: reduce WASM font size to not break ESC menu 2024-08-25 09:01:59 +02:00
Daniel Schmidt
475190c95f fix: WASM build not using TrueColor 2024-08-25 08:36:18 +02:00
Daniel Schmidt
312c3db924 fix: remove test from debug card 2024-08-24 15:42:35 +02:00
Daniel Schmidt
15305d9680 feat: 2 new enemies and a few fixes 2024-08-24 15:35:33 +02:00
Daniel Schmidt
a5734b48c6
Merge pull request #4 from tomholford/th/copy
chore: typo
2024-07-16 16:04:55 +02:00
tomholford
b39d55820f chore: typo 2024-07-15 21:49:58 -07:00
Daniel Schmidt
756e7cbf99 chore: fix docker build 2024-05-22 20:19:59 +02:00
Daniel Schmidt
3b52d8befe chore: add arm64 builds 2024-05-22 20:17:10 +02:00
Daniel Schmidt
d973e28b4f fix: deal correct damage 2024-05-22 20:17:01 +02:00
Daniel Schmidt
b17fbf50ab chore: update versions 2024-05-17 08:44:57 +02:00
Daniel Schmidt
509567f4ed fix: mouse support and card casting error 2024-05-15 19:49:04 +02:00
Daniel Schmidt
2cbf45a0f8 chore: update mouse handling 2024-01-23 21:29:45 +01:00
Daniel Schmidt
5587aba072 chore: add name to action 2024-01-23 21:14:01 +01:00
70 changed files with 1850 additions and 569 deletions

View File

@ -32,6 +32,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
@ -59,6 +65,7 @@ jobs:
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,3 +1,5 @@
name: Publish Releases
on:
release:
types: [created]
@ -11,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 }}
@ -41,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
@ -61,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
@ -104,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:
@ -131,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
@ -143,4 +147,97 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v1
with:
files: end_of_eden_gl-${{ github.ref_name }}-macos-amd64.zip
files: end_of_eden_gl-${{ github.ref_name }}-macos-amd64.zip
release-macos-term-arm64:
permissions: write-all
name: release macos/arm64 term
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Fetch Go
uses: actions/setup-go@v4
with:
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-arm64
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:
files: end_of_eden_term-${{ github.ref_name }}-macos-arm64.zip
release-macos-gl-arm64:
permissions: write-all
name: release macos/arm64 gl
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Fetch XCode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Fetch Go
uses: actions/setup-go@v4
with:
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
export BIN=end_of_eden_gl-$(basename ${GITHUB_REF})-macos-arm64
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:
files: end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip
release-itch:
permissions: write-all
name: release itch
runs-on: ubuntu-latest
needs:
[
release-windows-term-amd64,
release-windows-gl-amd64,
release-macos-term-amd64,
release-macos-gl-amd64,
release-macos-term-arm64,
release-macos-gl-arm64,
release-linux-term-amd64,
release-linux-gl-amd64,
release-wasm,
]
steps:
- name: Download Release
uses: robinraju/release-downloader@v1
with:
repository: "BigJk/end_of_eden"
tag: ${{ github.ref_name }}
fileName: "*.zip"
- name: Install Butler
run: |
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
rm butler.zip
./butler -V
- name: Push to Itch.io
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
run: |
./butler push end_of_eden_term-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-gl-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-gl-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-term-arm64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-gl-arm64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_term-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-term-amd64 --userversion ${{ github.ref_name }}
./butler push end_of_eden_gl-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-gl-amd64 --userversion ${{ github.ref_name }}
./butler push eoe.wasm-${{ github.ref_name }}-js-wasm.zip BigJk/end-of-eden:web --userversion ${{ github.ref_name }}

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS build-stage
FROM golang:1.23 AS build-stage
WORKDIR /build
RUN mkdir /app
@ -8,11 +8,10 @@ COPY . .
RUN apt-get update
RUN apt-get install -y libasound2-dev
RUN go build -tags no_audio -o /app/end_of_eden ./cmd/game
RUN go build -tags no_audio -o /app/fuzzy_tester ./cmd/internal/fuzzy_tester
# Disable SSH for now
# RUN go build -tags no_audio -o /app/end_of_eden_ssh ./cmd/game_ssh
RUN CGO_ENABLED=0 go build -tags no_audio -o /app/end_of_eden ./cmd/game
RUN CGO_ENABLED=0 go build -tags no_audio -o /app/end_of_eden_ssh ./cmd/game_ssh
RUN CGO_ENABLED=0 go build -tags no_audio -o /app/tester ./cmd/internal/tester
RUN CGO_ENABLED=0 go build -tags no_audio -o /app/fuzzy_tester ./cmd/internal/fuzzy_tester
# Release image
FROM debian:bullseye
@ -27,4 +26,4 @@ RUN apt-get install -y libasound2-dev
EXPOSE 8273
EXPOSE 8272
CMD ["/app/end_of_eden"]
CMD ["/app/end_of_eden"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

@ -19,6 +19,9 @@ GAME_STATE_EVENT = ""
--- Represents the fight game state.
GAME_STATE_FIGHT = ""
--- Represents the game over game state.
GAME_STATE_GAMEOVER = ""
--- Represents the merchant game state.
GAME_STATE_MERCHANT = ""
@ -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
@ -111,6 +124,10 @@ function play_music(sound) end
-- Game State
-- #####################################
--- get the number of action points per round.
---@return number
function get_action_points_per_round() end
--- Gets the ids of all the encountered events in the order of occurrence.
---@return string[]
function get_event_history() end
@ -142,6 +159,10 @@ function had_events(event_ids) end
---@return boolean
function had_events_any(eventIds) end
--- set the number of action points per round.
---@param points number
function set_action_points_per_round(points) end
--- Set event by id.
---@param event_id type_id
function set_event(event_id) end

View File

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

View File

@ -7,7 +7,7 @@ register_enemy("CLEAN_BOT", {
color = "#32a891",
initial_hp = 13,
max_hp = 13,
gold = 15,
gold = 30,
intend = function(ctx)
local self = get_actor(ctx.guid)
if self.hp <= 4 then

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

View File

@ -1,14 +1,14 @@
register_enemy("CYBER_SPIDER", {
name = "CYBER Spider",
description = "It waits for its prey to come closer",
look = [[/\o^o/\]],
look = [[/\o^o/\]],
color = "#ff4d6d",
initial_hp = 8,
max_hp = 8,
gold = 15,
gold = 40,
intend = function(ctx)
if ctx.round > 0 and ctx.round % 3 == 0 then
return "Deal " .. highlight(5) .. " damage"
return "Deal " .. highlight(5) .. " damage"
end
return "Wait..."

View File

@ -0,0 +1,51 @@
register_enemy("LASER_DRONE", {
name = "Laser Drone",
description = "A drone equipped with a powerful laser cannon.",
look = [[|o|]],
color = "#ff0000",
initial_hp = 7,
max_hp = 7,
gold = 40,
intend = function(ctx)
if ctx.round % 3 == 0 then
return "Charge up for a powerful laser attack"
elseif ctx.round % 3 == 1 then
return "Deal " .. highlight(2) .. " damage"
else
return "Deal " .. highlight(5) .. " damage"
end
end,
callbacks = {
on_turn = function(ctx)
if ctx.round % 3 == 0 then
give_status_effect("CHARGING", ctx.guid)
elseif ctx.round % 3 == 1 then
deal_damage(ctx.guid, PLAYER_ID, 2)
else
deal_damage(ctx.guid, PLAYER_ID, 5)
end
return nil
end
}
})
register_status_effect("CHARGING", {
name = "Charging",
description = "The drone is charging up for a powerful attack.",
look = "CHRG",
foreground = "#ff0000",
state = function(ctx)
return "Charging up for a powerful attack."
end,
can_stack = false,
decay = DECAY_NONE,
rounds = 1,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage
end
return nil
end
}
})

View File

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

View File

@ -0,0 +1,28 @@
register_enemy("PLASMA_GOLEM", {
name = "Plasma Golem",
description = "A golem made of pure plasma energy.",
look = [[
/\
/ \
/_xx_\]],
color = "#ff69b4",
initial_hp = 12,
max_hp = 12,
gold = 80,
intend = function(ctx)
if ctx.round % 2 == 0 then
return "Charge up for a powerful plasma attack"
else
return "Deal " .. highlight(8) .. " damage"
end
end,
callbacks = {
on_turn = function(ctx)
if ctx.round % 2 == 0 then
else
deal_damage(ctx.guid, PLAYER_ID, 8)
end
return nil
end
}
})

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

View File

@ -5,7 +5,7 @@ register_enemy("RUST_MITE", {
color = "#e6e65a",
initial_hp = 12,
max_hp = 12,
gold = 10,
gold = 35,
intend = function(ctx)
if ctx.round % 4 == 0 then
return "Load battery"

View File

@ -0,0 +1,19 @@
register_card("DEBUG_INSTA_KILL", {
name = l("cards.DEBUG_INSTA_KILL.name", "DEBUG Insta Kill"),
description = l("cards.DEBUG_INSTA_KILL.description", "..."),
state = function(ctx)
return "Kill"
end,
tags = {},
max_level = 1,
color = COLOR_GRAY,
need_target = true,
point_cost = 0,
price = -1,
callbacks = {
on_cast = function(ctx)
deal_damage_card(ctx.caster, ctx.guid, ctx.target, 10000)
return nil
end
},
})

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

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
register_artifact("COMBAT_GLOVES", {
name = "Combat Gloves",
description = "Whenever you play a " .. highlight("Meele (M)") .. " card, deal " .. highlight("1 additional damage"),
description = "Whenever you play a " .. highlight("Melee (M)") .. " card, deal " .. highlight("1 additional damage"),
tags = { "_ACT_0" },
price = 100,
order = 0,

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
}
})

View File

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

View File

@ -7,15 +7,44 @@ It seems to be eating the metal from the walls. It looks at you and after a few
**It seems to be hostile!**
]],
tags = {"_ACT_0_FIGHT"},
tags = { "_ACT_0_FIGHT" },
choices = {
{
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
}
@ -32,15 +61,18 @@ It looks at you and says "Corpse. Clean. Engage.".
**You're not sure what it means, but it doesn't seem to be friendly!**
]],
tags = {"_ACT_0_FIGHT"},
tags = { "_ACT_0_FIGHT" },
choices = {
{
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
}
@ -54,15 +86,93 @@ register_event("CYBER_SPIDER", {
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.
]],
tags = {"_ACT_0_FIGHT"},
tags = { "_ACT_0_FIGHT" },
choices = {
{
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
}
}
})
register_event("LASER_DRONE", {
name = "A menacing drone appears...",
description =
[[!!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!**
]],
tags = { "_ACT_0_FIGHT" },
choices = {
{
description = "Fight!",
callback = function()
add_actor_by_enemy("LASER_DRONE")
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
}
}
})
register_event("PLASMA_GOLEM", {
name = "A glowing figure emerges...",
description =
[[!!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!**
]],
tags = { "_ACT_0_FIGHT" },
choices = {
{
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
}

View File

@ -1,18 +1,50 @@
register_event("MERCHANT", {
name = "A strange figure",
description =
[[!!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...]],
tags = { "_ACT_0" },
choices = {
{
description = "Trade",
callback = function()
return GAME_STATE_MERCHANT
end
}, {
description = "Pass",
callback = function()
return GAME_STATE_RANDOM
end
}
},
on_end = function(ctx)
return nil
end
})
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"),
description = "Random Artifact " ..
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
@ -29,12 +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"),
description = "Random Artifact " ..
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)
@ -58,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 = {
@ -80,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",
@ -132,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" },
@ -155,20 +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"),
description = "50% " ..
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)
@ -178,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
@ -194,19 +209,16 @@ 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"),
description = "Upgrade a card " ..
highlight_success("Upgrade a card") .. " " .. highlight_warn("Take 2 damage"),
callback = function()
if math.random() < 0.5 then
deal_damage(PLAYER_ID, PLAYER_ID, 5, true)
return nil
end
local cards = fun.iter(get_cards(PLAYER_ID))
:filter(function(guid)
local type = get_card(guid)
@ -220,8 +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, 2, true)
return nil
end
@ -233,4 +246,4 @@ You find a old automatic workstation. You are able to get it working again. You
end
}
}
})
})

View File

@ -14,9 +14,8 @@ As you struggle to gather your bearings, you notice a blinking panel on the wall
choices = {
{
description = "Try to find a weapon. " ..
highlight('Find meele weapon') .. " " .. highlight_warn("Take 4 damage"),
highlight('Find melee weapon') .. " " .. highlight_warn("Take 4 damage"),
callback = function()
deal_damage(PLAYER_ID, PLAYER_ID, 4, true)
give_artifact(
choose_weighted_by_price(find_artifacts_by_tags({ "HND", "M" })), PLAYER_ID
)
@ -38,9 +37,16 @@ As you struggle to gather your bearings, you notice a blinking panel on the wall
on_enter = function()
play_music("energetic_orthogonal_expansions")
end,
on_end = function()
actor_set_max_hp(PLAYER_ID, 10)
actor_set_hp(PLAYER_ID, 10)
on_end = function(ctx)
local player_hp = 12
actor_set_max_hp(PLAYER_ID, player_hp)
if ctx.choice == 1 then
actor_set_hp(PLAYER_ID, player_hp - 4)
else
actor_set_hp(PLAYER_ID, player_hp)
end
give_card("BLOCK", PLAYER_ID)
give_card("BLOCK", PLAYER_ID)

View File

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

View File

@ -1,54 +1,48 @@
register_story_teller("_ACT_0", {
active = function()
if #get_event_history() <= 6 then
return 1
end
return 0
-- if #get_event_history() <= 6 then
-- return 1
-- end
--
-- Keep active for now
return 1
end,
decide = function()
local history = get_event_history()
local possible = { }
local possible = {}
-- every 3 events, play a non-combat event
if #get_event_history() % 2 == 0 then
local events = #history - 1;
if events ~= 0 and events % 2 == 0 then
possible = find_events_by_tags({ "_ACT_0" })
else
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 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" })
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
})

View File

@ -0,0 +1,225 @@
delete_base_game("event")
register_enemy("TUTORIAL_DUMMY_1", {
name = l("enemies.TUTORIAL_DUMMY_1.name", "Dummy"),
description = l("enemies.TUTORIAL_DUMMY_1.description", "A dummy enemy for the tutorial"),
look = "D",
color = "#e6e65a",
initial_hp = 4,
max_hp = 4,
gold = 0,
intend = function(ctx)
return "Deal " .. highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, 1)) .. " damage"
end,
callbacks = {
on_turn = function(ctx)
deal_damage(ctx.guid, PLAYER_ID, 1)
return nil
end
}
})
register_enemy("TUTORIAL_DUMMY_2", {
name = l("enemies.TUTORIAL_DUMMY_2.name", "Dummy"),
description = l("enemies.TUTORIAL_DUMMY_2.description", "A dummy enemy for the tutorial"),
look = "D",
color = "#e6e65a",
initial_hp = 3,
max_hp = 3,
gold = 0,
intend = function(ctx)
return "Apply " .. highlight("Weakness")
end,
callbacks = {
on_turn = function(ctx)
give_status_effect("WEAKNESS", PLAYER_ID)
return nil
end
}
})
register_status_effect("WEAKNESS", {
name = "Weakness",
description = "Decreases damage dealt by 1",
look = "W",
foreground = COLOR_RED,
state = function(ctx)
return "Deals " .. highlight(1) .. " less damage"
end,
rounds = 2,
decay = DECAY_ONE,
can_stack = false,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage - 1
end
return ctx.damage
end
}
})
register_card("MELEE_HIT", {
name = l("cards.MELEE_HIT.name", "Melee Hit"),
description = l("cards.MELEE_HIT.description", "Use your bare hands to deal 2 (+1 for each upgrade) damage."),
state = function(ctx)
return string.format(l("cards.MELEE_HIT.state", "Use your bare hands to deal %s damage."),
highlight(2 + ctx.level))
end,
tags = { "ATK", "M", "HND" },
max_level = 1,
color = COLOR_GRAY,
need_target = true,
point_cost = 1,
price = -1,
callbacks = {
on_cast = function(ctx)
deal_damage_card(ctx.caster, ctx.guid, ctx.target, 2 + ctx.level)
return nil
end
},
test = function()
return assert_cast_damage("MELEE_HIT", 2)
end
})
register_event("START", {
name = "Welcome!",
description = [[Welcome to *End of Eden*!
This game is a roguelite deckbuilder where you explore a post-apocalyptic world, collect cards and artifacts and fight enemies. **Try to stay alive as long as possible!**
**Lets start with some keyboard shortcuts**
- *ESC* - Open the menu where you can see your cards, artifacts, ... or abort choices
- *SPACE* - End your turn
- *ARROW LEFT / ARROW RIGHT* - Select card or enemy to hit
- *ENTER* - Confirm your choice
- *X* - If you hover over a enemy and press X, you can see more infos about a enemy
- *S* - Open player status
You can also use the **mouse** to select cards, enemies and click buttons.
**Cards**
You have a deck of cards that you can use to attack, defend or apply status effects. You can see your cards in the bottom of the screen. All cards cost action points that reset on each turn. Use them wisely!
**Combat**
If you press Continue you will fight a dummy enemy. See if you are able to kill it!
]],
choices = {
{
description = "Continue",
callback = function()
return nil
end
},
},
on_enter = function()
end,
on_end = function(ctx)
add_actor_by_enemy("TUTORIAL_DUMMY_1")
give_player_gold(500)
give_card("MELEE_HIT", PLAYER_ID)
set_event("TUTORIAL_1")
return GAME_STATE_FIGHT
end
})
register_event("TUTORIAL_1", {
name = "Status Effects",
description = [[*Awesome! You have defeated the dummy enemy!*
Now you will face a enemy that will apply a *status effect* on you. Status effects can be positive or negative and can be applied by cards, enemies or other sources. Status effects that are applied to you are shown on the bottom of the screen. You can click on them or press *S* to see more information.
If you press Continue you will fight some dummy enemies. See if you are able to kill them!
]],
choices = {
{
description = "Continue",
callback = function()
return nil
end
},
},
on_enter = function()
end,
on_end = function(ctx)
add_actor_by_enemy("TUTORIAL_DUMMY_1")
add_actor_by_enemy("TUTORIAL_DUMMY_2")
set_event("TUTORIAL_2")
give_card("BLOCK", PLAYER_ID)
return GAME_STATE_FIGHT
end
})
register_event("TUTORIAL_2", {
name = "The Merchant",
description = [[*Awesome! You have defeated the dummy enemies!*
Every now and then you will encounter a merchant. The merchant will offer you cards and artifacts that you can buy with gold. You can also remove or upgrade cards. Gold is earned by defeating enemies.
If you press Continue you will meet the merchant. *Try to buy or upgrade something!*
]],
choices = {
{
description = "Continue",
callback = function()
return nil
end
},
},
on_enter = function()
end,
on_end = function(ctx)
set_event("TUTORIAL_3")
return GAME_STATE_MERCHANT
end
})
register_event("TUTORIAL_3", {
name = "Finished!",
description = [[*Awesome! You have bought some stuff!*
This is the end of the tutorial. You can now continue to explore the world and fight enemies. Good luck!
]],
choices = {
{
description = "Continue",
callback = function()
return nil
end
},
},
on_enter = function()
end,
on_end = function(ctx)
add_actor_by_enemy("TUTORIAL_DUMMY_1")
add_actor_by_enemy("TUTORIAL_DUMMY_2")
add_actor_by_enemy("TUTORIAL_DUMMY_1")
set_event("TUTORIAL_4")
return GAME_STATE_FIGHT
end
})
register_event("TUTORIAL_4", {
name = "Be gone!",
description = [[*It is time to go...*]],
choices = {
{
description = "Continue",
callback = function()
return nil
end
},
},
on_enter = function()
end,
on_end = function(ctx)
deal_damage("TUTORIAL", PLAYER_ID, 1000, true)
return GAME_STATE_GAMEOVER
end
})

View File

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

View File

@ -78,6 +78,8 @@ func initSystems(hasAudio bool) {
}
func main() {
os.Setenv("EOE_IMG_TRUECOLOR", "1")
testArgs := testargs.New()
flag.Parse()

View File

@ -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 | 8 |
| Enemies | 4 |
| Events | 16 |
| 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 **Meele (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
```
@ -86,42 +90,53 @@ title Card Types
| ID | Name | Description | Look | Foreground | Can Stack | Decay | Rounds | Used Callbacks | Test Present |
|------------------------|--------------------|---------------------------------------------------|------|------------|--------------------|-----------|--------|------------------|--------------------|
| ``CHARGED`` | Charged | Attacks will deal more damage per stack. | CHRG | #207BE7 | :heavy_check_mark: | DecayNone | 0 | ``OnDamageCalc`` | :no_entry_sign: |
| ``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: |
| ``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: |
| ``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 meele 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: |

View File

@ -53,6 +53,12 @@ Represents the fight game state.
</details>
<details> <summary><b><code>GAME_STATE_GAMEOVER</code></b> </summary> <br/>
Represents the game over game state.
</details>
<details> <summary><b><code>GAME_STATE_MERCHANT</code></b> </summary> <br/>
Represents the merchant game state.
@ -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.
@ -288,6 +318,18 @@ Functions that modify the general game state.
None
### Functions
<details> <summary><b><code>get_action_points_per_round</code></b> </summary> <br/>
get the number of action points per round.
**Signature:**
```
get_action_points_per_round() -> number
```
</details>
<details> <summary><b><code>get_event_history</code></b> </summary> <br/>
Gets the ids of all the encountered events in the order of occurrence.
@ -372,6 +414,18 @@ had_events_any(eventIds : string[]) -> boolean
</details>
<details> <summary><b><code>set_action_points_per_round</code></b> </summary> <br/>
set the number of action points per round.
**Signature:**
```
set_action_points_per_round(points : number) -> None
```
</details>
<details> <summary><b><code>set_event</code></b> </summary> <br/>
Set event by id.

View File

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

View File

@ -11,6 +11,7 @@ type Enemy struct {
Description string
InitialHP int
MaxHP int
Gold int
Look string
Color string
Intend luhelp.OwnedCallback

View File

@ -1,14 +1,15 @@
package game
import (
"path/filepath"
"strings"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
luhelp2 "github.com/BigJk/end_of_eden/internal/lua/luhelp"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/gen/faces"
"github.com/BigJk/end_of_eden/system/localization"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/samber/lo"
@ -69,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.")
@ -87,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")))
@ -282,6 +299,18 @@ fun = require "fun"
return 1
}))
d.Function("get_action_points_per_round", "get the number of action points per round.", "number")
l.SetGlobal("get_action_points_per_round", l.NewFunction(func(state *lua.LState) int {
state.Push(lua.LNumber(session.GetPointsPerRound()))
return 1
}))
d.Function("set_action_points_per_round", "set the number of action points per round.", "", "points : number")
l.SetGlobal("set_action_points_per_round", l.NewFunction(func(state *lua.LState) int {
session.SetPointsPerRound(int(state.ToNumber(1)))
return 0
}))
// Actor Operations
d.Category("Actor Operations", "Functions that modify or access the actors. Actors are either the player or enemies.", 6)

View File

@ -1,6 +1,9 @@
package game
import "encoding/gob"
import (
"encoding/gob"
"math/rand/v2"
)
func init() {
gob.Register(SavedState{})
@ -10,11 +13,14 @@ 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
CurrentEvent string
CurrentFight FightState
PointsPerRound int
Merchant MerchantState
EventHistory []string
StateCheckpoints []StateCheckpoint

View File

@ -6,6 +6,15 @@ import (
"encoding/gob"
"errors"
"fmt"
"io"
"log"
"math/rand/v2"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
"github.com/BigJk/end_of_eden/system/gen"
@ -15,20 +24,12 @@ import (
"github.com/samber/lo"
lua "github.com/yuin/gopher-lua"
"golang.org/x/exp/slices"
"io"
"log"
"math/rand"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
)
func init() {
@ -54,8 +55,8 @@ const (
// DefaultRemoveCost is the default cost for removing a card.
DefaultRemoveCost = 50
// PointsPerRound is the amount of points the player gets per round.
PointsPerRound = 3
// DefaultPointsPerRound is the default amount of points the player gets per round.
DefaultPointsPerRound = 3
// DrawSize is the amount of cards the player draws per round.
DrawSize = 3
@ -101,19 +102,23 @@ type Session struct {
log *log.Logger
luaState *lua.LState
luaDocs *ludoc.Docs
seed uint64
randSrc *rand.PCG
rand *rand.Rand
resources *ResourcesManager
state GameState
actors map[string]Actor
instances map[string]any
stagesCleared int
currentEvent string
currentFight FightState
merchant MerchantState
eventHistory []string
randomHistory []string
ctxData map[string]any
hooks map[Hook][]func()
state GameState
actors map[string]Actor
instances map[string]any
stagesCleared int
currentEvent string
currentFight FightState
pointsPerRound int
merchant MerchantState
eventHistory []string
randomHistory []string
ctxData map[string]any
hooks map[Hook][]func()
loadedMods []string
stateCheckpoints []StateCheckpoint
@ -126,14 +131,20 @@ 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),
},
instances: map[string]any{},
ctxData: map[string]any{},
pointsPerRound: DefaultPointsPerRound,
instances: map[string]any{},
ctxData: map[string]any{},
hooks: map[Hook][]func(){
HookNextFightEnd: {},
},
@ -146,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 {
@ -154,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
})
@ -198,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) {
@ -238,17 +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,
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,
}
}
@ -257,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()
})
@ -264,6 +317,7 @@ func (s *Session) LoadSavedState(save SavedState) {
s.stagesCleared = save.StagesCleared
s.currentEvent = save.CurrentEvent
s.currentFight = save.CurrentFight
s.pointsPerRound = save.PointsPerRound
s.merchant = save.Merchant
s.eventHistory = save.EventHistory
s.stateCheckpoints = lo.Map(save.StateCheckpoints, func(item StateCheckpoint, index int) StateCheckpoint {
@ -336,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)
@ -422,6 +488,11 @@ func (s *Session) GetFormerState(index int) *Session {
// GetGameState returns the current game state.
func (s *Session) GetGameState() GameState {
player := s.GetActor(PlayerActorID)
if player.HP == 0 {
return GameStateGameOver
}
return s.state
}
@ -446,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)
}
}
}
@ -465,7 +544,7 @@ func (s *Session) GetEvent() *Event {
// CleanUpFight resets the fight state.
func (s *Session) CleanUpFight() {
s.currentFight.CurrentPoints = PointsPerRound
s.currentFight.CurrentPoints = s.pointsPerRound
s.currentFight.Deck = lo.Shuffle(s.GetPlayer().Cards.ToSlice())
s.currentFight.Hand = []string{}
s.currentFight.Exhausted = []string{}
@ -487,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)
@ -508,6 +589,16 @@ func (s *Session) GetStagesCleared() int {
return s.stagesCleared
}
// GetPointsPerRound returns the amount of action points the player gets each round.
func (s *Session) GetPointsPerRound() int {
return s.pointsPerRound
}
// SetPointsPerRound sets the amount of action points the player gets each round.
func (s *Session) SetPointsPerRound(points int) {
s.pointsPerRound = points
}
// FinishPlayerTurn signals that the player is done with its turn. All enemies act now, status effects are
// evaluated, if the fight is over is checked and if not this will advance to the next round and draw cards
// for the player.
@ -573,7 +664,7 @@ func (s *Session) FinishPlayerTurn() {
}
// Advance to new Round
s.currentFight.CurrentPoints = PointsPerRound
s.currentFight.CurrentPoints = s.pointsPerRound
s.currentFight.Round += 1
s.currentFight.Used = append(s.currentFight.Used, s.currentFight.Hand...)
s.currentFight.Hand = []string{}
@ -637,8 +728,6 @@ func (s *Session) FinishFight() bool {
// If an event is already set we switch to it
if len(s.currentEvent) > 0 {
s.SetGameState(GameStateEvent)
} else if s.stagesCleared%10 == 0 {
s.SetEvent("MERCHANT")
} else {
s.SetGameState(GameStateRandom)
}
@ -765,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)
}
@ -777,7 +871,7 @@ func (s *Session) GetMerchant() MerchantState {
// GetMerchantGoldMax returns what the max cost of a artifact or card is that the merchant might offer.
func (s *Session) GetMerchantGoldMax() int {
return 150 + s.stagesCleared*30
return 180 + s.stagesCleared*30
}
func (s *Session) PushRandomHistory(id string) {
@ -790,7 +884,7 @@ func (s *Session) PushRandomHistory(id string) {
// GetRandomArtifact returns the type id of a random artifact with a price lower than the given value.
func (s *Session) GetRandomArtifact(maxGold int) string {
possible := lo.Filter(lo.Values(s.resources.Artifacts), func(item *Artifact, index int) bool {
return item.Price >= 0 && item.Price < maxGold
return item.Price > 0 && item.Price < maxGold
})
possibleNoDupes := lo.Filter(possible, func(item *Artifact, index int) bool {
@ -814,7 +908,7 @@ func (s *Session) GetRandomArtifact(maxGold int) string {
// GetRandomCard returns the type id of a random card with a price lower than the given value.
func (s *Session) GetRandomCard(maxGold int) string {
possible := lo.Filter(lo.Values(s.resources.Cards), func(item *Card, index int) bool {
return item.Price >= 0 && item.Price < maxGold
return item.Price > 0 && item.Price < maxGold
})
possibleNoDupes := lo.Filter(possible, func(item *Card, index int) bool {
@ -951,11 +1045,19 @@ func (s *Session) ActiveTeller() *StoryTeller {
return nil
}
slices.SortFunc(teller, func(a, b *StoryTeller) bool {
slices.SortFunc(teller, func(a, b *StoryTeller) int {
aOrder, _ := a.Active(CreateContext("type_id", a.ID))
bOrder, _ := b.Active(CreateContext("type_id", b.ID))
return aOrder.(float64) > bOrder.(float64)
if aOrder.(float64) > bOrder.(float64) {
return 1
}
if aOrder.(float64) < bOrder.(float64) {
return -1
}
return 0
})
return teller[0]
@ -1860,6 +1962,7 @@ func (s *Session) AddActorFromEnemy(id string) string {
actor.Description = base.Description
actor.HP = base.InitialHP
actor.MaxHP = base.MaxHP
actor.Gold = base.Gold
// Its important we add the actor before any callbacks so that it's instance is available
// to add cards etc. to!

43
go.mod
View File

@ -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
@ -14,15 +12,17 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161
github.com/BigJk/crt v0.0.14
github.com/BigJk/imeji v0.0.3
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/alexeyco/simpletable v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/lipgloss v0.7.1
github.com/charmbracelet/log v0.1.2
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
github.com/charmbracelet/wish v1.1.0
github.com/charmbracelet/lipgloss v0.10.0
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917
github.com/charmbracelet/wish v1.4.0
github.com/faiface/beep v1.1.0
github.com/fatih/structs v1.1.0
github.com/gobeam/stringy v0.0.6
@ -40,10 +40,10 @@ require (
github.com/samber/lo v1.38.1
github.com/sanity-io/litter v1.5.5
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
github.com/yuin/gopher-lua v1.1.0
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
golang.org/x/sys v0.13.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/sys v0.18.0
gopkg.in/yaml.v3 v3.0.1
oss.terrastruct.com/d2 v0.4.1
)
@ -52,18 +52,18 @@ require (
cdr.dev/slog v1.4.2-0.20221206192828-e4803b10ae17 // indirect
cloud.google.com/go/logging v1.7.0 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/chroma/v2 v2.5.0 // indirect
github.com/alexeyco/simpletable v1.0.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/anthonynsimon/bild v0.13.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/charmbracelet/keygen v0.3.0 // indirect
github.com/charmbracelet/keygen v0.5.0 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 // indirect
@ -89,13 +89,12 @@ require (
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mazznoer/csscolorparser v0.1.3 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
@ -107,15 +106,15 @@ require (
github.com/yuin/goldmark v1.5.3 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/image v0.12.0 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gonum.org/v1/plot v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect

79
go.sum
View File

@ -92,8 +92,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
@ -101,17 +99,21 @@ github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM2
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/charmbracelet/log v0.1.2 h1:xmKMxo0T/lcftgggQOhUkS32exku2/ID55FGYbr4nKQ=
github.com/charmbracelet/log v0.1.2/go.mod h1:86XdIdmrubqtL/6u0z+jGFol1bQejBGG/qPSTwGZuQQ=
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE635jWwT1dISgfjbpUcEma+fbPKSMCU=
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
github.com/charmbracelet/wish v1.1.0 h1:0ArX9SOG70saqd23NYjoS56oLPVNgqcQegkz1Lw+4zY=
github.com/charmbracelet/wish v1.1.0/go.mod h1:yHbm0hs/qX4lFE7nrhAcXjFYc8bxMIfSqJOfOYfwyYo=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 h1:NZKjJ7d/pzk/AfcJYEzmF8M48JlIrrY00RR5JdDc3io=
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U=
github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc=
github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -126,6 +128,8 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -226,8 +230,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -315,7 +319,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@ -326,7 +329,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -338,7 +340,6 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyex
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@ -373,8 +374,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -413,9 +414,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@ -458,9 +458,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -471,8 +470,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg=
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@ -541,14 +540,13 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -570,8 +568,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -622,22 +620,20 @@ golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -649,13 +645,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View File

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

3
run_tests.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.parent, nil
}
case tea.MouseMsg:
if msg.Type == tea.MouseLeft && m.zones.Get("back").InBounds(msg) {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) && m.zones.Get("back").InBounds(msg) {
audio.Play("btn_menu")
return m.parent, nil

View File

@ -84,11 +84,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.MouseMsg:
m.LastMouse = msg
if msg.Type == tea.MouseLeft || msg.Type == tea.MouseMotion {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) || msg.Action == tea.MouseActionMotion {
if m.session.GetEvent() != nil {
for i := 0; i < len(m.session.GetEvent().Choices); i++ {
if choiceZone := m.zones.Get(fmt.Sprintf("%s%d", ZoneChoice, i)); choiceZone.InBounds(msg) {
if msg.Type == tea.MouseLeft && m.selectedChoice == i {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) && m.selectedChoice == i {
audio.Play("btn_menu")
m = m.tryFinishEvent()
break

View File

@ -85,7 +85,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.MouseMsg:
m.lastMouse = msg
if msg.Type == tea.MouseLeft && m.zones.Get(ZoneToMenu).InBounds(msg) {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) && m.zones.Get(ZoneToMenu).InBounds(msg) {
m.session.Close()
return nil, nil
}

View File

@ -75,7 +75,7 @@ func (m DamageAnimationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil, nil
}
case tea.MouseMsg:
if m.elapsed > 0.2 && msg.Type == tea.MouseLeft {
if m.elapsed > 0.2 && (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) {
return nil, nil
}
case DamageAnimationFrame:

View File

@ -55,7 +55,7 @@ func (m DeathAnimationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil, nil
}
case tea.MouseMsg:
if m.progress > 0.1 && msg.Type == tea.MouseLeft {
if m.progress > 0.1 && (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) {
return nil, nil
}
case DeathAnimationFrame:

View File

@ -56,7 +56,7 @@ func (m EndTurnAnimationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil, nil
}
case tea.MouseMsg:
if m.elapsed > 0.1 && msg.Type == tea.MouseLeft {
if m.elapsed > 0.1 && (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) {
return nil, nil
}
case EndTurnAnimationFrame:

View File

@ -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
}
}
//
@ -159,7 +163,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.LastMouse = msg
if len(m.animations) == 0 {
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
cmds = append(cmds, root.TooltipClear())
switch m.Session.GetGameState() {
@ -172,13 +176,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if msg.Type == tea.MouseLeft || msg.Type == tea.MouseMotion {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) || msg.Action == tea.MouseActionMotion {
switch m.Session.GetGameState() {
case game.GameStateFight:
if m.inOpponentSelection {
for i := 0; i < m.Session.GetOpponentCount(game.PlayerActorID); i++ {
if m.zones.Get(fmt.Sprintf("%s%d", ZoneEnemy, i)).InBounds(msg) {
if msg.Type == tea.MouseLeft && m.selectedOpponent == i {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
m.selectedOpponent = i
m = m.tryCast()
}
}
@ -188,7 +193,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
for i := 0; i < len(m.Session.GetFight().Hand); i++ {
if m.zones.Get(fmt.Sprintf("%s%d", ZoneCard, i)).InBounds(msg) {
onCard = true
if msg.Type == tea.MouseLeft && m.selectedCard == i {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) && m.selectedCard == i {
m = m.tryCast()
} else {
m.selectedCard = i
@ -196,11 +201,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if !onCard && msg.Type == tea.MouseMotion {
if !onCard && msg.Action == tea.MouseActionMotion {
m.selectedCard = -1
}
if !m.inOpponentSelection && msg.Type == tea.MouseLeft {
if !m.inOpponentSelection && msg.Button == tea.MouseButtonLeft {
for i := 0; i < m.Session.GetOpponentCount(game.PlayerActorID); i++ {
if m.zones.Get(fmt.Sprintf("%s%d", ZoneEnemy, i)).InBounds(msg) {
m.selectedOpponent = i
@ -314,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())
@ -427,7 +434,7 @@ func (m Model) tryCast() Model {
before := m.Session.MarkState()
hand := m.Session.GetFight().Hand
if len(hand) > 0 && m.selectedCard < len(hand) {
if len(hand) > 0 && m.selectedCard < len(hand) && m.selectedCard >= 0 {
card, _ := m.Session.GetCard(hand[m.selectedCard])
if card.NeedTarget {
if m.inOpponentSelection {
@ -514,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(`
··
··
·`))),
))
}

View File

@ -135,7 +135,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil, nil
}
case tea.MouseMsg:
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
if m.zones.Get(ZoneCopy).InBounds(msg) {
if os.Getenv("NO_CLIPBOARD") == "1" {
return m, nil

View File

@ -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
@ -106,7 +119,7 @@ func (m ChoicesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
audio.Play("interface_move", -1.5)
}
case tea.MouseMsg:
if msg.Type == tea.MouseLeft || msg.Type == tea.MouseMotion {
if (msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft) || msg.Action == tea.MouseActionMotion {
for i := range m.choices {
if m.zones.Get("choice_"+string(m.choices[i].(choiceItem).key)).InBounds(msg) || m.zones.Get("choice_desc_"+string(m.choices[i].(choiceItem).key)).InBounds(msg) {
if m.list.Index() != i {
@ -114,7 +127,7 @@ func (m ChoicesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.list.Select(i)
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
m.selected = m.choices[i].(choiceItem).key
}
break

View File

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

View File

@ -71,7 +71,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.state {
case StateMain:
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
if m.zones.Get(ZoneBuyItem).InBounds(msg) {
audio.Play("btn_menu")
m = m.merchantBuy()
@ -91,7 +91,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case StateUpgrade:
fallthrough
case StateRemove:
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
if m.zones.Get(ZoneBuyItem).InBounds(msg) {
audio.Play("btn_menu")
if m.state == StateUpgrade {

View File

@ -199,7 +199,7 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
audio.Play("interface_move", -1.5)
}
case tea.MouseMsg:
if msg.Type == tea.MouseLeft {
if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft {
for i := range m.choices {
if m.zones.Get(ZoneChoices + string(m.choices[i].(choiceItem).key)).InBounds(msg) {
m.list.Select(i)

View File

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