Files
Lucksmith/lucksmith.lua

2296 lines
72 KiB
Lua

-- 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
-- <TILES>
-- 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
-- </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
-- </SPRITES>
-- <WAVES>
-- 000:00000000ffffffff00000000ffffffff
-- 001:0123456789abcdeffedcba9876543210
-- 002:0123456789abcdef0123456789abcdef
-- 003:00022345555555554433222225888888
-- </WAVES>
-- <SFX>
-- 000:03070306130523043303e302f301f301f300f30ff30ef30ef30df30cf30bf30bf300f300f300f301f303f302f302f302e302e302e301e301e301e300300000000000
-- </SFX>
-- <TRACKS>
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </TRACKS>
-- <PALETTE>
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
-- </PALETTE>