mirror of
https://github.com/BigJk/end_of_eden.git
synced 2026-02-06 10:48:09 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdfd8e319f | ||
|
|
f2bd471d3e | ||
|
|
7e7afb4dce | ||
|
|
a3ae4898d8 | ||
|
|
747101d9ca | ||
|
|
4bce308ef5 | ||
|
|
efbc74077a | ||
|
|
1604f876a8 | ||
|
|
1c4ecf2a26 | ||
|
|
54179ae189 | ||
|
|
b03a6520cf | ||
|
|
43d44d5751 |
45
.github/workflows/release.yaml
vendored
45
.github/workflows/release.yaml
vendored
@ -43,6 +43,7 @@ jobs:
|
||||
ldflags: "-X 'github.com/BigJk/end_of_eden/internal/git.Tag=${{ github.ref_name }}' -X 'github.com/BigJk/end_of_eden/internal/git.CommitHash=${{ github.sha }}'"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
compress_assets: zip
|
||||
release-linux-gl-amd64:
|
||||
permissions: write-all
|
||||
name: release linux/amd64 gl
|
||||
@ -63,6 +64,7 @@ jobs:
|
||||
ldflags: "-X 'github.com/BigJk/end_of_eden/internal/git.Tag=${{ github.ref_name }}' -X 'github.com/BigJk/end_of_eden/internal/git.CommitHash=${{ github.sha }}'"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
compress_assets: zip
|
||||
release-windows-term-amd64:
|
||||
permissions: write-all
|
||||
name: release windows/amd64 term
|
||||
@ -196,3 +198,46 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip
|
||||
release-itch:
|
||||
permissions: write-all
|
||||
name: release itch
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
release-windows-term-amd64,
|
||||
release-windows-gl-amd64,
|
||||
release-macos-term-amd64,
|
||||
release-macos-gl-amd64,
|
||||
release-macos-term-arm64,
|
||||
release-macos-gl-arm64,
|
||||
release-linux-term-amd64,
|
||||
release-linux-gl-amd64,
|
||||
release-wasm,
|
||||
]
|
||||
steps:
|
||||
- name: Download Release
|
||||
uses: robinraju/release-downloader@v1
|
||||
with:
|
||||
repository: "BigJk/end_of_eden"
|
||||
tag: ${{ github.ref_name }}
|
||||
fileName: "*.zip"
|
||||
- name: Install Butler
|
||||
run: |
|
||||
curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||
unzip butler.zip
|
||||
chmod +x butler
|
||||
rm butler.zip
|
||||
./butler -V
|
||||
- name: Push to Itch.io
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
|
||||
run: |
|
||||
./butler push end_of_eden_term-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-term-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_gl-${{ github.ref_name }}-windows-amd64.zip BigJk/end-of-eden:windows-gl-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_term-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-term-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-amd64.zip BigJk/end-of-eden:macosx-gl-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_term-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-term-arm64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_gl-${{ github.ref_name }}-macos-arm64.zip BigJk/end-of-eden:macosx-gl-arm64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_term-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-term-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push end_of_eden_gl-${{ github.ref_name }}-linux-amd64.zip BigJk/end-of-eden:linux-gl-amd64 --userversion ${{ github.ref_name }}
|
||||
./butler push eoe.wasm-${{ github.ref_name }}-js-wasm.zip BigJk/end-of-eden:web --userversion ${{ github.ref_name }}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.21 AS build-stage
|
||||
FROM golang:1.23 AS build-stage
|
||||
|
||||
WORKDIR /build
|
||||
RUN mkdir /app
|
||||
@ -26,4 +26,4 @@ RUN apt-get install -y libasound2-dev
|
||||
EXPOSE 8273
|
||||
EXPOSE 8272
|
||||
|
||||
CMD ["/app/end_of_eden"]
|
||||
CMD ["/app/end_of_eden"]
|
||||
|
||||
BIN
assets/images/nanobot_swarm.jpg
Normal file
BIN
assets/images/nanobot_swarm.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@ -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 = ""
|
||||
|
||||
|
||||
21
assets/scripts/enemies/nanobot_swarm.lua
Normal file
21
assets/scripts/enemies/nanobot_swarm.lua
Normal file
@ -0,0 +1,21 @@
|
||||
register_enemy("NANOBOT_SWARM", {
|
||||
name = l("enemies.NANOBOT_SWARM.name", "Nanobot Swarm"),
|
||||
description = l("enemies.NANOBOT_SWARM.description", "A growing swarm of nanobots."),
|
||||
look = ".*#.-",
|
||||
color = "#9b5de5",
|
||||
initial_hp = 15,
|
||||
max_hp = 100,
|
||||
gold = 50,
|
||||
intend = function(ctx)
|
||||
return "Deal " ..
|
||||
highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, ctx.round + 1)) .. " damage. Heal " .. highlight(1) .. " HP."
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
deal_damage(ctx.guid, PLAYER_ID, ctx.round + 1)
|
||||
heal(ctx.guid, ctx.guid, 1)
|
||||
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
@ -16,9 +16,11 @@ register_artifact("ARM_MOUNTED_GUN", {
|
||||
|
||||
register_card("ARM_MOUNTED_GUN", {
|
||||
name = l("cards.ARM_MOUNTED_GUN.name", "Arm Mounted Gun"),
|
||||
description = l("cards.ARM_MOUNTED_GUN.description", "Exhaust. Use your arm mounted gun to deal 15 (+3 for each upgrade) damage."),
|
||||
description = l("cards.ARM_MOUNTED_GUN.description",
|
||||
"Exhaust. Use your arm mounted gun to deal 15 (+3 for each upgrade) damage."),
|
||||
state = function(ctx)
|
||||
return string.format(l("cards.ARM_MOUNTED_GUN.state", "Use your arm mounted gun to deal %s damage."), highlight(7 + ctx.level * 3))
|
||||
return string.format(l("cards.ARM_MOUNTED_GUN.state", "%s. Use your arm mounted gun to deal %s damage."),
|
||||
highlight("Exhaust"), highlight(7 + ctx.level * 3))
|
||||
end,
|
||||
tags = { "ATK", "R", "T", "ARM" },
|
||||
max_level = 1,
|
||||
@ -33,7 +35,7 @@ register_card("ARM_MOUNTED_GUN", {
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function ()
|
||||
test = function()
|
||||
return assert_cast_damage("ARM_MOUNTED_GUN", 7)
|
||||
end
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
local hand_warning = "**Important:** If you already carry a artifact in your hand, you will have to drop it and related cards to pick up the new one."
|
||||
local hand_warning =
|
||||
"**Important:** If you already carry a artifact in your hand, you will have to drop it and related cards to pick up the new one."
|
||||
|
||||
HAND_WEAPONS = {
|
||||
{
|
||||
@ -53,9 +54,11 @@ HAND_WEAPONS_ARTIFACT_IDS = fun.iter(HAND_WEAPONS):map(function(w) return w.id e
|
||||
for _, weapon in pairs(HAND_WEAPONS) do
|
||||
register_card(weapon.id, {
|
||||
name = l("cards." .. weapon.id .. ".name", weapon.name),
|
||||
description = l("cards." .. weapon.id .. ".description", string.format("Use to deal %s (+3 for each upgrade) damage.", weapon.base_damage)),
|
||||
description = l("cards." .. weapon.id .. ".description",
|
||||
string.format("Use to deal %s (+1 for each upgrade) damage.", weapon.base_damage)),
|
||||
state = function(ctx)
|
||||
return string.format(l("cards." .. weapon.id .. ".state", "Use to deal %s damage."), highlight(weapon.base_damage + ctx.level * 3))
|
||||
return string.format(l("cards." .. weapon.id .. ".state", "Use to deal %s damage."),
|
||||
highlight(weapon.base_damage + ctx.level * 1))
|
||||
end,
|
||||
tags = weapon.tags,
|
||||
max_level = 3,
|
||||
@ -121,7 +124,8 @@ for _, weapon in pairs(HAND_WEAPONS) do
|
||||
}
|
||||
})
|
||||
|
||||
add_found_artifact_event(weapon.id, weapon.image, string.format("%s\n\n%s", weapon.description, hand_warning), registered.card[weapon.id].description, weapon.event_tags)
|
||||
add_found_artifact_event(weapon.id, weapon.image, string.format("%s\n\n%s", weapon.description, hand_warning),
|
||||
registered.card[weapon.id].description, weapon.event_tags)
|
||||
end
|
||||
|
||||
---hand_weapon_event returns a random hand weapon event weighted by price.
|
||||
@ -130,4 +134,4 @@ function hand_weapon_event()
|
||||
local ids = fun.iter(HAND_WEAPONS):map(function(w) return w.id end):totable()
|
||||
local prices = fun.iter(HAND_WEAPONS):map(function(w) return 500 - w.price end):totable()
|
||||
return choose_weighted(ids, prices)
|
||||
end
|
||||
end
|
||||
|
||||
25
assets/scripts/equipment/permanents/bio_recycler.lua
Normal file
25
assets/scripts/equipment/permanents/bio_recycler.lua
Normal file
@ -0,0 +1,25 @@
|
||||
register_artifact("BIO_RECYCLER", {
|
||||
name = "Bio Recycler",
|
||||
description = "Heal 1 on kill.",
|
||||
tags = { "_ACT_0" },
|
||||
price = 200,
|
||||
order = 0,
|
||||
callbacks = {
|
||||
on_actor_die = function(ctx)
|
||||
if ctx.source == PLAYER_ID then
|
||||
heal(PLAYER_ID, PLAYER_ID, 1)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function()
|
||||
local dummy = add_actor_by_enemy("DUMMY")
|
||||
deal_damage(dummy, PLAYER_ID, 1, true)
|
||||
local hp_before = get_player().hp
|
||||
deal_damage(PLAYER_ID, dummy, 100)
|
||||
local hp_after = get_player().hp
|
||||
if hp_after - hp_before ~= 1 then
|
||||
return "Expected 1 HP heal, got " .. (hp_after - hp_before)
|
||||
end
|
||||
end
|
||||
})
|
||||
22
assets/scripts/equipment/permanents/gold_scrapper.lua
Normal file
22
assets/scripts/equipment/permanents/gold_scrapper.lua
Normal file
@ -0,0 +1,22 @@
|
||||
register_artifact("GOLD_SCRAPPER", {
|
||||
name = "Gold Scrapper",
|
||||
description = "Gain 15 gold on kill.",
|
||||
tags = { "_ACT_0" },
|
||||
price = 200,
|
||||
order = 0,
|
||||
callbacks = {
|
||||
on_actor_die = function(ctx)
|
||||
if ctx.source == PLAYER_ID then
|
||||
give_player_gold(15)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function()
|
||||
local dummy = add_actor_by_enemy("DUMMY")
|
||||
deal_damage(PLAYER_ID, dummy, 100)
|
||||
if get_player().gold ~= 15 then
|
||||
return "Expected 15 gold, got " .. get_player().gold
|
||||
end
|
||||
end
|
||||
})
|
||||
23
assets/scripts/equipment/permanents/headbut_helmet.lua
Normal file
23
assets/scripts/equipment/permanents/headbut_helmet.lua
Normal file
@ -0,0 +1,23 @@
|
||||
register_artifact("HEADBUT_HELMET", {
|
||||
name = "Headbut Helmet",
|
||||
description = "Gain 1 Knock Out card.",
|
||||
tags = { "_ACT_0" },
|
||||
price = 200,
|
||||
order = 0,
|
||||
callbacks = {
|
||||
on_pick_up = function(ctx)
|
||||
give_card("KNOCK_OUT", PLAYER_ID)
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function()
|
||||
local cards = get_cards(PLAYER_ID)
|
||||
for _, card_guid in ipairs(cards) do
|
||||
local card = get_card_instance(card_guid)
|
||||
if card.type_id == "KNOCK_OUT" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return "Expected to find KNOCK_OUT card, but did not."
|
||||
end
|
||||
})
|
||||
20
assets/scripts/equipment/permanents/long_rest.lua
Normal file
20
assets/scripts/equipment/permanents/long_rest.lua
Normal file
@ -0,0 +1,20 @@
|
||||
register_card("LONG_REST", {
|
||||
name = l("cards.LONG_REST.name", "Long Rest"),
|
||||
description = l("cards.REST.description", "Heal for 4 (+1 per level)."),
|
||||
state = function(ctx)
|
||||
return string.format(l("cards.REST.state", "Take a short rest. Heal for %s."), highlight(4 + ctx.level))
|
||||
end,
|
||||
tags = { "HEAL" },
|
||||
max_level = 2,
|
||||
color = COLOR_GREEN,
|
||||
need_target = false,
|
||||
does_exhaust = true,
|
||||
point_cost = 3,
|
||||
price = 300,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
heal(ctx.caster, ctx.caster, 4 + ctx.level)
|
||||
return nil
|
||||
end
|
||||
},
|
||||
})
|
||||
52
assets/scripts/equipment/permanents/nullify.lua
Normal file
52
assets/scripts/equipment/permanents/nullify.lua
Normal file
@ -0,0 +1,52 @@
|
||||
register_card("NULLIFY", {
|
||||
name = l("cards.NULLIFY.name", "Nullify"),
|
||||
description = string.format(
|
||||
l("cards.NULLIFY.description", "%s\n\nDeploy a temporary damage nullifier. %s all damage this round."),
|
||||
highlight("Exhaust"), highlight("Negates")
|
||||
),
|
||||
tags = { "DEF", "_ACT_0" },
|
||||
max_level = 0,
|
||||
color = COLOR_BLUE,
|
||||
need_target = false,
|
||||
does_exhaust = true,
|
||||
point_cost = 3,
|
||||
price = 200,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
give_status_effect("NULLIFY", ctx.caster, 1 + ctx.level)
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_status_effect("NULLIFY", {
|
||||
name = l("status_effects.NULLIFY.name", "Nullify Field"),
|
||||
description = l("status_effects.NULLIFY.description", "Negates all damage this round."),
|
||||
look = "NF",
|
||||
foreground = COLOR_BLUE,
|
||||
can_stack = false,
|
||||
decay = DECAY_ALL,
|
||||
rounds = 1,
|
||||
order = 100,
|
||||
callbacks = {
|
||||
on_damage_calc = function(ctx)
|
||||
if ctx.target == ctx.owner then
|
||||
return 0
|
||||
end
|
||||
return ctx.damage
|
||||
end,
|
||||
},
|
||||
test = function()
|
||||
return assert_chain({
|
||||
function() return assert_status_effect_count(1) end,
|
||||
function() return assert_status_effect("NULLIFY", 1) end,
|
||||
function()
|
||||
local dummy = add_actor_by_enemy("DUMMY")
|
||||
local damage = deal_damage(dummy, PLAYER_ID, 100)
|
||||
if damage ~= 0 then
|
||||
return "Expected 0 damage, got " .. damage
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
})
|
||||
19
assets/scripts/equipment/permanents/rest.lua
Normal file
19
assets/scripts/equipment/permanents/rest.lua
Normal file
@ -0,0 +1,19 @@
|
||||
register_card("REST", {
|
||||
name = l("cards.REST.name", "Short Rest"),
|
||||
description = l("cards.REST.description", "Heal for 1 (+1 per level)."),
|
||||
state = function(ctx)
|
||||
return string.format(l("cards.REST.state", "Take a short rest. Heal for %s."), highlight(1 + ctx.level))
|
||||
end,
|
||||
tags = { "HEAL" },
|
||||
max_level = 3,
|
||||
color = COLOR_GREEN,
|
||||
need_target = false,
|
||||
point_cost = 1,
|
||||
price = 120,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
heal(ctx.caster, ctx.caster, 1 + ctx.level)
|
||||
return nil
|
||||
end
|
||||
},
|
||||
})
|
||||
@ -25,6 +25,32 @@ It seems to be eating the metal from the walls. It looks at you and after a few
|
||||
}
|
||||
})
|
||||
|
||||
register_event("NANOBOT_SWARM", {
|
||||
name = "Is this a swarm of...",
|
||||
description = [[!!nanobot_swarm.jpg
|
||||
|
||||
You are walking through the facility hoping to find a way out. After a few turns you hear a strange noise. You look around and come across a swarm of nanobots.
|
||||
|
||||
**It continues to grow and it looks like it's going to attack you.**
|
||||
]],
|
||||
tags = { "_ACT_0_FIGHT" },
|
||||
choices = {
|
||||
{
|
||||
description = "Fight!",
|
||||
callback = function()
|
||||
add_actor_by_enemy("NANOBOT_SWARM")
|
||||
if random() < 0.25 then
|
||||
add_actor_by_enemy("NANOBOT_SWARM")
|
||||
end
|
||||
if random() < 0.05 then
|
||||
add_actor_by_enemy("REPAIR_DRONE")
|
||||
end
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
register_event("CLEAN_BOT", {
|
||||
name = "Corpse. Clean. Engage.",
|
||||
description = [[!!clean_bot.jpg
|
||||
|
||||
@ -92,9 +92,9 @@ You found a chest with a strange symbol on it. The chest is protected by a stran
|
||||
})
|
||||
|
||||
register_event("GAIN_GOLD_ACT_0", {
|
||||
name = "",
|
||||
name = "Old Gold Cache",
|
||||
description = [[
|
||||
...
|
||||
You find an old chest filled with gold. You can either take it or leave.
|
||||
]],
|
||||
tags = { "_ACT_0" },
|
||||
choices = {
|
||||
|
||||
@ -19,42 +19,29 @@ register_story_teller("_ACT_0", {
|
||||
possible = find_events_by_tags({ "_ACT_0_FIGHT" })
|
||||
end
|
||||
|
||||
print(#get_event_history())
|
||||
print("[ACT_0 ST] history:", get_event_history())
|
||||
|
||||
-- filter out events by id that have already been played
|
||||
possible = fun.iter(possible):filter(function(event)
|
||||
return event == "MERCHANT" or not table.contains(history, event.id)
|
||||
return event.id == "MERCHANT" or not table.contains(history, event.id)
|
||||
end):totable()
|
||||
|
||||
print("[ACT_0 ST] possible:", fun.iter(possible):map(function(e) return e.id end):totable())
|
||||
|
||||
-- fallback for now
|
||||
if #possible == 0 then
|
||||
possible = find_events_by_tags({ "_ACT_0_FIGHT" })
|
||||
end
|
||||
|
||||
local choosen = possible[random_int(0, #possible)]
|
||||
local choosen_id = random_int(0, #possible);
|
||||
print("[ACT_0 ST] choosen_id:", choosen_id)
|
||||
|
||||
local choosen = possible[1 + choosen_id]
|
||||
if choosen ~= nil then
|
||||
print("[ACT_0 ST] choosen:", choosen.id)
|
||||
set_event(choosen.id)
|
||||
end
|
||||
|
||||
-- if we cleared a stage, give the player a random artifact
|
||||
local last_stage_count = fetch("last_stage_count")
|
||||
local current_stage_count = get_stages_cleared()
|
||||
if last_stage_count ~= current_stage_count then
|
||||
local gets_random_artifact = random() < 0.25
|
||||
|
||||
if gets_random_artifact then
|
||||
local player_artifacts = fun.iter(get_actor(PLAYER_ID).artifacts):map(function(id)
|
||||
return get_artifact(id).id
|
||||
end):totable()
|
||||
local artifacts = find_artifacts_by_tags({ "_ACT_0" })
|
||||
if #artifacts > 0 then
|
||||
local artifact = choose_weighted_by_price(artifacts)
|
||||
if not table.contains(player_artifacts, artifact) then
|
||||
give_artifact(PLAYER_ID, artifact)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return GAME_STATE_EVENT
|
||||
end
|
||||
|
||||
225
assets/tutorial/tutorial.lua
Normal file
225
assets/tutorial/tutorial.lua
Normal file
@ -0,0 +1,225 @@
|
||||
delete_base_game("event")
|
||||
|
||||
register_enemy("TUTORIAL_DUMMY_1", {
|
||||
name = l("enemies.TUTORIAL_DUMMY_1.name", "Dummy"),
|
||||
description = l("enemies.TUTORIAL_DUMMY_1.description", "A dummy enemy for the tutorial"),
|
||||
look = "D",
|
||||
color = "#e6e65a",
|
||||
initial_hp = 4,
|
||||
max_hp = 4,
|
||||
gold = 0,
|
||||
intend = function(ctx)
|
||||
return "Deal " .. highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, 1)) .. " damage"
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
deal_damage(ctx.guid, PLAYER_ID, 1)
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_enemy("TUTORIAL_DUMMY_2", {
|
||||
name = l("enemies.TUTORIAL_DUMMY_2.name", "Dummy"),
|
||||
description = l("enemies.TUTORIAL_DUMMY_2.description", "A dummy enemy for the tutorial"),
|
||||
look = "D",
|
||||
color = "#e6e65a",
|
||||
initial_hp = 3,
|
||||
max_hp = 3,
|
||||
gold = 0,
|
||||
intend = function(ctx)
|
||||
return "Apply " .. highlight("Weakness")
|
||||
end,
|
||||
callbacks = {
|
||||
on_turn = function(ctx)
|
||||
give_status_effect("WEAKNESS", PLAYER_ID)
|
||||
return nil
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_status_effect("WEAKNESS", {
|
||||
name = "Weakness",
|
||||
description = "Decreases damage dealt by 1",
|
||||
look = "W",
|
||||
foreground = COLOR_RED,
|
||||
state = function(ctx)
|
||||
return "Deals " .. highlight(1) .. " less damage"
|
||||
end,
|
||||
rounds = 2,
|
||||
decay = DECAY_ONE,
|
||||
can_stack = false,
|
||||
callbacks = {
|
||||
on_damage_calc = function(ctx)
|
||||
if ctx.source == ctx.owner then
|
||||
return ctx.damage - 1
|
||||
end
|
||||
return ctx.damage
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
register_card("MELEE_HIT", {
|
||||
name = l("cards.MELEE_HIT.name", "Melee Hit"),
|
||||
description = l("cards.MELEE_HIT.description", "Use your bare hands to deal 2 (+1 for each upgrade) damage."),
|
||||
state = function(ctx)
|
||||
return string.format(l("cards.MELEE_HIT.state", "Use your bare hands to deal %s damage."),
|
||||
highlight(2 + ctx.level))
|
||||
end,
|
||||
tags = { "ATK", "M", "HND" },
|
||||
max_level = 1,
|
||||
color = COLOR_GRAY,
|
||||
need_target = true,
|
||||
point_cost = 1,
|
||||
price = -1,
|
||||
callbacks = {
|
||||
on_cast = function(ctx)
|
||||
deal_damage_card(ctx.caster, ctx.guid, ctx.target, 2 + ctx.level)
|
||||
return nil
|
||||
end
|
||||
},
|
||||
test = function()
|
||||
return assert_cast_damage("MELEE_HIT", 2)
|
||||
end
|
||||
})
|
||||
|
||||
register_event("START", {
|
||||
name = "Welcome!",
|
||||
description = [[Welcome to *End of Eden*!
|
||||
|
||||
This game is a roguelite deckbuilder where you explore a post-apocalyptic world, collect cards and artifacts and fight enemies. **Try to stay alive as long as possible!**
|
||||
|
||||
**Lets start with some keyboard shortcuts**
|
||||
|
||||
- *ESC* - Open the menu where you can see your cards, artifacts, ... or abort choices
|
||||
- *SPACE* - End your turn
|
||||
- *ARROW LEFT / ARROW RIGHT* - Select card or enemy to hit
|
||||
- *ENTER* - Confirm your choice
|
||||
- *X* - If you hover over a enemy and press X, you can see more infos about a enemy
|
||||
- *S* - Open player status
|
||||
|
||||
You can also use the **mouse** to select cards, enemies and click buttons.
|
||||
|
||||
**Cards**
|
||||
|
||||
You have a deck of cards that you can use to attack, defend or apply status effects. You can see your cards in the bottom of the screen. All cards cost action points that reset on each turn. Use them wisely!
|
||||
|
||||
**Combat**
|
||||
|
||||
If you press Continue you will fight a dummy enemy. See if you are able to kill it!
|
||||
|
||||
]],
|
||||
choices = {
|
||||
{
|
||||
description = "Continue",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
},
|
||||
},
|
||||
on_enter = function()
|
||||
end,
|
||||
on_end = function(ctx)
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_1")
|
||||
give_player_gold(500)
|
||||
give_card("MELEE_HIT", PLAYER_ID)
|
||||
set_event("TUTORIAL_1")
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
})
|
||||
|
||||
register_event("TUTORIAL_1", {
|
||||
name = "Status Effects",
|
||||
description = [[*Awesome! You have defeated the dummy enemy!*
|
||||
|
||||
Now you will face a enemy that will apply a *status effect* on you. Status effects can be positive or negative and can be applied by cards, enemies or other sources. Status effects that are applied to you are shown on the bottom of the screen. You can click on them or press *S* to see more information.
|
||||
|
||||
If you press Continue you will fight some dummy enemies. See if you are able to kill them!
|
||||
]],
|
||||
choices = {
|
||||
{
|
||||
description = "Continue",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
},
|
||||
},
|
||||
on_enter = function()
|
||||
end,
|
||||
on_end = function(ctx)
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_1")
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_2")
|
||||
set_event("TUTORIAL_2")
|
||||
give_card("BLOCK", PLAYER_ID)
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
})
|
||||
|
||||
register_event("TUTORIAL_2", {
|
||||
name = "The Merchant",
|
||||
description = [[*Awesome! You have defeated the dummy enemies!*
|
||||
|
||||
Every now and then you will encounter a merchant. The merchant will offer you cards and artifacts that you can buy with gold. You can also remove or upgrade cards. Gold is earned by defeating enemies.
|
||||
|
||||
If you press Continue you will meet the merchant. *Try to buy or upgrade something!*
|
||||
]],
|
||||
choices = {
|
||||
{
|
||||
description = "Continue",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
},
|
||||
},
|
||||
on_enter = function()
|
||||
end,
|
||||
on_end = function(ctx)
|
||||
set_event("TUTORIAL_3")
|
||||
return GAME_STATE_MERCHANT
|
||||
end
|
||||
})
|
||||
|
||||
register_event("TUTORIAL_3", {
|
||||
name = "Finished!",
|
||||
description = [[*Awesome! You have bought some stuff!*
|
||||
|
||||
This is the end of the tutorial. You can now continue to explore the world and fight enemies. Good luck!
|
||||
]],
|
||||
choices = {
|
||||
{
|
||||
description = "Continue",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
},
|
||||
},
|
||||
on_enter = function()
|
||||
end,
|
||||
on_end = function(ctx)
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_1")
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_2")
|
||||
add_actor_by_enemy("TUTORIAL_DUMMY_1")
|
||||
set_event("TUTORIAL_4")
|
||||
return GAME_STATE_FIGHT
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
register_event("TUTORIAL_4", {
|
||||
name = "Be gone!",
|
||||
description = [[*It is time to go...*]],
|
||||
choices = {
|
||||
{
|
||||
description = "Continue",
|
||||
callback = function()
|
||||
return nil
|
||||
end
|
||||
},
|
||||
},
|
||||
on_enter = function()
|
||||
end,
|
||||
on_end = function(ctx)
|
||||
deal_damage("TUTORIAL", PLAYER_ID, 1000, true)
|
||||
return GAME_STATE_GAMEOVER
|
||||
end
|
||||
})
|
||||
@ -197,6 +197,65 @@
|
||||
term.onData((data) => bubbletea_write(data));
|
||||
}
|
||||
|
||||
function ensureAllFiles() {
|
||||
// Wait for WASM to be loaded
|
||||
if (!globalThis.version) {
|
||||
setTimeout(ensureAllFiles, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear cache if version mismatch
|
||||
const lastVersion = globalThis.settings.getString("lastCachedVersion");
|
||||
if (!lastVersion || lastVersion === "" || globalThis.version !== lastVersion) {
|
||||
console.log("Clearing file cache due to version mismatch");
|
||||
|
||||
const keys = Object.keys(window.localStorage);
|
||||
for (const key of keys) {
|
||||
if (key.indexOf("assets") !== -1) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache files if they don't exist
|
||||
fetch("./assets/file_index.json")
|
||||
.then((index) => index.json())
|
||||
.then((index) => {
|
||||
const promises = [];
|
||||
for (const file of index) {
|
||||
if (!file.isFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// only ensure files that end in .lua or .text
|
||||
if (!file.path.endsWith(".lua") && !file.path.endsWith(".txt")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (window.fsRead(file.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(
|
||||
fetch(file.path)
|
||||
.then((response) => response.text())
|
||||
.then((text) => {
|
||||
window.fsWrite(file.path, text);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
console.log("All files loaded");
|
||||
globalThis.settings.set("lastCachedVersion", globalThis.version);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("./eoe.wasm"), go.importObject).then((result) => {
|
||||
@ -206,10 +265,11 @@
|
||||
});
|
||||
|
||||
// Init terminal. This should be done after bubbletea is initialized. For now, I use a timeout.
|
||||
document.fonts.load((globalThis.settings.getInt("font_size") ?? 14) + 'px "IosevkaTermNerdFontMono"').then(() => initTerminal());
|
||||
document.fonts.load((globalThis.settings.getInt("font_size") ?? 12) + 'px "IosevkaTermNerdFontMono"').then(() => initTerminal());
|
||||
});
|
||||
}
|
||||
|
||||
ensureAllFiles();
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -78,9 +78,9 @@ title Action Points
|
||||
```mermaid
|
||||
pie
|
||||
title Card Types
|
||||
"Consume" : 11
|
||||
"Exhaust" : 1
|
||||
"Normal" : 9
|
||||
"Consume" : 11
|
||||
```
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ title Card Types
|
||||
|------------------------|-----------------------|-------------------------------------------------------------------|------------|--------|---------|------------------------------|-----------------|
|
||||
| ``CYBER_SPIDER`` | CYBER Spider | It waits for its prey to come closer | 8 | 8 | #ff4d6d | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``CLEAN_BOT`` | Cleaning Bot | It never stopped cleaning... | 13 | 13 | #32a891 | ``OnTurn``, ``OnPlayerTurn`` | :no_entry_sign: |
|
||||
| ``CYBER_SLIME`` | Cyber Slime | A cybernetic slime that splits into smaller slimes when defeated. | 10 | 10 | #00ff00 | ``OnTurn``, ``OnActorDie`` | :no_entry_sign: |
|
||||
| ``CYBER_SLIME`` | Cyber Slime | A cybernetic slime that splits into smaller slimes when defeated. | 10 | 10 | #00ff00 | ``OnActorDie``, ``OnTurn`` | :no_entry_sign: |
|
||||
| ``CYBER_SLIME_MINION`` | Cyber Slime Offspring | A smaller version of the Cyber Slime. | 4 | 4 | #00ff00 | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``DUMMY`` | Dummy | End me... | 100 | 100 | #deeb6a | ``OnTurn`` | :no_entry_sign: |
|
||||
| ``LASER_DRONE`` | Laser Drone | A drone equipped with a powerful laser cannon. | 7 | 7 | #ff0000 | ``OnTurn`` | :no_entry_sign: |
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -70,11 +70,13 @@ fun = require "fun"
|
||||
d.Global("GAME_STATE_EVENT", "Represents the event game state.")
|
||||
d.Global("GAME_STATE_MERCHANT", "Represents the merchant game state.")
|
||||
d.Global("GAME_STATE_RANDOM", "Represents the random game state in which the active story teller will decide what happens next.")
|
||||
d.Global("GAME_STATE_GAMEOVER", "Represents the game over game state.")
|
||||
|
||||
l.SetGlobal("GAME_STATE_FIGHT", lua.LString(GameStateFight))
|
||||
l.SetGlobal("GAME_STATE_EVENT", lua.LString(GameStateEvent))
|
||||
l.SetGlobal("GAME_STATE_MERCHANT", lua.LString(GameStateMerchant))
|
||||
l.SetGlobal("GAME_STATE_RANDOM", lua.LString(GameStateRandom))
|
||||
l.SetGlobal("GAME_STATE_GAMEOVER", lua.LString(GameStateGameOver))
|
||||
|
||||
d.Global("DECAY_ONE", "Status effect decays by 1 stack per turn.")
|
||||
d.Global("DECAY_ALL", "Status effect decays by all stacks per turn.")
|
||||
|
||||
@ -157,6 +157,8 @@ func NewSession(options ...func(s *Session)) *Session {
|
||||
session.SetOnLuaError(nil)
|
||||
|
||||
session.luaState, session.luaDocs = SessionAdapter(session)
|
||||
session.resources = NewResourcesManager(session.luaState, session.luaDocs, session.log)
|
||||
session.resources.MarkBaseGame()
|
||||
|
||||
for i := range options {
|
||||
if options[i] == nil {
|
||||
@ -165,8 +167,6 @@ func NewSession(options ...func(s *Session)) *Session {
|
||||
options[i](session)
|
||||
}
|
||||
|
||||
session.resources = NewResourcesManager(session.luaState, session.luaDocs, session.log)
|
||||
session.resources.MarkBaseGame()
|
||||
session.loadMods(session.loadedMods)
|
||||
|
||||
session.log.Println("Session started!")
|
||||
@ -212,6 +212,15 @@ func WithMods(mods []string) func(s *Session) {
|
||||
}
|
||||
}
|
||||
|
||||
// WithLuaString sets the lua code that should be executed.
|
||||
func WithLuaString(lua string) func(s *Session) {
|
||||
return func(s *Session) {
|
||||
if err := s.luaState.DoString(lua); err != nil {
|
||||
s.logLuaError("WithLuaString", "", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeed sets the seed for the random number generator.
|
||||
func WithSeed(seed uint64) func(s *Session) {
|
||||
return func(s *Session) {
|
||||
@ -272,20 +281,25 @@ func (s *Session) LuaErrors() chan LuaError {
|
||||
// ToSavedState creates a saved state of the session that can be serialized with Gob.
|
||||
func (s *Session) ToSavedState() SavedState {
|
||||
return SavedState{
|
||||
State: s.state,
|
||||
Seed: s.seed,
|
||||
Rand: s.randSrc,
|
||||
Actors: s.actors,
|
||||
Instances: s.instances,
|
||||
StagesCleared: s.stagesCleared,
|
||||
CurrentEvent: s.currentEvent,
|
||||
CurrentFight: s.currentFight,
|
||||
PointsPerRound: s.pointsPerRound,
|
||||
Merchant: s.merchant,
|
||||
EventHistory: s.eventHistory,
|
||||
StateCheckpoints: s.stateCheckpoints,
|
||||
CtxData: s.ctxData,
|
||||
LoadedMods: s.loadedMods,
|
||||
State: s.state,
|
||||
Seed: s.seed,
|
||||
Rand: s.randSrc,
|
||||
Actors: s.actors,
|
||||
Instances: s.instances,
|
||||
StagesCleared: s.stagesCleared,
|
||||
CurrentEvent: s.currentEvent,
|
||||
CurrentFight: s.currentFight,
|
||||
PointsPerRound: s.pointsPerRound,
|
||||
Merchant: s.merchant,
|
||||
EventHistory: s.eventHistory,
|
||||
StateCheckpoints: lo.Map(s.stateCheckpoints, func(item StateCheckpoint, index int) StateCheckpoint {
|
||||
return StateCheckpoint{
|
||||
Session: nil,
|
||||
Events: item.Events,
|
||||
}
|
||||
}),
|
||||
CtxData: s.ctxData,
|
||||
LoadedMods: s.loadedMods,
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,6 +390,18 @@ func (s *Session) logLuaError(callback string, typeId string, err error) {
|
||||
|
||||
func (s *Session) loadMods(mods []string) {
|
||||
for i := range mods {
|
||||
// Load single lua files
|
||||
if strings.HasSuffix(mods[i], ".lua") && filepath.IsAbs(mods[i]) {
|
||||
luaBytes, err := fs.ReadFile(mods[i])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := s.luaState.DoString(string(luaBytes)); err != nil {
|
||||
s.logLuaError("ModLoader", "", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
mod, err := ModDescription(filepath.Join("./mods", mods[i]))
|
||||
if err != nil {
|
||||
log.Println("Error loading mod:", err)
|
||||
@ -491,6 +517,14 @@ func (s *Session) SetEvent(id string) {
|
||||
if _, ok := s.resources.Events[id]; ok {
|
||||
s.eventHistory = append(s.eventHistory, id)
|
||||
_, _ = s.resources.Events[id].OnEnter.Call(CreateContext("type_id", id))
|
||||
} else {
|
||||
s.log.Println("Event not found:", id)
|
||||
s.currentEvent = ""
|
||||
|
||||
// If we can't find the event, we just go to the next state
|
||||
if s.state == GameStateEvent {
|
||||
s.SetGameState(GameStateRandom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -820,8 +854,13 @@ func (s *Session) SetupMerchant() {
|
||||
}
|
||||
}
|
||||
|
||||
// LeaveMerchant finishes the merchant state and lets the storyteller decide what to do next.
|
||||
// LeaveMerchant finishes the merchant state and lets the storyteller decide what to do next. If an event is still set we switch to it.
|
||||
func (s *Session) LeaveMerchant() {
|
||||
if s.currentEvent != "" {
|
||||
s.SetGameState(GameStateEvent)
|
||||
return
|
||||
}
|
||||
|
||||
s.SetGameState(GameStateRandom)
|
||||
}
|
||||
|
||||
|
||||
@ -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
3
run_tests.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
EOE_TESTER_WORKING_DIR=$(pwd) go test ./cmd/internal/tester -v
|
||||
@ -140,7 +140,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// Show tooltip
|
||||
if msg.String() == "x" {
|
||||
switch msg.String() {
|
||||
case "x":
|
||||
for i := 0; i < m.Session.GetOpponentCount(game.PlayerActorID); i++ {
|
||||
if m.zones.Get(fmt.Sprintf("%s%d", ZoneEnemy, i)).InBounds(m.LastMouse) {
|
||||
cmds = append(cmds, root.TooltipCreate(root.Tooltip{
|
||||
@ -151,6 +152,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}))
|
||||
}
|
||||
}
|
||||
case "s":
|
||||
m.inPlayerView = !m.inPlayerView
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
@ -17,6 +17,7 @@ type Choice string
|
||||
const (
|
||||
ChoiceWaiting = Choice("WAITING")
|
||||
ChoiceContinue = Choice("CONTINUE")
|
||||
ChoiceTutorial = Choice("TUTORIAL")
|
||||
ChoiceNewGame = Choice("NEW_GAME")
|
||||
ChoiceNewGameSOD = Choice("NEW_GAME_SOD")
|
||||
ChoiceAbout = Choice("ABOUT")
|
||||
@ -45,6 +46,7 @@ type ChoicesModel struct {
|
||||
func NewChoicesModel(zones *zone.Manager, hideSettings bool) ChoicesModel {
|
||||
choices := []list.Item{
|
||||
choiceItem{zones, "Continue", "Ready to continue dying?", ChoiceContinue},
|
||||
choiceItem{zones, "Tutorial", "Learn the basics.", ChoiceTutorial},
|
||||
choiceItem{zones, "New Game", "Start a new try.", ChoiceNewGame},
|
||||
choiceItem{zones, "New Game: Seed of the Day", "Start a new try with the daily seed.", ChoiceNewGameSOD},
|
||||
choiceItem{zones, "About", "Want to know more?", ChoiceAbout},
|
||||
|
||||
@ -147,6 +147,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
lo.Ternary(os.Getenv("EOE_DEBUG") == "1", game.WithDebugEnabled(8272), nil),
|
||||
))),
|
||||
)
|
||||
case ChoiceTutorial:
|
||||
audio.Play("btn_menu")
|
||||
|
||||
tutorialLua, err := fs.ReadFile("./assets/tutorial/tutorial.lua")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.choices = m.choices.Clear()
|
||||
return m, tea.Sequence(
|
||||
cmd,
|
||||
root.Push(gameview.New(m, m.zones, game.NewSession(
|
||||
game.WithLuaString(string(tutorialLua)),
|
||||
))),
|
||||
)
|
||||
case ChoiceAbout:
|
||||
audio.Play("btn_menu")
|
||||
|
||||
|
||||
@ -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{
|
||||
` ██████
|
||||
██ ████
|
||||
██ ██ ██
|
||||
████ ██
|
||||
` ██████
|
||||
██ ████
|
||||
██ ██ ██
|
||||
████ ██
|
||||
██████`,
|
||||
` ██
|
||||
███
|
||||
██
|
||||
██
|
||||
██ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
` ██
|
||||
███
|
||||
██
|
||||
██
|
||||
██ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
███████ `,
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
`██████
|
||||
██
|
||||
█████
|
||||
██
|
||||
██████ `,
|
||||
`██ ██
|
||||
██ ██
|
||||
███████
|
||||
██
|
||||
`██ ██
|
||||
██ ██
|
||||
███████
|
||||
██
|
||||
██`,
|
||||
`███████
|
||||
██
|
||||
███████
|
||||
██
|
||||
`███████
|
||||
██
|
||||
███████
|
||||
██
|
||||
███████`,
|
||||
` ██████
|
||||
██
|
||||
███████
|
||||
██ ██
|
||||
` ██████
|
||||
██
|
||||
███████
|
||||
██ ██
|
||||
██████`,
|
||||
`███████
|
||||
██
|
||||
██
|
||||
██
|
||||
`███████
|
||||
██
|
||||
██
|
||||
██
|
||||
██`,
|
||||
` █████
|
||||
██ ██
|
||||
█████
|
||||
██ ██
|
||||
` █████
|
||||
██ ██
|
||||
█████
|
||||
██ ██
|
||||
█████`,
|
||||
` █████
|
||||
██ ██
|
||||
██████
|
||||
██
|
||||
` █████
|
||||
██ ██
|
||||
██████
|
||||
██
|
||||
█████`,
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user