Files
Lucksmith/lucksmith.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>