2296 lines
72 KiB
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>
|
|
|