-- title: Lucksmith -- author: DigNZ -- desc: card + dice roguelike prototype -- script: lua -- === LIBRARIES === -- Copyright (c) 2017 Crutiatix -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ticuare = { name = "ticuare", elements = {}, z = 1, hz = nil } ticuare.__index = ticuare; ticuare.me = { nothing = 0, click = 1, noclick = 2, none = 3 } local a = { __index = ticuare } local function b(c, d, e, f, g, h) return c > e and c < e + g and d > f and d < f + h end local function i(j, k, l) for m, n in pairs(k) do if type(n) == "table" then if type(j[m] or false) == "table" then i(j[m] or {}, k[m] or {}, l) else if not j[m] or l then j[m] = n end end else if not j[m] or l then j[m] = n end end end return j end local function o(p, q) if p == nil and q == nil then for r = 0, 15 do poke4(0x3FF0 * 2 + r, r) end else poke4(0x3FF0 * 2 + p, q) end end function ticuare.lerp(s, t, m) if s == t then return s else if math.abs(s - t) < 0.005 then return t else return s * (1 - m) + t * m end end end local function u(v) local w = {} local function x(v) if type(v) ~= "table" then return v elseif w[v] then return w[v] end local y = {} w[v] = y; for r, n in pairs(v) do y[x(r)] = x(n) end return setmetatable(y, getmetatable(v)) end return x(v) end local function z(A, B, C, D, E) if A then return C elseif B then return D else return E end end function ticuare.print(F, e, f, G, H, I) H = H or false; I = I or 1; local J, K = F:gsub("\n", "") local L, M = 0, 0; if G then L, M = print(F, e, f, G, H, I), (6 + K) * I * (K + 1) end return L, M end function ticuare.font(F, e, f, N, O, P, Q, H, I) O = O or -1; P = P or 8; Q = Q or 8; H = H or false; I = I or 1; local J, K = F:gsub("\n", "") if type(O) == "table" and type(O[1]) == "table" then for R, S in ipairs(O[1]) do if type(N) == "table" then o(S, N[R]) else o(S, N) end end O = O[2] end local L; if N then L = font(F, e, f, O, P, Q, H, I) end o() return L, (Q + K) * I * (1 + K) end function ticuare.element(T, U) if not U then U = T; T = "element" end local V = U; setmetatable(V, ticuare) V.hover, V.click = false, false; V.activity = U.activity or true; V.drag = U.drag or { activity = false } V.align = U.align or { x = 0, y = 0 } V.visibility = U.visibility or true; if V.content then if not V.content.scroll then V.content.scroll = { x = 0, y = 0 } end V.content.w, V.content.h = V.content.w or V.w, V.content.h or V.h end V.type, V.z = T, ticuare.z; ticuare.z = ticuare.z + 1; ticuare.hz = ticuare.z; table.insert(ticuare.elements, V) return V end function ticuare.Element(W) return ticuare.element("element", W) end function ticuare.Style(W) return W end function ticuare.Group() local X = { type = "group", elements = {} } setmetatable(X, ticuare) return X end function ticuare:updateSelf(Y) if Y.mouse_x and Y.mouse_y and Y.event then mouse_x = Y.mouse_x; mouse_y = Y.mouse_y; mouse_press = Y.press; mouse_event = Y.event; local Z, _, a0, a1, a2, a3, a4; local a5, a6, a7 = ticuare.me, self.x - (self.align.x == 1 and self.w * .5 or (self.align.x == 2 and self.w or 0)), self.y - (self.align.y == 1 and self.h * .5 - 1 or (self.align.y == 2 and self.h - 1 or 0)) Z = mouse_event ~= a5.none and mouse_press or false; _ = b(mouse_x, mouse_y, a6, a7, self.w, self.h) a0 = mouse_event ~= a5.none and _ or false; a1, a2 = self.hover, self.hold; self.hover = a0 or self.drag.active and ticuare.draging_obj and ticuare.draging_obj.obj == self; self.hold = mouse_event == a5.click and a0 and true or Z and self.hold or a0 and mouse_event ~= a5.noclick and self.hold; if mouse_event == a5.click and a0 and self.onClick then self.onClick(self) elseif mouse_event == a5.noclick and a0 and a2 and self.onCleanRelease then self.onCleanRelease(self) elseif (mouse_event == a5.noclick and a0 and a2 or self.hold and not a0) and self.onRelease then self.onRelease(self) elseif self.hold and self.onPress then self.onPress(self) elseif not a1 and self.hover and self.onStartHover then self.onStartHover(self) elseif self.hover and self.onHover then self.onHover(self) elseif a1 and not self.hover and self.onReleaseHover then self.onReleaseHover(self) end if self.hold and (not a0 or self.drag.active) and not ticuare.draging_obj then self.hold = self.drag.active; ticuare.draging_obj = { obj = self, d = { x = a6 - mouse_x, y = a7 - mouse_y } } elseif not self.hold and a0 and (ticuare.draging_obj and ticuare.draging_obj.obj == self) then self.hold = true; ticuare.draging_obj = nil end if ticuare.draging_obj and ticuare.draging_obj.obj == self and self.drag.active then self.x = (not self.drag.fixed or not self.drag.fixed.x) and mouse_x + ticuare.draging_obj.d.x or self.x; self.y = (not self.drag.fixed or not self.drag.fixed.y) and mouse_y + ticuare.draging_obj.d.y or self.y; local a8 = self.drag.bounds; if a8 then if a8.x then self.x = a8.x[1] and self.x < a8.x[1] and a8.x[1] or self.x; self.x = a8.x[2] and self.x > a8.x[2] and a8.x[2] or self.x end if a8.y then self.y = a8.y[1] and self.y < a8.y[1] and a8.y[1] or self.y; self.y = a8.y[2] and self.y > a8.y[2] and a8.y[2] or self.y end end if self.track then self:anchor(self.track.ref) end end return a0 elseif Y.focused_element and Y.event then local a5, a9, aa, a0, a1, a2 = ticuare.me; a9 = Y.event ~= a5.none and Y.press or false; aa = self == Y.focused_element; a0 = Y.event ~= a5.none and aa or false; a1, a2 = self.hover, self.hold; self.hover = a0; self.hold = Y.event == a5.click and a0 and true or a9 and self.hold or a0 and Y.event ~= a5.noclick and self.hold; if Y.event == a5.click and a0 and self.onClick then self.onClick(self) elseif Y.event == a5.noclick and a0 and a2 and self.onCleanRelease then self.onCleanRelease(self) elseif (Y.event == a5.noclick and a0 and a2 or self.hold and not a0) and self.onRelease then self.onRelease(self) elseif self.hold and self.onPress then self.onPress(self) elseif not a1 and self.hover and self.onStartHover then self.onStartHover(self) elseif self.hover and self.onHover then self.onHover(self) elseif a1 and not self.hover and self.onReleaseHover then self.onReleaseHover(self) end return a0 else error("updateSelf error in arguments!") end end function ticuare:updateTrack() local a8, ab = self.drag.bounds, self.track; if ab then self.x, self.y = ab.ref.x + ab.d.x, ab.ref.y + ab.d.y; if a8 and a8.relative then if a8.x then a8.x[1] = ab.ref.x + ab.b.x[1] or nil; a8.x[2] = ab.ref.x + ab.b.x[2] or nil end if a8.y then a8.y[1] = ab.ref.y + ab.b.y[1] or nil; a8.y[2] = ab.ref.y + ab.b.y[2] or nil end end end end function ticuare:drawSelf() if self.visibility then local G, ac, ad, ae, af, ag, ah, ai, a6, a7, aj, ak, al, am, an, ao, ap, aq, ar, as, at, au, av, aw, ax; local ay, az, F, aA, aB, N = self.shadow, self.border, self.text, self.icon, self.tiled, self.colors; a6 = self.x - (self.align.x == 1 and self.w * .5 - 1 or (self.align.x == 2 and self.w - 1 or 0)) a7 = self.y - (self.align.y == 1 and self.h * .5 - 1 or (self.align.y == 2 and self.h - 1 or 0)) if ay and ay.colors then ay.offset = ay.offset or { x = 1, y = 1 } ac = z(self.hold, self.hover, ay.colors[3], ay.colors[2], ay.colors[1]) if ac then rect(a6 + ay.offset.x, a7 + ay.offset.y, self.w, self.h, ac) end end if N then G = z(self.hold, self.hover, N[3], N[2], N[1]) if G then rect(a6, a7, self.w, self.h, G) end end as = az and az.width or 0; aw = 2 * as; if aB then aB.scale = aB.scale or 1; aB.key = aB.key or -1; aB.flip = aB.flip or 0; aB.rotate = aB.rotate or 0; aB.w = aB.w or 1; aB.h = aB.h or 1; av = z(self.hold, self.hover, aB.sprites[3], aB.sprites[2], aB.sprites[1]) if av then clip(a6 + as, a7 + as, self.w - aw, self.h - aw) for e = 0, self.w + 8 * aB.w * aB.scale, 8 * aB.w * aB.scale do for f = 0, self.h + 8 * aB.h * aB.scale, 8 * aB.h * aB.scale do spr(av, a6 + e + as, a7 + f + as, aB.key, aB.scale, aB.flip, aB.rotate, aB.w, aB.h) end end clip() end end if self.content and self.drawContent then if self.content.wrap and clip then clip(a6 + as, a7 + as, self.w - aw, self.h - aw) end self:renderContent() if self.content.wrap and clip then clip() end end if az and az.colors then an = az.colors; ad = z(self.hold, self.hover, an[3], an[2], an[1]) if ad then for t = 0, az.width - 1 do rectb(a6 + t, a7 + t, self.w - 2 * t, self.h - 2 * t, ad) end end end if az and az.sprites then at = az.key or -1; au = z(self.hold, self.hover, az.sprites[3], az.sprites[2], az.sprites[1]) if au then clip(a6 + 8, a7, self.w - 16 + 1, self.h) for e = 8, self.w - 9, 8 do spr(au[2], a6 + e, a7, at, 1, 0, 0) spr(au[2], a6 + e, a7 + self.h - 8, at, 1, 0, 2) end clip() spr(au[1], a6, a7, at, 1, 0, 0) spr(au[1], a6 + self.w - 8, a7, at, 1, 0, 1) clip(a6, a7 + 8, self.w, self.h - 16 + 1) for f = 8, self.h - 9, 8 do spr(au[2], a6, a7 + f, at, 1, 0, 3) spr(au[2], a6 + self.w - 8, a7 + f, at, 1, 2, 1) end clip() spr(au[1], a6 + self.w - 8, a7 + self.h - 8, at, 1, 0, 2) spr(au[1], a6, a7 + self.h - 8, at, 1, 0, 3) end end if aA and aA.sprites and #aA.sprites > 0 then ai = self.hold and aA.sprites[3] and aA.sprites[3] or self.hover and aA.sprites[2] and aA.sprites[2] or aA.sprites[1] ao = aA.offset or { x = 0, y = 0 } aA.align = aA.align or { x = 0, y = 0 } spr(ai, a6 + (aA.align.x == 1 and self.w * .5 - aA.scale * 8 / 2 or (aA.align.x == 2 and self.w - aA.scale * 8 or 0)) + ao.x, a7 + (aA.align.y == 1 and self.h * .5 - aA.scale * 8 / 2 or (aA.align.y == 2 and self.h - aA.scale * 8 or 0)) + ao.y, aA.key, aA.scale, aA.flip, aA.rotate, aA.w, aA.h) end if F and F.print then ah = F.colors or {15, 15, 15} ah[1] = ah[1] or 15; if not F.font and type(F.colors[1]) == "table" then trace("If text.font is true, then text.colors has to be array of numbers!", 6) trace("Traceback: Element with text:\"" .. F.print .. "\"") exit() end af = z(self.hold, self.hover, ah[3], ah[2], ah[1]) if F.shadow then ag = F.shadow; ae = z(self.hold, self.hover, ag.colors[3], ag.colors[2], ag.colors[1]) aq = ag.offset or { x = 1, y = 1 } end ap = F.offset or { x = 0, y = 0 } if F.font then F.space = F.space or { w = 8, h = 8 } aj, ak = ticuare.font(F.print, 0, 200, -1, F.key, F.space.w, F.space.h, F.fixed, F.scale) else aj, ak = ticuare.print(F.print, 0, 200, -1, F.fixed, F.scale) end ax = F.align or { x = 0, y = 0 } al = ax.x == 1 and a6 + self.w * .5 - aj * .5 + ap.x or (ax.x == 2 and a6 + self.w - aj + ap.x - as or a6 + ap.x + as) am = ax.y == 1 and a7 + self.h * .5 - ak * .5 + ap.y or (ax.y == 2 and a7 + self.h - ak + ap.y - as or a7 + ap.y + as) if F.font then if type(ae) == "table" then ticuare.font(F.print, al + aq.x, am + aq.y, ae, F.key, F.space.w, F.space.h, F.fixed, F.scale) end ticuare.font(F.print, al, am, af, F.key, F.space.w, F.space.h, F.fixed, F.scale) else if ae then ticuare.print(F.print, al + aq.x, am + aq.y, ae, F.fixed, F.scale) end ticuare.print(F.print, al, am, af, F.fixed, F.scale) end end end end function ticuare:renderContent() local aC, aD, az, aE, aF, aG; aG = self.align; aC = self.x - (aG.x == 1 and self.w * .5 or (aG.x == 2 and self.w or 0)) aD = self.y - (aG.y == 1 and self.h * .5 - 1 or (aG.y == 2 and self.h - 1 or 0)) az = self.border and self.border.width or 1; aE = aC - (self.content.scroll.x or 0) * (self.content.w - self.w) + az; aF = aD - (self.content.scroll.y or 0) * (self.content.h - self.h) + az; self.drawContent(self, aE, aF) end function ticuare:Content(W) self.drawContent = W; return self end function ticuare:scroll(W) if W ~= nil then W.x = W.x or 0; W.y = W.y or 0; if self.content then W.x = W.x < 0 and 0 or W.x > 1 and 1 or W.x; W.y = W.y < 0 and 0 or W.y > 1 and 1 or W.y; self.content.scroll.x, self.content.scroll.y = W.x or self.content.scroll.x, W.y or self.content.scroll.y end return self else if self.content then return self.content.scroll end end end function ticuare.update(mouse_x, mouse_y, aH) local a5, aI = ticuare.me, ticuare.elements; local mouse_event, aJ, aK, aL = a5.nothing, false, {}, nil; if type(mouse_x) == "table" then aH = mouse_y end if mouse_x then if ticuare.click and not aH then ticuare.click = false; mouse_event = a5.noclick; ticuare.draging_obj = nil elseif not ticuare.click and aH then ticuare.click = true; mouse_event = a5.click; ticuare.draging_obj = nil end for r = 1, #aI do table.insert(aK, aI[r]) end table.sort(aK, function(s, t) return s.z > t.z end) for r = 1, #aK do aL = aK[r] if aL then if type(mouse_x) == "table" then if aL:updateSelf{ focused_element = mouse_x, press = aH, event = (aJ or not aL.activity) and a5.none or mouse_event } then aJ = true end elseif mouse_x and mouse_y and type(mouse_x) ~= "table" then if aL:updateSelf{ mouse_x = mouse_x, mouse_y = mouse_y, press = aH, event = (aJ or ticuare.draging_obj and ticuare.draging_obj.obj ~= aL or not aL.activity) and a5.none or mouse_event } then aJ = true end else error("Wrong arguments for update()") end end end for r = #aI, 1, -1 do if aI[r] then aI[r]:updateTrack() end end end end function ticuare.draw() local aM = {} for r = 1, #ticuare.elements do if ticuare.elements[r].draw then table.insert(aM, ticuare.elements[r]) end end table.sort(aM, function(s, t) return s.z < t.z end) for r = 1, #aM do aM[r]:drawSelf() end end function ticuare:style(aN) if self.type == "group" then for m, n in pairs(self.elements) do i(n, u(aN), false) end else i(self, u(aN), false) end return self end function ticuare:anchor(aO) if self.type == "group" then for m, n in pairs(self.elements) do n:anchor(aO) end else local a8, aP, aQ, aR, aS = self.drag.bounds, nil, nil, nil, nil; if a8 and a8.x then aP = a8.x[1] - aO.x; aQ = a8.x[2] - aO.x elseif a8 and a8.y then aR = a8.y[1] - aO.y; aS = a8.y[2] - aO.y end self.track = { ref = aO, d = { x = self.x - aO.x, y = self.y - aO.y }, b = { x = {aP, aQ}, y = {aR, aS} } } end return self end function ticuare:group(aT, aU) if aU then aT.elements[aU] = self else table.insert(aT.elements, self) end return self end function ticuare:active(aV) if aV ~= nil then if self.type == "group" then for m, n in pairs(self.elements) do n:active(aV) end else self.activity = aV end return self else if self.type == "group" then local aW = {} for m, n in pairs(self.elements) do aW[m] = n:active() end return aW else if self.activity ~= nil then return self.activity end end end end function ticuare:visible(aV) if aV ~= nil then if self.type == "group" then for m, n in pairs(self.elements) do n:visible(aV) end else self.visibility = aV end return self else if self.type == "group" then local aW = {} for m, n in pairs(self.elements) do aW[m] = n:visible() end return aW else if self.activity ~= nil then return self.visibility end end end end function ticuare:dragBounds(a8) if a8 ~= nil then self.drag.bounds = a8 else return self.drag.bounds end end function ticuare:horizontalRange(aX) local a8 = self.drag.bounds; if aX ~= nil then self.x = a8.x[1] + (a8.x[2] - a8.x[1]) * aX else assert(a8 and a8.x and #a8.x == 2, "X bounds error!") return (self.x - a8.x[1]) / (a8.x[2] - a8.x[1]) end end function ticuare:verticalRange(aX) local a8 = self.drag.bounds; if aX ~= nil then self.y = a8.y[1] + (a8.y[2] - a8.y[1]) * aX else assert(a8 and a8.y and #a8.y == 2, "Y bounds error!") return (self.y - a8.y[1]) / (a8.y[2] - a8.y[1]) end end function ticuare:index(aY) if aY ~= nil then if self.type == "group" then local aZ; for m, n in pairs(self.elements) do if not aZ or n.z < aZ then aZ = n.z end end for m, n in pairs(self.elements) do local a_ = n.z - aZ + aY; n:index(a_) end else self.z = aY; if aY > ticuare.hz then ticuare.hz = aY end end else return self.z end return end function ticuare:toFront() if self.z < ticuare.hz or self.type == "group" then return self:index(ticuare.hz + 1) end end function ticuare:remove() for r = #ticuare.elements, 1, -1 do if ticuare.elements[r] == self then table.remove(ticuare.elements, r) self = nil end end end function ticuare.empty() for r = 1, #ticuare.elements do ticuare.elements[r] = nil end end -- === GAME STATE === player = { hp = 10, roll_mod = 0, mult = 1, energy = 0 } 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 }} level = 1 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 enemy = get_enemy(level) msg = "" -- State phase = "choose" phase_time = 0 roll_text = "" damage_applied = false clash_shown = false current_damage = { player = 0, enemy = 0 } -- store damage for display pending_effects = { player = {}, enemy = {}, previous_player = {}, previous_enemy = {} } -- Card tracking for two-round strategy previous_card = nil previous_enemy_card = nil -- animation + shake player.display_roll = 0 enemy.display_roll = 0 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 -- 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 }} function sword_clash_animation(player_total, enemy_total) local clash_position = 120 -- Center of the screen for the clash local player_start = 50 -- Starting x-position for player swords local enemy_start = 190 -- Starting x-position for enemy swords local y_position = 90 -- Y-position for the swords local sword_speed = 1 -- Speed of sword movement per frame -- Initialize sword positions local player_sword_positions = {} local enemy_sword_positions = {} for i = 1, player_total do table.insert(player_sword_positions, player_start + (i - 1) * 8) end for i = 1, enemy_total do table.insert(enemy_sword_positions, enemy_start - (i - 1) * 8) end -- Animate swords moving toward the center while #player_sword_positions > 0 and #enemy_sword_positions > 0 do cls(0) -- Clear the screen -- Move player swords for i = #player_sword_positions, 1, -1 do local x = player_sword_positions[i] if x + sword_speed < clash_position then player_sword_positions[i] = x + sword_speed spr(21, player_sword_positions[i], y_position, 0) else -- Sword reaches the clash point table.remove(player_sword_positions, i) table.remove(enemy_sword_positions, #enemy_sword_positions) -- Remove one enemy sword spr(22, clash_position, y_position, 0) -- Explosion sprite end end -- Move enemy swords for i = #enemy_sword_positions, 1, -1 do local x = enemy_sword_positions[i] if x - sword_speed > clash_position then enemy_sword_positions[i] = x - sword_speed spr(21, enemy_sword_positions[i], y_position, 0, 1, 1) -- Flipped horizontally end end -- Yield for the next frame coroutine.yield() end -- Pause briefly after the clash for delay = 1, 150 do coroutine.yield() end -- Display remaining swords on the winning side cls(0) -- Clear the screen if #player_sword_positions > 0 then for i = 1, #player_sword_positions do spr(21, clash_position + (i - 1) * 8, y_position, 0) end elseif #enemy_sword_positions > 0 then for i = 1, #enemy_sword_positions do spr(21, clash_position - (i - 1) * 8, y_position, 0, 1, 1) -- Flipped horizontally end end -- Hold the final frame for a moment for delay = 1, 30 do coroutine.yield() end end -- player's starting hand (random subset) hand = {} card_choice_options = {} -- for level up card selection 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 player_current = ticuare.element { x = 4, y = 36, w = 52, h = 14, border = { colors = {2, 2, 2}, width = 1 }, text = { print = "", font = false, align = { x = 1, y = 1 }, colors = {2, 2, 2}, space = { x = 8, y = 8 } } } enemy_current = ticuare.element { x = 186, y = 36, w = 52, h = 14, border = { colors = {6, 6, 6}, width = 1 }, text = { print = "", font = false, align = { x = 1, y = 1 }, colors = {6, 6, 6}, space = { x = 8, y = 8 } } } player_previous = ticuare.element { x = 4, y = 20, w = 52, h = 14, border = { colors = {2, 2, 2}, width = 1 }, text = { print = "", font = false, align = { x = 0, y = 1 }, colors = {2, 2, 2}, space = { x = 8, y = 8 } } } enemy_previous = ticuare.element { x = 186, y = 20, w = 52, h = 14, border = { colors = {6, 6, 6}, width = 1 }, text = { print = "", font = false, align = { x = 0, y = 1 }, colors = {6, 6, 6}, space = { x = 8, y = 8 } } } 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 -- player's starting hand (random subset) 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 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() -- === CORE TURN LOGIC === function apply_effects(timing, player_won) -- Apply current card effects for _, effect in ipairs(pending_effects.player) do if effect.timing == timing then if timing == "post_result" then effect.use(player, enemy, player_won) else effect.use(player, enemy) end end end -- Apply previous card effects if they exist for _, effect in ipairs(pending_effects.previous_player) do if effect.timing == timing then if timing == "post_result" then effect.use(player, enemy, player_won) else effect.use(player, enemy) end end end -- Apply current enemy card effects for _, effect in ipairs(pending_effects.enemy) do if effect.timing == timing then if timing == "post_result" then effect.use(player, enemy, player_won) else effect.use(player, enemy) end end end -- Apply previous enemy card effects if they exist for _, effect in ipairs(pending_effects.previous_enemy) do if effect.timing == timing then if timing == "post_result" then effect.use(player, enemy, player_won) else effect.use(player, enemy) end end end 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 {} -- Apply immediate effects apply_effects("immediate") -- Apply pre-roll effects apply_effects("pre_roll") -- Now roll dice player.roll = math.random(1, 6) enemy.roll = math.random(1, 6) apply_effects("post_roll") player.total = player.roll + player.roll_mod enemy.total = enemy.roll + enemy.roll_mod player.display_roll = 0 enemy.display_roll = 0 phase = "roll" phase_time = 0 end picked = 1 card_pick_selection = 1 -- === DRAWING FUNCTIONS === 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 player_current.text.print = current_card and current_card.name or "" enemy_current.text.print = enemy_card and enemy_card.name or "" player_current:visible(phase ~= "choose" and current_card ~= nil) enemy_current:visible(phase ~= "choose" and enemy_card ~= nil) if previous_card then player_previous:visible(true) player_previous.text.print = previous_card.name else player_previous:visible(false) end if previous_enemy_card then enemy_previous:visible(true) enemy_previous.text.print = previous_enemy_card.name else enemy_previous:visible(false) end 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 function draw_card(card, x, is_selected, can_afford) local border_color = can_afford and (is_selected and 11 or 9) or (is_selected and 13 or 14) local y = is_selected and 38 or 40 rectb(x, y, 52, 72, border_color) if is_selected then rect(x + 1, y + 1, 50, 70, 8) end 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 + 18, text_color, false, 1, true) -- Show energy cost/gain --local energy_text = card.energy >= 0 and ("+" .. card.energy) or tostring(card.energy) for i = 1, math.abs(card.energy) do local sprite_id = card.energy >= 0 and 18 or 17 local offset_x = x + 50 - (i * 8) spr(sprite_id, offset_x, y + 2, 0) end -- spr(17, x + 30, y + 4, 0) --print(energy_text, x + 40, y + 6, card.energy >= 0 and 6 or 2, false, 1, true) end -- === UI Elements === function centered_msg(text, active) active = active or false return ticuare.element({ x = 60, y = 100, w = 120, h = 16, border = { colors = {10, 10, 10}, width = 1 }, colors = {8, 8, 8}, text = { print = text, colors = {7, 8, 9}, font = false, align = { x = 1, y = 1 }, offset = { x = 0, y = 0 }, space = { x = 8, y = 8 }, scale = 1 } }):active(active) end -- === INPUT === function TIC() cls(0) ticuare.update(mouse()) 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 -- 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 if phase == "choose" then -- 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 cards for i, card in ipairs(hand) do local x = 38 + (i - 1) * 54 local can_afford = (card.energy >= 0) or (player.energy >= -card.energy) local is_selected = i == picked draw_card(card, x, is_selected, can_afford) end local selected_card = hand[picked] print(selected_card.name, 4, 115, 5) local can_afford = (selected_card.energy >= 0) or (player.energy >= -selected_card.energy) print(selected_card.desc, 4, 123, can_afford and 6 or 14, false, 1, true) if not can_afford then print("Not enough energy!", 4, 130, 3, false, 1, true) end print(msg, 4, 40, 15) if btnp(4) and can_afford then start_turn(hand[picked]) end elseif phase == "card_reward" then -- Card selection after level victory print("Choose a card to add to your deck:", 4, 30, 12) 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 for i, card_index in ipairs(card_choice_options) do local card = cards[card_index] local x = 10 + (i - 1) * 64 local is_selected = i == card_pick_selection draw_card(card, x, is_selected, 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]] print(selected_card.desc, 4, 110, 15) 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 = { hp = 10, roll_mod = 0, mult = 1, energy = 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 damage_applied = false clash_shown = false end elseif phase == "roll" then phase_time = phase_time + 1 -- flicker numbers every few frames if phase_time < 30 then if phase_time % 3 == 0 then player.display_roll = math.random(1, 6) enemy.display_roll = math.random(1, 6) end -- lock in final rolls after 30 frames else player.display_roll = player.roll enemy.display_roll = enemy.roll local count = math.min(player.display_roll, (phase_time - 30) // 6) local enemy_count = math.min(enemy.display_roll, (phase_time - 30) // 6) for i = 1, count do spr(21, 50 + (i - 1) * 8, 90, 0) -- Player's dice sprites end for i = 1, enemy_count do spr(21, 150 - (i - 1) * 8, 90, 0, 1, 1) -- Enemy's dice sprites (flipped) end end if phase_time > 60 then phase = "mod" phase_time = 0 end card_text = "" -- "You played " .. current_card.name enemy_card_text = "" -- enemy.name .. " played " .. enemy_card.name print(card_text, 4, 40, 2) print(enemy_card_text, 4, 50, 6) -- show dice rolls as sprites 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, 60, 0, 1, 0, 0, 2, 2) -- player dice on left, 16x16 spr(enemy_dice_sprite, 150, 60, 0, 1, 0, 0, 2, 2) -- enemy dice on right, 16x16 elseif phase == "mod" then if player.roll_mod == 0 and enemy.roll_mod == 0 then -- Skip mod phase if no modifiers phase = "total" phase_time = 0 return end -- show dice rolls as sprites 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, 60, 0, 1, 0, 0, 2, 2) -- player dice on left, 16x16 spr(enemy_dice_sprite, 150, 60, 0, 1, 0, 0, 2, 2) -- enemy dice on right, 16x16 -- Draw the roll sprites for the player local positive_mod = math.max(0, player.roll_mod) local negative_mod = math.min(math.abs(math.min(0, player.roll_mod)), player.roll) local adjusted_roll = math.max(0, player.roll - negative_mod) -- Reduce roll by negative mod -- Draw adjusted roll (sprite #21) for i = 1, adjusted_roll do spr(21, 50 + (i - 1) * 8, 90, 0) -- Player's dice sprites end -- Replace the remaining roll with negative mod (sprite #22) for i = 1, negative_mod do spr(22, 50 + (adjusted_roll + i - 1) * 8, 90, 0) -- Negative modifier sprite end -- Add positive modifiers (sprite #23) for i = 1, positive_mod do spr(23, 50 + (adjusted_roll + negative_mod + i - 1) * 8, 90, 0) -- Positive modifier sprite end -- Draw the roll sprites for the enemy positive_mod = math.max(0, enemy.roll_mod) local negative_mod = math.min(math.abs(math.min(0, enemy.roll_mod)), enemy.roll) adjusted_roll = math.max(0, enemy.roll - negative_mod) -- Reduce roll by negative mod -- Draw adjusted roll (sprite #21) for i = 1, adjusted_roll do spr(21, 150 - (i - 1) * 8, 90, 0, 1, 1) -- Enemy's dice sprites (flipped) end -- Replace the remaining roll with negative mod (sprite #22) for i = 1, negative_mod do spr(22, 150 - (adjusted_roll + i - 1) * 8, 90, 0, 1, 1) -- Negative modifier sprite (flipped) end -- Add positive modifiers (sprite #23) for i = 1, positive_mod do spr(23, 150 - (adjusted_roll + negative_mod + i - 1) * 8, 90, 0, 1, 1) -- Positive modifier sprite (flipped) end -- Increment the phase time and transition to the total phase phase_time = phase_time + 1 if phase_time > 60 then phase = "total" phase_time = 0 end elseif phase == "total" then -- show dice rolls as sprites 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, 60, 0, 1, 0, 0, 2, 2) -- player dice on left, 16x16 spr(enemy_dice_sprite, 150, 60, 0, 1, 0, 0, 2, 2) -- enemy dice on right, 16x16 -- if not clash_shown then -- clash_shown = true -- queue_effect(function() -- sword_clash_animation(player.total, enemy.total) -- end, 0) -- end -- Show total result using sprite #21 for i = 1, player.total do spr(21, 50 + (i - 1) * 8, 90, 0) -- Player's total end for i = 1, enemy.total do spr(21, 150 - (i - 1) * 8, 90, 0, 1, 1) -- Enemy's total (flipped) end phase_time = phase_time + 1 if phase_time > 120 then phase = "result" phase_time = 0 end elseif phase == "result" then if player.total > enemy.total then player_won = true local dmg = 2 * player.mult -- Apply rage bonus to increase damage local rage_bonus = player.rage or 0 dmg = dmg + rage_bonus -- Apply weaken debuff (player is weakened, so their attacks do less damage) local weaken_reduction = player.weaken or 0 dmg = math.max(0, dmg - weaken_reduction) if not damage_applied then -- Sequence: flash background then run the unified sequenced damage effect queue_effect(function() flash_background(dmg * 8) end, 40) queue_effect(function() sequenced_damage("enemy", dmg, { flash_cycles = 8, damage_display_frames = 20, step_wait = 10 }) end, 0) end msgtxt = "You win!" elseif enemy.total > player.total then player_won = false local dmg = 2 * enemy.mult -- Apply rage bonus to increase damage local rage_bonus = enemy.rage or 0 dmg = dmg + rage_bonus -- Apply weaken debuff (enemy is weakened, so their attacks do less damage) local weaken_reduction = enemy.weaken or 0 dmg = math.max(0, dmg - weaken_reduction) if not damage_applied then -- Apply shield reduction (calculate now, but do visual/HP changes via sequenced_damage) local shield_reduction = player.shield or 0 local final_dmg = math.max(0, dmg - shield_reduction) -- Queue flash + sequenced stepped HP change for player -- queue_effect(flash_background, 40) queue_effect(function() sequenced_damage("player", final_dmg, { flash_cycles = 8, damage_display_frames = 20, step_wait = 10 }) end, 0) -- After damage is applied, remove one shield if it was used and set shake if shield_reduction > 0 then queue_effect(function() player.shield = math.max(0, (player.shield or 0) - 1) shake = 5 * shield_reduction coroutine.yield() end, 0) else queue_effect(function() shake = 5 * final_dmg coroutine.yield() end, 0) end end msgtxt = enemy.name .. " wins!" else msgtxt = "Tie!" end if not damage_applied then apply_effects("post_result", player_won) local color = player_won == true and 2 or (player_won == false and 6 or 9) -- draw centered, highlighted result text win_message = centered_msg(msgtxt, false) win_message.text.colors = {color, color + 1, color + 2} end damage_applied = true if btnp(4) then win_message:remove() -- reset mods & back to choose player.roll_mod, enemy.roll_mod = 0, 0 player.mult, enemy.mult = 1, 1 player.weaken, enemy.weaken = 0, 0 -- reset weaken debuffs player.rage, enemy.rage = 0, 0 -- reset rage bonuses -- Don't reset shield - it persists until damage taken -- Add 1 energy at end of turn player.energy = math.min(5, player.energy + 1) -- Reset damage display current_damage.player = 0 current_damage.enemy = 0 -- Move current cards to previous when returning to choose phase previous_card = current_card previous_enemy_card = enemy_card current_card = nil enemy_card = nil draw_hand() -- Draw new hand from deck phase = (enemy.hp <= 0 or player.hp <= 0) and "game_over" or "choose" phase_time = 0 damage_applied = false end elseif phase == "game_over" then previous_card = nil previous_enemy_card = nil if enemy.hp <= 0 and player.hp > 0 then local victory_text = "You defeated " .. enemy.name .. "!" local victory_x = 120 - (#victory_text * 3) -- center text (6 pixel font, so 3 pixels per char from center) print(victory_text, victory_x, 60, 6) local continue_text = "Press Z to choose new card" local continue_x = 120 - (#continue_text * 3) print(continue_text, continue_x, 100, 9) else local defeat_text = enemy.name .. " defeated you!" local defeat_x = 120 - (#defeat_text * 3) print(defeat_text, defeat_x, 60, 2) local restart_text = "Press X to restart" local restart_x = 120 - (#restart_text * 3) print(restart_text, restart_x, 100, 14) 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 = { hp = 10, roll_mod = 0, mult = 1, energy = 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 damage_applied = false elseif btnp(5) and (enemy.hp > 0 or player.hp <= 0) then -- Loss - restart from level 1 level = 1 player = { hp = 10, roll_mod = 0, mult = 1, energy = 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 = "" damage_applied = false end end run_effects() ticuare.draw() end -- === EFFECTS HELPERS === -- 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 -- === EFFECT QUEUE (restore missing functions) === -- simple sequential effect queue used by queued visual/audio effects 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 function effects_running() return #effect_queue > 0 end -- -- 000:3200000032000000322200003332000033322200333332003333322200000000 -- 001:0ff00ff0f22ff33ff22f223ff222223f0f2222f000f22f00000ff00000000000 -- 002:0ff00ff0f66ff55ff66f665ff666665f0f6666f000f66f00000ff00000000000 -- 003:0ff00ff0feeffddffeefeedffeeeeedf0feeeef000feef00000ff00000000000 -- 004:0aaaaaa00aa99aa00a9999a00a9999a00a9999a00a9999a00aa99aa00aaaaaa0 -- 005:0eeeeee00eeffee00effffe00effffe00effffe00effffe00eeffee00eeeeee0 -- 016:0011110001333310133223311333233113323331013323100013310000011000 -- 017:0000000000333300033443300344c43003444430033443300033330000000000 -- 018:0000000000eeee000eeddee00eddcde00edddde00eeddee000eeee0000000000 -- 019:0006600000666600066666600666666000066000000660000006600000066000 -- 020:0002200000022000000220000002200002222220022222200022220000022000 -- 021:0000003300000343000034301003430001143000012100001211000011001000 -- 022:00000000000000000000e000f00ede000ffdede00f2f0edef2ff00edff00f00e -- 023:0000006600000676000067601006760001176000012100001211000011001000 -- 032:22222222222222222222ffff222ff222222f2222222f2222222f2222222f2222 -- 033:2222222222222222ff22222222f2222222ff2222f22ff2222222fff2222222f2 -- 034:6666666666666666666666886666668666666886666668666668866666886666 -- 035:6666666666666666886666666888866666668866866668866666668666666686 -- 048:222f222f222f2222222f2222222f22222222fff2222222ff2222222222222222 -- 049:f2222ff2fff2ff222222f2222222f22222ff2222fff222222222222222222222 -- 050:6886666668888866666668666666688666666688666666666666666666666666 -- 051:8866668668866686666668666666886666688666888866666666666666666666 -- -- -- 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 -- -- -- 000:00000000ffffffff00000000ffffffff -- 001:0123456789abcdeffedcba9876543210 -- 002:0123456789abcdef0123456789abcdef -- 003:00022345555555554433222225888888 -- -- -- 000:03070306130523043303e302f301f301f300f30ff30ef30ef30df30cf30bf30bf300f300f300f301f303f302f302f302e302e302e301e301e301e300300000000000 -- -- -- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -- -- -- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57 --