1554 lines
52 KiB
Lua
1554 lines
52 KiB
Lua
-- title: Lucksmith
|
|
-- author: DigNZ
|
|
-- desc: card + dice roguelike prototype
|
|
-- script: lua
|
|
|
|
-- ============================================================
|
|
-- CONTENTS
|
|
-- 1. helpers - lerp() for the card-play animation
|
|
-- 2. game data - enemies, cards, enemy_cards
|
|
-- 3. entity factories - get_enemy(), reset_player()
|
|
-- 4. state - all mutable game state, declared up front
|
|
-- 5. deck & hand - shuffle/draw/reshuffle/unlock
|
|
-- 6. combat logic - resolve_turn (reveals + applies card effects/damage), start_turn
|
|
-- 7. effects queue - queue_effect/run_effects + flash_background/sequenced_damage
|
|
-- 8. ui widgets - print_centered + played-card labels + win message
|
|
-- 9. drawing helpers - draw_hud, draw_health_bars, draw_card
|
|
-- 10. phase handlers - one function per game phase (title/choose/resolve/...)
|
|
-- 11. TIC() - thin per-frame dispatcher
|
|
-- ============================================================
|
|
|
|
-- === 1. HELPERS ===
|
|
-- Simple linear interpolation, used by the hand -> center card-play animation.
|
|
function lerp(a, b, t)
|
|
return a + (b - a) * t
|
|
end
|
|
|
|
-- === 2. GAME DATA ===
|
|
enemies = {{
|
|
name = "Coinwright",
|
|
hp = 4,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}, {
|
|
name = "Charmcaster",
|
|
hp = 6,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}, {
|
|
name = "Diceforger",
|
|
hp = 8,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}, {
|
|
name = "Cardmancer",
|
|
hp = 10,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}, {
|
|
name = "Fateshaper",
|
|
hp = 12,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}, {
|
|
name = "Lucksmith",
|
|
hp = 14,
|
|
roll_mod = 0,
|
|
mult = 1
|
|
}}
|
|
|
|
-- basic card pool
|
|
cards = {{
|
|
name = "Bless",
|
|
desc = "+2 to roll",
|
|
timing = "pre_roll",
|
|
energy = -1, -- costs 1 energy
|
|
use = function(p, e)
|
|
p.roll_mod = p.roll_mod + 2
|
|
end
|
|
}, {
|
|
name = "Hex",
|
|
desc = "-2 enemy roll",
|
|
timing = "pre_roll",
|
|
energy = -1, -- costs 1 energy
|
|
use = function(p, e)
|
|
e.roll_mod = e.roll_mod - 2
|
|
end
|
|
}, {
|
|
name = "Fury",
|
|
desc = "win/lose double dmg",
|
|
timing = "pre_roll",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e)
|
|
p.mult = p.mult * 2
|
|
e.mult = e.mult * 2
|
|
end
|
|
}, {
|
|
name = "Caution",
|
|
desc = "win/lose half dmg",
|
|
timing = "pre_roll",
|
|
energy = 0, -- reduced from +1 since we get +1 per turn
|
|
use = function(p, e)
|
|
p.mult = p.mult * 0.5
|
|
e.mult = e.mult * 0.5
|
|
end
|
|
}, {
|
|
name = "Heal",
|
|
desc = "+2 hp",
|
|
timing = "immediate",
|
|
energy = -1, -- costs 1 energy
|
|
use = function(p, e)
|
|
p.hp = math.min(10, p.hp + 2)
|
|
current_damage.player = current_damage.player - 2 -- show damage taken
|
|
end
|
|
}, {
|
|
name = "Counter",
|
|
desc = "hit enemy for 1 if you lose",
|
|
timing = "post_result",
|
|
energy = 0, -- reduced from -1
|
|
use = function(p, e, player_won)
|
|
if player_won == false then -- fixed: should trigger when player loses
|
|
e.hp = e.hp - 1
|
|
current_damage.enemy = current_damage.enemy + 1 -- show damage taken
|
|
end
|
|
end
|
|
}, {
|
|
name = "Weaken",
|
|
desc = "-1 enemy dmg",
|
|
timing = "pre_roll",
|
|
energy = 0, -- reduced from -1
|
|
use = function(p, e)
|
|
e.weaken = (e.weaken or 0) + 1 -- enemy is weakened (does less damage)
|
|
end
|
|
}, {
|
|
name = "Rage",
|
|
desc = "+1 your dmg",
|
|
timing = "pre_roll", -- changed from post_result for clarity
|
|
energy = -1, -- reduced from -1
|
|
use = function(p, e)
|
|
p.rage = (p.rage or 0) + 1 -- add rage bonus
|
|
end
|
|
}, {
|
|
name = "Break",
|
|
desc = "if tied, you win",
|
|
timing = "post_result",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e, player_won)
|
|
if player_won == nil then -- tie occurred
|
|
e.hp = e.hp - (2 * p.mult)
|
|
current_damage.enemy = current_damage.enemy + (2 * p.mult) -- show damage taken
|
|
end
|
|
end
|
|
}, {
|
|
name = "Shield",
|
|
desc = "take 1 less dmg until damaged",
|
|
timing = "immediate",
|
|
energy = -1, -- increased from 0
|
|
use = function(p, e)
|
|
p.shield = (p.shield or 0) + 1
|
|
end
|
|
}, {
|
|
name = "Poison",
|
|
desc = "enemy loses 1 hp at turn end",
|
|
timing = "post_result",
|
|
energy = 0, -- reduced from -1
|
|
use = function(p, e, player_won)
|
|
e.hp = e.hp - 1
|
|
current_damage.enemy = current_damage.enemy + 1 -- show damage taken
|
|
end
|
|
}, {
|
|
name = "Spike",
|
|
desc = "deal 1 dmg to whoever wins",
|
|
timing = "post_result",
|
|
energy = 1, -- now gives energy since it's risky
|
|
use = function(p, e, player_won)
|
|
if player_won == true then
|
|
p.hp = p.hp - 1 -- hurt yourself if you win
|
|
current_damage.player = current_damage.player + 1 -- show damage taken
|
|
elseif player_won == false then
|
|
e.hp = e.hp - 1 -- hurt enemy if they win
|
|
current_damage.enemy = current_damage.enemy + 1 -- show damage taken
|
|
end
|
|
-- ties do nothing
|
|
end
|
|
}, {
|
|
name = "Lucky",
|
|
desc = "reroll if you roll 1-3",
|
|
timing = "post_roll",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e)
|
|
if p.roll <= 3 then
|
|
p.roll = math.random(1, 6)
|
|
p.total = p.roll + p.roll_mod
|
|
end
|
|
end
|
|
}, {
|
|
name = "Curse",
|
|
desc = "enemy rerolls if they roll 4-6",
|
|
timing = "post_roll",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e)
|
|
if e.roll >= 4 then
|
|
e.roll = math.random(1, 6)
|
|
e.total = e.roll + e.roll_mod
|
|
end
|
|
end
|
|
}, {
|
|
name = "Intuition",
|
|
desc = "Pick the higher roll",
|
|
timing = "post_roll",
|
|
energy = -3, -- reduced from -3
|
|
use = function(p, e)
|
|
if p.roll >= e.roll then
|
|
return -- no change needed
|
|
end
|
|
local temp = p.roll
|
|
p.roll = e.roll
|
|
e.roll = temp
|
|
p.total = p.roll + p.roll_mod
|
|
e.total = e.roll + e.roll_mod
|
|
end
|
|
}, {
|
|
name = "Mirror",
|
|
desc = "copy enemy's roll modifier",
|
|
timing = "pre_roll",
|
|
energy = -1, -- reduced from -1
|
|
use = function(p, e)
|
|
p.roll_mod = p.roll_mod + e.roll_mod
|
|
end
|
|
}, {
|
|
name = "Double Vision",
|
|
desc = "copy enemy's multiplier",
|
|
timing = "post_roll",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e)
|
|
p.mult = e.mult
|
|
end
|
|
}, {
|
|
name = "Drain",
|
|
desc = "heal 1 if you win",
|
|
timing = "post_result",
|
|
energy = 0, -- reduced from -1
|
|
use = function(p, e, player_won)
|
|
if player_won == true then
|
|
p.hp = math.min(10, p.hp + 1)
|
|
current_damage.player = current_damage.player - 1 -- show healing
|
|
end
|
|
end
|
|
}, {
|
|
name = "Gamble",
|
|
desc = "50% chance: +3 roll or -1 roll",
|
|
timing = "pre_roll",
|
|
energy = 1, -- now gives energy since it's risky
|
|
use = function(p, e)
|
|
if math.random() < 0.5 then
|
|
p.roll_mod = p.roll_mod + 3
|
|
else
|
|
p.roll_mod = p.roll_mod - 1
|
|
end
|
|
end
|
|
}, {
|
|
name = "Impulse",
|
|
desc = "+0.5 dmg per hp missing",
|
|
timing = "pre_roll",
|
|
energy = -1, -- reduced from -1
|
|
use = function(p, e)
|
|
local missing_hp = 10 - p.hp
|
|
p.mult = p.mult + (missing_hp * 0.5) -- increased effect
|
|
end
|
|
}, {
|
|
name = "Turtle",
|
|
desc = "set your roll to 3",
|
|
timing = "post_roll",
|
|
energy = 2, -- increased from 1
|
|
use = function(p, e)
|
|
p.roll = 3
|
|
p.total = p.roll + p.roll_mod
|
|
end
|
|
}, {
|
|
name = "Berserk",
|
|
desc = "2x dmg but take 1 dmg",
|
|
timing = "immediate",
|
|
energy = -1, -- reduced from -2
|
|
use = function(p, e)
|
|
p.mult = p.mult * 2
|
|
p.hp = p.hp - 1
|
|
current_damage.player = current_damage.player + 1 -- show damage taken
|
|
end
|
|
}, {
|
|
name = "Focus",
|
|
desc = "+2 energy",
|
|
timing = "immediate",
|
|
energy = 2, -- gives 2 energy (net +3 with the +1 per turn)
|
|
use = function(p, e)
|
|
-- Energy gain handled automatically
|
|
end
|
|
}, {
|
|
name = "Rest",
|
|
desc = "+1 energy, +1 hp",
|
|
timing = "immediate",
|
|
energy = 1, -- gives 1 energy (net +2 with the +1 per turn)
|
|
use = function(p, e)
|
|
current_damage.player = current_damage.player - 1 -- show healing
|
|
p.hp = math.min(10, p.hp + 1)
|
|
end
|
|
}}
|
|
|
|
enemy_cards = {{
|
|
name = "Bless",
|
|
desc = "+2 to roll",
|
|
timing = "pre_roll",
|
|
energy = -1,
|
|
use = function(p, e)
|
|
e.roll_mod = e.roll_mod + 2
|
|
end
|
|
}, {
|
|
name = "Hex",
|
|
desc = "-2 enemy roll",
|
|
timing = "pre_roll",
|
|
energy = -2,
|
|
use = function(p, e)
|
|
p.roll_mod = p.roll_mod - 2
|
|
end
|
|
}, {
|
|
name = "Fury",
|
|
desc = "win/lose double dmg",
|
|
timing = "pre_roll",
|
|
energy = -2,
|
|
use = function(p, e)
|
|
e.mult = e.mult * 2
|
|
p.mult = p.mult * 2
|
|
end
|
|
}, {
|
|
name = "Caution",
|
|
desc = "win/lose half dmg",
|
|
timing = "pre_roll",
|
|
energy = 1,
|
|
use = function(p, e)
|
|
e.mult = e.mult * 0.5
|
|
p.mult = p.mult * 0.5
|
|
end
|
|
}, {
|
|
name = "Heal",
|
|
desc = "+2 hp",
|
|
timing = "immediate",
|
|
energy = -1,
|
|
use = function(p, e)
|
|
e.hp = math.min(e.max_hp, e.hp + 2)
|
|
current_damage.enemy = current_damage.enemy - 2 -- show damage taken
|
|
end
|
|
}, {
|
|
name = "Counter",
|
|
desc = "hit enemy for 1 if you lose",
|
|
timing = "post_result",
|
|
energy = -1,
|
|
use = function(p, e, player_won)
|
|
-- player_won is from player's perspective, so enemy perspective is flipped
|
|
if player_won == true then -- enemy lost (player won)
|
|
p.hp = p.hp - 1
|
|
current_damage.player = current_damage.player + 1 -- show damage taken
|
|
end
|
|
end
|
|
}, {
|
|
name = "Weaken",
|
|
desc = "-1 enemy dmg",
|
|
timing = "pre_roll",
|
|
energy = -1,
|
|
use = function(p, e)
|
|
p.weaken = (p.weaken or 0) + 1 -- player is weakened (does less damage)
|
|
end
|
|
}, {
|
|
name = "Rage",
|
|
desc = "+1 your dmg",
|
|
timing = "pre_roll",
|
|
energy = -1,
|
|
use = function(p, e)
|
|
e.rage = (e.rage or 0) + 1 -- add rage bonus for enemy
|
|
end
|
|
}, {
|
|
name = "Break",
|
|
desc = "if tie, you win",
|
|
timing = "post_result",
|
|
energy = -2,
|
|
use = function(p, e, player_won)
|
|
if player_won == nil then -- tie occurred
|
|
-- Enemy wins the tie, so player takes damage
|
|
p.hp = p.hp - (2 * e.mult)
|
|
current_damage.player = current_damage.player + (2 * e.mult) -- show damage taken
|
|
end
|
|
end
|
|
}}
|
|
|
|
-- === 3. ENTITY FACTORIES ===
|
|
function get_enemy(level)
|
|
return {
|
|
name = enemies[level].name,
|
|
hp = enemies[level].hp,
|
|
max_hp = enemies[level].hp, -- store original max HP
|
|
roll_mod = enemies[level].roll_mod,
|
|
mult = enemies[level].mult
|
|
}
|
|
end
|
|
|
|
function reset_player(energy)
|
|
return {
|
|
hp = 10,
|
|
roll_mod = 0,
|
|
mult = 1,
|
|
energy = energy
|
|
}
|
|
end
|
|
|
|
-- === 4. STATE ===
|
|
level = 1
|
|
player = reset_player(0)
|
|
enemy = get_enemy(level)
|
|
-- animation + shake
|
|
player.display_roll = 0
|
|
enemy.display_roll = 0
|
|
|
|
msg = ""
|
|
phase = "title"
|
|
phase_time = 0
|
|
roll_text = ""
|
|
current_damage = {
|
|
player = 0,
|
|
enemy = 0
|
|
} -- store damage for display
|
|
pending_effects = {
|
|
player = {},
|
|
enemy = {},
|
|
previous_player = {},
|
|
previous_enemy = {}
|
|
}
|
|
|
|
-- turn resolution (see resolve_turn()) - what the steady-state draw should show
|
|
resolve_stage = nil -- "rolling" | "mods" | "total" | nil
|
|
resolve_highlight = nil -- "current_player" | "previous_player" | "current_enemy" | "previous_enemy" | nil
|
|
resolve_message_title = nil -- set/cleared via resolve_message()/clear_resolve_message()
|
|
resolve_message_subtitle = nil
|
|
resolve_message_color = 12
|
|
|
|
-- Card tracking for two-round strategy
|
|
current_card = nil
|
|
enemy_card = nil
|
|
previous_card = nil
|
|
previous_enemy_card = nil
|
|
|
|
shake = 0
|
|
d = 2 -- shake intensity
|
|
|
|
original_palette = {}
|
|
for i = 0, 15 do
|
|
original_palette[i] = peek(0x3FC0 + i)
|
|
end
|
|
|
|
-- Deck system
|
|
draw_pile = {}
|
|
discard_pile = {}
|
|
unlocked_cards = {} -- tracks which cards are in your deck
|
|
hand = {} -- player's current hand
|
|
card_choice_options = {} -- for level up card selection
|
|
|
|
picked = 1
|
|
card_pick_selection = 1
|
|
|
|
-- card-playing animation (hand -> center, before the dice roll)
|
|
playing_card = nil
|
|
play_from_x = 0
|
|
play_from_y = 0
|
|
|
|
-- === 5. DECK & HAND ===
|
|
function shuffle_deck(deck)
|
|
for i = #deck, 2, -1 do
|
|
local j = math.random(i)
|
|
deck[i], deck[j] = deck[j], deck[i]
|
|
end
|
|
end
|
|
|
|
function initialize_deck()
|
|
draw_pile = {}
|
|
discard_pile = {}
|
|
|
|
-- Start with first 8 cards for level 1, or use unlocked_cards for higher levels
|
|
local cards_to_add = {}
|
|
if level == 1 then
|
|
unlocked_cards = {}
|
|
for i = 1, 8 do
|
|
unlocked_cards[i] = true
|
|
cards_to_add = {1, 2, 3, 4, 5, 6, 7, 8}
|
|
end
|
|
end
|
|
|
|
-- Add 2 copies of each unlocked card to deck
|
|
for i, card in ipairs(cards) do
|
|
if unlocked_cards[i] then
|
|
table.insert(draw_pile, card)
|
|
table.insert(draw_pile, card)
|
|
end
|
|
end
|
|
|
|
shuffle_deck(draw_pile)
|
|
end
|
|
|
|
function generate_card_choices()
|
|
card_choice_options = {}
|
|
local available_cards = {}
|
|
|
|
-- Find cards not yet unlocked
|
|
for i, card in ipairs(cards) do
|
|
if not unlocked_cards[i] then
|
|
table.insert(available_cards, i)
|
|
end
|
|
end
|
|
|
|
-- If we have available cards, pick 3 random ones
|
|
if #available_cards > 0 then
|
|
shuffle_deck(available_cards)
|
|
for i = 1, math.min(3, #available_cards) do
|
|
table.insert(card_choice_options, available_cards[i])
|
|
end
|
|
end
|
|
end
|
|
|
|
function add_card_to_deck(card_index)
|
|
unlocked_cards[card_index] = true
|
|
local card = cards[card_index]
|
|
-- Add 2 copies to discard pile (will be shuffled in next game)
|
|
table.insert(discard_pile, card)
|
|
table.insert(discard_pile, card)
|
|
end
|
|
|
|
function reshuffle_if_needed()
|
|
if #draw_pile < 3 and #discard_pile > 0 then
|
|
-- Move discard pile to draw pile and shuffle
|
|
for _, card in ipairs(discard_pile) do
|
|
table.insert(draw_pile, card)
|
|
end
|
|
discard_pile = {}
|
|
shuffle_deck(draw_pile)
|
|
end
|
|
end
|
|
|
|
function draw_hand()
|
|
hand = {}
|
|
reshuffle_if_needed()
|
|
|
|
-- If player has no energy, ensure at least one energy-positive card
|
|
if player.energy == 0 then
|
|
local energy_positive_found = false
|
|
local temp_hand = {}
|
|
|
|
-- Try to draw 3 cards, checking for energy-positive ones
|
|
for i = 1, 3 do
|
|
if #draw_pile > 0 then
|
|
local card = table.remove(draw_pile, 1)
|
|
table.insert(temp_hand, card)
|
|
if card.energy >= 0 then
|
|
energy_positive_found = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If no energy-positive card found, replace one with an energy-positive card
|
|
if not energy_positive_found and #temp_hand > 0 then
|
|
-- Find an energy-positive card in remaining deck or discard
|
|
local energy_card = nil
|
|
|
|
-- Look in draw pile first
|
|
for i, card in ipairs(draw_pile) do
|
|
if card.energy >= 0 then
|
|
energy_card = table.remove(draw_pile, i)
|
|
break
|
|
end
|
|
end
|
|
|
|
-- If not found, look in discard pile
|
|
if not energy_card then
|
|
for i, card in ipairs(discard_pile) do
|
|
if card.energy >= 0 then
|
|
energy_card = table.remove(discard_pile, i)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Replace first card with energy-positive card
|
|
if energy_card then
|
|
table.insert(draw_pile, temp_hand[1]) -- Put replaced card back
|
|
temp_hand[1] = energy_card
|
|
end
|
|
end
|
|
|
|
hand = temp_hand
|
|
else
|
|
-- Normal draw when player has energy
|
|
for i = 1, 3 do
|
|
if #draw_pile > 0 then
|
|
table.insert(hand, table.remove(draw_pile, 1))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fill remaining slots if deck was too small
|
|
while #hand < 3 and (#draw_pile > 0 or #discard_pile > 0) do
|
|
reshuffle_if_needed()
|
|
if #draw_pile > 0 then
|
|
table.insert(hand, table.remove(draw_pile, 1))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Initialize deck at start
|
|
initialize_deck()
|
|
draw_hand()
|
|
|
|
-- === 6. COMBAT LOGIC ===
|
|
-- How long resolve_turn() pauses at each kind of beat, in frames (60/sec).
|
|
-- Slower than a snappy UI on purpose - this is the one place the player needs
|
|
-- to actually read something before it changes.
|
|
local RESOLVE_REVEAL_PAUSE = 35 -- highlight + message showing, before it applies
|
|
local RESOLVE_SETTLE_PAUSE = 45 -- holding the number after it changes
|
|
local RESOLVE_STAGE_PAUSE = 40 -- between major stages (roll settle, mods -> total, ...)
|
|
local RESOLVE_READ_PAUSE = 60 -- total / result / damage announcements
|
|
|
|
-- Which pending_effects list backs each on-screen played-card label, in the
|
|
-- order effects of a given timing resolve (matches the original apply_effects order).
|
|
local resolve_effect_groups = {
|
|
{key = "player", side = "current_player"},
|
|
{key = "previous_player", side = "previous_player"},
|
|
{key = "enemy", side = "current_enemy"},
|
|
{key = "previous_enemy", side = "previous_enemy"}
|
|
}
|
|
|
|
-- Sets/clears the one message box everyone (card reveals, totals, win/lose,
|
|
-- damage announcements, menus) draws through - see draw_message().
|
|
function resolve_message(title, color, subtitle)
|
|
resolve_message_title = title
|
|
resolve_message_color = color
|
|
resolve_message_subtitle = subtitle
|
|
end
|
|
|
|
function clear_resolve_message()
|
|
resolve_message_title = nil
|
|
resolve_message_subtitle = nil
|
|
end
|
|
|
|
-- Reveals every pending effect of a given timing one at a time: highlight the
|
|
-- card responsible, show its name/description, apply it, then hold briefly so
|
|
-- the resulting number change is readable before moving to the next one.
|
|
function reveal_and_apply_effects(timing, player_won)
|
|
for _, group in ipairs(resolve_effect_groups) do
|
|
for _, effect in ipairs(pending_effects[group.key]) do
|
|
if effect.timing == timing then
|
|
resolve_highlight = group.side
|
|
resolve_message(effect.name, 12, effect.desc)
|
|
for i = 1, RESOLVE_REVEAL_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
if timing == "post_result" then
|
|
effect.use(player, enemy, player_won)
|
|
else
|
|
effect.use(player, enemy)
|
|
end
|
|
if resolve_stage == "mods" then
|
|
-- reflect rerolls (Lucky/Curse/Turtle/Intuition) on the dice
|
|
-- sprite immediately rather than waiting for the group to finish
|
|
player.display_roll = player.roll
|
|
enemy.display_roll = enemy.roll
|
|
end
|
|
for i = 1, RESOLVE_SETTLE_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
resolve_highlight = nil
|
|
clear_resolve_message()
|
|
end
|
|
|
|
-- The full turn resolution, run as a coroutine on the gameplay effect queue so
|
|
-- it can never be skipped or overlap the next turn's. Runs once per turn:
|
|
-- reveal the cards' effects on the roll, then the totals, then the winner,
|
|
-- then the post-result effects, and only then the actual HP damage - each
|
|
-- step announced through the same message box so nothing lands as a surprise.
|
|
function resolve_turn()
|
|
for i = 1, RESOLVE_STAGE_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
|
|
reveal_and_apply_effects("immediate")
|
|
|
|
-- Roll the dice: flicker, then settle on the real (unmodified) values
|
|
resolve_stage = "rolling"
|
|
resolve_message("Rolling...", 12)
|
|
for f = 1, 30 do
|
|
if f % 3 == 0 then
|
|
player.display_roll = math.random(1, 6)
|
|
enemy.display_roll = math.random(1, 6)
|
|
end
|
|
coroutine.yield()
|
|
end
|
|
player.display_roll = player.roll
|
|
enemy.display_roll = enemy.roll
|
|
clear_resolve_message()
|
|
for i = 1, RESOLVE_STAGE_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- Modifiers land on top of the raw roll, one card at a time
|
|
resolve_stage = "mods"
|
|
reveal_and_apply_effects("pre_roll")
|
|
reveal_and_apply_effects("post_roll")
|
|
player.display_roll = player.roll
|
|
enemy.display_roll = enemy.roll
|
|
for i = 1, RESOLVE_STAGE_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- Total
|
|
player.total = player.roll + player.roll_mod
|
|
enemy.total = enemy.roll + enemy.roll_mod
|
|
resolve_stage = "total"
|
|
resolve_message("You: " .. player.total, 12, enemy.name .. ": " .. enemy.total)
|
|
for i = 1, RESOLVE_READ_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
clear_resolve_message()
|
|
resolve_stage = nil
|
|
|
|
-- Determine the winner and how much base combat damage is on the table
|
|
local player_won, damage_target, dmg
|
|
if player.total > enemy.total then
|
|
player_won = true
|
|
damage_target = "enemy"
|
|
-- mult can be fractional (Caution, Impulse, ...) - round to a whole
|
|
-- number since the hearts display can't show a half a heart
|
|
dmg = math.floor(math.max(0, 2 * player.mult + (player.rage or 0) - (enemy.weaken or 0)) + 0.5)
|
|
resolve_message("You win!", 2)
|
|
elseif enemy.total > player.total then
|
|
player_won = false
|
|
damage_target = "player"
|
|
dmg = math.floor(math.max(0, 2 * enemy.mult + (enemy.rage or 0) - (player.weaken or 0)) + 0.5)
|
|
resolve_message(enemy.name .. " wins!", 6)
|
|
else
|
|
resolve_message("Tie!", 9)
|
|
end
|
|
for i = 1, RESOLVE_READ_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
clear_resolve_message()
|
|
|
|
-- Post-result card effects (Counter, Break, Poison, Spike, Drain, ...)
|
|
reveal_and_apply_effects("post_result", player_won)
|
|
|
|
-- Announce the incoming damage before it actually lands, so the HP bar
|
|
-- dropping is never a surprise
|
|
if damage_target == "enemy" then
|
|
resolve_message(enemy.name .. " takes " .. dmg .. " damage!", 6)
|
|
for i = 1, RESOLVE_REVEAL_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
flash_background(dmg * 8)
|
|
sequenced_damage("enemy", dmg, {
|
|
flash_cycles = 8,
|
|
damage_display_frames = 20,
|
|
step_wait = 10
|
|
})
|
|
elseif damage_target == "player" then
|
|
local shield_reduction = player.shield or 0
|
|
local final_dmg = math.max(0, dmg - shield_reduction)
|
|
resolve_message("You take " .. final_dmg .. " damage!", 2)
|
|
for i = 1, RESOLVE_REVEAL_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
sequenced_damage("player", final_dmg, {
|
|
flash_cycles = 8,
|
|
damage_display_frames = 20,
|
|
step_wait = 10
|
|
})
|
|
if shield_reduction > 0 then
|
|
player.shield = math.max(0, shield_reduction - 1)
|
|
shake = 5 * shield_reduction
|
|
else
|
|
shake = 5 * final_dmg
|
|
end
|
|
end
|
|
clear_resolve_message()
|
|
for i = 1, RESOLVE_STAGE_PAUSE do
|
|
coroutine.yield()
|
|
end
|
|
|
|
resolve_message("Press Z to continue", 12)
|
|
while not btnp(4) do
|
|
coroutine.yield()
|
|
end
|
|
clear_resolve_message()
|
|
|
|
-- Reset for the next turn
|
|
player.roll_mod, enemy.roll_mod = 0, 0
|
|
player.mult, enemy.mult = 1, 1
|
|
player.weaken, enemy.weaken = 0, 0
|
|
player.rage, enemy.rage = 0, 0
|
|
player.energy = math.min(5, player.energy + 1)
|
|
current_damage.player = 0
|
|
current_damage.enemy = 0
|
|
|
|
previous_card = current_card
|
|
previous_enemy_card = enemy_card
|
|
current_card = nil
|
|
enemy_card = nil
|
|
|
|
draw_hand()
|
|
phase = (enemy.hp <= 0 or player.hp <= 0) and "game_over" or "choose"
|
|
phase_time = 0
|
|
end
|
|
|
|
function start_turn(card)
|
|
-- Don't store previous cards here anymore - they're already set when returning to choose phase
|
|
current_card = card
|
|
enemy_card = enemy_cards[math.random(#enemy_cards)]
|
|
|
|
-- Apply card energy cost/gain
|
|
player.energy = math.max(0, math.min(5, player.energy + card.energy))
|
|
|
|
-- Discard all cards in hand
|
|
for _, hand_card in ipairs(hand) do
|
|
table.insert(discard_pile, hand_card)
|
|
end
|
|
hand = {}
|
|
|
|
-- Store effects for proper timing - both current and previous cards
|
|
pending_effects.player = {card}
|
|
pending_effects.enemy = {enemy_card}
|
|
pending_effects.previous_player = previous_card and {previous_card} or {}
|
|
pending_effects.previous_enemy = previous_enemy_card and {previous_enemy_card} or {}
|
|
|
|
-- Roll now, but don't reveal until resolve_turn() gets to it
|
|
player.roll = math.random(1, 6)
|
|
enemy.roll = math.random(1, 6)
|
|
player.display_roll = 0
|
|
enemy.display_roll = 0
|
|
|
|
phase = "resolve"
|
|
phase_time = 0
|
|
queue_effect(resolve_turn, 0)
|
|
end
|
|
|
|
-- === 7. EFFECTS QUEUE & HELPERS ===
|
|
effect_queue = {}
|
|
effect_queue_timers = {}
|
|
|
|
-- queue_effect(fn, delay)
|
|
-- fn: function() coroutine.yield() ... end -- a coroutine body (no explicit coroutine.create needed here)
|
|
-- delay: optional number of frames to wait before starting this effect
|
|
function queue_effect(fn, delay)
|
|
local co = coroutine.create(fn)
|
|
table.insert(effect_queue, co)
|
|
table.insert(effect_queue_timers, delay or 0)
|
|
end
|
|
|
|
-- run_effects() -- call once per frame (already called at end of TIC)
|
|
-- Processes only the first queued effect (sequential). If the effect has an initial delay,
|
|
-- we decrement the timer each frame and only resume the coroutine once the delay reaches 0.
|
|
function run_effects()
|
|
if #effect_queue == 0 then
|
|
return
|
|
end
|
|
|
|
-- only run the front effect to keep effects sequential
|
|
local timer = effect_queue_timers[1]
|
|
if timer > 0 then
|
|
effect_queue_timers[1] = timer - 1
|
|
return
|
|
end
|
|
|
|
local co = effect_queue[1]
|
|
local ok, err = coroutine.resume(co)
|
|
if not ok then
|
|
-- error: remove and discard the effect to avoid blocking the queue
|
|
-- (keep message-free to avoid spamming; developer can add logging if needed)
|
|
table.remove(effect_queue, 1)
|
|
table.remove(effect_queue_timers, 1)
|
|
return
|
|
end
|
|
|
|
-- if coroutine finished, remove it; otherwise leave it for the next resume
|
|
if coroutine.status(co) == "dead" then
|
|
table.remove(effect_queue, 1)
|
|
table.remove(effect_queue_timers, 1)
|
|
end
|
|
end
|
|
|
|
-- flash_background(frames)
|
|
-- frames: optional number of frames to flash (defaults to 40)
|
|
function flash_background(frames)
|
|
local f = frames or 40
|
|
-- play impact sound safely
|
|
pcall(function()
|
|
sfx(0, "E-4", 12, 0)
|
|
end)
|
|
for i = 1, f do
|
|
if (i % 4) < 2 then
|
|
poke(0x3FC0, 255) -- red tint
|
|
else
|
|
poke(0x3FC0, original_palette[0])
|
|
end
|
|
coroutine.yield()
|
|
end
|
|
-- ensure palette restored
|
|
poke(0x3FC0, original_palette[0])
|
|
end
|
|
|
|
-- sequenced_damage(target_key, amount, opts)
|
|
-- target_key: "enemy" or "player" (used to pick table and current_damage key)
|
|
-- amount: integer damage to apply
|
|
-- opts: table optional fields:
|
|
-- flash_cycles (default 8) - number of flash toggles for damage readout
|
|
-- damage_display_frames (default 20) - frames to hold the damage readout steady
|
|
-- step_wait (default 10) - frames to wait between each HP decrement step
|
|
function sequenced_damage(target_key, amount, opts)
|
|
opts = opts or {}
|
|
local flash_cycles = opts.flash_cycles or 8
|
|
local damage_display_frames = opts.damage_display_frames or 20
|
|
local step_wait = opts.step_wait or 10
|
|
|
|
local target = (target_key == "enemy") and enemy or player
|
|
local dmg_key = (target_key == "enemy") and "enemy" or "player"
|
|
|
|
-- play impact sound once
|
|
pcall(function()
|
|
sfx(0, "E-4", 12, 0)
|
|
end)
|
|
|
|
-- Flash the damage readout on/off for visibility
|
|
for i = 1, flash_cycles do
|
|
if (i % 2) == 0 then
|
|
current_damage[dmg_key] = amount
|
|
else
|
|
current_damage[dmg_key] = 0
|
|
end
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- Hold the damage readout steady for a short period
|
|
current_damage[dmg_key] = amount
|
|
for i = 1, damage_display_frames do
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- Apply HP reduction stepwise so the hearts animate
|
|
for step = 1, amount do
|
|
target.hp = math.max(0, target.hp - 1)
|
|
pcall(function()
|
|
sfx(0, "E-4", 12, 0)
|
|
end)
|
|
for w = 1, step_wait do
|
|
coroutine.yield()
|
|
end
|
|
end
|
|
|
|
-- Clear damage display
|
|
current_damage[dmg_key] = 0
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- === 8. UI WIDGETS ===
|
|
-- print() returns the true pixel width of what it just drew, so measure with
|
|
-- an off-screen draw first and use that for exact centering - no guessing.
|
|
function text_width(text)
|
|
return print(text, 0, -20, 0, false, 1, true)
|
|
end
|
|
|
|
function print_centered(text, cx, y, color)
|
|
print(text, cx - text_width(text) / 2, y, color, false, 1, true)
|
|
end
|
|
|
|
-- Small bordered label showing the name of a played card (or nothing, if there
|
|
-- isn't one) - used for the current/previous card shown for player and enemy.
|
|
-- Turns bright while resolve_turn() is actively revealing this card's effect.
|
|
function draw_played_card_label(x, y, w, h, color, card, highlighted)
|
|
if not card then
|
|
return
|
|
end
|
|
local border = highlighted and 12 or color
|
|
rectb(x, y, w, h, border)
|
|
local icon_w = 10 -- 8px icon + a little breathing room
|
|
spr(card_art_id(card), x + 2, y + (h - 8) / 2, 0)
|
|
print_centered(card.name, x + icon_w + (w - icon_w) / 2, y + h / 2 - 3, border)
|
|
end
|
|
|
|
-- The ONE message box shape used everywhere in the game - same border, same
|
|
-- background, same precise centering - but drawn in one of two colour
|
|
-- schemes so it's visually obvious which kind of message you're reading:
|
|
-- info - passive, browsing (card descriptions while choosing/rewarding)
|
|
-- event - something is actively happening (turn resolution, win/lose, game over)
|
|
MESSAGE_W = 200
|
|
MESSAGE_X = 120 - MESSAGE_W / 2
|
|
MESSAGE_INFO_BG, MESSAGE_INFO_BORDER = 8, 10 -- cool blue
|
|
MESSAGE_EVENT_BG, MESSAGE_EVENT_BORDER = 1, 3 -- warm purple/orange
|
|
|
|
function draw_message(y, color, title, subtitle, bg, border)
|
|
local line_h = 8
|
|
local h = subtitle and (line_h * 2 + 6) or (line_h + 6)
|
|
rect(MESSAGE_X, y, MESSAGE_W, h, bg)
|
|
rectb(MESSAGE_X, y, MESSAGE_W, h, border)
|
|
if subtitle then
|
|
print_centered(title, 120, y + 3, color)
|
|
print_centered(subtitle, 120, y + 3 + line_h, color)
|
|
else
|
|
print_centered(title, 120, y + 3, color)
|
|
end
|
|
end
|
|
|
|
function draw_info_message(y, color, title, subtitle)
|
|
draw_message(y, color, title, subtitle, MESSAGE_INFO_BG, MESSAGE_INFO_BORDER)
|
|
end
|
|
|
|
function draw_event_message(y, color, title, subtitle)
|
|
draw_message(y, color, title, subtitle, MESSAGE_EVENT_BG, MESSAGE_EVENT_BORDER)
|
|
end
|
|
|
|
-- === 9. DRAWING HELPERS ===
|
|
function draw_hud()
|
|
-- Draw level
|
|
spr(0, 110, 2, 0)
|
|
print(level, 115, 2, 7, false, 1, true)
|
|
|
|
-- Draw energy sprites
|
|
for i = 1, player.energy do
|
|
local x = -4 + (8 * i)
|
|
spr(17, x, 10, 0)
|
|
end
|
|
|
|
for i = player.energy + 1, 5 do
|
|
local x = -4 + (8 * i)
|
|
spr(18, x, 10, 0)
|
|
end
|
|
|
|
-- Draw deck info
|
|
spr(4, 220, 115, 0)
|
|
print(#draw_pile, 230, 116, 10, false, 1, true)
|
|
spr(5, 220, 125, 0)
|
|
print(#discard_pile, 230, 126, 14, false, 1, true)
|
|
|
|
-- Update played cards UI
|
|
|
|
if phase ~= "choose" then
|
|
draw_played_card_label(4, 36, 52, 14, 2, current_card, resolve_highlight == "current_player")
|
|
draw_played_card_label(186, 36, 52, 14, 6, enemy_card, resolve_highlight == "current_enemy")
|
|
end
|
|
draw_played_card_label(4, 20, 52, 14, 2, previous_card, resolve_highlight == "previous_player")
|
|
draw_played_card_label(186, 20, 52, 14, 6, previous_enemy_card, resolve_highlight == "previous_enemy")
|
|
end
|
|
|
|
function draw_health_bars()
|
|
-- Draw player health bar
|
|
for i = 1, 10 do
|
|
local x = 4 + (i - 1) * 8
|
|
if i <= player.hp then
|
|
spr(1, x, 2, 0) -- healthy HP
|
|
else
|
|
spr(3, x, 2, 0) -- damaged HP
|
|
end
|
|
end
|
|
|
|
-- Show player damage
|
|
if current_damage.player ~= 0 then
|
|
print((current_damage.player < 0 and "+" or "") .. -math.floor(current_damage.player), 88, 3,
|
|
current_damage.player > 0 and 2 or 6, false, 1, true)
|
|
end
|
|
|
|
-- Draw enemy health bar
|
|
for i = 1, enemy.max_hp do
|
|
local x = 240 - (enemy.max_hp * 8) + (i - 1) * 8
|
|
if i <= (enemy.max_hp - enemy.hp) then
|
|
spr(3, x, 2, 0) -- damaged HP on the left
|
|
else
|
|
spr(2, x, 2, 0) -- healthy HP on the right
|
|
end
|
|
end
|
|
|
|
-- Show enemy damage
|
|
if current_damage.enemy > 0 then
|
|
local enemy_bar_start = 240 - (enemy.max_hp * 8) - 8
|
|
print((current_damage.enemy < 0 and "+" or "") .. -math.floor(current_damage.enemy), enemy_bar_start, 3,
|
|
current_damage.enemy > 0 and 2 or 6, false, 1, true)
|
|
end
|
|
|
|
-- Draw enemy name under health bar
|
|
local enemy_name_x = 240 - (#enemy.name * 4)
|
|
print(enemy.name, enemy_name_x, 12, 7, false, 1, true)
|
|
|
|
-- Show shield sprite if player has shield
|
|
if player.shield and player.shield > 0 then
|
|
for i = 1, player.shield do
|
|
spr(16, 44 + (i - 1) * 8, 12, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Hand layout + play-in animation (choose phase)
|
|
CARD_W = 28
|
|
CARD_H = 36
|
|
HAND_Y = 96
|
|
HAND_LIFT = 8 -- how far the selected card pops up out of the hand
|
|
HAND_X0 = 74
|
|
HAND_SPACING = 34
|
|
PLAY_TARGET_X = 106
|
|
PLAY_TARGET_Y = 44
|
|
PLAY_DURATION = 15 -- frames for the hand -> center play animation
|
|
REWARD_Y = 50 -- vertical center for the level-up card reward screen
|
|
|
|
-- placeholder art: one icon per effect timing, until real card art exists
|
|
local card_art_by_timing = {
|
|
immediate = 6,
|
|
pre_roll = 7,
|
|
post_roll = 8,
|
|
post_result = 9
|
|
}
|
|
|
|
function card_art_id(card)
|
|
return card.art or card_art_by_timing[card.timing] or 6
|
|
end
|
|
|
|
function draw_card(card, x, y, w, h, is_selected, can_afford, show_name)
|
|
local border_color = can_afford and (is_selected and 11 or 9) or (is_selected and 13 or 14)
|
|
rectb(x, y, w, h, border_color)
|
|
if is_selected then
|
|
rect(x + 1, y + 1, w - 2, h - 2, 8)
|
|
end
|
|
|
|
spr(card_art_id(card), x + w / 2 - 4, y + 4, 0)
|
|
|
|
if show_name then
|
|
local text_color = can_afford and (is_selected and 11 or 9) or (is_selected and 0 or 14)
|
|
print(card.name, x + 3, y + h - 12, text_color, false, 1, true)
|
|
end
|
|
|
|
-- Show energy cost/gain in the card's top-right corner
|
|
for i = 1, math.abs(card.energy) do
|
|
local sprite_id = card.energy >= 0 and 18 or 17
|
|
local offset_x = x + w - 10 - (i - 1) * 6
|
|
spr(sprite_id, offset_x, y + 2, 0)
|
|
end
|
|
end
|
|
|
|
-- === 10. PHASE HANDLERS ===
|
|
function phase_title()
|
|
spr(320, 60, 20, 0, 1, 0, 0, 16, 8)
|
|
print("smith", 98, 60, 10, false, 2, false)
|
|
print("Press X to Start", 84, 100, 14, false, 1, true)
|
|
if btnp(5) then
|
|
phase = "choose"
|
|
level = 1
|
|
player = reset_player(3)
|
|
enemy = get_enemy(level)
|
|
initialize_deck()
|
|
draw_hand()
|
|
end
|
|
end
|
|
|
|
function phase_choose()
|
|
-- Card Picker
|
|
if btnp(2) then
|
|
picked = picked - 1
|
|
end
|
|
|
|
if btnp(3) then
|
|
picked = picked + 1
|
|
end
|
|
picked = math.min(3, math.max(1, picked))
|
|
|
|
-- Show hand fanned out along the bottom of the screen
|
|
for i, card in ipairs(hand) do
|
|
local x = HAND_X0 + (i - 1) * HAND_SPACING
|
|
local is_selected = i == picked
|
|
local y = is_selected and (HAND_Y - HAND_LIFT) or HAND_Y
|
|
local can_afford = (card.energy >= 0) or (player.energy >= -card.energy)
|
|
draw_card(card, x, y, CARD_W, CARD_H, is_selected, can_afford, true)
|
|
end
|
|
|
|
local selected_card = hand[picked]
|
|
local can_afford = (selected_card.energy >= 0) or (player.energy >= -selected_card.energy)
|
|
draw_info_message(40, can_afford and 12 or 14, selected_card.name, selected_card.desc)
|
|
|
|
if btnp(4) and can_afford then
|
|
playing_card = selected_card
|
|
play_from_x = HAND_X0 + (picked - 1) * HAND_SPACING
|
|
play_from_y = HAND_Y - HAND_LIFT
|
|
phase = "play_card"
|
|
phase_time = 0
|
|
end
|
|
end
|
|
|
|
function phase_play_card()
|
|
phase_time = phase_time + 1
|
|
local t = math.min(1, phase_time / PLAY_DURATION)
|
|
local x = lerp(play_from_x, PLAY_TARGET_X, t)
|
|
local y = lerp(play_from_y, PLAY_TARGET_Y, t)
|
|
draw_card(playing_card, x, y, CARD_W, CARD_H, true, true, true)
|
|
|
|
if phase_time >= PLAY_DURATION then
|
|
start_turn(playing_card)
|
|
end
|
|
end
|
|
|
|
function phase_card_reward()
|
|
-- Card selection after level victory
|
|
draw_info_message(18, 12, "Choose a card to add to your deck")
|
|
|
|
if btnp(2) then
|
|
card_pick_selection = card_pick_selection - 1
|
|
end
|
|
if btnp(3) then
|
|
card_pick_selection = card_pick_selection + 1
|
|
end
|
|
card_pick_selection = math.min(#card_choice_options, math.max(1, card_pick_selection))
|
|
|
|
-- Show card options, centered in the middle of the screen at hand-card size
|
|
for i, card_index in ipairs(card_choice_options) do
|
|
local card = cards[card_index]
|
|
local x = HAND_X0 + (i - 1) * HAND_SPACING
|
|
local is_selected = i == card_pick_selection
|
|
local y = is_selected and (REWARD_Y - HAND_LIFT) or REWARD_Y
|
|
draw_card(card, x, y, CARD_W, CARD_H, is_selected, true, true) -- always affordable in reward phase
|
|
end
|
|
|
|
-- Show selected card description
|
|
if #card_choice_options > 0 then
|
|
local selected_card = cards[card_choice_options[card_pick_selection]]
|
|
draw_info_message(96, 12, selected_card.desc)
|
|
end
|
|
|
|
if btnp(4) and #card_choice_options > 0 then
|
|
add_card_to_deck(card_choice_options[card_pick_selection])
|
|
-- Advance to next level
|
|
level = math.min(level + 1, #enemies)
|
|
player = reset_player(0)
|
|
enemy = get_enemy(level)
|
|
-- Clear card displays for new level
|
|
current_card = nil
|
|
enemy_card = nil
|
|
previous_card = nil
|
|
previous_enemy_card = nil
|
|
initialize_deck()
|
|
draw_hand()
|
|
phase = "choose"
|
|
phase_time = 0
|
|
end
|
|
end
|
|
|
|
-- Draws `count` plain pips (sprite #21) for one side's roll/total.
|
|
-- Pushed down from the old y=60/90 so the message box (see draw_message) has
|
|
-- a clear band above them, between the played-card labels and the dice.
|
|
RESOLVE_DICE_Y = 78
|
|
RESOLVE_PIP_Y = 100
|
|
|
|
function draw_pip_row(count, x, flipped)
|
|
for i = 1, count do
|
|
if flipped then
|
|
spr(21, x - (i - 1) * 8, RESOLVE_PIP_Y, 0, 1, 1)
|
|
else
|
|
spr(21, x + (i - 1) * 8, RESOLVE_PIP_Y, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draws a roll with its modifier broken out: kept pips, pips lost to a
|
|
-- negative modifier (#22), and pips gained from a positive one (#23).
|
|
function draw_mod_pip_row(roll, mod, x, flipped)
|
|
local positive_mod = math.max(0, mod)
|
|
local negative_mod = math.min(math.abs(math.min(0, mod)), roll)
|
|
local adjusted_roll = math.max(0, roll - negative_mod)
|
|
|
|
local function pip(sprite, i)
|
|
local offset = (adjusted_roll + i - 1) * 8
|
|
if flipped then
|
|
spr(sprite, x - offset, RESOLVE_PIP_Y, 0, 1, 1)
|
|
else
|
|
spr(sprite, x + offset, RESOLVE_PIP_Y, 0)
|
|
end
|
|
end
|
|
|
|
draw_pip_row(adjusted_roll, x, flipped)
|
|
for i = 1, negative_mod do
|
|
pip(22, i)
|
|
end
|
|
for i = 1, positive_mod do
|
|
pip(23, negative_mod + i)
|
|
end
|
|
end
|
|
|
|
-- The dice/roll numeral sprites shown throughout the whole resolve phase.
|
|
function draw_dice_sprites()
|
|
if player.display_roll == 0 and enemy.display_roll == 0 then
|
|
return
|
|
end
|
|
local player_dice_sprite = 256 + (player.display_roll - 1) * 2
|
|
local enemy_dice_sprite = 288 + (enemy.display_roll - 1) * 2
|
|
spr(player_dice_sprite, 50, RESOLVE_DICE_Y, 0, 1, 0, 0, 2, 2)
|
|
spr(enemy_dice_sprite, 150, RESOLVE_DICE_Y, 0, 1, 0, 0, 2, 2)
|
|
end
|
|
|
|
-- Everything here is steady-state: it just draws whatever resolve_turn() has
|
|
-- set so far. resolve_turn() (queued from start_turn(), driven by run_effects())
|
|
-- is what actually advances the turn and mutates game state.
|
|
function phase_resolve()
|
|
draw_dice_sprites()
|
|
|
|
if resolve_stage == "rolling" then
|
|
draw_pip_row(player.display_roll, 50, false)
|
|
draw_pip_row(enemy.display_roll, 150, true)
|
|
elseif resolve_stage == "mods" then
|
|
draw_mod_pip_row(player.roll, player.roll_mod, 50, false)
|
|
draw_mod_pip_row(enemy.roll, enemy.roll_mod, 150, true)
|
|
elseif resolve_stage == "total" then
|
|
draw_pip_row(player.total, 50, false)
|
|
draw_pip_row(enemy.total, 150, true)
|
|
end
|
|
|
|
if resolve_message_title then
|
|
draw_event_message(50, resolve_message_color, resolve_message_title, resolve_message_subtitle)
|
|
end
|
|
end
|
|
|
|
function phase_game_over()
|
|
previous_card = nil
|
|
previous_enemy_card = nil
|
|
if enemy.hp <= 0 and player.hp > 0 then
|
|
draw_event_message(50, 6, "You defeated " .. enemy.name .. "!", "Press Z to choose new card")
|
|
else
|
|
draw_event_message(50, 2, enemy.name .. " defeated you!", "Press X to restart")
|
|
end
|
|
if btnp(4) and enemy.hp <= 0 and player.hp > 0 then
|
|
-- Victory - go to card selection
|
|
generate_card_choices()
|
|
if #card_choice_options > 0 then
|
|
phase = "card_reward"
|
|
card_pick_selection = 1
|
|
else
|
|
-- No more cards to unlock, just advance level
|
|
level = math.min(level + 1, #enemies)
|
|
player = reset_player(0)
|
|
enemy = get_enemy(level)
|
|
-- Clear card displays for new level
|
|
current_card = nil
|
|
enemy_card = nil
|
|
previous_card = nil
|
|
previous_enemy_card = nil
|
|
initialize_deck()
|
|
draw_hand()
|
|
phase = "choose"
|
|
end
|
|
phase_time = 0
|
|
elseif btnp(5) and (enemy.hp > 0 or player.hp <= 0) then
|
|
-- Loss - restart from level 1
|
|
level = 1
|
|
player = reset_player(0)
|
|
enemy = get_enemy(level)
|
|
-- Clear card displays on restart
|
|
current_card = nil
|
|
enemy_card = nil
|
|
previous_card = nil
|
|
previous_enemy_card = nil
|
|
msg = ""
|
|
initialize_deck()
|
|
draw_hand()
|
|
phase = "choose"
|
|
phase_time = 0
|
|
roll_text = ""
|
|
end
|
|
end
|
|
|
|
local phase_handlers = {
|
|
choose = phase_choose,
|
|
play_card = phase_play_card,
|
|
card_reward = phase_card_reward,
|
|
resolve = phase_resolve,
|
|
game_over = phase_game_over
|
|
}
|
|
|
|
-- === 11. MAIN LOOP ===
|
|
function TIC()
|
|
cls(0)
|
|
|
|
if shake > 0 then
|
|
poke(0x3FF9, math.random(-d, d))
|
|
poke(0x3FF9 + 1, math.random(-d, d))
|
|
shake = shake - 1
|
|
if shake == 0 then
|
|
memset(0x3FF9, 0, 2)
|
|
end
|
|
end
|
|
|
|
if phase == "title" then
|
|
phase_title()
|
|
return
|
|
end
|
|
|
|
-- draw player and enemy portraits
|
|
spr(32, 5, 70, 0, 1, 0, 0, 2, 2) -- player
|
|
spr(34, 220, 70, 0, 1, 0, 0, 2, 2) -- enemy
|
|
|
|
-- Draw HUD and health bars
|
|
if phase ~= "card_reward" then
|
|
draw_hud()
|
|
draw_health_bars()
|
|
end
|
|
|
|
local handler = phase_handlers[phase]
|
|
if handler then
|
|
handler()
|
|
end
|
|
|
|
run_effects()
|
|
end
|
|
|
|
-- <TILES>
|
|
-- 000:3200000032000000322200003332000033322200333332003333322200000000
|
|
-- 001:0ff00ff0f22ff33ff22f223ff222223f0f2222f000f22f00000ff00000000000
|
|
-- 002:0ff00ff0f66ff55ff66f665ff666665f0f6666f000f66f00000ff00000000000
|
|
-- 003:0ff00ff0feeffddffeefeedffeeeeedf0feeeef000feef00000ff00000000000
|
|
-- 004:0aaaaaa00aa99aa00a9999a00a9999a00a9999a00a9999a00aa99aa00aaaaaa0
|
|
-- 005:0eeeeee00eeffee00effffe00effffe00effffe00effffe00eeffee00eeeeee0
|
|
-- 006:0000000000099000009999000999999009999990009999000009900000000000
|
|
-- 007:0004400000444400044444400044440000444400004444000044440000000000
|
|
-- 008:00aaaa000a0000a0a000000aa000000aa000000aa000000a0a0000a000aaaa00
|
|
-- 009:0002000000020000000200000222220000020000000200000002000000000000
|
|
-- 016:0011110001333310133223311333233113323331013323100013310000011000
|
|
-- 017:0000000000333300033443300344c43003444430033443300033330000000000
|
|
-- 018:0000000000eeee000eeddee00eddcde00edddde00eeddee000eeee0000000000
|
|
-- 019:0006600000666600066666600666666000066000000660000006600000066000
|
|
-- 020:0002200000022000000220000002200002222220022222200022220000022000
|
|
-- 021:0000003300000343000034301003430001143000012100001211000011001000
|
|
-- 022:00000000000000000000e000f00ede000ffdede00f2f0edef2ff00edff00f00e
|
|
-- 023:0000006600000676000067601006760001176000012100001211000011001000
|
|
-- 032:22222222222222222222ffff222ff222222f2222222f2222222f2222222f2222
|
|
-- 033:2222222222222222ff22222222f2222222ff2222f22ff2222222fff2222222f2
|
|
-- 034:6666666666666666666666886666668666666886666668666668866666886666
|
|
-- 035:6666666666666666886666666888866666668866866668666666668666666686
|
|
-- 048:222f222f222f2222222f2222222f22222222fff2222222ff2222222222222222
|
|
-- 049:f2222ff2fff2ff222222f2222222f22222ff2222fff222222222222222222222
|
|
-- 050:6886666668888866666668666666688666666688666666666666666666666666
|
|
-- 051:8866668668866686666668666666886666688666888866666666666666666666
|
|
-- </TILES>
|
|
|
|
-- <SPRITES>
|
|
-- 000:0000000000022222002222220222222202222222022222220222222f022222ff
|
|
-- 001:000000002222200022222200222222202222222022222220f2222220ff222220
|
|
-- 002:000000000002222200222222022f222202fff22202fff222022f222202222222
|
|
-- 003:0000000022222000222222002222222022222220222222202222222022222220
|
|
-- 004:000000000002222200222222022f222202fff22202fff222022f222f022222ff
|
|
-- 005:000000002222200022222200222222202222222022222220f2222220ff222220
|
|
-- 006:000000000002222200222222022f222202fff22202fff222022f222202222222
|
|
-- 007:0000000022222000222222002222f220222fff20222fff202222f22022222220
|
|
-- 008:000000000002222200222222022f222202fff22202fff222022f222f022222ff
|
|
-- 009:0000000022222000222222002222f220222fff20222fff20f222f220ff222220
|
|
-- 010:000000000002222200222222022f222f02fff2ff02fff2ff022f222202222222
|
|
-- 011:0000000022222000222222002222f220ff2fff20ff2fff20f222f22022222220
|
|
-- 016:022222ff0222222f022222220222222202222222002222220002222200000000
|
|
-- 017:ff222220f2222220222222202222222022222220222222002222200000000000
|
|
-- 018:0222222202222222022222220222222202222222002222220002222200000000
|
|
-- 019:2222222022222220222f222022fff22022fff220222f22002222200000000000
|
|
-- 020:022222ff0222222f022222220222222202222222002222220002222200000000
|
|
-- 021:ff222220f222f220222fff20222fff202222f220222222002222200000000000
|
|
-- 022:02222222022f222202fff22202fff222022f2222002222220002222200000000
|
|
-- 023:222222202222f220222fff20222fff202222f220222222002222200000000000
|
|
-- 024:022222ff022f222f02fff22202fff222022f2222002222220002222200000000
|
|
-- 025:ff222220f222f220222fff20222fff202222f220222222002222200000000000
|
|
-- 026:02222222022f222202fff2ff02fff2ff022f222f002222220002222200000000
|
|
-- 027:22222220f222f220ff2fff20ff2fff202222f220222222002222200000000000
|
|
-- 032:0000000000066666006666660666666606666666066666660666666f066666ff
|
|
-- 033:000000006666600066666600666666606666666066666660f6666660ff666660
|
|
-- 034:000000000006666600666666066f666606fff66606fff666066f666606666666
|
|
-- 035:0000000066666000666666006666666066666660666666606666666066666660
|
|
-- 036:000000000006666600666666066f666606fff66606fff666066f666f066666ff
|
|
-- 037:000000006666600066666600666666606666666066666660f6666660ff666660
|
|
-- 038:000000000006666600666666066f666606fff66606fff666066f666606666666
|
|
-- 039:0000000066666000666666006666f660666fff60666fff606666f66066666660
|
|
-- 040:000000000006666600666666066f666606fff66606fff666066f666f066666ff
|
|
-- 041:0000000066666000666666006666f660666fff60666fff60f666f660ff666660
|
|
-- 042:000000000006666600666666066f666f06fff6ff06fff6ff066f666606666666
|
|
-- 043:0000000066666000666666006666f660ff6fff60ff6fff60f666f66066666660
|
|
-- 048:066666ff0666666f066666660666666606666666006666660006666600000000
|
|
-- 049:ff666660f6666660666666606666666066666660666666006666600000000000
|
|
-- 050:0666666606666666066666660666666606666666006666660006666600000000
|
|
-- 051:6666666066666660666f666066fff66066fff660666f66006666600000000000
|
|
-- 052:066666ff0666666f066666660666666606666666006666660006666600000000
|
|
-- 053:ff666660f666f660666fff60666fff606666f660666666006666600000000000
|
|
-- 054:06666666066f666606fff66606fff666066f6666006666660006666600000000
|
|
-- 055:666666606666f660666fff60666fff606666f660666666006666600000000000
|
|
-- 056:066666ff066f666f06fff66606fff666066f6666006666660006666600000000
|
|
-- 057:ff666660f666f660666fff60666fff606666f660666666006666600000000000
|
|
-- 058:06666666066f666606fff6ff06fff6ff066f666f006666660006666600000000
|
|
-- 059:66666660f666f660ff6fff60ff6fff606666f660666666006666600000000000
|
|
-- 068:0000000000000000000000000000000000000000000000000000000000000022
|
|
-- 069:0000000000000000000000000000000000000000002220002222222022222222
|
|
-- 075:0000000000000000000000000000000000000000000220000222220022222220
|
|
-- 081:0000000000000000000000000000000000000000000002220000222200022222
|
|
-- 082:0000000000000000000000000000000002200000222200002220000000000000
|
|
-- 084:0000022200002222000222200002220000222000022220000222000022200000
|
|
-- 085:2200002200000002000000020000000200000002000000220000002200000222
|
|
-- 086:2000000020000000200000002000000020000000200000002000000000000000
|
|
-- 090:0000000200000022000002220000222000022200000220000022200002220000
|
|
-- 091:2200022020000220000022200000220000022200000222000022200002220000
|
|
-- 097:0022220002222000022220000222000002200000022000000222000002222000
|
|
-- 099:0000000000000002000000020000002200000022000002220000022200000220
|
|
-- 100:2220000022000000220000002000000020000000000000220000222200222222
|
|
-- 101:0000222200022220002222000222200022220000222000022200002200000022
|
|
-- 104:0000000000000000000000000000000000000000000000000022222002222222
|
|
-- 105:0000000000000000000000000000000200000002000000220002222200222222
|
|
-- 106:0220000022200002220000222000022220022220222222022222222222222222
|
|
-- 107:2220000022000000200000000000000000000000222000002222000000220000
|
|
-- 113:0022222000022222000002220000000000000000000000000000000000000000
|
|
-- 114:0000000022222222222222222222222200000000000000000000000000000000
|
|
-- 115:0000222022222222222222222222222000022200000222000022200022220000
|
|
-- 116:2222220022220000220000000000000000000000000000000000000000000000
|
|
-- 117:0000022200000222000022220002222000022200002220000222000002220000
|
|
-- 118:2000000000000022000002220000222200022220002222200222220022222200
|
|
-- 119:0000000200000022000002220000022200002220000222200022222002222200
|
|
-- 120:2222222222000022200000220000022000002220000002000000000000000000
|
|
-- 121:0000222200002200000022000002220200022002000220220022022002222200
|
|
-- 122:2022220002220000222000002200002220022222002222200222000002200000
|
|
-- 123:0022000002220000222000002200000020000000000000000000000000000000
|
|
-- 128:0000000000000002000022220002222200222200002220000222000002220000
|
|
-- 129:0000000022222222222222222220022200000000000000000000000000000000
|
|
-- 130:0000000000000002222000222222222200222222000022220002220200222200
|
|
-- 131:2220000022000000220000002000000000000000220000002222000002222220
|
|
-- 132:0000000000000002000000020000000200000002000000020000000000000000
|
|
-- 133:2220000222000022220002222200222022222200222200002200000000000000
|
|
-- 134:2202200020022000002220220022222200222220000220000000000000000000
|
|
-- 135:2222200022222000202220000022200000222000002222220002222200000000
|
|
-- 136:0000000000000000000000020000222200222220222220002200000000000000
|
|
-- 137:2222220022222000222220002222000022200000222000002200000022000000
|
|
-- 138:0220000002200000222000002220000022200000022000020022222200000000
|
|
-- 139:0000022000002200000220000222000022200000220000002000000000000000
|
|
-- 144:0022200000222200000222220000222200000002000000000000000000000000
|
|
-- 145:0000000000000022222222222222222022220000000000000000000000000000
|
|
-- 146:2222200022200000220000000000000000000000000000000000000000000000
|
|
-- 147:0002222200000222000000000000000000000000000000000000000000000000
|
|
-- 148:2000000022220000222222200022222200000222000000000000000000000000
|
|
-- 149:0000000000000000000000002200000022222000222222220002222200000000
|
|
-- 150:0000000000000000000000000000000000000000222000002200000000000000
|
|
-- </SPRITES>
|
|
|
|
-- <WAVES>
|
|
-- 000:00000000ffffffff00000000ffffffff
|
|
-- 001:0123456789abcdeffedcba9876543210
|
|
-- 002:0123456789abcdef0123456789abcdef
|
|
-- 003:00022345555555554433222225888888
|
|
-- </WAVES>
|
|
|
|
-- <SFX>
|
|
-- 000:03070306130523043303e302f301f301f300f30ff30ef30ef30df30cf30bf30bf300f300f300f301f303f302f302f302e302e302e301e301e301e300300000000000
|
|
-- </SFX>
|
|
|
|
-- <TRACKS>
|
|
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
-- </TRACKS>
|
|
|
|
-- <PALETTE>
|
|
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
|
|
-- </PALETTE>
|