From d3afc3b36ce0887255d3ed2d21b1bfa558003afc Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Tue, 16 Jan 2024 22:39:20 +0100 Subject: [PATCH] feat: act_0 basic content --- assets/scripts/_test.lua | 2 +- assets/scripts/_util.lua | 16 +- assets/scripts/definitions/api.lua | 14 +- assets/scripts/definitions/callbacks.lua | 1 + assets/scripts/enemies/cyber_spider.lua | 25 ++ assets/scripts/equipment/_colors.lua | 3 +- assets/scripts/equipment/_util.lua | 18 +- .../equipment/consumeable/bounce_shield.lua | 2 +- .../equipment/consumeable/energy_drink.lua | 58 +++++ .../equipment/consumeable/flash_bang.lua | 2 +- .../equipment/consumeable/flash_shield.lua | 2 +- .../equipment/consumeable/nano_charger.lua | 2 +- .../equipment/consumeable/stim_pack.lua | 2 +- .../consumeable/ultra_flash_shield.lua | 61 +++++ .../equipment/permanents/arm_mounted_gun.lua | 2 +- .../permanents/basic_hand_weapons.lua | 8 +- .../equipment/permanents/combat_glasses.lua | 22 ++ .../equipment/permanents/combat_gloves.lua | 22 ++ .../equipment/permanents/interval_juicer.lua | 15 ++ .../equipment/permanents/melee_hit.lua | 2 +- .../equipment/permanents/portable_buffer.lua | 13 + .../equipment/permanents/speed_enhancer.lua | 15 ++ assets/scripts/events/act_0/enemies.lua | 31 ++- assets/scripts/events/act_0/other.lua | 236 ++++++++++++++++++ assets/scripts/events/base/merchant.lua | 1 + assets/scripts/story_teller/act_0.lua | 38 ++- docs/LUA_API_DOCS.md | 24 +- game/lua.go | 27 +- game/session.go | 16 +- ui/components/card.go | 4 + ui/menus/gameview/gameview.go | 130 +++++----- ui/menus/merchant/merchant.go | 2 +- 32 files changed, 700 insertions(+), 116 deletions(-) create mode 100644 assets/scripts/enemies/cyber_spider.lua create mode 100644 assets/scripts/equipment/consumeable/energy_drink.lua create mode 100644 assets/scripts/equipment/consumeable/ultra_flash_shield.lua create mode 100644 assets/scripts/equipment/permanents/combat_glasses.lua create mode 100644 assets/scripts/equipment/permanents/combat_gloves.lua create mode 100644 assets/scripts/equipment/permanents/interval_juicer.lua create mode 100644 assets/scripts/equipment/permanents/portable_buffer.lua create mode 100644 assets/scripts/equipment/permanents/speed_enhancer.lua create mode 100644 assets/scripts/events/act_0/other.lua diff --git a/assets/scripts/_test.lua b/assets/scripts/_test.lua index c0647dc..6daf077 100644 --- a/assets/scripts/_test.lua +++ b/assets/scripts/_test.lua @@ -58,7 +58,7 @@ function assert_cast_damage(id, dmg) local cards = get_cards(PLAYER_ID) if not cards[1] then - return "Card not in hand" + return "Card " .. id .. " not in hand" end local card = get_card_instance(cards[1]) diff --git a/assets/scripts/_util.lua b/assets/scripts/_util.lua index 9607ef7..8b7065f 100644 --- a/assets/scripts/_util.lua +++ b/assets/scripts/_util.lua @@ -1,3 +1,8 @@ +local function _escape_color(number) + local escapeString = string.char(27) .. '[%sm' + return escapeString:format(number) +end + ---highlight some value ---@param val any function highlight(val) @@ -7,7 +12,13 @@ end ---highlight_warn some value with warning colors ---@param val any function highlight_warn(val) - return text_underline(text_bold(text_red("[" .. tostring(val) .. "]"))) + 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")) end ---choose_weighted chooses an item from a list of choices, with a weight for each item. @@ -35,6 +46,9 @@ end ---table.contains check if a table contains an element. function table.contains(table, element) + if table == nil then + return false + end for _, value in pairs(table) do if value == element then return true diff --git a/assets/scripts/definitions/api.lua b/assets/scripts/definitions/api.lua index 8d84725..ed96628 100644 --- a/assets/scripts/definitions/api.lua +++ b/assets/scripts/definitions/api.lua @@ -66,11 +66,6 @@ function text_bold(value) end ---@return string function text_italic(value) end ---- Makes the text colored red. ----@param value any ----@return string -function text_red(value) end - --- Makes the text underlined. ---@param value any ---@return string @@ -342,6 +337,15 @@ function upgrade_random_card(actor_guid) end ---@return number function deal_damage(source, target, damage, flat) end +--- Deal damage from one source to a target from a card. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt. +---@param source guid +---@param card guid +---@param target guid +---@param damage number +---@param flat? boolean +---@return number +function deal_damage_card(source, card, target, damage, flat) end + --- Deal damage to multiple enemies from one source. If flat is true the damage can't be modified by status effects or artifacts. Returns a array of damages for each actor hit. ---@param source guid ---@param targets guid[] diff --git a/assets/scripts/definitions/callbacks.lua b/assets/scripts/definitions/callbacks.lua index fb57c76..ad13f6d 100644 --- a/assets/scripts/definitions/callbacks.lua +++ b/assets/scripts/definitions/callbacks.lua @@ -5,6 +5,7 @@ ---@field type_id? type_id ---@field guid? guid ---@field source? guid +---@field card? guid ---@field target? guid ---@field owner? guid ---@field caster? guid diff --git a/assets/scripts/enemies/cyber_spider.lua b/assets/scripts/enemies/cyber_spider.lua new file mode 100644 index 0000000..d478cb5 --- /dev/null +++ b/assets/scripts/enemies/cyber_spider.lua @@ -0,0 +1,25 @@ +register_enemy("CYBER_SPIDER", { + name = "CYBER Spider", + description = "It waits for its prey to come closer", + look = [[/\o^o/\]], + color = "#ff4d6d", + initial_hp = 8, + max_hp = 8, + gold = 15, + intend = function(ctx) + if ctx.round > 0 and ctx.round % 3 == 0 then + return "Deal " .. highlight(5) .. " damage" + end + + return "Wait..." + end, + callbacks = { + on_turn = function(ctx) + if ctx.round > 0 and ctx.round % 3 == 0 then + deal_damage(ctx.guid, PLAYER_ID, 5) + end + + return nil + end + } +}) diff --git a/assets/scripts/equipment/_colors.lua b/assets/scripts/equipment/_colors.lua index 4dab8f7..5df2064 100644 --- a/assets/scripts/equipment/_colors.lua +++ b/assets/scripts/equipment/_colors.lua @@ -2,4 +2,5 @@ COLOR_GRAY = "#2f3e46" COLOR_BLUE = "#219ebc" COLOR_PURPLE = "#725e9c" COLOR_RED = "#c1121f" -COLOR_GREEN = "#a7c957" \ No newline at end of file +COLOR_GREEN = "#a7c957" +COLOR_ORANGE = "#fb5607" \ No newline at end of file diff --git a/assets/scripts/equipment/_util.lua b/assets/scripts/equipment/_util.lua index dccfd1e..f260fee 100644 --- a/assets/scripts/equipment/_util.lua +++ b/assets/scripts/equipment/_util.lua @@ -1,7 +1,8 @@ -function add_found_artifact_event(id, picture, description, choice_description) +function add_found_artifact_event(id, picture, description, choice_description, tags) register_event(id, { name = "Found: " .. registered.artifact[id].name, description = string.format("!!%s\n\n**You found something!** %s", picture or "artifact_chest.jpg", description), + tags = tags, choices = { { description_fn = function() @@ -11,15 +12,16 @@ function add_found_artifact_event(id, picture, description, choice_description) give_artifact(id, PLAYER_ID) return nil end - }, { - description = "Leave...", - callback = function() - return nil - end - } + }, + { + description = "Leave...", + callback = function() + return nil + end + } }, on_end = function() return GAME_STATE_RANDOM end }) -end \ No newline at end of file +end diff --git a/assets/scripts/equipment/consumeable/bounce_shield.lua b/assets/scripts/equipment/consumeable/bounce_shield.lua index 3650310..59e47d0 100644 --- a/assets/scripts/equipment/consumeable/bounce_shield.lua +++ b/assets/scripts/equipment/consumeable/bounce_shield.lua @@ -4,7 +4,7 @@ register_card("BOUNCE_SHIELD", { l("cards.BOUNCE_SHIELD.description","%s\n\nDeploy a temporary shield. %s bounces the damage back, but still takes damage."), highlight("One-Time"), highlight("Negates") ), - tags = { "DEF" }, + tags = { "DEF", "_ACT_0" }, max_level = 0, color = COLOR_BLUE, need_target = false, diff --git a/assets/scripts/equipment/consumeable/energy_drink.lua b/assets/scripts/equipment/consumeable/energy_drink.lua new file mode 100644 index 0000000..49cf612 --- /dev/null +++ b/assets/scripts/equipment/consumeable/energy_drink.lua @@ -0,0 +1,58 @@ +local drinks = { + { + id = "ENERGY_DRINK", + name = "ENRGY Drink X91", + description = "Gain 1 action point.", + price = 150, + action_points = 1, + }, + { + id = "ENERGY_DRINK_2", + name = "ENRGY Drink X92", + description = "Gain 2 action points.", + price = 250, + action_points = 2, + }, + { + id = "ENERGY_DRINK_3", + name = "ENRGY Drink X93", + description = "Gain 3 action points.", + price = 350, + action_points = 3, + }, +} + +for _, drink in ipairs(drinks) do + register_card(drink.id, { + name = l("cards." .. drink.id .. ".name", drink.name), + description = string.format( + l("cards." .. drink.id .. ".description","%s\n\n%s"), + highlight("One-Time"), + drink.description + ), + tags = { "UTIL", "_ACT_0" }, + max_level = 0, + color = COLOR_ORANGE, + need_target = false, + does_consume = true, + point_cost = 0, + price = drink.price, + callbacks = { + on_cast = function(ctx) + player_give_action_points(drink.action_points) + return nil + end + }, + test = function () + return assert_chain({ + function() return assert_card_present(drink.id) end, + function() return assert_cast_card(drink.id) end, + function() + if get_fight().current_points ~= 3 + drink.action_points then + return "Expected " .. tostring(3 + drink.action_points) .. " points, got " .. get_fight().current_points + end + end, + }) + end + }) +end \ No newline at end of file diff --git a/assets/scripts/equipment/consumeable/flash_bang.lua b/assets/scripts/equipment/consumeable/flash_bang.lua index 6978664..daf1a69 100644 --- a/assets/scripts/equipment/consumeable/flash_bang.lua +++ b/assets/scripts/equipment/consumeable/flash_bang.lua @@ -1,7 +1,7 @@ register_card("FLASH_BANG", { name = l("cards.FLASH_BANG.name", "Flash Bang"), description = l("cards.FLASH_BANG.description", highlight("One-Time") .. "\n\nInflicts " .. highlight("Blinded") .. " on the target, causing them to deal less damage."), - tags = { "CC" }, + tags = { "CC", "_ACT_0" }, max_level = 0, color = COLOR_PURPLE, need_target = true, diff --git a/assets/scripts/equipment/consumeable/flash_shield.lua b/assets/scripts/equipment/consumeable/flash_shield.lua index 79e9f84..12f91f4 100644 --- a/assets/scripts/equipment/consumeable/flash_shield.lua +++ b/assets/scripts/equipment/consumeable/flash_shield.lua @@ -4,7 +4,7 @@ register_card("FLASH_SHIELD", { l("cards.FLASH_SHIELD.description","%s\n\nDeploy a temporary shield. %s the next attack."), highlight("One-Time"), highlight("Negates") ), - tags = { "DEF" }, + tags = { "DEF", "_ACT_0" }, max_level = 0, color = COLOR_BLUE, need_target = false, diff --git a/assets/scripts/equipment/consumeable/nano_charger.lua b/assets/scripts/equipment/consumeable/nano_charger.lua index 912408d..1a37b59 100644 --- a/assets/scripts/equipment/consumeable/nano_charger.lua +++ b/assets/scripts/equipment/consumeable/nano_charger.lua @@ -4,7 +4,7 @@ register_card("NANO_CHARGER", { l("cards.NANO_CHARGER.description","%s\n\nSupercharge your next attack. Deals %s damage."), highlight("One-Time"), highlight("Double") ), - tags = { "BUFF" }, + tags = { "BUFF", "_ACT_0" }, max_level = 0, color = COLOR_RED, need_target = false, diff --git a/assets/scripts/equipment/consumeable/stim_pack.lua b/assets/scripts/equipment/consumeable/stim_pack.lua index a2cbced..5d014f3 100644 --- a/assets/scripts/equipment/consumeable/stim_pack.lua +++ b/assets/scripts/equipment/consumeable/stim_pack.lua @@ -1,7 +1,7 @@ register_card("STIM_PACK", { name = l("cards.STIM_PACK.name", "Stim Pack"), description = l("cards.STIM_PACK.description", highlight("One-Time") .. "\n\nRestores " .. highlight(5) .. " HP."), - tags = { "HEAL" }, + tags = { "HEAL", "_ACT_0" }, max_level = 0, color = COLOR_BLUE, need_target = false, diff --git a/assets/scripts/equipment/consumeable/ultra_flash_shield.lua b/assets/scripts/equipment/consumeable/ultra_flash_shield.lua new file mode 100644 index 0000000..9a21708 --- /dev/null +++ b/assets/scripts/equipment/consumeable/ultra_flash_shield.lua @@ -0,0 +1,61 @@ +register_card("ULTRA_FLASH_SHIELD", { + name = l("cards.ULTRA_FLASH_SHIELD.name", "Ultra Flash Shield"), + description = string.format( + l("cards.ULTRA_FLASH_SHIELD.description","%s\n\nDeploy a temporary shield. %s all attack this turn."), + highlight("One-Time"), highlight("Negates") + ), + tags = { "DEF", "_ACT_0" }, + max_level = 0, + color = COLOR_BLUE, + need_target = false, + does_consume = true, + point_cost = 3, + price = 250, + callbacks = { + on_cast = function(ctx) + give_status_effect("ULTRA_FLASH_SHIELD", ctx.caster, 1 + ctx.level) + return nil + end + } +}) + +register_status_effect("ULTRA_FLASH_SHIELD", { + name = l("status_effects.ULTRA_FLASH_SHIELD.name", "Ultra Flash Shield"), + description = l("status_effects.ULTRA_FLASH_SHIELD.description", "Negates all attacks."), + look = "UFS", + foreground = COLOR_BLUE, + can_stack = false, + decay = DECAY_ALL, + rounds = 1, + order = 100, + callbacks = { + on_damage_calc = function(ctx) + if ctx.simulated then + return ctx.damage + end + + 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("ULTRA_FLASH_SHIELD", 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 + + damage = deal_damage(dummy, PLAYER_ID, 2) + if damage ~= 0 then + return "Expected 0 damage, got " .. damage + end + end + }) + end +}) diff --git a/assets/scripts/equipment/permanents/arm_mounted_gun.lua b/assets/scripts/equipment/permanents/arm_mounted_gun.lua index 00b12d6..7b0242e 100644 --- a/assets/scripts/equipment/permanents/arm_mounted_gun.lua +++ b/assets/scripts/equipment/permanents/arm_mounted_gun.lua @@ -29,7 +29,7 @@ register_card("ARM_MOUNTED_GUN", { price = -1, callbacks = { on_cast = function(ctx) - deal_damage(ctx.caster, ctx.target, 7 + ctx.level * 3) + deal_damage_card(ctx.caster, ctx.guid, ctx.target, 7 + ctx.level * 3) return nil end }, diff --git a/assets/scripts/equipment/permanents/basic_hand_weapons.lua b/assets/scripts/equipment/permanents/basic_hand_weapons.lua index 48dfd74..1b41bc9 100644 --- a/assets/scripts/equipment/permanents/basic_hand_weapons.lua +++ b/assets/scripts/equipment/permanents/basic_hand_weapons.lua @@ -9,6 +9,7 @@ HAND_WEAPONS = { base_damage = 2, base_cards = 3, tags = { "ATK", "M", "T", "HND" }, + event_tags = { "_ACT_0" }, additional_cards = { "KNOCK_OUT" }, price = 80 }, @@ -19,6 +20,7 @@ HAND_WEAPONS = { base_damage = 3, base_cards = 3, tags = { "ATK", "M", "T", "HND" }, + event_tags = { "_ACT_0" }, additional_cards = { "VIBRO_OVERCLOCK" }, price = 180 }, @@ -29,6 +31,7 @@ HAND_WEAPONS = { base_damage = 4, base_cards = 3, tags = { "ATK", "R", "T", "HND" }, + event_tags = { "_ACT_1" }, additional_cards = { "LZR_OVERCHARGE" }, price = 280 }, @@ -39,6 +42,7 @@ HAND_WEAPONS = { base_damage = 5, base_cards = 3, tags = { "ATK", "R", "T", "HND" }, + event_tags = { "_ACT_1" }, additional_cards = { "HAR_BURST", "TARGET_PAINTER" }, price = 380 } @@ -61,7 +65,7 @@ for _, weapon in pairs(HAND_WEAPONS) do price = 0, callbacks = { on_cast = function(ctx) - deal_damage(ctx.caster, ctx.target, weapon.base_damage + ctx.level * 3) + deal_damage_card(ctx.caster, ctx.guid, ctx.target, weapon.base_damage + ctx.level * 3) return nil end }, @@ -117,7 +121,7 @@ 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) + 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. diff --git a/assets/scripts/equipment/permanents/combat_glasses.lua b/assets/scripts/equipment/permanents/combat_glasses.lua new file mode 100644 index 0000000..7e81b33 --- /dev/null +++ b/assets/scripts/equipment/permanents/combat_glasses.lua @@ -0,0 +1,22 @@ +register_artifact("COMBAT_GLASSES", { + name = "Combat Glasses", + description = "Whenever you play a " .. highlight("Ranged (R)") .. " card, deal " .. highlight("1 additional damage"), + tags = { "_ACT_0" }, + price = 100, + order = 0, + callbacks = { + on_damage_calc = function(ctx) + local card = get_card(ctx.card) + if card ~= nil then + if table.contains(card.tags, "R") and ctx.source == ctx.owner and ctx.target ~= ctx.owner then + return ctx.damage + 1 + end + end + return ctx.damage + end + }, + test = function() + give_card(HAND_WEAPONS[2].id, PLAYER_ID) + return assert_cast_damage(HAND_WEAPONS[2].id, HAND_WEAPONS[2].base_damage + 1) + end +}); diff --git a/assets/scripts/equipment/permanents/combat_gloves.lua b/assets/scripts/equipment/permanents/combat_gloves.lua new file mode 100644 index 0000000..b998b31 --- /dev/null +++ b/assets/scripts/equipment/permanents/combat_gloves.lua @@ -0,0 +1,22 @@ +register_artifact("COMBAT_GLOVES", { + name = "Combat Gloves", + description = "Whenever you play a " .. highlight("Meele (M)") .. " card, deal " .. highlight("1 additional damage"), + tags = { "_ACT_0" }, + price = 100, + order = 0, + callbacks = { + on_damage_calc = function (ctx) + local card = get_card(ctx.card) + if card ~= nil then + if table.contains(card.tags, "M") and ctx.source == ctx.owner and ctx.target ~= ctx.owner then + return ctx.damage + 1 + end + end + return ctx.damage + end + }, + test = function () + give_card("MELEE_HIT", PLAYER_ID) + return assert_cast_damage("MELEE_HIT", 2) + end +}); diff --git a/assets/scripts/equipment/permanents/interval_juicer.lua b/assets/scripts/equipment/permanents/interval_juicer.lua new file mode 100644 index 0000000..7c685d9 --- /dev/null +++ b/assets/scripts/equipment/permanents/interval_juicer.lua @@ -0,0 +1,15 @@ +register_artifact("INTERVA_JUICER", { + name = "Interval Juicer", + description = highlight("Heal 2") .. " at the beginning of combat", + tags = { "_ACT_0" }, + price = 200, + order = 0, + callbacks = { + on_player_turn = function(ctx) + if ctx.owner == PLAYER_ID and ctx.round == 0 then + heal(PLAYER_ID, PLAYER_ID, 2) + end + return nil + end + } +}); diff --git a/assets/scripts/equipment/permanents/melee_hit.lua b/assets/scripts/equipment/permanents/melee_hit.lua index 468938f..fbf3052 100644 --- a/assets/scripts/equipment/permanents/melee_hit.lua +++ b/assets/scripts/equipment/permanents/melee_hit.lua @@ -12,7 +12,7 @@ register_card("MELEE_HIT", { price = -1, callbacks = { on_cast = function(ctx) - deal_damage(ctx.caster, ctx.target, 1 + ctx.level) + deal_damage_card(ctx.caster, ctx.guid, ctx.target, 1 + ctx.level) return nil end }, diff --git a/assets/scripts/equipment/permanents/portable_buffer.lua b/assets/scripts/equipment/permanents/portable_buffer.lua new file mode 100644 index 0000000..b54386e --- /dev/null +++ b/assets/scripts/equipment/permanents/portable_buffer.lua @@ -0,0 +1,13 @@ +register_artifact("PORTABLE_BUFFER", { + name = "PRTBL Buffer", + description = "Start each turn with 1 " .. highlight("Block"), + tags = { "_ACT_0" }, + price = 100, + order = 0, + callbacks = { + on_player_turn = function(ctx) + give_status_effect("BLOCK", PLAYER_ID, 1) + return nil + end + } +}); diff --git a/assets/scripts/equipment/permanents/speed_enhancer.lua b/assets/scripts/equipment/permanents/speed_enhancer.lua new file mode 100644 index 0000000..220275e --- /dev/null +++ b/assets/scripts/equipment/permanents/speed_enhancer.lua @@ -0,0 +1,15 @@ +register_artifact("SPEED_ENHANCER", { + name = "Speed Enhancer", + description = "Start with a additional card at the beginning of combat.", + tags = { "_ACT_0" }, + price = 100, + order = 0, + callbacks = { + on_player_turn = function(ctx) + if ctx.owner == PLAYER_ID and ctx.round == 0 then + player_draw_card(1) + end + return nil + end + } +}); diff --git a/assets/scripts/events/act_0/enemies.lua b/assets/scripts/events/act_0/enemies.lua index 413c3e8..51cb6e4 100644 --- a/assets/scripts/events/act_0/enemies.lua +++ b/assets/scripts/events/act_0/enemies.lua @@ -6,12 +6,15 @@ 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"}, + tags = {"_ACT_0_FIGHT"}, choices = { { description = "Fight!", callback = function() add_actor_by_enemy("RUST_MITE") + if math.random() < 0.25 then + add_actor_by_enemy("RUST_MITE") + end return GAME_STATE_FIGHT end } @@ -27,12 +30,36 @@ 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"}, + tags = {"_ACT_0_FIGHT"}, choices = { { description = "Fight!", callback = function() add_actor_by_enemy("CLEAN_BOT") + if math.random() < 0.25 then + add_actor_by_enemy("CLEAN_BOT") + end + return GAME_STATE_FIGHT + end + } + } +}) + +register_event("CYBER_SPIDER", { + name = "What is this thing at the ceiling?", + description = [[ +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"}, + choices = { + { + description = "Fight!", + callback = function() + add_actor_by_enemy("CYBER_SPIDER") + if math.random() < 0.25 then + add_actor_by_enemy("CYBER_SPIDER") + end return GAME_STATE_FIGHT end } diff --git a/assets/scripts/events/act_0/other.lua b/assets/scripts/events/act_0/other.lua new file mode 100644 index 0000000..7532159 --- /dev/null +++ b/assets/scripts/events/act_0/other.lua @@ -0,0 +1,236 @@ +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"), + 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) + end + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) + +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"), + callback = function() + local possible = fun.iter(find_cards_by_tags({ "_ACT_0" })) + :filter(function(card) + return card.does_consume + end):totable() + local choosen = choose_weighted_by_price(possible) + if choosen then + give_card(choosen, PLAYER_ID) + deal_damage(PLAYER_ID, PLAYER_ID, 2, true) + end + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) + +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("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", + description = [[ +You find an old vending machine, it seems to be still working. You can either pay 20 Gold to get 5 HP or leave. + ]], + tags = { "_ACT_0" }, + choices = { + { + description = "Pay " .. highlight_warn("20 Gold") .. " " .. highlight_success("Gain 5 HP"), + callback = function() + if get_actor(PLAYER_ID).gold < 20 then + return nil + end + give_player_gold(-20) + heal(PLAYER_ID, PLAYER_ID, 5) + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) + +register_event("MAX_LIFE_ACT_0", { + name = "Symbiotic Parasite", + description = [[ +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" }, + choices = { + { + description = "Accept it! " .. highlight_success("Gain 5 Max HP"), + callback = function() + actor_add_max_hp(PLAYER_ID, 5) + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) + +register_event("GAMBLE_1_ACT_0", { + name = "Electro Barrier", + description = [[ +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"), + 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 + local choosen = choose_weighted_by_price(possible_artifacts) + if choosen then + give_artifact(choosen, PLAYER_ID) + end + choosen = choose_weighted_by_price(possible_consumeables) + if choosen then + give_card(choosen, PLAYER_ID) + end + else + deal_damage(PLAYER_ID, PLAYER_ID, 5, true) + end + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) + +register_event("UPRAGDE_CARD_ACT_0", { + name = "Upgrade Station", + description = [[ +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"), + 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) + local instance = get_card_instance(guid) + + return instance.level < type.max_level + end) + :totable() + + if #cards == 0 then + return nil + end + + local choosen = cards[math.random(#cards)] + upgrade_card(choosen) + + return nil + end + }, + { + description = "Leave!", + callback = function() + return nil + end + } + } +}) \ No newline at end of file diff --git a/assets/scripts/events/base/merchant.lua b/assets/scripts/events/base/merchant.lua index 5d445d7..a0cc16c 100644 --- a/assets/scripts/events/base/merchant.lua +++ b/assets/scripts/events/base/merchant.lua @@ -8,6 +8,7 @@ The merchant is a tall, lanky figure draped in a long, tattered coat made of pla 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", "_ACT_1", "_ACT_2", "_ACT_3"}, choices = { { description = "Trade", diff --git a/assets/scripts/story_teller/act_0.lua b/assets/scripts/story_teller/act_0.lua index 8515b0f..e320112 100644 --- a/assets/scripts/story_teller/act_0.lua +++ b/assets/scripts/story_teller/act_0.lua @@ -1,13 +1,22 @@ - register_story_teller("ACT_0", { +register_story_teller("_ACT_0", { active = function() - if #get_event_history() < 5 then + if #get_event_history() <= 6 then return 1 end return 0 end, decide = function() - local possible = find_events_by_tags({"ACT_0"}) local history = get_event_history() + local possible = { } + + -- every 3 events, play a non-combat event + if #get_event_history() % 2 == 0 then + possible = find_events_by_tags({ "_ACT_0" }) + else + possible = find_events_by_tags({ "_ACT_0_FIGHT" }) + end + + print(#get_event_history()) -- filter out events by id that have already been played possible = fun.iter(possible):filter(function(event) @@ -16,11 +25,30 @@ -- fallback for now if #possible == 0 then - possible = find_events_by_tags({"ACT_0"}) + possible = find_events_by_tags({ "_ACT_0" }) 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 + + 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 }) diff --git a/docs/LUA_API_DOCS.md b/docs/LUA_API_DOCS.md index 2ebf76f..720d4f0 100644 --- a/docs/LUA_API_DOCS.md +++ b/docs/LUA_API_DOCS.md @@ -165,18 +165,6 @@ text_italic(value : any) -> string -
text_red
- -Makes the text colored red. - -**Signature:** - -``` -text_red(value : any) -> string -``` - -
-
text_underline
Makes the text underlined. @@ -849,6 +837,18 @@ deal_damage(source : guid, target : guid, damage : number, (optional) flat : boo
+
deal_damage_card
+ +Deal damage from one source to a target from a card. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt. + +**Signature:** + +``` +deal_damage_card(source : guid, card : guid, target : guid, damage : number, (optional) flat : boolean) -> number +``` + +
+
deal_damage_multi
Deal damage to multiple enemies from one source. If flat is true the damage can't be modified by status effects or artifacts. Returns a array of damages for each actor hit. diff --git a/game/lua.go b/game/lua.go index c252885..84b6fb8 100644 --- a/game/lua.go +++ b/game/lua.go @@ -127,12 +127,6 @@ fun = require "fun" return 1 })) - d.Function("text_red", "Makes the text colored red.", "string", "value : any") - l.SetGlobal("text_red", l.NewFunction(func(state *lua.LState) int { - state.Push(lua.LString("\x1b[38;5;9m" + luhelp2.ToString(state.Get(1), mapper))) - return 1 - })) - d.Function("text_bg", "Makes the text background colored. Takes hex values like #ff0000.", "string", "color : string", "value : any") l.SetGlobal("text_bg", l.NewFunction(func(state *lua.LState) int { state.Push(lua.LString(removeAnsiReset(lipgloss.NewStyle().Background(lipgloss.Color(luhelp2.ToString(state.Get(1), mapper))).Render(luhelp2.ToString(state.Get(2), mapper))))) @@ -184,7 +178,8 @@ fun = require "fun" session.log.Printf("[LUA :: %d %s] %s \n", dbg.CurrentLine, dbg.Source, strings.Join(lo.Map(make([]any, state.GetTop()), func(_ any, index int) string { val := state.Get(1 + index) - return luhelp2.ToString(val, mapper) + str := luhelp2.ToString(val, mapper) + return str }), " ")) return 0 @@ -510,9 +505,19 @@ fun = require "fun" d.Function("deal_damage", "Deal damage from one source to a target. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt.", "number", "source : guid", "target : guid", "damage : number", "(optional) flat : boolean") l.SetGlobal("deal_damage", l.NewFunction(func(state *lua.LState) int { if state.GetTop() == 3 { - state.Push(lua.LNumber(session.DealDamage(state.ToString(1), state.ToString(2), int(state.ToNumber(3)), false))) + state.Push(lua.LNumber(session.DealDamage(state.ToString(1), "", state.ToString(2), int(state.ToNumber(3)), false))) + } else { + state.Push(lua.LNumber(session.DealDamage(state.ToString(1), "", state.ToString(2), int(state.ToNumber(3)), bool(state.ToBool(4))))) + } + return 1 + })) + + d.Function("deal_damage_card", "Deal damage from one source to a target from a card. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt.", "number", "source : guid", "card : guid", "target : guid", "damage : number", "(optional) flat : boolean") + l.SetGlobal("deal_damage_card", l.NewFunction(func(state *lua.LState) int { + if state.GetTop() == 4 { + state.Push(lua.LNumber(session.DealDamage(state.ToString(1), state.ToString(2), state.ToString(3), int(state.ToNumber(4)), false))) } else { - state.Push(lua.LNumber(session.DealDamage(state.ToString(1), state.ToString(2), int(state.ToNumber(3)), bool(state.ToBool(4))))) + state.Push(lua.LNumber(session.DealDamage(state.ToString(1), state.ToString(2), state.ToString(3), int(state.ToNumber(4)), bool(state.ToBool(5))))) } return 1 })) @@ -547,9 +552,9 @@ fun = require "fun" } if state.GetTop() == 3 { - state.Push(luhelp2.ToLua(state, session.DealDamageMulti(state.ToString(1), guids, int(state.ToNumber(3)), false))) + state.Push(luhelp2.ToLua(state, session.DealDamageMulti(state.ToString(1), "", guids, int(state.ToNumber(3)), false))) } else { - state.Push(luhelp2.ToLua(state, session.DealDamageMulti(state.ToString(1), guids, int(state.ToNumber(3)), bool(state.ToBool(4))))) + state.Push(luhelp2.ToLua(state, session.DealDamageMulti(state.ToString(1), "", guids, int(state.ToNumber(3)), bool(state.ToBool(4))))) } return 1 })) diff --git a/game/session.go b/game/session.go index fa4c8cc..d21ff99 100644 --- a/game/session.go +++ b/game/session.go @@ -838,6 +838,14 @@ func (s *Session) GetRandomCard(maxGold int) string { // AddMerchantArtifact adds another artifact to the wares of the merchant. func (s *Session) AddMerchantArtifact() { if val := s.GetRandomArtifact(s.GetMerchantGoldMax()); len(val) > 0 { + // Don't add duplicates + if lo.SomeBy(s.GetPlayer().Artifacts.ToSlice(), func(guid string) bool { + a, _ := s.GetArtifact(guid) + return a.ID == val + }) { + return + } + s.merchant.Artifacts = append(s.merchant.Artifacts, val) } } @@ -1593,7 +1601,7 @@ func (s *Session) UpgradeRandomCard(owner string) bool { // // DealDamage deals damage to a target. If flat is true it will not trigger any callbacks which modify the damage. -func (s *Session) DealDamage(source string, target string, damage int, flat bool) int { +func (s *Session) DealDamage(source string, card string, target string, damage int, flat bool) int { if _, ok := s.actors[source]; !ok { return 0 } @@ -1615,7 +1623,7 @@ func (s *Session) DealDamage(source string, target string, damage int, flat bool reducer, float64(damage), "damage", - CreateContext("source", source, "target", target, "damage", damage)), + CreateContext("source", source, "card", card, "target", target, "damage", damage)), ) } @@ -1713,9 +1721,9 @@ func (s *Session) SimulateDealDamage(source string, target string, damage int, f // DealDamageMulti will deal damage to multiple targets and return the amount of damage dealt to each target. // If flat is true it will not trigger any OnDamageCalc callbacks which modify the damage. -func (s *Session) DealDamageMulti(source string, targets []string, damage int, flat bool) []int { +func (s *Session) DealDamageMulti(source string, card string, targets []string, damage int, flat bool) []int { return lo.Map(targets, func(guid string, index int) int { - return s.DealDamage(source, guid, damage, flat) + return s.DealDamage(source, card, guid, damage, flat) }) } diff --git a/ui/components/card.go b/ui/components/card.go index 3d4c800..66349d4 100644 --- a/ui/components/card.go +++ b/ui/components/card.go @@ -24,6 +24,10 @@ func HalfCard(session *game.Session, guid string, active bool, baseHeight int, m cardState := session.GetCardState(guid) pointText := strings.Repeat("•", card.PointCost) + if card.PointCost == 0 { + pointText = "FREE" + } + tagsText := strings.Join(card.PublicTags(), ", ") cardCol, _ := colorful.Hex(card.Color) diff --git a/ui/menus/gameview/gameview.go b/ui/menus/gameview/gameview.go index 7454173..49dd741 100644 --- a/ui/menus/gameview/gameview.go +++ b/ui/menus/gameview/gameview.go @@ -18,6 +18,7 @@ import ( zone "github.com/lrstanley/bubblezone" "github.com/samber/lo" "strings" + "time" ) const ( @@ -244,10 +245,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } - currentState := m.Session.GetGameState() - currentEvent := m.Session.GetEventID() - - switch currentState { + switch m.Session.GetGameState() { case game.GameStateFight: case game.GameStateMerchant: m.merchant, cmd = m.merchant.Update(msg) @@ -263,59 +261,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Show "New Artifacts" / "New Cards" if there are any. // - if currentState != m.lastGameState || currentEvent != m.lastEvent { - diff := m.BeforeStateSwitch.Diff(m.Session) - - m.BeforeStateSwitch = m.Session.MarkState() - m.lastGameState = currentState - m.lastEvent = currentEvent - - if len(diff) > 0 { - artifacts := lo.Map(lo.Filter(diff, func(item game.StateCheckpoint, index int) bool { - added, ok := item.Events[game.StateEventArtifactAdded].(game.StateEventArtifactAddedData) - return ok && !lo.SomeBy(diff, func(item game.StateCheckpoint) bool { - removed, ok := item.Events[game.StateEventArtifactRemoved].(game.StateEventArtifactRemovedData) - return ok && added.GUID == removed.GUID - }) - }), func(item game.StateCheckpoint, index int) string { - return components.ArtifactCard(m.Session, item.Events[game.StateEventArtifactAdded].(game.StateEventArtifactAddedData).GUID, 20, 45) - }) - - cards := lo.Map(lo.Filter(diff, func(item game.StateCheckpoint, index int) bool { - added, ok := item.Events[game.StateEventCardAdded].(game.StateEventCardAddedData) - return ok && !lo.SomeBy(diff, func(item game.StateCheckpoint) bool { - removed, ok := item.Events[game.StateEventCardRemoved].(game.StateEventCardRemovedData) - return ok && added.GUID == removed.GUID - }) - }), func(item game.StateCheckpoint, index int) string { - return components.HalfCard(m.Session, item.Events[game.StateEventCardAdded].(game.StateEventCardAddedData).GUID, false, 20, 20, false, 45, false) - }) - - var pushModels []tea.Model + // TODO: the state change is detected too late. It only registered if there is another input on the screen, + // TODO: which triggers this update function here. To avoid that I added a tick here, but this is not a good solution. + // TODO: revisit this in the future. - if len(cards) > 0 { - c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Cards", len(cards)), cards) - c.Size = m.Size - pushModels = append(pushModels, root.NewOnVisibleModel(c, func(model tea.Model) { - audio.Play("new_cards") - })) - } - - if len(artifacts) > 0 { - c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Artifacts", len(artifacts)), artifacts) - c.Size = m.Size - pushModels = append(pushModels, root.NewOnVisibleModel(c, func(model tea.Model) { - audio.Play("new_artifacts") - })) - } - - if len(pushModels) > 0 { - cmds = append(cmds, root.PushAll(pushModels...)) - } - } - - cmds = append(cmds, tea.ClearScreen) - } + var stateCmds []tea.Cmd + m, stateCmds = m.checkStateChange() + cmds = append(cmds, stateCmds...) + cmds = append(cmds, tea.Tick(time.Second/5, func(t time.Time) tea.Msg { + return "" + })) return m, tea.Batch(cmds...) } @@ -372,6 +327,69 @@ func (m Model) View() string { return fmt.Sprintf("Unknown State: %s", m.Session.GetGameState()) } +func (m Model) checkStateChange() (Model, []tea.Cmd) { + var cmds []tea.Cmd + + currentState := m.Session.GetGameState() + currentEvent := m.Session.GetEventID() + + if currentState != m.lastGameState || currentEvent != m.lastEvent { + diff := m.BeforeStateSwitch.Diff(m.Session) + + m.BeforeStateSwitch = m.Session.MarkState() + m.lastGameState = currentState + m.lastEvent = currentEvent + + if len(diff) > 0 { + artifacts := lo.Map(lo.Filter(diff, func(item game.StateCheckpoint, index int) bool { + added, ok := item.Events[game.StateEventArtifactAdded].(game.StateEventArtifactAddedData) + return ok && !lo.SomeBy(diff, func(item game.StateCheckpoint) bool { + removed, ok := item.Events[game.StateEventArtifactRemoved].(game.StateEventArtifactRemovedData) + return ok && added.GUID == removed.GUID + }) + }), func(item game.StateCheckpoint, index int) string { + return components.ArtifactCard(m.Session, item.Events[game.StateEventArtifactAdded].(game.StateEventArtifactAddedData).GUID, 20, 45) + }) + + cards := lo.Map(lo.Filter(diff, func(item game.StateCheckpoint, index int) bool { + added, ok := item.Events[game.StateEventCardAdded].(game.StateEventCardAddedData) + return ok && !lo.SomeBy(diff, func(item game.StateCheckpoint) bool { + removed, ok := item.Events[game.StateEventCardRemoved].(game.StateEventCardRemovedData) + return ok && added.GUID == removed.GUID + }) + }), func(item game.StateCheckpoint, index int) string { + return components.HalfCard(m.Session, item.Events[game.StateEventCardAdded].(game.StateEventCardAddedData).GUID, false, 20, 20, false, 45, false) + }) + + var pushModels []tea.Model + + if len(cards) > 0 { + c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Cards", len(cards)), cards) + c.Size = m.Size + pushModels = append(pushModels, root.NewOnVisibleModel(c, func(model tea.Model) { + audio.Play("new_cards") + })) + } + + if len(artifacts) > 0 { + c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Artifacts", len(artifacts)), artifacts) + c.Size = m.Size + pushModels = append(pushModels, root.NewOnVisibleModel(c, func(model tea.Model) { + audio.Play("new_artifacts") + })) + } + + if len(pushModels) > 0 { + cmds = append(cmds, root.PushAll(pushModels...)) + } + } + + cmds = append(cmds, tea.ClearScreen) + } + + return m, cmds +} + // // Actions // diff --git a/ui/menus/merchant/merchant.go b/ui/menus/merchant/merchant.go index f2c4ce3..c901640 100644 --- a/ui/menus/merchant/merchant.go +++ b/ui/menus/merchant/merchant.go @@ -214,7 +214,7 @@ func (m Model) View() string { var selectedItemLook string switch item := selectedItem.(type) { case *game.Artifact: - selectedItemLook = components.ArtifactCard(m.session, item.ID, 20, 20) + selectedItemLook = components.ArtifactCard(m.session, item.ID, 20, 30) canBuy = m.session.GetPlayer().Gold >= item.Price case *game.Card: selectedItemLook = components.HalfCard(m.session, item.ID, false, 20, 20, false, 0, false)