feat: artifact/card carousel, rework of game flow

This commit is contained in:
Daniel Schmidt 2024-01-10 23:13:49 +01:00
parent a77cf648b0
commit f59d0e1b06
73 changed files with 1151 additions and 1126 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ cpu*
heap*
profile*
file_index.json
__old/

View File

@ -11,6 +11,48 @@ function assert_chain(tests)
return nil
end
---assert_card_present asserts that the player's first card is of a certain type, returning an error message if not
---@param id type_id
---@return string|nil
function assert_card_present(id)
local cards = get_cards(PLAYER_ID)
if not cards[1] then
return "Card not in hand"
end
local card = get_card_instance(cards[1])
if card.type_id ~= id then
return "Card has wrong type: " .. card.type_id
end
return nil
end
---assert_cast_damage asserts that the player's first card deals a certain amount of damage, returning an error message if not
---@param id type_id
---@param dmg number
---@return string|nil
function assert_cast_damage(id, dmg)
local dummy = add_actor_by_enemy("DUMMY")
local cards = get_cards(PLAYER_ID)
if not cards[1] then
return "Card not in hand"
end
local card = get_card_instance(cards[1])
if card.type_id ~= id then
return "Card has wrong type: " .. card.type_id
end
cast_card(cards[1], dummy)
if get_actor(dummy).hp ~= 100 - dmg then
return "Expected " .. tostring(100 - dmg) .. " health, got " .. get_actor(dummy).hp
end
end
---assert_status_effect_count asserts that the player has a certain number of status effects, returning an error message if not
---@param count number
---@return string|nil

View File

@ -3,3 +3,131 @@
function highlight(val)
return text_underline(text_bold("[" .. tostring(val) .. "]"))
end
---highlight_warn some value with warning colors
---@param val any
function highlight_warn(val)
return text_underline(text_bold(text_red("[" .. tostring(val) .. "]")))
end
---choose_weighted chooses an item from a list of choices, with a weight for each item.
---@param choices table
---@param weights number[]
---@return string
function choose_weighted(choices, weights)
print(choices, weights)
local total_weight = 0
for _, weight in ipairs(weights) do
total_weight = total_weight + weight
end
local random = math.random() * total_weight
for i, weight in ipairs(weights) do
random = random - weight
if random <= 0 then
return choices[i]
end
end
return choices[#choices]
end
---table.contains check if a table contains an element.
function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end
---find_by_tags find all items with the given tags.
---@param items artifact|card
---@param tags string[]
function find_by_tags(items, tags)
local found = {}
for _, item in pairs(items) do
for _, tag in pairs(tags) do
if item.tags == nil then
goto continue
end
if not table.contains(item.tags, tag) then
goto continue
end
end
table.insert(found, item)
::continue::
end
return found
end
---find_artifacts_by_tags find all artifacts with the given tags.
---@param tags string[]
---@return artifact[]
function find_artifacts_by_tags(tags)
return find_by_tags(registered.artifact, tags)
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)
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)
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)
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()
)
end
---clear_cards_by_tag remove all cards with tag.
---@param tag string tag to remove
---@param excluded? table optional table of guids to exclude.
function clear_cards_by_tag(tag, excluded)
for _, guid in pairs(get_cards(PLAYER_ID)) do
if excluded and table.contains(excluded, guid) then
goto continue
end
local tags = get_card(guid).tags
if table.contains(tags, tag) then
remove_card(guid)
end
::continue::
end
end
---clear_artifacts_by_tag remove all artifacts with tag.
---@param tag string tag to remove
---@param excluded table optional table of guids to exclude.
function clear_artifacts_by_tag(tag, excluded)
for _, guid in pairs(get_artifacts(PLAYER_ID)) do
if excluded and table.contains(excluded, guid) then
goto continue
end
local tags = get_artifact(guid).tags
if table.contains(tags, tag) then
remove_artifact(guid)
end
::continue::
end
end

View File

@ -1,14 +0,0 @@
register_artifact("BAG_OF_HOLDING", {
name = "Bag of Holding",
description = "Start with a additional card at the beginning of combat.",
price = 50,
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
}
});

View File

@ -1,21 +0,0 @@
register_artifact("DEFLECTOR_SHIELD", {
name = "Deflector Shield",
description = "Gain 8 block at the start of combat.",
price = 50,
order = 0,
callbacks = {
on_player_turn = function(ctx)
if ctx.round == 0 then
give_status_effect("BLOCK", ctx.owner, 8)
end
return nil
end
},
test = function()
add_actor_by_enemy("DUMMY")
return assert_chain({
function () return assert_status_effect_count(1) end,
function () return assert_status_effect("BLOCK", 8) end
})
end
});

View File

@ -1,26 +0,0 @@
register_artifact("GIGANTIC_STRENGTH", {
name = "Stone Of Gigantic Strength",
description = "Double all damage dealt.",
price = 250,
order = 0,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage * 2
end
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
local hp_before = get_actor(dummy).hp
deal_damage(PLAYER_ID, dummy, 1)
local hp_after = get_actor(dummy).hp
if hp_after == hp_before - 2 then
return nil
end
return "Damage was not doubled. Before:" .. hp_before .. " After:" .. hp_after
end
})

View File

@ -1,22 +0,0 @@
register_artifact("GOLD_CONVERTER", {
name = "Gold Converter",
description = "Gain 10 extra gold for each killed enemy.",
price = 50,
order = 0,
callbacks = {
on_actor_die = function(ctx)
if ctx.owner == PLAYER_ID and ctx.owner == ctx.source then
give_player_gold(10)
end
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
deal_damage(PLAYER_ID, dummy, 10000)
if get_player().gold == 10 then
return nil
end
return "Expected 10 gold, got " .. get_player().gold
end
});

View File

@ -1,14 +0,0 @@
register_artifact("HOLY_GRAIL", {
name = "Holy Grail",
description = "At the start of each turn, heal for 2 HP for each card in your hand.",
price = 150,
order = 100, -- Evaluate late so that other draw artifacts have priority.
callbacks = {
on_player_turn = function(ctx)
local num_cards = #get_cards(ctx.owner)
local heal_amount = num_cards * 2
heal(ctx.owner, ctx.owner, heal_amount)
return nil
end
}
});

View File

@ -1,12 +0,0 @@
register_artifact("JUICY_FRUIT", {
name = "Juicy Fruit",
description = "Tastes good and boosts your HP.",
price = 80,
order = 0,
callbacks = {
on_pick_up = function(ctx)
actor_add_max_hp(ctx.owner, 10)
return nil
end
}
});

View File

@ -1,12 +0,0 @@
register_artifact("RADIANT_SEED", {
name = "Radiant Seed",
description = "A small glowing seed.",
price = 140,
order = 0,
callbacks = {
on_pick_up = function(ctx)
give_card("RADIANT_SEED", ctx.owner)
return nil
end
}
});

View File

@ -1,14 +0,0 @@
register_artifact("REPULSION_STONE", {
name = "Repulsion Stone",
description = "For each damage taken heal for 2",
price = 100,
order = 0,
callbacks = {
on_damage = function(ctx)
if ctx.target == ctx.owner then
heal(ctx.owner, ctx.owner, 2)
end
return nil
end
}
});

View File

@ -1,16 +0,0 @@
register_artifact("SHORT_RADIANCE", {
name = "Short Radiance",
description = "Apply 1 vulnerable at the start of combat.",
price = 50,
order = 0,
callbacks = {
on_player_turn = function(ctx)
if ctx.round == 0 then
each(function(val)
give_status_effect("VULNERABLE", val)
end, pairs(get_opponent_guids(ctx.owner)))
end
return nil
end
}
});

View File

@ -1,14 +0,0 @@
register_artifact("SPIKED_PLANT", {
name = "Spiked Plant",
description = "Deal 2 damage back to enemy attacks.",
price = 50,
order = 0,
callbacks = {
on_damage = function(ctx)
if ctx.source ~= ctx.owner and ctx.owner == ctx.target then
deal_damage(ctx.owner, ctx.source, 2)
end
return nil
end
}
});

View File

@ -1,22 +0,0 @@
register_card("BERSERKER_RAGE", {
name = "Berserker Rage",
description = "Gain " .. highlight("3 action points") .. ", but take 30% (-10% per level) of your HP as damage.",
state = function(ctx)
return "Gain " ..
highlight("3 action points") .. ", but take " .. highlight(tostring(30 - ctx.level * 10) .. "%") .. " (" ..
tostring(get_player().hp * (0.3 - ctx.level * 0.1)) .. ") of your HP as damage."
end,
tags = { "BUFF" },
max_level = 0,
color = "#d8a448",
need_target = false,
point_cost = 0,
price = 100,
callbacks = {
on_cast = function(ctx)
player_give_action_points(3)
deal_damage(ctx.caster, ctx.caster, get_player().hp * (0.3 - ctx.level * 0.1), true)
return nil
end
}
})

View File

@ -1,19 +0,0 @@
register_card("BLOCK", {
name = "Block",
description = "Shield yourself and gain 5 " .. highlight("block") .. ".",
state = function(ctx)
return "Shield yourself and gain " .. highlight(5 + ctx.level * 3) .. " block."
end,
tags = { "DEF" },
max_level = 1,
color = "#219ebc",
need_target = false,
point_cost = 1,
price = 40,
callbacks = {
on_cast = function(ctx)
give_status_effect("BLOCK", ctx.caster, 5 + ctx.level * 3)
return nil
end
}
})

View File

@ -1,64 +0,0 @@
register_card("BLOCK_SPIKES", {
name = "Block Spikes",
description = "Transforms " .. highlight("block") .. " to damage.",
state = function(ctx)
-- Fetch all BLOCK instances of owner
local blocks = fun.iter(pairs(get_actor_status_effects(ctx.owner))):map(get_status_effect_instance):filter(function(val)
return val.type_id == "BLOCK"
end):totable()
-- Sum stacks to get damage
local damage = fun.iter(pairs(blocks)):reduce(function(acc, val)
return acc + val.stacks
end, 0)
return "Transforms block to " .. highlight(damage) .. " damage."
end,
max_level = 0,
color = "#895cd6",
need_target = true,
point_cost = 1,
price = 100,
callbacks = {
on_cast = function(ctx)
-- Fetch all BLOCK instances of caster
local blocks = fun.iter(pairs(get_actor_status_effects(ctx.caster))):map(get_status_effect_instance):filter(function(val)
return val.type_id == "BLOCK"
end):totable()
-- Sum stacks to get damage
local damage = fun.iter(pairs(blocks)):reduce(function(acc, val)
return acc + val.stacks
end, 0)
if damage == 0 then
return "No block status effect present!"
end
-- Remove BLOCKs
fun.iter(pairs(blocks)):for_each(function(val)
remove_status_effect(val.guid)
end)
-- Deal Damage
deal_damage(ctx.caster, ctx.target, damage)
return nil
end
},
test = function ()
give_status_effect("BLOCK", PLAYER_ID, 10)
return assert_chain({
function () assert_status_effect_count(1) end,
function () assert_status_effect("BLOCK", 10) end,
function ()
local dummy = add_actor_by_enemy("DUMMY")
local cards = get_cards(PLAYER_ID)
cast_card(cards[1], dummy)
if get_actor(dummy).hp ~= 90 then
return "Expected 90 health, got " .. get_actor(dummy).hp
end
end
})
end
})

View File

@ -1,19 +0,0 @@
register_card("COMBINED_SHOT", {
name = "Combined Shot",
description = "Deal " .. highlight(5) .. " (+5 for each level) damage for each enemy.",
state = function(ctx)
return "Deal " .. highlight((5 + ctx.level * 5) * #get_opponent_guids(ctx.owner)) .. " damage for each enemy."
end,
tags = { "ATK" },
max_level = 1,
color = "#d8a448",
need_target = true,
point_cost = 1,
price = 150,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, (5 + ctx.level * 5) * #get_opponent_guids(ctx.owner))
return nil
end
}
})

View File

@ -1,19 +0,0 @@
register_card("FEAR", {
name = "Fear",
description = "Inflict " .. highlight("fear") .. " on the target, causing them to miss their next turn.",
state = function(ctx)
return nil
end,
tags = { "CC" },
max_level = 0,
color = "#725e9c",
need_target = true,
point_cost = 2,
price = 80,
callbacks = {
on_cast = function(ctx)
give_status_effect("FEAR", ctx.target)
return nil
end
}
})

View File

@ -1,23 +0,0 @@
register_card("RADIANT_SEED", {
name = "Radiant Seed",
description = "Inflict 10 (+2 for each upgrade) damage to all enemies, but also causes 5 (-2 for each upgrade) damage to the caster.",
state = function(ctx)
return "Inflict " .. highlight(10 + ctx.level * 2) .. " damage to all enemies, but also causes " .. highlight(5 - ctx.level * 2) ..
" damage to the caster."
end,
tags = { "ATK" },
max_level = 1,
color = "#82c93e",
need_target = false,
point_cost = 2,
price = 120,
callbacks = {
on_cast = function(ctx)
-- Deal damage to caster without any modifiers applying
deal_damage(ctx.caster, ctx.caster, 5 - ctx.level * 2, true)
-- Deal damage to opponents
deal_damage_multi(ctx.caster, get_opponent_guids(ctx.caster), 10 + ctx.level * 2)
return nil
end
}
})

View File

@ -1,19 +0,0 @@
register_card("RUPTURE", {
name = "Rupture",
description = "Inflict your enemy with " .. highlight("Vulnerable") .. ".",
state = function(ctx)
return "Inflict your enemy with " .. highlight(tostring(1 + ctx.level) .. " Vulnerable") .. "."
end,
tags = { "ATK" },
max_level = 3,
color = "#cf532d",
need_target = true,
point_cost = 1,
price = 30,
callbacks = {
on_cast = function(ctx)
give_status_effect("VULNERABLE", ctx.target, 1 + ctx.level)
return nil
end
}
})

View File

@ -1,47 +0,0 @@
register_card("SHIELD_BASH", {
name = "Shield Bash",
description = "Deal 4 (+2 for each upgrade) damage to the enemy and gain " .. highlight("block") ..
" status effect equal to the damage dealt.",
state = function(ctx)
return "Deal " .. highlight(4 + ctx.level * 2) .. " damage to the enemy and gain " .. highlight("block") ..
" status effect equal to the damage dealt."
end,
tags = { "ATK" },
max_level = 1,
color = "#ff5722",
need_target = true,
point_cost = 1,
price = 40,
callbacks = {
on_cast = function(ctx)
local damage = deal_damage(ctx.caster, ctx.target, 4 + ctx.level * 2)
give_status_effect("BLOCK", ctx.caster, damage)
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
local cards = get_cards(PLAYER_ID)
-- Check if the card is in the player's hand
if not cards[1] then
return "Card not in hand"
end
local card = get_card_instance(cards[1])
if card.type_id ~= "SHIELD_BASH" then
return "Card has wrong type: " .. card.type_id
end
cast_card(cards[1], dummy)
if get_actor(dummy).hp ~= 96 then
return "Expected 96 health, got " .. get_actor(dummy).hp
end
return assert_chain({
function () assert_status_effect_count(1) end,
function () assert_status_effect("BLOCK", 4) end
})
end
})

View File

@ -61,17 +61,16 @@ function text_bg(color, value) end
---@return string
function text_bold(value) end
--- Makes the text foreground colored. Takes hex values like #ff0000.
---@param color string
---@param value any
---@return string
function text_color(color, value) end
--- Makes the text italic.
---@param value any
---@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
@ -125,10 +124,14 @@ function get_event_history() end
---@return fight_state
function get_fight() end
--- Gets the number of stages cleared.
--- Gets the fight round.
---@return number
function get_fight_round() end
--- Gets the number of stages cleared.
---@return number
function get_stages_cleared() end
--- Checks if the event happened at least once.
---@param event_id type_id
---@return boolean
@ -170,6 +173,16 @@ function actor_add_hp(guid, amount) end
---@param amount number
function actor_add_max_hp(guid, amount) end
--- Sets the hp value of a actor to a number. This won't trigger any on_damage callbacks
---@param guid guid
---@param amount number
function actor_set_hp(guid, amount) end
--- Sets the max hp value of a actor to a number.
---@param guid guid
---@param amount number
function actor_set_max_hp(guid, amount) end
--- Creates a new enemy fighting against the player. Example ``add_actor_by_enemy("RUST_MITE")``.
---@param enemy_guid type_id
---@return string
@ -218,6 +231,11 @@ function get_artifact(id) end
---@return artifact_instance
function get_artifact_instance(guid) end
--- Returns all the artifacts guids from the given actor.
---@param actor_guid string
---@return guid[]
function get_artifacts(actor_guid) end
--- Gives a actor a artifact. Returns the guid of the newly created artifact.
---@param type_id type_id
---@param actor guid
@ -316,7 +334,7 @@ function upgrade_random_card(actor_guid) end
-- Damage & Heal
-- #####################################
--- Deal damage to a enemy from one source. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt.
--- 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.
---@param source guid
---@param target guid
---@param damage number
@ -338,6 +356,14 @@ function deal_damage_multi(source, targets, damage, flat) end
---@param amount number
function heal(source, target, amount) end
--- Simulate damage from a source to a target. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that would be dealt.
---@param source guid
---@param target guid
---@param damage number
---@param flat? boolean
---@return number
function simulate_deal_damage(source, target, damage, flat) end
-- #####################################
-- Player Operations
-- #####################################

View File

@ -4,6 +4,7 @@
---@field id? type_id
---@field name? string
---@field description? string
---@field tags? string[]
---@field price? number
---@field order? number
---@field callbacks? callbacks

View File

@ -11,6 +11,7 @@
---@field level? number
---@field tags? string[]
---@field damage? number
---@field simulated? boolean
---@field heal? number
---@field stacks? number
---@field round? number

View File

@ -20,6 +20,6 @@
---@field description string
---@field choices event_choice[]
---@field on_enter? fun(ctx:event_on_enter_ctx):nil
---@field on_end fun(ctx:event_choice_ctx):next_game_state|nil
---@field on_end? fun(ctx:event_choice_ctx):next_game_state|nil
---@field test? fun():nil|string
---@field base_game? boolean

View File

@ -1,7 +1,7 @@
---@meta
---@class story_teller
---@field id type_id
---@field active fun():next_game_state
---@field decide fun():number
---@field base_game boolean
---@field id? type_id
---@field active fun():number
---@field decide fun():next_game_state
---@field base_game? boolean

View File

@ -5,30 +5,30 @@ register_enemy("CLEAN_BOT", {
(* *)
)#(]],
color = "#32a891",
initial_hp = 25,
max_hp = 25,
initial_hp = 13,
max_hp = 13,
gold = 15,
intend = function(ctx)
local self = get_actor(ctx.guid)
if self.hp <= 8 then
return "Block " .. highlight(4)
if self.hp <= 4 then
return "Block " .. highlight(2)
end
return "Deal " .. highlight(7) .. " damage"
return "Deal " .. highlight(2) .. " damage"
end,
callbacks = {
on_player_turn = function(ctx)
local self = get_actor(ctx.guid)
if self.hp <= 8 then
give_status_effect("BLOCK", ctx.guid, 4)
if self.hp <= 4 then
give_status_effect("BLOCK", ctx.guid, 2)
end
end,
on_turn = function(ctx)
local self = get_actor(ctx.guid)
if self.hp > 8 then
deal_damage(ctx.guid, PLAYER_ID, 7)
if self.hp > 4 then
deal_damage(ctx.guid, PLAYER_ID, 2)
end
return nil

View File

@ -1,24 +1,24 @@
register_enemy("RUST_MITE", {
name = "Rust Mite",
description = "Loves to eat metal.",
name = l("enemies.RUST_MITE.name", "Rust Mite"),
description = l("enemies.RUST_MITE.description", "A small robot that eats metal."),
look = "/v\\",
color = "#e6e65a",
initial_hp = 22,
max_hp = 22,
initial_hp = 12,
max_hp = 12,
gold = 10,
intend = function(ctx)
if ctx.round % 4 == 0 then
return "Gather strength"
return "Load battery"
end
return "Deal " .. highlight(6) .. " damage"
return "Deal " .. highlight(simulate_deal_damage(ctx.guid, PLAYER_ID, 1)) .. " damage"
end,
callbacks = {
on_turn = function(ctx)
if ctx.round % 4 == 0 then
give_status_effect("RITUAL", ctx.guid)
give_status_effect("CHARGED", ctx.guid)
else
deal_damage(ctx.guid, PLAYER_ID, 6)
deal_damage(ctx.guid, PLAYER_ID, 1)
end
return nil
@ -26,21 +26,23 @@ register_enemy("RUST_MITE", {
}
})
register_status_effect("RITUAL", {
name = "Ritual",
description = "Gain strength each round",
look = "Rit",
foreground = "#bb3e03",
register_status_effect("CHARGED", {
name = l("status_effects.CHARGED.name", "Charged"),
description = l("status_effects.CHARGED.description", "Attacks will deal more damage per stack."),
look = "CHRG",
foreground = "#207BE7",
state = function(ctx)
return nil
return string.format(l("status_effects.CHARGED.state", "Attacks deal %s more damage"), highlight(ctx.stacks * 1))
end,
can_stack = true,
decay = DECAY_NONE,
rounds = 0,
callbacks = {
on_player_turn = function(ctx)
local guid = give_status_effect("STRENGTH", ctx.owner)
set_status_effect_stacks(guid, 3 + ctx.stacks)
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage + 1 * ctx.stacks
end
return nil
end
}
})

View File

@ -1,29 +0,0 @@
register_enemy("SAND_STALKER", {
name = "Sand Stalker",
description = "It waits for its prey to come closer.",
look = "( ° ° )",
color = "#8e4028",
initial_hp = 25,
max_hp = 25,
gold = 20,
intend = function(ctx)
if ctx.round % 4 == 0 then
return "Weaken your resolve"
end
return "Deal " .. highlight(7) .. " damage"
end,
callbacks = {
on_turn = function(ctx)
if ctx.round % 4 == 0 then
if deal_damage(ctx.guid, PLAYER_ID, 5) > 0 then
give_status_effect("WEAKEN", PLAYER_ID, 1)
end
else
deal_damage(ctx.guid, PLAYER_ID, 7)
end
return nil
end
}
})

View File

@ -1,70 +0,0 @@
register_enemy("SHADOW_ASSASSIN", {
name = "Shadow Assassin",
description = "A master of stealth and deception.",
look = "???",
color = "#6c5b7b",
initial_hp = 20,
max_hp = 20,
gold = 30,
intend = function(ctx)
local bleeds = fun.iter(pairs(get_actor_status_effects(PLAYER_ID)))
:map(get_status_effect_instance)
:filter(function(val)
return val.type_id == "BLEED"
end):totable()
if #bleeds > 0 then
return "Deal " .. highlight(10) .. " damage"
elseif ctx.round % 3 == 0 then
return "Inflict bleed"
else
return "Deal " .. highlight(5) .. " damage"
end
return nil
end,
callbacks = {
on_turn = function(ctx)
-- Count bleed stacks
local bleeds = fun.iter(pairs(get_actor_status_effects(PLAYER_ID)))
:map(get_status_effect_instance)
:filter(function(
val)
return val.type_id == "BLEED"
end):totable()
if #bleeds > 0 then
-- If bleeding do more damage
deal_damage(ctx.guid, PLAYER_ID, 10)
elseif ctx.round % 3 == 0 then
-- Try to bleed every 2 rounds with 3 dmg
if deal_damage(ctx.guid, PLAYER_ID, 3) > 0 then
give_status_effect("BLEED", PLAYER_ID, 2)
end
else
-- Just hit with 5 damage
deal_damage(ctx.guid, PLAYER_ID, 5)
end
return nil
end
}
})
register_status_effect("BLEED", {
name = "Bleed",
description = "Losing some red sauce.",
look = "Bld",
foreground = "#ff0000",
state = function(ctx)
return nil
end,
can_stack = false,
decay = DECAY_ONE,
rounds = 2,
callbacks = {
on_turn = function(ctx)
return nil
end
}
})

View File

@ -0,0 +1,3 @@
COLOR_GRAY = "#2f3e46"
COLOR_BLUE = "#219ebc"
COLOR_PURPLE = "#725e9c"

View File

@ -0,0 +1,25 @@
function add_found_artifact_event(id, picture, description, choice_description)
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),
choices = {
{
description_fn = function()
return "Take " .. registered.artifact[id].name .. "... (" .. choice_description .. ")"
end,
callback = function(ctx)
give_artifact(id, PLAYER_ID)
return nil
end
}, {
description = "Leave...",
callback = function()
return nil
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})
end

View File

@ -0,0 +1,39 @@
register_artifact("ARM_MOUNTED_GUN", {
name = "Arm Mounted Gun",
description = "Weapon that is mounted on your arm. It is very powerful.",
tags = { "ARM" },
price = 190,
order = 0,
callbacks = {
on_pick_up = function(ctx)
clear_artifacts_by_tag("ARM", { ctx.guid })
clear_cards_by_tag("ARM")
give_card("ARM_MOUNTED_GUN", PLAYER_ID)
return nil
end
}
});
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."),
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))
end,
tags = { "ATK", "R", "T", "ARM" },
max_level = 1,
color = COLOR_GRAY,
need_target = true,
does_exhaust = true,
point_cost = 3,
price = -1,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, 7 + ctx.level * 3)
return nil
end
},
test = function ()
return assert_cast_damage("ARM_MOUNTED_GUN", 7)
end
})

View File

@ -0,0 +1,129 @@
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 = {
{
id = "CROWBAR",
name = "Crowbar",
image = "red_room.jpg",
description = "A crowbar. It's a bit rusty, but it should still be useful!",
base_damage = 2,
base_cards = 3,
tags = { "ATK", "M", "T", "HND" },
additional_cards = { "KNOCK_OUT" },
price = 80
},
{
id = "VIBRO_KNIFE",
name = "VIBRO Knife",
description = "A VIBRO knife. Uses ultrasonic vibrations to cut through almost anything.",
base_damage = 3,
base_cards = 3,
tags = { "ATK", "M", "T", "HND" },
additional_cards = { "VIBRO_OVERCLOCK" },
price = 180
},
{
id = "LZR_PISTOL",
name = "LZR Pistol",
description = "A LZR pistol. Fires a concentrated beam of light.",
base_damage = 4,
base_cards = 3,
tags = { "ATK", "R", "T", "HND" },
additional_cards = { "LZR_OVERCHARGE" },
price = 280
},
{
id = "HAR_II",
name = "HAR-II",
description = "A HAR-II. A heavy assault rifle with a high rate of fire.",
base_damage = 5,
base_cards = 3,
tags = { "ATK", "R", "T", "HND" },
additional_cards = { "HAR_BURST", "TARGET_PAINTER" },
price = 380
}
}
HAND_WEAPONS_ARTIFACT_IDS = fun.iter(HAND_WEAPONS):map(function(w) return w.id end):totable()
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)),
state = function(ctx)
return string.format(l("cards." .. weapon.id .. ".state", "Use to deal %s damage."), highlight(weapon.base_damage + ctx.level * 3))
end,
tags = weapon.tags,
max_level = 3,
color = COLOR_GRAY,
need_target = true,
point_cost = 1,
price = 0,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, weapon.base_damage + ctx.level * 3)
return nil
end
},
test = function()
local dummy = add_actor_by_enemy("DUMMY")
local cards = get_cards(PLAYER_ID)
-- Check if the card is in the player's hand
if not cards[1] then
return "Card not in hand"
end
local card = get_card_instance(cards[1])
if card.type_id ~= weapon.id then
return "Card has wrong type: " .. card.type_id
end
cast_card(cards[1], dummy)
if get_actor(dummy).hp ~= 100 - weapon.base_damage then
return "Expected " .. tostring(100 - weapon.base_damage) .. " health, got " .. get_actor(dummy).hp
end
return nil
end
})
register_artifact(weapon.id, {
name = weapon.name,
description = weapon.description .. " Can be used in your hand.",
tags = weapon.tags,
price = weapon.price,
order = 0,
callbacks = {
on_pick_up = function(ctx)
clear_artifacts_by_tag("HND", { ctx.guid })
clear_cards_by_tag("HND")
-- add basic cards
for i = 1, weapon.base_cards do
give_card(weapon.id, PLAYER_ID)
end
-- add additional cards
if weapon.additional_cards then
for _, card in pairs(weapon.additional_cards) do
give_card(card, PLAYER_ID)
end
end
return nil
end
}
})
add_found_artifact_event(weapon.id, weapon.image, string.format("%s\n\n%s", weapon.description, hand_warning), registered.card[weapon.id].description)
end
---hand_weapon_event returns a random hand weapon event weighted by price.
---@return string
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

View File

@ -1,8 +1,28 @@
register_card("BLOCK", {
name = "Block",
description = "Shield yourself and gain 5 " .. highlight("block") .. ".",
state = function(ctx)
return "Shield yourself and gain " .. highlight(1 + ctx.level) .. " block."
end,
tags = { "DEF" },
max_level = 1,
color = COLOR_BLUE,
need_target = false,
point_cost = 1,
price = 40,
callbacks = {
on_cast = function(ctx)
give_status_effect("BLOCK", ctx.caster, 1 + ctx.level)
return nil
end
}
})
register_status_effect("BLOCK", {
name = "Block",
description = "Decreases incoming damage for each stack",
look = "Blk",
foreground = "#219ebc",
foreground = COLOR_BLUE,
state = function(ctx)
return "Takes " .. highlight(ctx.stacks) .. " less damage"
end,
@ -12,12 +32,16 @@ register_status_effect("BLOCK", {
order = 100,
callbacks = {
on_damage_calc = function(ctx)
if ctx.simulated then
return ctx.damage
end
if ctx.target == ctx.owner then
add_status_effect_stacks(ctx.guid, -ctx.damage)
return ctx.damage - ctx.stacks
end
return ctx.damage
end
end,
},
test = function()
return assert_chain({

View File

@ -0,0 +1,49 @@
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" },
max_level = 0,
color = COLOR_PURPLE,
need_target = true,
does_consume = true,
point_cost = 1,
price = -1,
callbacks = {
on_cast = function(ctx)
give_status_effect("FLASH_BANG", ctx.target)
return nil
end
}
})
register_status_effect("FLASH_BANG", {
name = l("cards.FLASH_BANG.name", "Blinded"),
description = l("cards.FLASH_BANG.description", "Causing " .. highlight("25%") .. " less damage."),
look = "FL",
foreground = COLOR_PURPLE,
state = function(ctx) return nil end,
can_stack = true,
decay = DECAY_ONE,
rounds = 1,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage * 0.75
end
return ctx.damage
end
},
test = function()
return assert_chain({
function() return assert_status_effect_count(1) end,
function() return assert_status_effect("FLASH_BANG", 1) end,
function ()
local dummy = add_actor_by_enemy("DUMMY")
local damage = deal_damage(PLAYER_ID, dummy, 10)
if damage ~= 7 then
return "Expected 7 damage, got " .. damage
end
end
})
end
})

View File

@ -0,0 +1,34 @@
register_card("KNOCK_OUT", {
name = l("cards.KNOCK_OUT.name", "Knock Out"),
description = l("cards.KNOCK_OUT.description", "Inflicts " .. highlight("Knock Out") .. " on the target, causing them to miss their next turn."),
tags = { "CC" },
max_level = 0,
color = COLOR_PURPLE,
need_target = true,
point_cost = 2,
price = -1,
callbacks = {
on_cast = function(ctx)
give_status_effect("KNOCK_OUT", ctx.target)
return nil
end
}
})
register_status_effect("KNOCK_OUT", {
name = l("status_effects.KNOCK_OUT.name", "Knock Out"),
description = l("status_effects.KNOCK_OUT.description", "Can't act"),
look = "K",
foreground = COLOR_PURPLE,
state = function(ctx)
return string.format(l("status_effects.KNOCK_OUT.state", "Can't act for %s turns"), highlight(ctx.stacks))
end,
can_stack = true,
decay = DECAY_ONE,
rounds = 1,
callbacks = {
on_turn = function(ctx)
return true
end
}
})

View File

@ -1,19 +1,22 @@
register_card("MELEE_HIT", {
name = l("cards.MELEE_HIT.name", "Melee Hit"),
description = l("cards.MELEE_HIT.description", "Use your bare hands to deal 5 (+3 for each upgrade) damage."),
description = l("cards.MELEE_HIT.description", "Use your bare hands to deal 1 (+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(5 + ctx.level * 3))
return string.format(l("cards.MELEE_HIT.state", "Use your bare hands to deal %s damage."), highlight(1 + ctx.level))
end,
tags = { "ATK" },
tags = { "ATK", "M", "HND" },
max_level = 1,
color = "#2f3e46",
color = COLOR_GRAY,
need_target = true,
point_cost = 1,
price = 30,
price = -1,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, 5 + ctx.level * 3)
deal_damage(ctx.caster, ctx.target, 1 + ctx.level)
return nil
end
}
},
test = function ()
return assert_cast_damage("MELEE_HIT", 1)
end
})

View File

@ -0,0 +1,33 @@
register_event("RUST_MITE", {
name = "Tasty metals...",
description = [[
You 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.
]],
tags = {"ACT_0"},
choices = {
{
description = "Fight!",
callback = function()
add_actor_by_enemy("RUST_MITE")
return GAME_STATE_FIGHT
end
}
}
})
register_event("CLEAN_BOT", {
name = "Corpse. Clean. Engage.",
description = [[
You come across a strange robot. It seems to be cleaning up the area. 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"},
choices = {
{
description = "Fight!",
callback = function()
add_actor_by_enemy("CLEAN_BOT")
return GAME_STATE_FIGHT
end
}
}
})

View File

@ -13,43 +13,38 @@ As you struggle to gather your bearings, you notice a blinking panel on the wall
**Shortly after you realize that you are not alone...**]],
choices = {
{
description = "Try to escape the facility before it finds you...",
description = "Try to find a weapon. " ..
highlight('Find meele weapon') .. " " .. highlight_warn("Take 4 damage"),
callback = function()
-- Try to escape
if math.random() < 0.5 then
set_event(stage_1_init_events[math.random(#stage_1_init_events)])
return GAME_STATE_EVENT
end
deal_damage(PLAYER_ID, PLAYER_ID, 4, true)
give_artifact(
choose_weighted_by_price(find_artifacts_by_tags({ "HND", "M" })), PLAYER_ID
)
-- Let OnEnd handle the state change
return nil
end
}, {
},
{
description = "Gather your strength and attack it!",
callback = function()
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
return nil
end
}
},
on_enter = function()
play_music("energetic_orthogonal_expansions")
-- Give the player it's start cards
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
give_card("MELEE_HIT", PLAYER_ID)
give_card("RUPTURE", PLAYER_ID)
give_card("BLOCK", PLAYER_ID)
give_card("BLOCK", PLAYER_ID)
give_card("BLOCK", PLAYER_ID)
give_artifact(random_artifact(150), PLAYER_ID)
end,
on_end = function()
actor_set_max_hp(PLAYER_ID, 10)
actor_set_hp(PLAYER_ID, 10)
give_card("BLOCK", PLAYER_ID)
give_card("BLOCK", PLAYER_ID)
return GAME_STATE_RANDOM
end
})

View File

@ -1,45 +0,0 @@
register_event("RAISING_THE_BAR", {
name = "Raising The Bar",
description = [[!!red_room.jpg
...]],
choices = {
{
description_fn = function()
return "Take Crowbar... (" .. registered.card["CROWBAR"].description .. ")"
end,
callback = function(ctx)
give_card("CROWBAR", PLAYER_ID)
return nil
end
}, {
description = "Leave...",
callback = function()
return nil
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})
register_card("CROWBAR", {
name = "Crowbar",
description = "Deal " .. highlight(22) .. " damage.",
state = function(ctx)
return nil
end,
max_level = 0,
color = "#f37b21",
need_target = true,
exhaust = true,
point_cost = 3,
price = -1,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, 22)
return nil
end
}
})

View File

@ -1,53 +0,0 @@
register_event("RECYCLE_DEVICE", {
name = "Talking Being",
description = [[!!artifact_chest.jpg
...]],
choices = {
{
description_fn = function()
return "Take Device... (" .. registered.card["RECYCLE"].description .. ")"
end,
callback = function(ctx)
give_card("RECYCLE", PLAYER_ID)
return nil
end
}, {
description = "Leave...",
callback = function()
return nil
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})
register_card("RECYCLE", {
name = "Recycle",
description = "Deal " ..
highlight(12) .. " damage. If " .. highlight("fatal") .. " upgrade random card. " .. highlight("Exhaust") ..
".",
state = function(ctx)
return nil
end,
max_level = 0,
color = "#d8a448",
need_target = true,
exhaust = true,
point_cost = 2,
price = -1,
callbacks = {
on_cast = function(ctx)
local op_before = #get_opponent_guids(ctx.caster)
deal_damage(ctx.caster, ctx.target, 12)
if op_before > #get_opponent_guids(ctx.caster) then
upgrade_random_card(ctx.caster)
end
return nil
end
}
})

View File

@ -1,59 +0,0 @@
register_event("TALKING_BEING", {
name = "Talking Being",
description = [[!!alien2.jpg
Suddenly, a massive vine with a gaping, tooth-filled maw emerges from the shadows. It towers over you, its presence imposing and otherworldly.
*"Hello, little one,"* the creature speaks in a deep, rumbling voice. *"I have been watching you. I see potential in you. I offer you a gift, something that will aid you on your journey."*
You take a step back, unsure if you can trust this strange being.
*"My blood,"* the creature says. *"It is not like any substance you have encountered before. It will grant you extraordinary abilities. But it demands a price. Some of your blood, in exchange for this gift."*
The creature assures you that there are dangers to wielding such power and that it will change you in ways you cannot yet imagine. But the offer is tempting. Will you accept and risk the unknown, or do you refuse and potentially miss out on a powerful ally?
**The decision is yours...**]],
choices = {
{
description_fn = function()
return "Offer blood... " .. text_italic("(deals " .. highlight(get_player().hp * 0.2) .. " damage)")
end,
callback = function(ctx)
actor_add_hp(PLAYER_ID, -get_player().hp * 0.2)
give_card("VINE_VOLLEY", PLAYER_ID)
give_card("VINE_VOLLEY", PLAYER_ID)
give_card("VINE_VOLLEY", PLAYER_ID)
return nil
end
}, {
description = "Leave...",
callback = function()
return nil
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})
register_card("VINE_VOLLEY", {
name = "Vine Volley",
description = "Deal " .. highlight("3x" .. tostring(3)) .. " damage.",
state = function(ctx)
return nil
end,
max_level = 0,
color = "#588157",
need_target = true,
point_cost = 1,
price = 100,
callbacks = {
on_cast = function(ctx)
deal_damage(ctx.caster, ctx.target, 3)
deal_damage(ctx.caster, ctx.target, 3)
deal_damage(ctx.caster, ctx.target, 3)
return nil
end
}
})

View File

@ -1,20 +0,0 @@
register_event("BIO_KINGDOM", {
name = "Bio Kingdom",
description = [[!!plant_enviroment.jpg
You finally find a way leading to the outside, and step out of the cryo facility into a world you no longer recognize.
The air is thick with humidity and the sounds of the jungle are overwhelming. Strange, mutated plants tower over you, their vines twisting and tangling around each other in a macabre dance. The colors of the leaves and flowers are sickly, a greenish hue that reminds you of illness rather than life. The ruins of buildings are visible in the distance, swallowed up by the overgrowth. You can hear the chirping and buzzing of insects, but it's mixed with something else - something that sounds almost like whispers or moans. The "jungle" seems to be alive, but not in any way that you would have imagined.]],
choices = {
{
description = "Go...",
callback = function()
set_event("MERCHANT")
return GAME_STATE_EVENT
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})

View File

@ -1,20 +0,0 @@
register_event("THE_CORE", {
name = "The Wasteland",
description = [[!!underground1.jpg
You finally find a way you thought would lead to the outside, only to discover that you're still inside the massive facility known as *"The Core."*
As you step out of the cryo facility, the eerie silence is broken by the sound of metal scraping against metal and distant whirring of malfunctioning machinery. The flickering lights and sparks from faulty wires cast a sickly glow on the cold metal walls. You realize that this place is not as deserted as you initially thought, and the unsettling feeling in your gut only grows stronger as you make your way through the dimly lit corridors, surrounded by the echoes of your own footsteps and the sound of flickering computer screens.]],
choices = {
{
description = "Go...",
callback = function()
set_event("MERCHANT")
return GAME_STATE_EVENT
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})

View File

@ -1,20 +0,0 @@
register_event("THE_WASTELAND", {
name = "The Wasteland",
description = [[!!dark_city1.jpg
You finally find a way leading to the outside, and with a deep breath, you step out into the unforgiving wasteland.
The scorching sun beats down on you as the sand whips against your skin, a reminder of the horrors that have befallen the world. In the distance, the remains of once-great cities jut up from the ground like jagged teeth, now nothing more than crumbling ruins. The air is thick with the acrid smell of decay and the oppressive silence is only broken by the occasional howl of some mutated creature. As you take your first steps into this new world, you realize that survival will not be easy, and that the journey ahead will be fraught with danger at every turn...]],
choices = {
{
description = "Go...",
callback = function()
set_event("MERCHANT")
return GAME_STATE_EVENT
end
}
},
on_end = function()
return GAME_STATE_RANDOM
end
})

View File

@ -1,18 +0,0 @@
register_status_effect("BURN", {
name = "Burning",
description = "The enemy burns and receives damage.",
look = "Brn",
foreground = "#d00000",
state = function(ctx)
return "Takes " .. highlight(ctx.stacks * 4) .. " damage per turn"
end,
can_stack = true,
decay = DECAY_ALL,
rounds = 1,
callbacks = {
on_turn = function(ctx)
deal_damage(ctx.guid, ctx.owner, ctx.stacks * 2, true)
return nil
end
}
})

View File

@ -1,17 +0,0 @@
register_status_effect("FEAR", {
name = "Fear",
description = "Can't act.",
look = "Fear",
foreground = "#bb3e03",
state = function(ctx)
return "Can't act for " .. highlight(ctx.stacks) .. " turns"
end,
can_stack = true,
decay = DECAY_ONE,
rounds = 1,
callbacks = {
on_turn = function(ctx)
return true
end
}
})

View File

@ -1,20 +0,0 @@
register_status_effect("STRENGTH", {
name = "Strength",
description = "Increases damage for each stack",
look = "Str",
foreground = "#d00000",
state = function(ctx)
return "Deal " .. highlight(ctx.stacks) .. " more damage"
end,
can_stack = true,
decay = DECAY_ALL,
rounds = 1,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage + ctx.stacks
end
return ctx.damage
end
}
})

View File

@ -1,20 +0,0 @@
register_status_effect("VULNERABLE", {
name = "Vulnerable",
description = "Increases received damage for each stack",
look = "Vur",
foreground = "#ffba08",
state = function(ctx)
return "Takes " .. highlight(ctx.stacks * 25) .. "% more damage"
end,
can_stack = true,
decay = DECAY_ONE,
rounds = 1,
callbacks = {
on_damage_calc = function(ctx)
if ctx.target == ctx.owner then
return ctx.damage * (1.0 + 0.25 * ctx.stacks)
end
return ctx.damage
end
}
})

View File

@ -1,20 +0,0 @@
register_status_effect("WEAKEN", {
name = "Weaken",
description = "Weakens damage for each stack",
look = "W",
foreground = "#ed985f",
state = function()
return "Deals " .. highlight(ctx.stacks * 2) .. " less damage"
end,
can_stack = true,
decay = DECAY_ALL,
rounds = 1,
callbacks = {
on_damage_calc = function(ctx)
if ctx.source == ctx.owner then
return ctx.damage - ctx.stacks * 2
end
return ctx.damage
end
}
})

View File

@ -0,0 +1,26 @@
register_story_teller("ACT_0", {
active = function()
if #get_event_history() < 5 then
return 1
end
return 0
end,
decide = function()
local possible = find_events_by_tags({"ACT_0"})
local history = get_event_history()
print("possible")
print(possible)
print("history")
print(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)
end):totable()
set_event(possible[math.random(#possible)].id)
return GAME_STATE_EVENT
end
})

View File

@ -1,38 +0,0 @@
stage_1_init_events = { "THE_CORE", "BIO_KINGDOM", "THE_WASTELAND" }
register_story_teller("STAGE_0", {
active = function(ctx)
if not had_events_any(stage_1_init_events) then
return 1
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage >= 3 then
-- If we didn't skip the pre-stage we get another artifact
set_event(create_artifact_choice({ random_artifact(get_merchant_gold_max()), random_artifact(get_merchant_gold_max()) }, {
description = [[As you explore the abandoned cryo facility, a feeling of dread washes over you. The facility is eerily quiet, with malfunctioning computers and flickering lights being the only signs of life. As you move through the winding corridors, you stumble upon a hidden door. It's almost as if the facility itself is trying to keep you from finding what lies beyond.
After some effort, you manage to open the door and find yourself in a small room. The room is dark, and you can barely make out a chest in the center of the room. As you approach it, the feeling of unease grows stronger. What secret artifact could be hidden inside this chest? Is it something that will aid you on your journey or something more sinister? You take a deep breath, steeling yourself for whatever you may find inside, and reach for the lid...]],
on_end = function()
set_event(stage_1_init_events[math.random(#stage_1_init_events)])
return GAME_STATE_EVENT
end
}))
return GAME_STATE_EVENT
end
-- Fight against rust mites or clean bots
local d = math.random(2)
if d == 1 then
add_actor_by_enemy("RUST_MITE")
elseif d == 2 then
add_actor_by_enemy("CLEAN_BOT")
end
return GAME_STATE_FIGHT
end
})

View File

@ -1,31 +0,0 @@
stage_1_bio_kingdom = {
fights = { { "RUST_MITE", "RUST_MITE", "RUST_MITE" }, { "SHADOW_ASSASSIN", "SHADOW_ASSASSIN" }, { "SHADOW_ASSASSIN" } }
}
register_story_teller("STAGE_1_BIO_KINGDOM", {
active = function(ctx)
if had_event("BIO_KINGDOM") then
return 1
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage == 10 then
-- BOSS
end
-- 10% chance to find a random artifact
if math.random() < 0.1 then
set_event(create_artifact_choice({ random_artifact(get_merchant_gold_max()), random_artifact(get_merchant_gold_max()) }))
end
local choice = stage_1_bio_kingdom.fights[math.random(#stage_1_bio_kingdom.fights)]
for _, v in ipairs(choice) do
add_actor_by_enemy(v)
end
return GAME_STATE_FIGHT
end
})

View File

@ -1,29 +0,0 @@
stage_1_the_core = { fights = { { "RUST_MITE", "RUST_MITE", "RUST_MITE" }, { "CLEAN_BOT", "CLEAN_BOT" } } }
register_story_teller("STAGE_1_THE_CORE", {
active = function(ctx)
if had_event("THE_CORE") then
return 1
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage == 10 then
-- BOSS
end
-- 10% chance to find a random artifact
if math.random() < 0.1 then
set_event(create_artifact_choice({ random_artifact(get_merchant_gold_max()), random_artifact(get_merchant_gold_max()) }))
end
local choice = stage_1_the_core.fights[math.random(#stage_1_the_core.fights)]
for _, v in ipairs(choice) do
add_actor_by_enemy(v)
end
return GAME_STATE_FIGHT
end
})

View File

@ -1,29 +0,0 @@
stage_1_the_wasteland = { fights = { { "SAND_STALKER" }, { "SAND_STALKER", "SAND_STALKER" } } }
register_story_teller("STAGE_1_THE_WASTELAND", {
active = function(ctx)
if had_event("THE_WASTELAND") then
return 1
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage == 10 then
-- BOSS
end
-- 10% chance to find a random artifact
if math.random() < 0.1 then
set_event(create_artifact_choice({ random_artifact(get_merchant_gold_max()), random_artifact(get_merchant_gold_max()) }))
end
local choice = stage_1_the_wasteland.fights[math.random(#stage_1_the_wasteland.fights)]
for _, v in ipairs(choice) do
add_actor_by_enemy(v)
end
return GAME_STATE_FIGHT
end
})

View File

@ -1,19 +0,0 @@
register_story_teller("STAGE_2", {
active = function(ctx)
if had_events_any(stage_1_init_events) and get_stages_cleared() > 10 then
return 2
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage == 20 then
-- BOSS
end
add_actor_by_enemy("DUMMY")
return GAME_STATE_FIGHT
end
})

View File

@ -1,19 +0,0 @@
register_story_teller("STAGE_3", {
active = function(ctx)
if had_events_any(stage_1_init_events) and get_stages_cleared() > 20 then
return 3
end
return 0
end,
decide = function(ctx)
local stage = get_stages_cleared()
if stage == 30 then
-- BOSS
end
add_actor_by_enemy("DUMMY")
return GAME_STATE_FIGHT
end
})

View File

@ -153,18 +153,6 @@ text_bold(value : any) -> string
</details>
<details> <summary><b><code>text_color</code></b> </summary> <br/>
Makes the text foreground colored. Takes hex values like #ff0000.
**Signature:**
```
text_color(color : string, value : any) -> string
```
</details>
<details> <summary><b><code>text_italic</code></b> </summary> <br/>
Makes the text italic.
@ -177,6 +165,18 @@ text_italic(value : any) -> string
</details>
<details> <summary><b><code>text_red</code></b> </summary> <br/>
Makes the text colored red.
**Signature:**
```
text_red(value : any) -> string
```
</details>
<details> <summary><b><code>text_underline</code></b> </summary> <br/>
Makes the text underlined.
@ -326,7 +326,7 @@ get_fight() -> fight_state
<details> <summary><b><code>get_fight_round</code></b> </summary> <br/>
Gets the number of stages cleared.
Gets the fight round.
**Signature:**
@ -336,6 +336,18 @@ get_fight_round() -> number
</details>
<details> <summary><b><code>get_stages_cleared</code></b> </summary> <br/>
Gets the number of stages cleared.
**Signature:**
```
get_stages_cleared() -> number
```
</details>
<details> <summary><b><code>had_event</code></b> </summary> <br/>
Checks if the event happened at least once.
@ -441,6 +453,30 @@ actor_add_max_hp(guid : guid, amount : number) -> None
</details>
<details> <summary><b><code>actor_set_hp</code></b> </summary> <br/>
Sets the hp value of a actor to a number. This won't trigger any on_damage callbacks
**Signature:**
```
actor_set_hp(guid : guid, amount : number) -> None
```
</details>
<details> <summary><b><code>actor_set_max_hp</code></b> </summary> <br/>
Sets the max hp value of a actor to a number.
**Signature:**
```
actor_set_max_hp(guid : guid, amount : number) -> None
```
</details>
<details> <summary><b><code>add_actor_by_enemy</code></b> </summary> <br/>
Creates a new enemy fighting against the player. Example ``add_actor_by_enemy("RUST_MITE")``.
@ -558,6 +594,18 @@ get_artifact_instance(guid : guid) -> artifact_instance
</details>
<details> <summary><b><code>get_artifacts</code></b> </summary> <br/>
Returns all the artifacts guids from the given actor.
**Signature:**
```
get_artifacts(actor_guid : string) -> guid[]
```
</details>
<details> <summary><b><code>give_artifact</code></b> </summary> <br/>
Gives a actor a artifact. Returns the guid of the newly created artifact.
@ -791,7 +839,7 @@ None
### Functions
<details> <summary><b><code>deal_damage</code></b> </summary> <br/>
Deal damage to a enemy from one source. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that was dealt.
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.
**Signature:**
@ -825,6 +873,18 @@ heal(source : guid, target : guid, amount : number) -> None
</details>
<details> <summary><b><code>simulate_deal_damage</code></b> </summary> <br/>
Simulate damage from a source to a target. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that would be dealt.
**Signature:**
```
simulate_deal_damage(source : guid, target : guid, damage : number, (optional) flat : boolean) -> number
```
</details>
## Player Operations
Functions that are related to the player.

View File

@ -13,6 +13,7 @@ type Artifact struct {
ID string
Name string
Description string
Tags []string
Order int
Price int
Callbacks map[string]luhelp.OwnedCallback

View File

@ -20,6 +20,7 @@ type Card struct {
PointCost int
MaxLevel int
DoesExhaust bool
DoesConsume bool
NeedTarget bool
Price int
Callbacks map[string]luhelp.OwnedCallback

View File

@ -21,6 +21,10 @@ const (
StateEventDamage = StateEvent("Damage")
StateEventHeal = StateEvent("Heal")
StateEventMoney = StateEvent("Money")
StateEventArtifactAdded = StateEvent("ArtifactAdded")
StateEventArtifactRemoved = StateEvent("ArtifactRemoved")
StateEventCardAdded = StateEvent("CardAdded")
StateEventCardRemoved = StateEvent("CardRemoved")
)
type StateEventDeathData struct {
@ -45,6 +49,30 @@ type StateEventMoneyData struct {
Money int
}
type StateEventArtifactAddedData struct {
Owner string
GUID string
TypeID string
}
type StateEventArtifactRemovedData struct {
Owner string
GUID string
TypeID string
}
type StateEventCardAddedData struct {
Owner string
GUID string
TypeID string
}
type StateEventCardRemovedData struct {
Owner string
GUID string
TypeID string
}
// StateCheckpoint saves the state of a session at a certain point. This can be used
// to retroactively check what happened between certain actions.
type StateCheckpoint struct {

View File

@ -16,6 +16,7 @@ type Event struct {
ID string
Name string
Description string
Tags []string
Choices []EventChoice
OnEnter luhelp.OwnedCallback
OnEnd luhelp.OwnedCallback

View File

@ -127,9 +127,9 @@ fun = require "fun"
return 1
}))
d.Function("text_color", "Makes the text foreground colored. Takes hex values like #ff0000.", "string", "color : string", "value : any")
l.SetGlobal("text_color", l.NewFunction(func(state *lua.LState) int {
state.Push(lua.LString(removeAnsiReset(lipgloss.NewStyle().Foreground(lipgloss.Color(luhelp2.ToString(state.Get(1), mapper))).Render(luhelp2.ToString(state.Get(2), mapper)))))
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
}))
@ -239,7 +239,7 @@ fun = require "fun"
return 1
}))
d.Function("get_fight_round", "Gets the number of stages cleared.", "number")
d.Function("get_stages_cleared", "Gets the number of stages cleared.", "number")
l.SetGlobal("get_stages_cleared", l.NewFunction(func(state *lua.LState) int {
state.Push(lua.LNumber(session.GetStagesCleared()))
return 1
@ -333,12 +333,30 @@ fun = require "fun"
return 0
}))
d.Function("actor_set_max_hp", "Sets the max hp value of a actor to a number.", "", "guid : guid", "amount : number")
l.SetGlobal("actor_set_max_hp", l.NewFunction(func(state *lua.LState) int {
session.UpdateActor(state.ToString(1), func(actor *Actor) bool {
actor.MaxHP = int(state.ToNumber(2))
return true
})
return 0
}))
d.Function("actor_add_hp", "Increases the hp value of a actor by a number. Can be negative value to decrease it. This won't trigger any on_damage callbacks", "", "guid : guid", "amount : number")
l.SetGlobal("actor_add_hp", l.NewFunction(func(state *lua.LState) int {
session.ActorAddHP(state.ToString(1), int(state.ToNumber(2)))
return 0
}))
d.Function("actor_set_hp", "Sets the hp value of a actor to a number. This won't trigger any on_damage callbacks", "", "guid : guid", "amount : number")
l.SetGlobal("actor_set_hp", l.NewFunction(func(state *lua.LState) int {
session.UpdateActor(state.ToString(1), func(actor *Actor) bool {
actor.HP = int(state.ToNumber(2))
return true
})
return 0
}))
d.Function("add_actor_by_enemy", "Creates a new enemy fighting against the player. Example ``add_actor_by_enemy(\"RUST_MITE\")``.", "string", "enemy_guid : type_id")
l.SetGlobal("add_actor_by_enemy", l.NewFunction(func(state *lua.LState) int {
state.Push(lua.LString(session.AddActorFromEnemy(state.ToString(1))))
@ -361,6 +379,12 @@ fun = require "fun"
return 0
}))
d.Function("get_artifacts", "Returns all the artifacts guids from the given actor.", "guid[]", "actor_guid : string")
l.SetGlobal("get_artifacts", l.NewFunction(func(state *lua.LState) int {
state.Push(luhelp2.ToLua(state, session.GetArtifacts(state.ToString(1))))
return 1
}))
d.Function("get_artifact", "Returns the artifact definition. Can take either a guid or a typeId. If it's a guid it will fetch the type behind the instance.", "artifact", "id : string")
l.SetGlobal("get_artifact", l.NewFunction(func(state *lua.LState) int {
art, _ := session.GetArtifact(state.ToString(1))
@ -483,7 +507,7 @@ fun = require "fun"
d.Category("Damage & Heal", "Functions that deal damage or heal.", 10)
d.Function("deal_damage", "Deal damage to a enemy from one source. 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")
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)))
@ -493,6 +517,16 @@ fun = require "fun"
return 1
}))
d.Function("simulate_deal_damage", "Simulate damage from a source to a target. If flat is true the damage can't be modified by status effects or artifacts. Returns the damage that would be dealt.", "number", "source : guid", "target : guid", "damage : number", "(optional) flat : boolean")
l.SetGlobal("simulate_deal_damage", l.NewFunction(func(state *lua.LState) int {
if state.GetTop() == 3 {
state.Push(lua.LNumber(session.SimulateDealDamage(state.ToString(1), state.ToString(2), int(state.ToNumber(3)), false)))
} else {
state.Push(lua.LNumber(session.SimulateDealDamage(state.ToString(1), state.ToString(2), int(state.ToNumber(3)), bool(state.ToBool(4)))))
}
return 1
}))
d.Function("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.", "number[]", "source : guid", "targets : guid[]", "damage : number", "(optional) flat : boolean")
l.SetGlobal("deal_damage_multi", l.NewFunction(func(state *lua.LState) int {
var guids []string

View File

@ -67,8 +67,8 @@ func NewResourcesManager(state *lua.LState, docs *ludoc.Docs, logger *log.Logger
// Load all local scripts
_ = fs.Walk("./assets/scripts", func(path string, isDir bool) error {
// Don't load libs
if strings.Contains(path, "scripts/libs") || strings.Contains(path, "scripts/definitions") {
// Don't load libs, definitions and paths containing two __
if strings.Contains(path, "scripts/libs") || strings.Contains(path, "scripts/definitions") || strings.Contains(path, "__") {
return nil
}
@ -132,7 +132,10 @@ func (man *ResourcesManager) luaRegisterArtifact(l *lua.LState) int {
man.log.Println("Registered artifact:", def.ID, def.Name)
man.Artifacts[def.ID] = &def
man.registered.RawGetString("artifact").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("artifact").(*lua.LTable).RawSetString(def.ID, table)
return 0
}
@ -151,7 +154,10 @@ func (man *ResourcesManager) luaRegisterCard(l *lua.LState) int {
man.log.Println("Registered card:", def.ID, def.Name)
man.Cards[def.ID] = &def
man.registered.RawGetString("card").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("card").(*lua.LTable).RawSetString(def.ID, table)
return 0
}
@ -170,7 +176,10 @@ func (man *ResourcesManager) luaRegisterEnemy(l *lua.LState) int {
man.log.Println("Registered enemy:", def.ID, def.Name)
man.Enemies[def.ID] = &def
man.registered.RawGetString("enemy").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("enemy").(*lua.LTable).RawSetString(def.ID, table)
return 0
}
@ -187,7 +196,10 @@ func (man *ResourcesManager) luaRegisterEvent(l *lua.LState) int {
man.log.Println("Registered event:", def.ID, def.Name)
man.Events[def.ID] = &def
man.registered.RawGetString("event").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("event").(*lua.LTable).RawSetString(def.ID, table)
return 0
}
@ -206,7 +218,10 @@ func (man *ResourcesManager) luaRegisterStatusEffect(l *lua.LState) int {
man.log.Println("Registered status_effect:", def.ID, def.Name)
man.StatusEffects[def.ID] = &def
man.registered.RawGetString("status_effect").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("status_effect").(*lua.LTable).RawSetString(def.ID, table)
return 0
}
@ -223,7 +238,10 @@ func (man *ResourcesManager) luaRegisterStoryTeller(l *lua.LState) int {
man.log.Println("Registered story_teller:", def.ID)
man.StoryTeller[def.ID] = &def
man.registered.RawGetString("story_teller").(*lua.LTable).RawSetString(def.ID, l.ToTable(2))
table := l.ToTable(2)
l.SetTable(table, lua.LString("id"), lua.LString(def.ID))
man.registered.RawGetString("story_teller").(*lua.LTable).RawSetString(def.ID, table)
return 0
}

View File

@ -61,6 +61,12 @@ const (
DrawSize = 3
)
type Hook string
const (
HookNextFightEnd = Hook("NextFightEnd")
)
// FightState represents the current state of the fight in regard to the
// deck of the player.
type FightState struct {
@ -107,6 +113,7 @@ type Session struct {
eventHistory []string
randomHistory []string
ctxData map[string]any
hooks map[Hook][]func()
loadedMods []string
stateCheckpoints []StateCheckpoint
@ -127,9 +134,14 @@ func NewSession(options ...func(s *Session)) *Session {
},
instances: map[string]any{},
ctxData: map[string]any{},
hooks: map[Hook][]func(){
HookNextFightEnd: {},
},
stagesCleared: 0,
onLuaError: nil,
luaErrors: make(chan LuaError, 25),
eventHistory: []string{},
randomHistory: []string{},
}
session.SetOnLuaError(nil)
@ -145,7 +157,6 @@ func NewSession(options ...func(s *Session)) *Session {
session.resources = NewResourcesManager(session.luaState, session.luaDocs, session.log)
session.resources.MarkBaseGame()
session.loadMods(session.loadedMods)
session.SetEvent("START")
session.log.Println("Session started!")
@ -156,6 +167,8 @@ func NewSession(options ...func(s *Session)) *Session {
return true
})
session.SetEvent("START")
return session
}
@ -331,12 +344,16 @@ func (s *Session) loadMods(mods []string) {
}
_ = fs.Walk(filepath.Join("./mods", mods[i]), func(path string, isDir bool) error {
if strings.Contains(path, "__") {
return nil
}
// If we find a locals folder we add it to the localization
if isDir && filepath.Base(path) == "locals" {
_ = localization.Global.AddFolder(path)
}
if isDir && strings.HasSuffix(path, ".lua") {
if !isDir && strings.HasSuffix(path, ".lua") {
luaBytes, err := fs.ReadFile(path)
if err != nil {
// TODO: error handling
@ -505,10 +522,10 @@ func (s *Session) FinishPlayerTurn() {
for _, guid := range instanceKeys {
switch instance := s.instances[guid].(type) {
case StatusEffectInstance:
// If it was applied this round we never remove it.
if instance.RoundEntered == s.currentFight.Round {
continue
}
// TODO: investigate why this was here
// if instance.Owner == PlayerActorID && instance.RoundEntered == s.currentFight.Round {
// continue
// }
se := s.resources.StatusEffects[instance.TypeID]
@ -625,6 +642,9 @@ func (s *Session) FinishFight() bool {
} else {
s.SetGameState(GameStateRandom)
}
// Trigger HookNextFightEnd
s.TriggerHooks(HookNextFightEnd)
}
return false
}
@ -649,22 +669,28 @@ func (s *Session) FinishEvent(choice int) {
if nextState != nil {
if len(nextState.(string)) > 0 {
s.SetGameState(GameState(nextState.(string)))
} else {
s.SetGameState(GameStateRandom)
}
_, _ = event.OnEnd(CreateContext("type_id", event.ID, "choice", choice+1))
_, _ = event.OnEnd.Call(CreateContext("type_id", event.ID, "choice", choice+1))
return
}
// Otherwise we allow OnEnd to dictate the new state
nextState, _ = event.OnEnd(CreateContext("type_id", event.ID, "choice", choice+1))
nextState, _ = event.OnEnd.Call(CreateContext("type_id", event.ID, "choice", choice+1))
if nextState != nil && len(nextState.(string)) > 0 {
s.SetGameState(GameState(nextState.(string)))
} else {
s.SetGameState(GameStateRandom)
}
return
}
nextState, _ := event.OnEnd(CreateContext("type_id", event.ID, "choice", nil))
nextState, _ := event.OnEnd.Call(CreateContext("type_id", event.ID, "choice", nil))
if nextState != nil && len(nextState.(string)) > 0 {
s.SetGameState(GameState(nextState.(string)))
} else {
s.SetGameState(GameStateRandom)
}
}
@ -1282,6 +1308,14 @@ func (s *Session) GiveArtifact(typeId string, owner string) string {
s.logLuaError(CallbackOnPickUp, instance.TypeID, err)
}
s.PushState(map[StateEvent]any{
StateEventArtifactAdded: StateEventArtifactAddedData{
Owner: owner,
TypeID: typeId,
GUID: instance.GUID,
},
})
return instance.GUID
}
@ -1293,6 +1327,14 @@ func (s *Session) RemoveArtifact(guid string) {
}
s.actors[instance.Owner].Artifacts.Remove(instance.GUID)
delete(s.instances, guid)
s.PushState(map[StateEvent]any{
StateEventArtifactRemoved: StateEventArtifactRemovedData{
Owner: instance.Owner,
TypeID: instance.TypeID,
GUID: instance.GUID,
},
})
}
//
@ -1333,6 +1375,15 @@ func (s *Session) GiveCard(typeId string, owner string) string {
}
s.instances[instance.GUID] = instance
s.actors[owner].Cards.Add(instance.GUID)
s.PushState(map[StateEvent]any{
StateEventCardAdded: StateEventCardAddedData{
Owner: owner,
TypeID: typeId,
GUID: instance.GUID,
},
})
return instance.GUID
}
@ -1341,6 +1392,14 @@ func (s *Session) RemoveCard(guid string) {
instance := s.instances[guid].(CardInstance)
s.actors[instance.Owner].Cards.Remove(instance.GUID)
delete(s.instances, guid)
s.PushState(map[StateEvent]any{
StateEventCardRemoved: StateEventCardRemovedData{
Owner: instance.Owner,
TypeID: instance.TypeID,
GUID: instance.GUID,
},
})
}
// CastCard calls the OnCast callback for a card, casting it.
@ -1351,13 +1410,15 @@ func (s *Session) CastCard(guid string, target string) bool {
s.logLuaError(CallbackOnCast, instance.TypeID, err)
}
if val, ok := res.(bool); ok {
if val {
TriggerCallbackSimple(s, CallbackOnActorDidCast, TriggerAll, EmptyContext, CreateContext("type_id", card.ID, "guid", guid, "caster", instance.Owner, "target", target, "level", instance.Level, "tags", card.Tags))
}
return val
}
}
if !val {
return false
}
TriggerCallbackSimple(s, CallbackOnActorDidCast, TriggerAll, EmptyContext, CreateContext("type_id", card.ID, "guid", guid, "caster", instance.Owner, "target", target, "level", instance.Level, "tags", card.Tags))
return true
}
}
return true
}
// GetCards returns all cards owned by a actor.
@ -1398,7 +1459,8 @@ func (s *Session) PlayerCastHand(i int, target string) error {
cardId := s.currentFight.Hand[i]
// Only cast a card if castable and points are available and subtract them.
if card, _ := s.GetCard(cardId); card != nil {
card, _ := s.GetCard(cardId)
if card != nil {
if !card.Callbacks[CallbackOnCast].Present() {
return errors.New("card is not castable")
}
@ -1418,12 +1480,16 @@ func (s *Session) PlayerCastHand(i int, target string) error {
})
// Cast and exhaust if needed.
exhaust := s.CastCard(cardId, target)
if exhaust {
didCast := s.CastCard(cardId, target)
if didCast {
if card.DoesExhaust {
s.currentFight.Exhausted = append(s.currentFight.Exhausted, cardId)
} else if card.DoesConsume {
s.RemoveCard(cardId)
} else {
s.currentFight.Used = append(s.currentFight.Used, cardId)
}
}
s.FinishFight()
@ -1610,6 +1676,41 @@ func (s *Session) DealDamage(source string, target string, damage int, flat bool
return damage
}
// SimulateDealDamage will simulate damage to a target. If flat is true it will not trigger any callbacks which modify the damage.
func (s *Session) SimulateDealDamage(source string, target string, damage int, flat bool) int {
if _, ok := s.actors[source]; !ok {
return 0
}
_, ok := s.actors[target]
if !ok {
return 0
}
// If not flat we will modify the damage based on the OnDamageCalc callbacks.
if !flat {
reducer := func(cur float64, val float64) float64 {
return val
}
damage = int(TriggerCallbackReduce[float64](
s,
CallbackOnDamageCalc,
TriggerAll,
reducer,
float64(damage),
"damage",
CreateContext("source", source, "target", target, "damage", damage, "simulated", true)),
)
}
// Negative damage aka heal is not allowed!
if damage < 0 {
return 0
}
return damage
}
// 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 {
@ -1699,7 +1800,7 @@ func (s *Session) GetActor(id string) Actor {
return NewActor("")
}
// UpdateActor updates an actor.
// UpdateActor updates an actor. If the update function returns true the actor will be updated.
func (s *Session) UpdateActor(id string, update func(actor *Actor) bool) {
actor := s.GetActor(id)
if update(&actor) {
@ -1893,6 +1994,23 @@ func (s *Session) GivePlayerGold(amount int) {
})
}
//
// Hooks
//
// AddHook adds a hook to the session.
func (s *Session) AddHook(hook Hook, callback func()) {
s.hooks[hook] = append(s.hooks[hook], callback)
}
// TriggerHooks triggers all hooks of a certain type.
func (s *Session) TriggerHooks(hook Hook) {
for _, callback := range s.hooks[hook] {
callback()
}
s.hooks[hook] = []func(){}
}
//
// Misc Functions
//

View File

@ -3,27 +3,35 @@ package components
import (
"fmt"
"github.com/BigJk/end_of_eden/game"
"github.com/BigJk/end_of_eden/ui"
"github.com/BigJk/end_of_eden/ui/style"
"github.com/charmbracelet/lipgloss"
"strings"
)
var (
artifactStyle = lipgloss.NewStyle().Padding(1, 2).Margin(0, 2)
)
func ArtifactCard(session *game.Session, guid string, baseHeight int, maxHeight int) string {
func ArtifactCard(session *game.Session, guid string, baseHeight int, maxHeight int, optionalWidth ...int) string {
art, _ := session.GetArtifact(guid)
width := 30
if len(optionalWidth) > 0 {
width = optionalWidth[0]
}
artifactStyle := artifactStyle.Copy().
Width(30).
Width(width).
Border(lipgloss.ThickBorder(), true, false, false, false).
BorderBackground(lipgloss.Color("#495057")).
BorderForeground(lipgloss.Color("#495057")).
Background(lipgloss.Color("#343a40")).
Foreground(style.BaseWhite)
tagsText := strings.Join(art.Tags, ", ")
return artifactStyle.
Height(baseHeight).
Render(fmt.Sprintf("%s\n\n%s\n\n%s", style.BoldStyle.Render(art.Name), art.Description, lipgloss.NewStyle().Bold(true).Foreground(style.BaseYellow).Render(fmt.Sprintf("%d$", art.Price))))
Render(fmt.Sprintf("%s\n\n%s\n\n%s", style.BoldStyle.Render(art.Name, strings.Repeat(" ", ui.Max(width-6-lipgloss.Width(art.Name)-lipgloss.Width(tagsText), 0)), tagsText), art.Description, lipgloss.NewStyle().Bold(true).Foreground(style.BaseYellow).Render(fmt.Sprintf("%d$", art.Price))))
}

View File

@ -13,36 +13,44 @@ import (
var (
cardStyle = lipgloss.NewStyle().Padding(1, 2).Margin(0, 2)
headerStlye = lipgloss.NewStyle().Bold(true)
cantCastStyle = lipgloss.NewStyle().Foreground(style.BaseRed)
)
func HalfCard(session *game.Session, guid string, active bool, baseHeight int, maxHeight int, minimal bool) string {
func HalfCard(session *game.Session, guid string, active bool, baseHeight int, maxHeight int, minimal bool, optionalWidth ...int) string {
fight := session.GetFight()
card, _ := session.GetCard(guid)
canCast := fight.CurrentPoints >= card.PointCost
cardState := session.GetCardState(guid)
pointText := strings.Repeat("•", card.PointCost)
if !canCast {
pointText = cantCastStyle.Render(pointText)
}
tagsText := strings.Join(card.Tags, ", ")
cardCol, _ := colorful.Hex(card.Color)
bgCol, _ := colorful.MakeColor(style.BaseGrayDarker)
width := 30
if len(optionalWidth) > 0 {
width = optionalWidth[0]
}
cardStyle := cardStyle.Copy().
Width(lo.Ternary(minimal && !active, 10, 30)).
Width(lo.Ternary(minimal && !active, 10, width)).
Border(lipgloss.NormalBorder(), true, false, false, false).
BorderBackground(lipgloss.Color(card.Color)).
BorderForeground(lo.Ternary(active, style.BaseGray, lipgloss.Color(card.Color))).
Background(lipgloss.Color(cardCol.BlendRgb(bgCol, 0.6).Hex())).
Foreground(style.BaseWhite)
header := headerStlye.Render(fmt.Sprintf("%s%s%s", pointText, strings.Repeat(" ", ui.Max(width-4-lipgloss.Width(pointText)-lipgloss.Width(tagsText), 0)), tagsText))
if !canCast {
header = cantCastStyle.Render(header)
}
if active {
return cardStyle.
Height(ui.Min(maxHeight-1, baseHeight+5)).
Render(fmt.Sprintf("%s%s%s\n\n%s\n\n%s", pointText, strings.Repeat(" ", 30-2-len(pointText)-len(tagsText)), tagsText, style.BoldStyle.Render(card.Name), cardState))
Render(fmt.Sprintf("%s\n\n%s\n\n%s", header, style.BoldStyle.Render(card.Name), cardState))
}
if minimal {
@ -53,6 +61,6 @@ func HalfCard(session *game.Session, guid string, active bool, baseHeight int, m
return cardStyle.
Height(baseHeight).
Render(fmt.Sprintf("%s%s%s\n\n%s\n\n%s", pointText, strings.Repeat(" ", 30-2-len(pointText)-len(tagsText)), tagsText, style.BoldStyle.Render(card.Name), cardState))
Render(fmt.Sprintf("%s\n\n%s\n\n%s", header, style.BoldStyle.Render(card.Name), cardState))
}

View File

@ -0,0 +1,104 @@
package carousel
import (
"github.com/BigJk/end_of_eden/ui"
"github.com/BigJk/end_of_eden/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
zone "github.com/lrstanley/bubblezone"
"github.com/samber/lo"
"strings"
)
const (
ZoneLeftButton = "left_button"
ZoneRightButton = "right_button"
ZoneDoneButton = "done_button"
)
type Model struct {
ui.MenuBase
zones *zone.Manager
parent tea.Model
lastMouse tea.MouseMsg
title string
items []string
selected int
}
func New(parent tea.Model, zones *zone.Manager, title string, items []string) Model {
return Model{
zones: zones,
parent: parent,
title: title,
items: items,
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.Size = msg
case tea.KeyMsg:
if msg.Type == tea.KeyEscape {
return m.parent, nil
} else if msg.Type == tea.KeyEnter {
return m.parent, nil
} else if msg.Type == tea.KeyLeft {
if m.selected > 0 {
m.selected--
}
} else if msg.Type == tea.KeyRight {
if m.selected < len(m.items)-1 {
m.selected++
}
}
case tea.MouseMsg:
m.LastMouse = msg
if msg.Type == tea.MouseLeft {
if m.zones.Get(ZoneLeftButton).InBounds(msg) {
if m.selected > 0 {
m.selected--
}
}
if m.zones.Get(ZoneLeftButton).InBounds(msg) {
if m.selected < len(m.items)-1 {
m.selected++
}
}
if m.zones.Get(ZoneDoneButton).InBounds(msg) {
return m.parent, nil
}
}
}
return m, nil
}
func (m Model) View() string {
title := style.BoldStyle.Copy().MarginBottom(4).Render(m.title)
leftButton := m.zones.Mark(ZoneLeftButton, style.HeaderStyle.Copy().Background(lo.Ternary(m.zones.Get(ZoneLeftButton).InBounds(m.LastMouse), style.BaseRed, style.BaseRedDarker)).Margin(0, 2).Render("<--"))
rightButton := m.zones.Mark(ZoneRightButton, style.HeaderStyle.Copy().Background(lo.Ternary(m.zones.Get(ZoneRightButton).InBounds(m.LastMouse), style.BaseRed, style.BaseRedDarker)).Margin(0, 2).Render("-->"))
middle := lipgloss.JoinHorizontal(lipgloss.Center,
leftButton,
m.items[m.selected],
rightButton,
)
dots := lipgloss.NewStyle().Margin(2, 0).Render(strings.Join(lo.Map(m.items, func(item string, index int) string {
if index == m.selected {
return "●"
}
return "○"
}), " "))
doneButton := m.zones.Mark(ZoneDoneButton, style.HeaderStyle.Copy().Background(lo.Ternary(m.zones.Get(ZoneDoneButton).InBounds(m.LastMouse), style.BaseRed, style.BaseRedDarker)).MarginTop(2).Render("Continue"))
return lipgloss.Place(m.Size.Width, m.Size.Height, lipgloss.Center, lipgloss.Center, lipgloss.JoinVertical(lipgloss.Center, title, middle, dots, doneButton))
}

View File

@ -173,7 +173,7 @@ func (m Model) eventUpdateContent() Model {
var chunks []string
var mds []bool
lines := strings.Split(m.session.GetEvent().Description, "\n")
lines := strings.Split(strings.TrimSpace(m.session.GetEvent().Description), "\n")
for i := range lines {
if strings.HasPrefix(lines[i], "!!") {

View File

@ -6,6 +6,7 @@ import (
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/ui"
"github.com/BigJk/end_of_eden/ui/components"
"github.com/BigJk/end_of_eden/ui/menus/carousel"
"github.com/BigJk/end_of_eden/ui/menus/eventview"
"github.com/BigJk/end_of_eden/ui/menus/gameover"
"github.com/BigJk/end_of_eden/ui/menus/merchant"
@ -39,11 +40,15 @@ type Model struct {
animations []tea.Model
ctrlDown bool
lastGameState game.GameState
lastEvent string
event tea.Model
merchant tea.Model
Session *game.Session
Start game.StateCheckpointMarker
BeforeStateSwitch game.StateCheckpointMarker
}
func New(parent tea.Model, zones *zone.Manager, session *game.Session) Model {
@ -57,6 +62,7 @@ func New(parent tea.Model, zones *zone.Manager, session *game.Session) Model {
Session: session,
Start: session.MarkState(),
BeforeStateSwitch: session.MarkState(),
}
}
@ -248,6 +254,52 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return gameover.New(m.zones, m.Session, m.Start), nil
}
if m.Session.GetGameState() != m.lastGameState || m.Session.GetEventID() != m.lastEvent {
diff := m.BeforeStateSwitch.Diff(m.Session)
m.BeforeStateSwitch = m.Session.MarkState()
m.lastGameState = m.Session.GetGameState()
m.lastEvent = m.Session.GetEventID()
if len(diff) > 0 {
fmt.Println("DIFF", len(diff))
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, 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)
})
if len(artifacts) > 0 {
c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Artifacts", len(artifacts)), artifacts)
c.Size = m.Size
cmds = append(cmds, root.Push(c))
}
if len(cards) > 0 {
c := carousel.New(nil, m.zones, fmt.Sprintf("%d New Cards", len(cards)), cards)
c.Size = m.Size
cmds = append(cmds, root.Push(c))
}
}
cmds = append(cmds, tea.ClearScreen)
}
return m, tea.Batch(cmds...)
}

View File

@ -99,6 +99,7 @@ func New(parent tea.Model, zones *zone.Manager, session *game.Session) MenuModel
table.WithStyles(style.TableStyle),
table.WithColumns([]table.Column{
{Title: "Name", Width: 25},
{Title: "Tags", Width: 20},
{Title: "Level", Width: 5},
}),
),
@ -106,6 +107,7 @@ func New(parent tea.Model, zones *zone.Manager, session *game.Session) MenuModel
table.WithStyles(style.TableStyle),
table.WithColumns([]table.Column{
{Title: "Name", Width: 25},
{Title: "Tags", Width: 20},
{Title: "Price", Width: 5},
}),
),
@ -141,14 +143,14 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Update card table
m.cardTable.SetRows(lo.Map(cards, func(guid string, index int) table.Row {
card, instance := m.Session.GetCard(guid)
return table.Row{m.zones.Mark(ZoneCards+fmt.Sprint(index), card.Name), fmt.Sprint(instance.Level)}
return table.Row{m.zones.Mark(ZoneCards+fmt.Sprint(index), card.Name), strings.Join(card.Tags, ", "), fmt.Sprint(instance.Level)}
}))
m.cardTable.SetHeight(m.Size.Height - style.HeaderStyle.GetVerticalFrameSize() - 1 - 2)
// Update artifact table
m.artifactTable.SetRows(lo.Map(artifacts, func(guid string, index int) table.Row {
art, _ := m.Session.GetArtifact(guid)
return table.Row{m.zones.Mark(ZoneArtifacts+fmt.Sprint(index), art.Name), fmt.Sprintf("%d$", art.Price)}
return table.Row{m.zones.Mark(ZoneArtifacts+fmt.Sprint(index), art.Name), strings.Join(art.Tags, ", "), fmt.Sprintf("%d$", art.Price)}
}))
m.artifactTable.SetHeight(m.Size.Height - style.HeaderStyle.GetVerticalFrameSize() - 1 - 2)
@ -162,15 +164,17 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.updateLogViewport()
m.cardTable.SetWidth(m.contentWidth() - 40 - 2 - 2)
m.cardTable.SetWidth(m.contentWidth() - 55 - 4)
m.cardTable.SetColumns([]table.Column{
{Title: "Name", Width: m.contentWidth() - 40 - 2 - 2 - 10},
{Title: "Name", Width: m.contentWidth() - 55 - 4 - 10 - 20 - 4},
{Title: "Tags", Width: 20},
{Title: "Level", Width: 10},
})
m.artifactTable.SetWidth(m.contentWidth() - 40 - 2 - 2)
m.artifactTable.SetWidth(m.contentWidth() - 55 - 4)
m.artifactTable.SetColumns([]table.Column{
{Title: "Name", Width: m.contentWidth() - 40 - 2 - 2 - 10},
{Title: "Name", Width: m.contentWidth() - 55 - 4 - 10 - 20 - 4},
{Title: "Tags", Width: 20},
{Title: "Price", Width: 10},
})
case tea.KeyMsg:
@ -279,7 +283,7 @@ func (m MenuModel) View() string {
case ChoiceArtifacts:
var selected string
if m.artifactTable.Cursor() < len(m.Session.GetArtifacts(game.PlayerActorID)) {
selected = components.ArtifactCard(m.Session, m.Session.GetArtifacts(game.PlayerActorID)[m.artifactTable.Cursor()], 20, 40)
selected = components.ArtifactCard(m.Session, m.Session.GetArtifacts(game.PlayerActorID)[m.artifactTable.Cursor()], 20, 40, 45)
}
contentBox = contentStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
@ -292,7 +296,7 @@ func (m MenuModel) View() string {
case ChoiceCards:
var selected string
if m.artifactTable.Cursor() < len(m.Session.GetCards(game.PlayerActorID)) {
selected = components.HalfCard(m.Session, m.Session.GetCards(game.PlayerActorID)[m.cardTable.Cursor()], false, 20, 40, false)
selected = components.HalfCard(m.Session, m.Session.GetCards(game.PlayerActorID)[m.cardTable.Cursor()], false, 20, 40, false, 45)
}
contentBox = contentStyle.Render(lipgloss.JoinVertical(lipgloss.Left,