First commit

This commit is contained in:
Simon Cambier 2023-08-12 11:36:14 +02:00
commit 7c537e3c4f
37 changed files with 5252 additions and 0 deletions

136
.editorconfig Normal file
View File

@ -0,0 +1,136 @@
# see https://github.com/CppCXY/EmmyLuaCodeStyle
[*.lua]
# [basic]
# optional space/tab
indent_style = space
# if indent_style is space, this is valid
indent_size = 4
# if indent_style is tab, this is valid
tab_width = 4
# none/single/double
quote_style = double
continuation_indent = 4
# this mean utf8 length , if this is 'unset' then the line width is no longer checked
# this option decides when to chopdown the code
max_line_length = 120
# optional crlf/lf/cr/auto, if it is 'auto', in windows it is crlf other platforms are lf
# in neovim the value 'auto' is not a valid option, please use 'unset'
end_of_line = auto
#optional keep/never/always/smart
trailing_table_separator = keep
# keep/remove/remove_table_only/remove_string_only
call_arg_parentheses = keep
detect_end_of_line = false
# this will check text end with new line
insert_final_newline = true
# [space]
space_around_table_field_list = true
space_before_attribute = true
space_before_function_open_parenthesis = false
space_before_function_call_open_parenthesis = false
space_before_closure_open_parenthesis = false
# optional always/only_string/only_table/none
# or true/false
space_before_function_call_single_arg = always
space_before_open_square_bracket = false
space_inside_function_call_parentheses = false
space_inside_function_param_list_parentheses = false
space_inside_square_brackets = false
# like t[#t+1] = 1
space_around_table_append_operator = false
ignore_spaces_inside_function_call = false
space_before_inline_comment = 1
# [operator space]
space_around_math_operator = true
space_after_comma = true
space_after_comma_in_for_statement = true
space_around_concat_operator = true
# [align]
align_call_args = false
align_function_params = true
align_continuous_assign_statement = true
align_continuous_rect_table_field = true
align_continuous_line_space = 2
align_if_branch = false
align_array_table = true
align_continuous_inline_comment = true
# none/ always / only_call_stmt
align_chain_expr = none
# [indent]
never_indent_before_if_condition = false
never_indent_comment_on_if_branch = false
# [line space]
# The following configuration supports four expressions
# keep
# fixed(n)
# min(n)
# max(n)
# for eg. min(2)
line_space_after_if_statement = keep
line_space_after_do_statement = keep
line_space_after_while_statement = keep
line_space_after_repeat_statement = keep
line_space_after_for_statement = keep
line_space_after_local_or_assign_statement = keep
line_space_after_function_statement = fixed(2)
line_space_after_expression_statement = keep
line_space_after_comment = keep
# [line break]
break_all_list_when_line_exceed = false
auto_collapse_lines = false
# [preference]
ignore_space_after_colon = false
remove_call_expression_list_finish_comma = false

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build.lua
html.zip
html/

BIN
.local/02fd0eb/config.tic Normal file

Binary file not shown.

Binary file not shown.

BIN
.local/31ea041/config.tic Normal file

Binary file not shown.

Binary file not shown.

BIN
.local/31ea041/options.dat Normal file

Binary file not shown.

BIN
.local/ffd6965/config.tic Normal file

Binary file not shown.

Binary file not shown.

BIN
.local/ffd6965/options.dat Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

13
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"Lua.diagnostics.globals": [
"include"
],
"Lua.diagnostics.disable": [
"lowercase-global"
],
"Lua.runtime.version": "Lua 5.3",
"search.exclude": {
"build.lua": true,
"game.lua": true
}
}

125
ai.lua Normal file
View File

@ -0,0 +1,125 @@
--[[
List of actions, by order of priority:
- Complete a 3 dice combo 4-6
- Cancel a 3 dice combo 4-6
- Complete a 3 dice combo 1-3
- Complete a 2 dice combo 4-6
- Cancel a 3 dice combo 1-3
- Complete a 2 dice combo 1-3
- Cancel a 2 dice combo 4-6
- Cancel a 2 dice combo 1-3
- Look at the column's sum before removing a die.
It if is <=15, try to keep it as is, unless you're removing a 6 (or a 5)
]]
---Returns the column indexes (1-based) with completable combos
---@param dice table The 2d table with dice values
---@param value integer The dice value to place
---@param len 2|3 The possible combo length
---@return table The columns list (1-based)
function get_combos_to_complete(dice, value, len)
local ids = {}
for col = 1, 3 do
if table.count(dice[col], 0) >= 1
and table.count(dice[col], value) == len - 1 then
table.insert(ids, col)
end
end
return ids
end
---@param mine table "My" 2d table with dice values
---@param other table The "other" (opponent) 2d table with dice values
---@param value integer The dice value to place
---@param len 2|3 The possible combo length
---@return table The columns list (1-based)
function get_combos_to_cancel(mine, other, value, len)
local ids = {}
for col = 1, 3 do
if table.count(mine[col], 0) >= 1
and table.count(other[col], value) == len - 1 then
table.insert(ids, col)
end
end
return ids
end
function get_most_empty_cols(dice)
local ids = {}
for z = 3, 1, -1 do
for col = 1, 3 do
if table.count(dice[col], 0) == z then
table.insert(ids, col)
end
end
if #ids > 0 then return ids end
end
end
---Place the die in the best possible column
---@param mine table The 2d table of "my" dice
---@param other any The 2d table of the "other" dice
---@param die integer The die to place
---@return integer # The column where to place the die
function ai_pick_column(mine, other, die)
trace("--")
trace("AI")
trace("--")
if die >= 4 then
--#region Complete a 3 combo 4-5-6
local combos = get_combos_to_complete(mine, die, 3)
if #combos > 0 then
trace("Making a 3 combo with " .. die)
return math.randomitem(combos)
end
--#endregion Complete a 3 combo 4-5-6
-- #region Cancel a 3-dice combo 4-5-6
combos = get_combos_to_cancel(mine, other, die, 3)
if #combos > 0 then
trace("Cancelling a 3 combo with " .. die)
return math.randomitem(combos)
end
--#endregion Cancel a 3-dice combo 4-5-6
-- #region Complete a 2 combo 4-5-6
combos = get_combos_to_complete(mine, die, 2)
if #combos > 0 then
trace("Making a 2 combo with " .. die)
return math.randomitem(combos)
end
-- #endregion Complete a 2 combo 4-5-6
end
-- #region Complete a 3 combo 1-2-3
combos = get_combos_to_complete(mine, die, 3)
if #combos > 0 then
trace("Making a 3 combo with " .. die)
return math.randomitem(combos)
end
-- #endregion Complete a 3 combo 1-2-3
-- #region Complete a 2 combo 1-2-3
combos = get_combos_to_complete(mine, die, 2)
if #combos > 0 then
trace("Making a 2 combo with " .. die)
return math.randomitem(combos)
end
-- #endregion Complete a 2 combo 1-2-3
-- #region Random column
-- TODO: if 5-6, attack the opponent.
-- TODO: else, chose the column with the lowest sum
-- TODO: it's best to AVOID attacking the opponent when his sum is <= 15
-- TODO: it's best to avoid canceling 1-2 values
trace("Placing " .. die .. " at random")
return math.randomitem(get_most_empty_cols(mine))
-- #endregion Random column
end

127
board.lua Normal file
View File

@ -0,0 +1,127 @@
---@diagnostic disable: lowercase-global
include "scoring"
local cell_width = 22
local cell_height = 18
function draw_board()
local players = { "player", "enemy" }
for _, player in ipairs(players) do
-- columns
for col = 1, 3 do
local dice_col = PLACED_DICE[player][col]
for row = 1, 3 do
local pos = get_cell_coords(player, col, row)
-- cell background
rectb(pos.x, pos.y, cell_width, cell_height, 15)
-- dice
local value = dice_col[row]
local combo = table.count(dice_col, value)
if value > 0 then
swap_color(8, 0)
if combo == 2 then
swap_color(12, 4)
swap_color(13, 3)
elseif combo == 3 then
swap_color(12, 3)
swap_color(13, 2)
end
-- dice sprite
spr(die_spr[value], pos.x + 3, pos.y + 1, 0, 1, 0, 0, 2, 2)
reset_colors(8, 12, 13)
end
end
end
end
end
function draw_dice_boxes()
-- small trick to make the corners look better
swap_color(3, 0)
-- player
swap_color(12, COLOR_PLAYER) -- bones color
local r = get_dice_box("player")
draw9box(51, 52, r.x - 4, r.y - 4, r.width + 8, r.height + 8)
-- enemy
swap_color(12, COLOR_ENEMY) -- bones color
r = get_dice_box("enemy")
draw9box(51, 52, r.x - 4, r.y - 4, r.width + 8, r.height + 8)
-- reset colors
swap_color(3, 3)
swap_color(12, 12)
end
function print_names()
prn_border("You", 1, 50, 12, ALIGN.Left, COLOR_PLAYER)
prn_border("Opponent", 239, 135-50-8, 12, ALIGN.Right, COLOR_ENEMY)
end
function print_scores()
local players = { "player", "enemy" }
local sums = {}
for _, player in ipairs(players) do
local scores = SCORES[player]
local y
for col = 1, 3 do
local value = scores[col]
local pos = get_cell_coords(player, col, 1)
pos.y = pos.y + (player == "player" and -9 or 20)
y = pos.y
pos.x = pos.x + 11
prn_border(value, pos.x, pos.y, 12, ALIGN.Center)
end
-- sums
local sum = get_total_score(scores)
local x = player == "player" and 12 or 240 - 12
local align = player == "player" and ALIGN.Left or ALIGN.Right
table.insert(sums, {
sum = sum,
x = x,
y = y,
align = align,
bg = player == "player" and COLOR_PLAYER or COLOR_ENEMY
})
end
for _, sum in ipairs(sums) do
prn_border(sum.sum, sum.x, sum.y, 12, sum.align, sum.bg)
end
end
---comment
---@param player 'player'|'enemy'
---@param col number 1-3
---@param row number 1-3
---@return table
function get_cell_coords(player, col, row)
row = row - 1 -- fix 1-based index for y, makes it easier to calculate coords
local p = 14 -- padding
local cols = { 120 - cell_width - p, 120 - (cell_width / 2), 120 + p }
if player == "enemy" then
row = row == 0 and 2 or row == 2 and 0 or 1 -- "invert" the board for the enemy
return { x = cols[col], y = (cell_height * row + (row * 1)) + 1 }
else
return { x = cols[col], y = (cell_height * row + (row * 1)) + 79 }
end
end
--- @param player 'player'|'enemy'
--- @param column 1|2|3
function get_column_rect(player, column)
local c = get_cell_coords(player, column, player == "player" and 1 or 3)
return { x = c.x - 1, y = c.y - 1, width = cell_width + 2, height = cell_height * 3 + 4 }
end
--- @param player 'player'|'enemy'
function get_dice_box(player)
if player == "player" then
return { x = 10, y = 89, width = 60, height = 36 }
else
return { x = 170, y = 9, width = 60, height = 36 }
end
end

106
classes/die.lua Normal file
View File

@ -0,0 +1,106 @@
--- @class Die
Die = {
x = 0,
y = 0,
size = 16,
dx = math.random(0, 1) == 0 and -1 or 1,
dy = math.random(0, 1) == 0 and -1 or 1,
value = nil,
--
box = nil,
bouncing = false,
angle = 0,
scale = 1,
--
bg = nil,
--
---
---@param self Die
set_random_value = function(self)
-- Don't change the value if the die is barely moving
if (math.abs(self.dx)<0.25 or math.abs(self.dy)<0.25) then
return
end
local n
repeat
n = math.random(1, 6)
until n ~= self.value
self.value = n
end,
---
--- @param self Die
update = function(self)
-- bounce off walls
if math.abs(self.dx) < 0.05 then self.dx = 0 end
if math.abs(self.dy) < 0.05 then self.dy = 0 end
if self.box then
if self.x < self.box.x then
self.x = self.box.x
self.dx = -self.dx
self:set_random_value()
end
if self.x > self.box.x + self.box.width - self.size then
self.x = self.box.x + self.box.width - self.size
self.dx = -self.dx
self:set_random_value()
end
if self.y < self.box.y then
self.y = self.box.y
self.dy = -self.dy
self:set_random_value()
end
if self.y > self.box.y + self.box.height - self.size then
self.y = self.box.y + self.box.height - self.size
self.dy = -self.dy
self:set_random_value()
end
end
if not self.bouncing then
-- reduce speed
if self.dx ~= 0 then
self.dx = self.dx * 0.85
end
if self.dy ~= 0 then
self.dy = self.dy * 0.85
end
end
self.x = self.x + self.dx
self.y = self.y + self.dy
end,
---
---@param self Die
draw = function(self, colorkey)
colorkey = colorkey or 0
if self.bg then
swap_color(12, self.bg)
end
swap_color(8, 0)
-- aspr() uses the center as anchor point, so we offset the drawing position
-- while keeping the scaling centered
local offset = 8
aspr(die_spr[self.value], self.x + offset, self.y + offset, colorkey, self.scale, self.scale, 0, self.angle, 2, 2)
-- rectb(self.x, self.y, 16, 16, 2)
reset_colors(12, 8)
end
}
---comment
---@param o Die
---@return Die
function Die:new(o)
o = o or {} -- create object if user does not provide one
setmetatable(o, self)
self.__index = self
if o.box then
o.angle = math.random() < .5 and 180 or -180
o.x = o.box.x + math.random(0, o.box.width - o.size)
o.y = o.box.y + math.random(0, o.box.height - o.size)
end
return o
end

48
coroutines.lua Normal file
View File

@ -0,0 +1,48 @@
--
-- coroutines manager
--
local coroutines = {}
function addcoroutine(fn)
local c = coroutine.create(fn)
table.insert(coroutines, c)
return c
end
function startcoroutine(c)
coroutine.resume(c)
end
function stopcoroutine(c)
for k, v in ipairs(coroutines) do
if c == v then
table.remove(coroutines, k)
break
end
end
end
function _coresolve()
for k, co in ipairs(coroutines) do
if coroutine.status(co) ~= "dead" then
local _, msg = coroutine.resume(co)
assert(msg == nil, msg)
else
stopcoroutine(co)
end
end
end
--- @param frames number
function waitframes(frames)
local frame = 0
while frame < frames do
frame = frame + 1
coroutine.yield()
end
end
--- @param seconds number
function waitsecs(seconds)
return waitframes(seconds * 60)
end

1
dev.bat Normal file
View File

@ -0,0 +1 @@
..\tq-bundler.exe run game.lua main.lua --tic=..\tic80.exe

4
events.lua Normal file
View File

@ -0,0 +1,4 @@
EVENT_CHANGE_TURN = "change_turn"
EVENT_SET_STEP = "set_step"
EVENT_REMOVE_DIE = "remove_die"
EVENT_RESET_DIE = "reset_die"

2523
game.lua Normal file

File diff suppressed because it is too large Load Diff

109
globals.lua Normal file
View File

@ -0,0 +1,109 @@
include "events"
-- trace = function(msg)
-- end
ALIGN = {
Left = 0,
Center = 1,
Right = 2
}
PALETTE_MAP = 0x3ff0
COLOR_PLAYER = 8
COLOR_ENEMY = 1
dt = 1 / 60
pt = 0
die_spr = {
1, 3, 5, 7, 9, 11
}
--- @class PLACED_DICE
PLACED_DICE = {
player = {
-- Each line represents a column
{ 0, 0, 0 },
{ 0, 0, 0 },
{ 0, 0, 0 },
-- { 1, 2, 3 },
-- { 4, 5, 6 },
-- { 1, 2, 0 },
},
enemy = {
{ 0, 0, 0 },
{ 0, 0, 0 },
{ 0, 0, 0 },
-- { 4, 5, 6 },
-- { 1, 2, 3 },
-- { 4, 5, 0 },
},
---@param self PLACED_DICE
---@param player 'player'|'enemy'
---@param x number
---@param y number
---@param score number
set = function(self, player, x, y, score)
self[player][x][y] = score
end,
---@param self PLACED_DICE
---@param player 'player'|'enemy'
---@param x number
---@param y number
---@return number
get = function(self, player, x, y)
return self[player][x][y]
end,
---returns true if a column has a free slot
---@param self PLACED_DICE
---@param player 'player'|'enemy'
---@param col number
has_free_slot = function(self, player, col)
return table.count(self[player][col], 0) >= 1
end,
---Returns if one of the boards is full
---@param self PLACED_DICE
is_game_over = function(self)
for _, player in ipairs({ "player", "enemy" }) do
local game_over = true
for col = 1, 3 do
for row = 1, 3 do
if self[player][col][row] == 0 then
game_over = false
end
end
end
if game_over then return true end
end
return false
end
}
--- Score values for each column
--- Must be manually updated
SCORES = {
player = { 0, 0, 0 },
enemy = { 0, 0, 0 },
update = function(self)
local players = { "player", "enemy" }
for _, player in ipairs(players) do
for col = 1, 3 do
local dice_col = PLACED_DICE[player][col]
local score = 0
for row = 1, 3 do
local value = dice_col[row]
local combo = table.count(dice_col, value)
local mul = combo == 2 and 1.5 or combo == 3 and 2 or 1
-- A 4-4-4 combo == 4 + 4*2 + 4*3
-- score = score + value * mul
--- A 4-4-4 combo == 4*3 + 4*3 + 4*3
score = score + combo * value
end
self[player][col] = math.floor(score)
end
end
end
}

BIN
kunckles.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

94
main.lua Normal file
View File

@ -0,0 +1,94 @@
-- title: Knucklebones
-- author: Simon Cambier
-- desc: A game of risk and reward, from "The Cult of the Lamb"
-- script: lua
-- Some sprites from https://piiixl.itch.io/frames-1-bit
include "utils.utils"
include "utils.input"
include "utils.tween"
include "coroutines"
include "states.state_manager"
include "states.state_main_menu"
include "states.state_game"
Tween = require_tween()
--- @type StateManager
state_manager = StateManager:new()
local states = {
main_menu = state_main_menu(),
game = state_game()
}
frames = 0
function BOOT()
-- Save original palette
memcpy(0x14e04, 0x3fc0, 48)
state_manager:change_state(states.main_menu, { states = states })
end
function TIC()
dt = (time() - pt) / 1000
pt = time()
frames = frames + 1
cls(13)
Input:update_mouse()
_coresolve()
state_manager:update()
-- mouse position
-- local mx, my = mouse()
-- print_border(mx .. "," .. my, mx + 1, my - 5)
end
-- function BDR(scn)
-- local cl = (math.floor(frames / 1.2) % (135 + 40)) - 20
-- greyGlitch(scn, cl)
-- end
function vhsGlitch(scn, cl)
local height = 10
local ampl = 10
local n = clamp(0, normalize(cl, scn - height, scn + height), 1) * 2 * math.pi
local s = math.cos(n)
local r = math.random() * (s * ampl - ampl) * s * s
-- 0x3ff9: screen offset
poke(0x3ff9, r)
poke(0x3ffa, r / 2)
end
function greyGlitch(scn, cl)
-- Reset rgb values
memcpy(0x3fc0, 0x14e04, 48)
local h = 10
-- If current scanline is in range
if (scn >= cl - h and scn <= cl + h) then
-- Loop through colors in ram
if (math.random() < 0.8) then
if (math.random() > 0.01) then
for i = 0, 16 do
local c = i * 3
local grey =
peek(0x3fc0 + c + 0) * 0.2126 +
peek(0x3fc0 + c + 1) * 0.7152 +
peek(0x3fc0 + c + 2) * 0.0722
-- Alter the color
poke(0x3fc0 + c + 0, grey)
poke(0x3fc0 + c + 1, grey)
poke(0x3fc0 + c + 2, grey)
end
else
-- line(0, scn, SCREEN_WIDTH, scn, RNG.random() * 15)
end
end
end
end

8
scoring.lua Normal file
View File

@ -0,0 +1,8 @@
function get_total_score(scores)
local sum = 0
for col = 1, 3 do
local value = scores[col]
sum = sum + value
end
return sum
end

109
states/game/gameover.lua Normal file
View File

@ -0,0 +1,109 @@
function state_game_gameover()
local box
local event_bus
local y = 90
local buttons = {
-- {
-- hover = false,
-- label = "Play Again",
-- box = { x = 120 - prn_len("Play Again") / 2, y = y, width = prn_len("Play Again"), height = 10 },
-- cb = function()
-- trace("again")
-- end
-- },
{
hover = false,
label = "Main Menu",
box = { x = 120 - prn_len("Main Menu") / 2, y = y + 15, width = prn_len("Main Menu"), height = 10 },
cb = function()
reset()
end
}
}
local function open_box()
while box.w < 180 do
box.w = box.w * 1.1
waitframes(1)
end
if box.w > 180 then
box.w = 180
end
while box.h < 20 do
box.h = box.h + 1
waitframes(1)
end
end
local function show_buttons()
-- show a button "again", and another "main menu"
-- right under the box
for _, button in ipairs(buttons) do
button.hover = false
-- rect(button.box.x - 1, button.box.y - 1, button.box.width + 2, button.box.height + 2, 2)
-- Hover button
if mouse_in_rect(button.box) then
button.hover = true
-- Click button
if Input:mouse_released()
then
button.cb()
end
end
prn_border(button.label,
button.box.x, button.box.y,
button.hover and 12 or 13,
ALIGN.Left,
0)
end
end
local function update(self)
-- draw box
rect(120 - box.w / 2, 67 - box.h / 2, box.w, box.h, 0)
rectb(120 - box.w / 2, 67 - box.h / 2, box.w, box.h, 15)
clip(120 - box.w / 2, 67 - box.h / 2, box.w, box.h)
-- print who won according to score
local text
local color
local score_player = get_total_score(SCORES.player)
local score_enemy = get_total_score(SCORES.enemy)
if score_player > score_enemy then
color = COLOR_PLAYER
text = "YOU WON " .. score_player .. " - " .. score_enemy
elseif score_player < score_enemy then
color = COLOR_ENEMY
text = "YOU LOST " .. score_player .. " - " .. score_enemy
else
color = 14
text = "DRAW " .. score_player .. " - " .. score_enemy
end
prn_border(text, 120, 67 - 3, 12, ALIGN.Center, color)
clip()
show_buttons()
end
---comment
---@param self any
---@param die Die
---@param _event_bus EventBus
---@param turn "player"|"enemy"
local function _enter(self, die, _event_bus, turn)
assert(_event_bus, "event_bus is nil")
assert(turn, "turn is nil")
event_bus = _event_bus
event_bus:emit(EVENT_REMOVE_DIE)
box = {
h = 0, w = 1
}
addcoroutine(open_box)
end
return { update = update, _enter = _enter }
end

91
states/game/loading.lua Normal file
View File

@ -0,0 +1,91 @@
function state_game_loading()
local covering
local event_bus
local turn
local banner
local function show_transition()
-- slightly scale up the transition dice
for i, die in ipairs(covering) do
die.scale = die.scale - 0.03 / i * 80
die.angle = die.angle - i / 50
if die.scale < 0 then
die.scale = 0
end
swap_color(12, 15)
swap_color(13, 15)
swap_color(14, 15)
swap_color(0, 15)
die:draw(-1)
end
-- clean
if table.count(covering, function(d) return d.scale == 0 end) == #covering then
covering = {}
end
end
local function _enter(self, data)
assert(data.covering, "needs data.covering")
assert(data.event_bus, "needs data.event_bus")
covering = data.covering
event_bus = data.event_bus
turn = data.turn
banner = {
h = 0,
w = 1,
}
addcoroutine(function()
while banner.w < 240 do
banner.w = banner.w *1.1
waitframes(1)
end
if banner.w > 240 then
banner.w = 240
end
while banner.h < 20 do
banner.h = banner.h + 1
waitframes(1)
end
waitsecs(1.5)
while banner.h > 0.5 do
banner.h = banner.h - 1
waitframes(1)
end
while banner.w > 1 do
banner.w = banner.w * 0.9
waitframes(1)
end
waitsecs(0.5)
event_bus:emit(EVENT_RESET_DIE)
event_bus:emit(EVENT_SET_STEP, "rolling")
end)
end
local function update(self)
-- black banner in the middle
rect(120 - banner.w / 2, 67 - banner.h / 2, banner.w, banner.h, 0)
rectb(120 - banner.w / 2, 67 - banner.h / 2, banner.w, banner.h, 15)
clip(120 - banner.w / 2, 67 - banner.h / 2, banner.w, banner.h)
-- show who's going first
local text
local color
if turn == "player" then
color = COLOR_PLAYER
text = "You begin this round"
else
color = COLOR_ENEMY
text = "Your opponent begins this round"
end
prn_border(text, 120, 67 - 4, 12, ALIGN.Center, color)
clip()
if #covering > 0 then
show_transition()
end
end
return { _enter = _enter, update = update }
end

102
states/game/placing.lua Normal file
View File

@ -0,0 +1,102 @@
include "ai"
function state_game_placing()
--- @type Die
local die
--- @type EventBus
local event_bus
--- @type 'player'|'enemy'
local turn
local placing = false
local waiting = 30
--- Click on a column to place the die
local function place_die(col_id)
if placing then return end
placing = true
local cell_id = 0
for i = 1, 3 do
if PLACED_DICE[turn][col_id][i] == 0 then
cell_id = i
break
end
end
step = "waiting"
addcoroutine(function()
-- move the die to its cell
local cell = get_cell_coords(turn, col_id, cell_id)
die.box = nil
die.angle = die.angle % 360
-- get value closest to 0. e.g. 270 -> -90
if die.angle > 180 then
die.angle = die.angle - 360
end
local tween = Tween.new(0.5, die, { x = cell.x + 3, y = cell.y + 1, angle = 0 }, "outQuad")
repeat
coroutine.yield()
until tween:update(dt)
PLACED_DICE:set(turn, col_id, cell_id, die.value)
-- Animate the dice and update the scores
event_bus:emit(EVENT_REMOVE_DIE)
event_bus:emit(EVENT_SET_STEP, "scoring")
end)
end
---@param self any
---@param _die Die
---@param _event_bus EventBus
---@param _turn 'player'|'enemy'
local _enter = function(self, _die, _event_bus, _turn)
assert(_die, "missing die")
assert(_event_bus, "missing event_bus")
assert(_turn, "missing turn")
die = _die
event_bus = _event_bus
turn = _turn
waiting = 30
placing = false
end
local update = function(self)
waiting = waiting - 1
if turn == "enemy" then
if waiting <= 0 and not placing then
local col = ai_pick_column(PLACED_DICE.enemy, PLACED_DICE.player, die.value)
place_die(col)
end
else -- turn == "player"
-- Highlight the hovered column
local columns = {
get_column_rect(turn, 1),
get_column_rect(turn, 2),
get_column_rect(turn, 3),
}
local col, col_id = table.find(columns, function(c)
return mouse_in_rect(c)
end)
if col then
if PLACED_DICE:has_free_slot(turn, col_id) then
-- draw hovered column
rect(col.x, col.y, col.width, col.height, 14)
-- place the die if clicked
if Input:mouse_released() then
place_die(col_id)
end
end
end
end
-- Redraw the board because the hovered column is drawn on top of it
-- and i'm too lazy to fix this properly.
draw_board()
end
return {
_enter = _enter,
update = update,
}
end

40
states/game/rolling.lua Normal file
View File

@ -0,0 +1,40 @@
function state_game_rolling()
--- @type Die
local die
--- @type EventBus
local event_bus
local function roll_die()
die.bouncing = true
local rot = Tween.new(0.7, die, {
angle = (die.angle + math.random(180,360))-- die.angle + (math.random()<.5 and 1 or -1)* math.random(270, 360)
}, "outSine")
local done = false
repeat
done = rot:update(dt)
coroutine.yield()
until done
die.bouncing = false
event_bus:emit(EVENT_SET_STEP, "placing")
end
---@param self any
---@param _die Die
---@param _event_bus EventBus
local _enter = function(self, _die, _event_bus)
assert(_die, "states.game.rolling needs a die")
assert(_event_bus, "states.game.rolling needs an event_bus")
die = _die
event_bus = _event_bus
addcoroutine(roll_die)
end
local update = function(self)
end
return {
_enter = _enter,
update = update
}
end

93
states/game/scoring.lua Normal file
View File

@ -0,0 +1,93 @@
function state_game_scoring()
local dice_to_destroy = {}
--- @type EventBus
local event_bus
local destroyed = 0
local function update_board_and_score(player, col, row)
assert(player)
assert(col)
assert(row)
-- Create a Die and place it on the cell
local cell = get_cell_coords(player, col, row)
local die = Die:new({ value = PLACED_DICE:get(player, col, row), x = cell.x + 3, y = cell.y + 1 })
-- Set the score to 0
PLACED_DICE:set(player, col, row, 0)
table.insert(dice_to_destroy, die)
addcoroutine(function()
waitframes(10)
local tween = Tween.new(0.5, die, { scale = 0.1 }, "inOutQuad")
repeat
coroutine.yield()
until tween:update(dt)
table.remove_item(dice_to_destroy, die)
end)
end
---comment
---@param self any
---@param _die Die|nil
---@param _event_bus EventBus
---@param player 'player'|'enemy'
local function _enter(self, _die, _event_bus, player)
assert(_event_bus)
assert(player == "player" or player == "enemy")
event_bus = _event_bus
destroyed = 0
local other_player = player == "player" and "enemy" or "player"
-- Check if the newly placed die must remove other dice
local current_dice = PLACED_DICE[player]
local other_dice = PLACED_DICE[other_player]
-- Check the dice for each column, and remove corresponding dice in the opposing column
for col = 1, 3 do
for row = 1, 3 do
local value = current_dice[col][row]
for row2 = 1, 3 do
local value2 = other_dice[col][row2]
if value > 0 and value2 > 0 and value2 == value then
trace("destroy " .. other_player .. " " .. col .. " " .. row2 .. " = " .. value2)
update_board_and_score(other_player, col, row2)
destroyed = destroyed + 1
end
end
end
end
-- Update the scores and move on to next state
addcoroutine(function()
waitsecs((destroyed > 0) and 0.6 or 0.3)
SCORES:update()
-- Check if it's game over
if PLACED_DICE:is_game_over() then
SCORES:update()
event_bus:emit(EVENT_SET_STEP, "game_over")
else
event_bus:emit(EVENT_CHANGE_TURN)
end
end)
end
local function update(self)
for _, die in ipairs(dice_to_destroy) do
die:draw()
end
end
local function _leave(self)
end
return {
_enter = _enter,
update = update,
_leave = _leave
}
end

106
states/state_game.lua Normal file
View File

@ -0,0 +1,106 @@
include "board"
include "classes.die"
include "states.game.loading"
include "states.game.rolling"
include "states.game.placing"
include "states.game.scoring"
include "states.game.gameover"
include "utils.event_bus"
function state_game()
local game = {}
local event_bus = EventBus:new()
--- @type "player"|"enemy"
local turn = "player"
--- @type "waiting"|"rolling"|"placing"
local step = "rolling"
--- @type Die|nil
local die
local function reset_die()
die = Die:new({ box = get_dice_box(turn), bouncing = false })
end
--
-- #region State management
--
--- Sub-states
--- @type StateManager
local state_manager = StateManager:new()
local states = {
loading = state_game_loading(),
rolling = state_game_rolling(),
placing = state_game_placing(),
scoring = state_game_scoring(),
game_over = state_game_gameover(),
}
event_bus:on(EVENT_SET_STEP, function(new_step)
trace("[Event] set_step: " .. step .. " -> " .. new_step)
assert(states[new_step], "state " .. new_step .. " does not exist")
state_manager:change_state(states[new_step], die, event_bus, turn)
step = new_step
end)
event_bus:on(EVENT_CHANGE_TURN, function()
turn = turn == "player" and "enemy" or "player"
reset_die()
state_manager:change_state(states.rolling, die, event_bus)
step = "rolling"
end)
event_bus:on(EVENT_REMOVE_DIE, function()
die = nil
end)
event_bus:on(EVENT_RESET_DIE, function()
reset_die()
end)
--
-- #endregion State management
--
game._enter = function(self, data)
assert(data.covering, "needs data.covering")
trace("entering game state")
turn = math.random(1, 2) == 1 and "player" or "enemy"
state_manager:change_state(states.loading, {
turn = turn,
event_bus = event_bus,
covering = data.covering
})
end
game.update = function()
cls()
if die then die:update() end
draw_board()
print_scores()
print_names()
draw_dice_boxes()
state_manager:update()
if die and die.value then
die:draw()
end
-- prn_border("Turn: " .. turn, 1, 1, 2)
-- prn_border("Step: " .. step, 1, 10, 2)
reset_colors()
end
game._leave = function()
end
return game
end

130
states/state_main_menu.lua Normal file
View File

@ -0,0 +1,130 @@
function state_main_menu()
local covering = {}
local falling = {}
local states
local buttons = {
{
hover = false,
label = "Play",
box = { x = 120 - prn_len("Play") / 2, y = 100, w = prn_len("Play"), h = 10 },
cb = function()
trace("play")
for i = 0, 240, 16 do
for j = 0, 135, 16 do
table.insert(covering, Die:new({
value = math.random(1, 6),
angle = 360,
x = i,
y = j,
scale = 0
}))
end
end
end
}
}
local function show_transition()
-- slightly scale up the transition dice
for i, die in ipairs(covering) do
die.scale = die.scale + 0.03 / i * 80
die.angle = die.angle * 0.90
if die.scale > 1 then
die.scale = 1
end
swap_color(12, 15)
swap_color(0, 15)
die:draw(-1)
end
if table.count(covering, function(d) return d.scale == 1 end) == #covering then
state_manager:change_state(states.game, { covering = covering })
end
end
local function falling_dice()
local remove = {}
for i, die in ipairs(falling) do
die.y = die.y + 1
die.angle = die.angle + die.rot_speed
if die.y > 160 then
table.insert(remove, i)
end
end
for _, i in ipairs(remove) do
table.remove(falling, i)
end
end
local function draw_dice()
for _, die in ipairs(falling) do
swap_color(12, 15)
swap_color(13, 15)
swap_color(14, 15)
die:draw()
reset_colors(12)
end
end
local function _enter(self, data)
assert(data.states, "needs a states table")
states = data.states
covering = {}
end
local function update()
-- random sign
if math.random() < 0.05 then
local sign = math.random() < 0.5 and -1 or 1
-- Spawn a random die
local die = Die:new({
value = math.random(1, 6),
x = math.random(240),
y = -20,
angle = math.random(10, 40),
rot_speed = sign * math.random(6, 10) / 10,
})
table.insert(falling, die)
end
falling_dice()
cls()
draw_dice()
-- prn_border("KNUCKLEBONES", 118, 10, 1, ALIGN.Center, 0, 2, 0)
-- prn_border("KNUCKLEBONES", 122, 10, 2, ALIGN.Center, 0, 2, 1)
prn_border("KNUCKLEBONES", 120, 10, 12, ALIGN.Center, 0, 2)
local l = prn_len("A game of risk and reward")
local x = 120 - l / 2
local y = 50
x = x + prn_border("A game of ", x, y, 13, ALIGN.Left, 0)
x = x + prn_border_floaty("risk ", x, y + 5, 12, ALIGN.Left, 1, 1, 20)
x = x + prn_border("and ", x, y, 13, ALIGN.Left, 0)
x = x + prn_border_floaty("reward", x, y - 5, 12, ALIGN.Left, 8)
print_border("Originally from \"The Cult of the Lamb\"", 104, 130, 14, 0, false, 1, true)
local mx, my = mouse()
for i, button in ipairs(buttons) do
-- Print button
local x, y, w, h = button.box.x, button.box.y, button.box.w, button.box.h
local hover = mx >= x and mx <= x + w and my >= y and my <= y + h
button.hover = hover
local color = hover and 12 or 13
-- rect(x, y, w, h, color)
prn_border(button.label, x, y, color, ALIGN.Left, 0)
-- Click button
if hover and Input:mouse_pressed()
then
button.cb()
end
end
if #covering > 0 then
show_transition()
end
end
return { update = update, _enter = _enter }
end

60
states/state_manager.lua Normal file
View File

@ -0,0 +1,60 @@
--- States stack
--- @class StateManager
StateManager = {
states = {},
---
---@param self StateManager
new = function(self, o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end,
---
---@param self StateManager
---@return unknown
_get_current_state = function(self)
return self.states[#self.states]
end,
---
---@param self StateManager
---@param state any
_set_current_state = function(self, state)
if #self.states == 0 then
self.states = { state }
else
local last = self.states[#self.states]
if last._leave then last:_leave() end
self.states[#self.states] = state
end
end,
---
---@param self StateManager
---@param state any
change_state = function(self, state, ...)
local current = self:_get_current_state()
if current and current._leave then current:_leave() end
self:_set_current_state(state)
if state._enter then state:_enter(...) end
end,
---
---@param self StateManager
push_state = function(self, state, data)
table.insert(self.states, state)
if state._enter then state:_enter(data) end
end,
---
---@param self StateManager
pop_state = function(self)
local state = table.remove(self.states)
if #self.states == 0 then trace("No state left", 2) end
if state._leave then state:_leave() end
end,
---
---@param self StateManager
update = function(self)
local current = self:_get_current_state()
if not current then return end
if current.update then current:update() end
end,
}

403
tic.lua Normal file
View File

@ -0,0 +1,403 @@
---@diagnostic disable: lowercase-global, missing-return
-- This transcription is useful with VSCode to enable code completion for TIC-80 functions.
-- author: Júnior Garcia <itamarjr91@gmail.com>
-- updated by Simon Cambier
-- Main function. It's called at 60 fps (60 times every second).
function TIC() end
-- Allows you to execute code between the drawing of each scan line, for example, to manipulate the palette.
---@param line number The number of the line
function SCN(line) end
-- Called after each frame; draw calls from this function ignore palette swap and screen offset.
function OVR() end
-- This function allows you to read the status of one of the buttons attached to TIC. The function returns true if the key with the supplied id is currently in the pressed state. It remains true for as long as the key is held down. If you want to test if a key was just pressed, use btnp instead.
---@param id number The id of the key we want to interrogate, see the [key map](https://github.com/nesbox/TIC-80/wiki/key-map) for reference
---@return boolean
function btn(id) end
-- This function allows you to read the status of one of TIC's buttons.
-- It returns true only if the key has been pressed since the last frame.
-- You can also use the optional hold and period parameters which allow you to check if a button is being held down. After the time specified by hold has elapsed, btnp will return true each time period is passed if the key is still down. For example, to re-examine the state of button '0' after 2 seconds and continue to check its state every 1/10th of a second, you would use btnp(0, 120, 6). Since time is expressed in ticks and TIC runs at 60 frames per second, we use the value of 120 to wait 2 seconds and 6 ticks (ie 60/10) as the interval for re-checking.
---@param id number The id of the key we wish to interrogate - see the [key map](https://github.com/nesbox/TIC-80/wiki/key-map) for reference
---@param hold number The time (in ticks) the key must be pressed before re-checking
---@param period number The the amount of time (in ticks) after hold before this function will return true again.
---@return boolean
function btnp(id, hold, period) end
-- This function limits drawing to a clipping region or 'viewport' defined by x,y,w,h. Things drawn outside of this area will not be visible.
-- Calling clip() with no parameters will reset the drawing area to the entire screen.
---@param x? number x coordinate of the top left of the clipping region
---@param y? number y coordinate of the top left of the clipping region
---@param w? number Width of the drawing area in pixels
---@param h? number Height of the drawing area in pixels
function clip(x, y, w, h) end
-- Clear the screen.
-- When called this function clear all the screen using the color passed as argument. If no parameter is passed first color (0) is used.
-- Tips: Use a color over 15 to see some special fill pattern
---@param color? number The index (0 to 15) of the color in the current
function cls(color) end
-- This function draws a filled circle of the desired radius and color with its center at x, y. It uses the Bresenham algorithm.
---@param x number The x coordinate of the circle center
---@param y number The y coordinate of the circle center
---@param r number The radius of the circle in pixels
---@param color number The index of the desired color in the current [palette](https://github.com/nesbox/TIC-80/wiki/palette)
function circ(x, y, r, color) end
-- Draws the circumference of a circle with its center at x, y using the radius and color requested.
-- It uses the Bresenham algorithm.
---@param x number The x coordinate of the circle's center
---@param y number The y coordinate of the circle's center
---@param r number The radius of the circle in pixels
---@param color number The index of the desired color in the current [palette](https://github.com/nesbox/TIC-80/wiki/palette)
function circb(x, y, r, color) end
-- Interrupts program execution and returns to the console when the TIC function ends.
function exit() end
-- Returns true if the specified flag of the sprite is set. See [fset](https://github.com/nesbox/TIC-80/wiki/fset) for more details.
---@param index number Sprite index
---@param flag number Flag index (0-7) to check
---@return boolean enabled
function fget(index, flag) end
-- Print string with font defined in foreground sprites.
-- To simply print to the screen, check out `print`.
-- To print to the console, check out `trace`.
---@param text string Any string to be printed to the screen
---@param x number x coordinate where to print the text
---@param y number y coordinate where to print the text
---@param colorkey number? The colorkey to use as transparency.
---@param charWidth number? Width Width of characters to use for spacing, in pixels
---@param charHeight number? Height Height of characters to use for multiple line spacing, in pixels.
---@param fixed boolean? A flag indicating whether to fix the width of the characters, by default is not fixed
---@param scale number? Font scaling
---@return number [The width of the text in pixels.]
function font(text, x, y, colorkey, charWidth, charHeight, fixed, scale) end
-- Each sprite has eight flags which can be used to store information or signal different conditions. For example, flag 0 might be used to indicate that the sprite is invisible, flag 6 might indicate that the flag should be draw scaled etc.
-- See algo [fget](https://github.com/nesbox/TIC-80/wiki/fget) (0.80)
---@param index number Sprite index
---@param flag number Index of flag (0-7)
---@param bool boolean What state to set the flag, true or false
function fset(index, flag, bool) end
-- The function returns true if the key denoted by keycode is pressed.
-- * 01 = A
-- * 02 = B
-- * 03 = C
-- * 04 = D
-- * 05 = E
-- * 06 = F
-- * 07 = G
-- * 08 = H
-- * 09 = I
-- * 10 = J
-- * 11 = K
-- * 12 = L
-- * 13 = M
-- * 14 = N
-- * 15 = O
-- * 16 = P
-- * 17 = Q
-- * 18 = R
-- * 19 = S
-- * 20 = T
-- * 21 = U
-- * 22 = V
-- * 23 = W
-- * 24 = X
-- * 25 = Y
-- * 26 = Z
-- * 27 = 0
-- * 28 = 1
-- * 29 = 2
-- * 30 = 3
-- * 31 = 4
-- * 32 = 5
-- * 33 = 6
-- * 34 = 7
-- * 35 = 8
-- * 36 = 9
-- * 37 = MINUS
-- * 38 = EQUALS
-- * 39 = LEFTBRACKET
-- * 40 = RIGHTBRACKET
-- * 41 = BACKSLASH
-- * 42 = SEMICOLON
-- * 43 = APOSTROPHE
-- * 44 = GRAVE
-- * 45 = COMMA
-- * 46 = PERIOD
-- * 47 = SLASH
-- * 48 = SPACE
-- * 49 = TAB
-- * 50 = RETURN
-- * 51 = BACKSPACE
-- * 52 = DELETE
-- * 53 = INSERT
-- * 54 = PAGEUP
-- * 55 = PAGEDOWN
-- * 56 = HOME
-- * 57 = END
-- * 58 = UP
-- * 59 = DOWN
-- * 60 = LEFT
-- * 61 = RIGHT
-- * 62 = CAPSLOCK
-- * 63 = CTRL
-- * 64 = SHIFT
-- * 65 = ALT
---@param code number The key code (1..65) we want to check
---@return boolean [pressed]
function key(code) end
-- This function returns true if the given key is pressed but wasn't pressed in the previous frame.
-- Refer to [btnp](https://github.com/nesbox/TIC-80/wiki/btnp) for an explanation of the optional hold and period parameters
---@param code number The key code we want to check (see codes [here](https://github.com/nesbox/TIC-80/wiki/key#parameters))
---@param hold number Time in ticks before autorepeat
---@param period number Time in ticks for autorepeat interval
---@return boolean [pressed]
function keyp(code, hold, period) end
-- Draws a straight line from point (x0,y0) to point (x1,y1) in the specified color.
---@param x0 number The x coordinate where the line starts
---@param y0 number The y coordinate where the line starts
---@param x1 number The x coordinate where the line ends
---@param y1 number The y coordinate where the line ends
---@param color ?number The index of the color in the current [palette](https://github.com/nesbox/TIC-80/wiki/palette)
function line(x0, y0, x1, y1, color) end
-- The map consists of cells of 8x8 pixels, each of which can be filled with a sprite using the map editor. The map can be up to 240 cells wide by 136 deep. This function will draw the desired area of the map to a specified screen position. For example, map(5,5,12,10,0,0) will draw a 12x10 section of the map, starting from map co-ordinates (5,5) to screen position (0,0).
-- The map functions last parameter is a powerful callback function for changing how map cells (sprites) are drawn when map is called. It can be used to rotate, flip and replace sprites while the game is running. Unlike mset, which saves changes to the map, this special function can be used to create animated tiles or replace them completely. Some examples include changing sprites to open doorways, hiding sprites used to spawn objects in your game and even to emit the objects themselves.
-- The tilemap is laid out sequentially in RAM - writing 1 to 0x08000 will cause tile(sprite) #1 to appear at top left when map() is called. To set the tile immediately below this we need to write to 0x08000 + 240, ie 0x080F0
---@param x number The leftmost map cell to be drawn.
---@param y number The uppermost map cell to be drawn.
---@param w number The number of cells to draw horizontally.
---@param h number The number of cells to draw vertically.
---@param sx number The screen x coordinate where drawing of the map section will start.
---@param sy number The screen y coordinate where drawing of the map section will start.
---@param colorkey number Index (or array of indexes 0.80.0) of the color that will be used as transparent color. Not setting this parameter will make the map opaque.
---@param scale number Map scaling.
---@param remap ?function An optional function called before every tile is drawn. Using this callback function you can show or hide tiles, create tile animations or flip/rotate tiles during the map rendering stage: `callback [tile [x y] ] -> [tile [flip [rotate] ] ]`
function map(x, y, w, h, sx, sy, colorkey, scale, remap) end
-- This function allows you to copy a continuous block of TIC's 64k [RAM](https://github.com/nesbox/TIC-80/wiki/RAM) from one address to another. Addresses are specified are in hexadecimal format, values are decimal.
---@param toaddr number The address you want to write to
---@param fromaddr number The address you want to copy from
---@param len number The length of the memory block you want to copy
function memcpy(toaddr, fromaddr, len) end
-- This function allows you to set a continuous block of any part of TIC's [RAM](https://github.com/nesbox/TIC-80/wiki/RAM) to the same value. The address is specified in hexadecimal format, the value in decimal.
---@param addr number The address of the first byte of 64k [RAM](https://github.com/nesbox/TIC-80/wiki/RAM) you want to write to
---@param val number The value you want to write
---@param len number The length of the memory block you want to set
function memset(addr, val, len) end
-- Gets the sprite id at the given x and y map coordinate
---@param x number x coordinate on the map
---@param y number y coordinate on the map
---@return number [id]
function mget(x, y) end
-- This function returns the mouse coordinates and a boolean value for the state of each mouse button, with true indicating that a button is pressed.
---@return number x, number y, boolean left, boolean middle, boolean right, number scrollx, number scrolly
function mouse() end
-- This function will change the tile at the specified map coordinates. By default, changes made are only kept while the current game is running. To make permanent changes to the map, see [sync](https://github.com/nesbox/TIC-80/wiki/sync).
-- Related:
-- * [map](https://github.com/nesbox/TIC-80/wiki/map)
-- * [mget](https://github.com/nesbox/TIC-80/wiki/mget)
-- * [sync](https://github.com/nesbox/TIC-80/wiki/sync)
---@param x number x coordinate on the map
---@param y number y coordinate on the map
---@param id number The background tile (0-255) to place in map at specified coordinates.
function mset(x, y, id) end
-- This function starts playing a track created in the [Music Editor](https://github.com/nesbox/TIC-80/wiki/Home#music-editor). Call without arguments to *stop* the music.
---@param track ?number The id of the track to play from (0..7)
---@param frame ?number The index of the frame to play from (0..15)
---@param row ?number The index of the row to play from (0..63)
---@param loop ?number Loop music or play it once (true/false)
---@param sustain ?number Sustain notes after the end of each frame or stop them (true/false)
function music(track, frame, row, loop, sustain) end
-- This function allows to read the memory from TIC.
-- It's useful to access resources created with the integrated tools like [sprite](https://github.com/nesbox/TIC-80/wiki/sprite), maps, sounds, cartridges data? Never dream to sound a sprite?
-- Address are in hexadecimal format but values are decimal.
-- To write to a memory address, use [poke](https://github.com/nesbox/TIC-80/wiki/poke).
---@param addr number Any address of the 80k [RAM](https://github.com/nesbox/TIC-80/wiki/RAM) byte you want to read
---@return number [The value read from the addr parameter. Each address stores a byte, so the value will be an integer from 0 to 255.]
function peek(addr) end
-- This function enables you to read values from TIC's [RAM](https://github.com/nesbox/TIC-80/wiki/RAM). The address should be specified in hexadecimal format.
---@param addr4 number any address of the 80K RAM byte you want to read, divided in groups of 4 bits (nibbles). Therefore, to address the high nibble of position 0x2000 you should pass 0x4000 as addr4, and to access the low nibble (rightmost 4 bits) you would pass 0x4001.
---@return number [The 4-bit value (0-15) read from the specified address.]
function peek4(addr4) end
-- This function can read or write pixel color values. When called with a color parameter, the pixel at the specified coordinates is set to that color. Calling the function without a color parameter returns the color of the pixel at the specified position.
---@param x number x coordinate of the pixel to write
---@param y number y coordinate of the pixel to write
---@param color ?number The index of the color in the [palette](https://github.com/nesbox/TIC-80/wiki/palette) to apply at the desired coordinates
---@return number [The index (0-15) in the color [palette](https://github.com/nesbox/TIC-80/wiki/palette) at the specified x and y coordinates.]
function pix(x, y, color) end
-- This function allows you to save and retrieve data in one of the 256 individual 32-bit slots available in the cartridge's persistent memory. This is useful for saving high-scores, level advancement or achievements. The data is stored as unsigned 32-bit integers (from 0 to 4294967295).
-- Tips:
-- * pmem depends on the cartridge hash (md5), so don't change your lua script if you want to keep the data.
-- * Use _saveid_: with a personalized string in the header [metadata](https://github.com/nesbox/tic.computer/wiki#cartridge-metadata) to override the default MD5 calculation. This allows the user to update a cart without losing their saved data.
---@param index number The index of the value you want to save/read in the persistent memory
---@param val number The value you want to store in the memory. Omit this parameter if you want to read the memory.
---@return number [When function is call with only index parameters it'll return the value saved in that memory slot.]
function pmem(index, val) end
-- This function allows you to write a single byte to any address in TIC's [RAM](https://github.com/nesbox/TIC-80/wiki/RAM). The address should be specified in hexadecimal format, the value in decimal.
---@param addr number The address in [RAM](https://github.com/nesbox/TIC-80/wiki/RAM)
---@param val number The value to write
function poke(addr, val) end
-- This function allows you to write to the virtual [RAM](https://github.com/nesbox/TIC-80/wiki/RAM) of TIC. It differs from [poke](https://github.com/nesbox/TIC-80/wiki/poke) in that it divides memory in groups of 4 bits. Therefore, to address the high nibble of position 0x4000 you should pass 0x8000 as addr4, and to access the low nibble (rightmost 4 bits) you would pass 0x8001. The address should be specified in hexadecimal format, and values should be given in decimal.
---@param addr4 number the nibble (4 bits) address in RAM to which to write,
---@param val number the 4-bit value (0-15) to write to the specified address
function poke4(addr4, val) end
-- This will simply print text to the screen using the font defined in config. When set to true, the fixed width option ensures that each character will be printed in a 'box' of the same size, so the character 'i' will occupy the same width as the character 'w' for example. When fixed width is false, there will be a single space between each character. Refer to the [example](https://github.com/nesbox/TIC-80/wiki/print#example-1) for an illustration.
-- * To use a custom rastered font, check out [font](https://github.com/nesbox/TIC-80/wiki/font).
-- * To print to the console, check out [trace](https://github.com/nesbox/TIC-80/wiki/trace).
---@param text any string to be printed to the screen
---@param x ?number x coordinate where to print the text
---@param y ?number y coordinate where to print the text
---@param color ?number the color to use to draw the text to the screen
---@param fixed ?boolean a flag indicating whether fixed width printing is required
---@param scale ?number font scaling
---@param smallfont ?boolean use small font if true
---@return number [The width of the text in pixels.]
function print(text, x, y, color, fixed, scale, smallfont) end
-- This function draws a filled rectangle of the desired size and color at the specified position. If you only need to draw the the border or outline of a rectangle (ie not filled) see [rectb](https://github.com/nesbox/TIC-80/wiki/rectb)
---@param x number x coordinate of the top left corner of the rectangle
---@param y number y coordinate of the top left corner of the rectangle
---@param w number The width the rectangle in pixels
---@param h number The height of the rectangle in pixels
---@param color number The index of the color in the [palette](https://github.com/nesbox/TIC-80/wiki/palette) that will be used to fill the rectangle
function rect(x, y, w, h, color) end
-- This function draws a one pixel thick rectangle border at the position requested.
-- If you need to fill the rectangle with a color, see [rect](https://github.com/nesbox/TIC-80/wiki/rect) instead.
---@param x number x coordinate of the top left corner of the rectangle
---@param y number y coordinate of the top left corner of the rectangle
---@param w number The width the rectangle in pixels
---@param h number The height of the rectangle in pixels
---@param color number The index of the color in the [palette](https://github.com/nesbox/TIC-80/wiki/palette) that will be used to color the rectangle's border.
function rectb(x, y, w, h, color) end
-- Resets the cartridge. To return to the console, see the [exit](https://github.com/nesbox/TIC-80/wiki/exit) function.
function reset() end
-- This function will play the sound with *id* created in the sfx editor. Calling the function with id set to -1 will stop playing the channel.
-- The **note** can be supplied as an integer between 0 and 95 (representing 8 octaves of 12 notes each) or as a string giving the note name and octave. For example, a note value of '14' will play the note 'D' in the second octave. The same note could be specified by the string 'D-2'. Note names consist of two characters, the note itself (**in upper case**) followed by '-' to represent the natural note or '#' to represent a sharp. There is no option to indicate flat values. The available note names are therefore: C-, C#, D-, D#, E-, F-, F#, G-, G#, A-, A#, B-. The octave is specified using a single digit in the range 0 to 8.
-- The **duration** specifies how many ticks to play the sound for; since TIC-80 runs at 60 frames per second, a value of 30 represents half a second. A value of -1 will play the sound continuously.
-- The **channel** parameter indicates which of the four channels to use. Allowed values are 0 to 3.
-- **Volume** can be between 0 and 15.
-- **Speed** in the range -4 to 3 can be specified and means how many 'ticks+1' to play each step, so speed==0 means 1 tick per step.
---@param id number The sfx id, from 0 to 63
---@param note ?number The note number or name
---@param duration ?number Duration (-1 by default)
---@param channel ?number Which channel to use, 0..3
---@param volume ?number Volume (15 by default)
---@param speed ?number Speed (0 by default)
function sfx(id, note, duration, channel, volume, speed) end
-- Draws the sprite number index at the x and y coordinate.
-- You can specify a colorkey in the palette which will be used as the transparent color or use a value of -1 for an opaque sprite.
-- The sprite can be scaled up by a desired factor. For example, a scale factor of 2 means an 8x8 pixel sprite is drawn to a 16x16 area of the screen.
-- You can flip the sprite where:
-- * 0 = No Flip
-- * 1 = Flip horizontally
-- * 2 = Flip vertically
-- * 3 = Flip both vertically and horizontally
-- When you rotate the sprite, it's rotated clockwise in 90° steps:
-- * 0 = No rotation
-- * 1 = 90° rotation
-- * 2 = 180° rotation
-- * 3 = 270° rotation
-- You can draw a composite sprite (consisting of a rectangular region of sprites from the sprite sheet) by specifying the w and h parameters (which default to 1).
---@param id number Index of the sprite
---@param x number x coordinate where the sprite will be drawn, starting from top left corner.
---@param y number y coordinate where the sprite will be drawn, starting from top left corner.
---@param colorkey number? Index (or array of indexes) of the color in the sprite that will be used as transparent color. Use -1 if you want an opaque sprite.
---@param scale number? Scale factor applied to sprite.
---@param flip 0|1|2|3? Flip the sprite vertically or horizontally or both.
---@param rotate 0|1|2|3? Rotate the sprite by 0, 90, 180 or 270 degrees.
---@param w number? Width of composite sprite
---@param h number? Height of composite sprite
function spr(id, x, y, colorkey, scale, flip, rotate, w, h) end
-- The pro version of TIC-80 contains 8 memory banks. To switch between these banks, sync can be used to either load contents from a memory bank to runtime, or save contents from the active runtime to a bank. The function can only be called once per frame.
-- If you have manipulated the runtime memory (e.g. by using mset), you can reset the active state by calling sync(0,0,false). This resets the whole runtime memory to the contents of bank 0.
-- Note that sync is not used to load code from banks; this is done automatically.
---@param mask number Mask of sections you want to switch. See [here](https://github.com/nesbox/TIC-80/wiki/sync#parameters)
---@param bank number memory bank, can be 0...7.
---@param toCart boolean if `true`, save sprites/map/sound from runtime to bank, if `false` load data from bank to runtime.
function sync(mask, bank, toCart) end
-- This function returns the number of milliseconds elapsed since the cartridge began execution. Useful for keeping track of time, animating items and triggering events.
---@return number [The number of milliseconds elapsed since the application began.]
function time() end
-- This function returns the number of seconds elapsed since January 1st, 1970. Useful for creating persistent games which evolve over time between plays.
---@return number [The number of seconds that have passed since January 1st, 1970.]
function tstamp() end
-- This is a service function, useful for debugging your code. It prints the message parameter to the console in the (optional) color specified.
-- Tips:
-- 1. The Lua concatenator for strings is .. (two points)
-- 1. Use console cls command to clear the output from trace
---@param msg any The message to print in the console. Can be a 'string' or variable.
---@param color ?number Color for the msg text
function trace(msg, color) end
-- This function draws a triangle filled with color, using the supplied vertices.
---@param x1 number x coordinate of the first triangle corner
---@param y1 number y coordinate of the first triangle corner
---@param x2 number x coordinate of the second triangle corner
---@param y2 number y coordinate of the second triangle corner
---@param x3 number x coordinate of the third triangle corner
---@param y3 number y coordinate of the third triangle corner
---@param color number The index of the desired color in the current [palette](https://github.com/nesbox/TIC-80/wiki/palette)
function tri(x1, y1, x2, y2, x3, y3, color) end
-- It renders a triangle filled with texture from image ram or map ram
-- **Use in 3D graphics**
-- This function does not perform perspective correction, so it is not generally suitable for 3D graphics (except in some constrained scenarios). In particular, if the vertices in the triangle have different 3D depth, you may see some distortion.
-- These can be thought of as the window inside image ram (sprite sheet), or map ram. Note that the sprite sheet or map in this case is treated as a single large image, with U and V addressing its pixels directly, rather than by sprite ID. So for example the top left corner of sprite #2 would be located at u=16, v=0.
-- * **u1**: the U coordinate of the first triangle corner
-- * **v1**: the V coordinate of the first triangle corner
-- * **u2**: the U coordinate of the second triangle corner
-- * **v2**: the V coordinate of the second triangle corner
-- * **u3**: the U coordinate of the third triangle corner
-- * **v3**: the V coordinate of the third triangle corner
-- * **use_map**: if false (default), the triangle's texture is read from the image vram (sprite sheet). If true, the texture comes from the map ram.
-- * **colorkey**: index (or array of indexes 0.80.0) of the color that will be used as transparent color.
---@param x1 number The x coordinate of the first triangle corner
---@param y1 number The y coordinate of the first triangle corner
---@param x2 number The x coordinate of the second triangle corner
---@param y2 number The y coordinate of the second triangle corner
---@param x3 number The x coordinate of the third triangle corner
---@param y3 number The y coordinate of the third triangle corner
---@param u1 number The U coordinate of the first triangle corner
---@param v1 number The V coordinate of the first triangle corner
---@param u2 number The U coordinate of the second triangle corner
---@param v2 number The V coordinate of the second triangle corner
---@param u3 number The U coordinate of the third triangle corner
---@param v3 number The V coordinate of the third triangle corner
---@param texsrc number? if 0 (default), the triangle's texture is read from SPRITES RAM. If 1, the texture comes from the MAP RAM.
---@param chromakey number? index (or array of indexes 0.80) of the color(s) that will be used as transparent
---@param z1 number? depth parameters for texture correction
---@param z2 number? depth parameters for texture correction
---@param z3 number? depth parameters for texture correction
function ttri(x1, y1, x2, y2, x3, y3, u1, v1, u2, v2, u3, v3, texsrc, chromakey, z1, z2, z3) end

43
utils/event_bus.lua Normal file
View File

@ -0,0 +1,43 @@
--- @class EventBus
EventBus = {
_handlers = {},
new = function(self, o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end,
---
---@param self EventBus
---@param event any
---@param handler any
---@param index any
on = function(self, event, handler, index)
if not self._handlers[event] then self._handlers[event] = {} end
self:off(event, handler)
if not index then
table.insert(self._handlers[event], handler)
else
-- insertIntoTable(this.handlers[event], handler, index)
end
end,
---
---@param self EventBus
---@param event any
---@param handler any
off = function(self, event, handler)
if not self._handlers[event] then return end
self._handlers[event] = table.filter(self._handlers[event], function(o) return o ~= handler end)
end,
---
---@param self EventBus
---@param event any
---@param ... unknown
emit = function(self, event, ...)
trace("[EventBus] Emitted " .. event)
if not self._handlers[event] then return end
for k, handler in pairs(self._handlers[event]) do
handler(...)
end
end
}

40
utils/input.lua Normal file
View File

@ -0,0 +1,40 @@
-- manage clicks, since tic-80 only returns a "pressed" state
Input = {
--- @type boolean
lclick_prev_state = false,
--- @type 'none'|'pressed'|'released'
lclick_state = "none"
}
function Input.update_mouse(self)
local x, y, left = mouse()
-- left btn down
if left then
-- was not pressed before
if not self.lclick_prev_state then
self.lclick_state = "pressed"
self.lclick_prev_state = true
-- was already pressed
elseif self.lclick_prev_state then
self.lclick_state = "none"
end
-- left btn up
elseif not left then
if self.lclick_prev_state then
self.lclick_state = "released"
self.lclick_prev_state = false
elseif not self.lclick_prev_state then
self.lclick_state = "none"
end
end
end
function Input.mouse_pressed(self)
return self.lclick_state == "pressed"
end
function Input.mouse_released(self)
return self.lclick_state == "released"
end

98
utils/rendering.lua Normal file
View File

@ -0,0 +1,98 @@
function draw9box(corner, edge, x, y, width, height)
-- edges
for i = 8, width - 9, 8 do
spr(edge, x + i, y, 0) -- top
spr(edge, x + i, y + height - 8, 0, 1, 0, 2) -- bottom
end
for i = 8, height - 8,8 do
spr(edge, x, y + i, 0, 1, 0, 3) -- left
spr(edge, x + width - 8, y + i, 0, 1, 0, 1) -- right
end
-- corners
spr(corner, x, y, 0) -- top left
spr(corner, x + width - 8, y, 0, 1, 0, 1) -- top right
spr(corner, x, y + height - 8, 0, 1, 0, 3) -- bottom left
spr(corner, x + width - 8, y + height - 8, 0, 1, 0, 2) -- bottom right
end
local function rot(x, y, rad)
local sa = math.sin(rad)
local ca = math.cos(rad)
return x * ca - y * sa, x * sa + y * ca
end
--- Draw a sprite using two textured triangles.
--- Apply affine transformations: scale, shear, rotate, flip
--- https://cxong.github.io/tic-80-examples/affine-sprites
---comment
---@param id number
---@param x number
---@param y number
---@param colorkey? number
---@param sx? number scale x
---@param sy? number scale y
---@param flip? number
---@param rotate? number
---@param w? number
---@param h? number
---@param ox? number
---@param oy? number
---@param shx1? number
---@param shy1? number
---@param shx2? number
---@param shy2? number
function aspr(
id, x, y, colorkey, sx, sy, flip, rotate, w, h,
ox, oy, shx1, shy1, shx2, shy2
)
colorkey = colorkey or -1
sx = sx or 1
sy = sy or 1
flip = flip or 0
rotate = math.rad(rotate or 0)
w = w or 1
h = h or 1
ox = ox or w * 8 // 2
oy = oy or h * 8 // 2
shx1 = shx1 or 0
shy1 = shy1 or 0
shx2 = shx2 or 0
shy2 = shy2 or 0
-- scale / flip
if flip % 2 == 1 then
sx = -sx
end
if flip >= 2 then
sy = -sy
end
ox = ox * -sx
oy = oy * -sy
-- shear / rotate
shx1 = shx1 * -sx
shy1 = shy1 * -sy
shx2 = shx2 * -sx
shy2 = shy2 * -sy
local rx1, ry1 = rot(ox + shx1, oy + shy1, rotate)
local rx2, ry2 = rot(((w * 8) * sx) + ox + shx1, oy + shy2, rotate)
local rx3, ry3 = rot(ox + shx2, ((h * 8) * sy) + oy + shy1, rotate)
local rx4, ry4 = rot(((w * 8) * sx) + ox + shx2, ((h * 8) * sy) + oy + shy2, rotate)
local x1 = x + rx1
local y1 = y + ry1
local x2 = x + rx2
local y2 = y + ry2
local x3 = x + rx3
local y3 = y + ry3
local x4 = x + rx4
local y4 = y + ry4
-- UV coords
local u1 = (id % 16) * 8
local v1 = id // 16 * 8
local u2 = u1 + w * 8
local v2 = v1 + h * 8
ttri(x1, y1, x2, y2, x3, y3, u1, v1, u2, v1, u1, v2, 0, colorkey)
ttri(x3, y3, x4, y4, x2, y2, u1, v2, u2, v2, u2, v1, 0, colorkey)
end

65
utils/tables.lua Normal file
View File

@ -0,0 +1,65 @@
--- Finds the first value in a table that satisfies a condition
---@param table any
---@param cb any @function(v, i) return boolean end
---@return unknown @value or nil
---@return unknown @index or nil
function table.find(table, cb)
for i, v in pairs(table) do
if cb(v) then
return v, i
end
end
return nil, nil
end
function table.filter(table, cb)
local new = {}
for _, v in pairs(table) do
if cb(v) then
table.insert(new, v)
end
end
return new
end
function table.remove_item(list, item)
for i, v in pairs(list) do
if v == item then
table.remove(list, i)
return
end
end
end
---needle can be a value or a function
---@param list table
---@param needle function|number
---@return integer
function table.count(list, needle)
local count = 0
for _, v in pairs(list) do
if type(needle) == "function" and needle(v) or v == needle then
count = count + 1
end
end
return count
end
function table.max(list, cb)
local max = -math.huge
for _, v in pairs(list) do
local value = cb(v)
if value > max then
max = value
end
end
return max
end
function math.randomitem(tbl)
return tbl[math.random(#tbl)]
end
function table.random(tbl)
return tbl[math.random(#tbl)]
end

400
utils/tween.lua Normal file
View File

@ -0,0 +1,400 @@
-- https://github.com/kikito/tween.lua
--[[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
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.
]]
-- Modified by Simon Cambier to better fit TIC-80 restrictions
---@return Tween
function require_tween()
-- easing
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
-- For all easing functions:
-- t = time == how much time has to pass for the tweening to complete
-- b = begin == starting property value
-- c = change == ending - beginning
-- d = duration == running time. How much time has passed *right now*
---@class Tween
local Tween = {}
local Tween_mt = { __index = Tween }
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
-- linear
local function linear(t, b, c, d) return c * t / d + b end
-- quad
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 2) + b end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
local function outInQuad(t, b, c, d)
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
-- cubic
local function inCubic(t, b, c, d) return c * pow(t / d, 3) + b end
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * t * t * t + b end
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
local function outInCubic(t, b, c, d)
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
-- quart
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 4) + b end
return -c / 2 * (pow(t - 2, 4) - 2) + b
end
local function outInQuart(t, b, c, d)
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
-- quint
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 5) + b end
return c / 2 * (pow(t - 2, 5) + 2) + b
end
local function outInQuint(t, b, c, d)
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
-- sine
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
local function outInSine(t, b, c, d)
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
return inSine((t * 2) - d, b + c / 2, c / 2, d)
end
-- expo
local function inExpo(t, b, c, d)
if t == 0 then return b end
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
local function outExpo(t, b, c, d)
if t == d then return b + c end
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
end
local function outInExpo(t, b, c, d)
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
-- circ
local function inCirc(t, b, c, d) return (-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
local function outCirc(t, b, c, d) return (c * sqrt(1 - pow(t / d - 1, 2)) + b) end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
local function outInCirc(t, b, c, d)
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
-- elastic
local function calculatePAS(p, a, c, d)
p, a = p or d * 0.3, a or 0
if a < abs(c) then return p, c, p / 4 end -- p, a, s
return p, a, p / (2 * pi) * asin(c / a) -- p,a,s
end
local function inElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p, a, s = calculatePAS(p, a, c, d)
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
local function outElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p, a, s = calculatePAS(p, a, c, d)
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
local function inOutElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
p, a, s = calculatePAS(p, a, c, d)
t = t - 1
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) * 0.5 + c + b
end
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
-- back
local function inBack(t, b, c, d, s)
s = s or 1.70158
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
s = s or 1.70158
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
s = (s or 1.70158) * 1.525
t = t / d * 2
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
-- bounce
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
if t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
end
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
local function inOutBounce(t, b, c, d)
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
local function outInBounce(t, b, c, d)
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
Tween.easing = {
linear = linear,
inQuad = inQuad,
outQuad = outQuad,
inOutQuad = inOutQuad,
outInQuad = outInQuad,
inCubic = inCubic,
outCubic = outCubic,
inOutCubic = inOutCubic,
outInCubic = outInCubic,
inQuart = inQuart,
outQuart = outQuart,
inOutQuart = inOutQuart,
outInQuart = outInQuart,
inQuint = inQuint,
outQuint = outQuint,
inOutQuint = inOutQuint,
outInQuint = outInQuint,
inSine = inSine,
outSine = outSine,
inOutSine = inOutSine,
outInSine = outInSine,
inExpo = inExpo,
outExpo = outExpo,
inOutExpo = inOutExpo,
outInExpo = outInExpo,
inCirc = inCirc,
outCirc = outCirc,
inOutCirc = inOutCirc,
outInCirc = outInCirc,
inElastic = inElastic,
outElastic = outElastic,
inOutElastic = inOutElastic,
outInElastic = outInElastic,
inBack = inBack,
outBack = outBack,
inOutBack = inOutBack,
outInBack = outInBack,
inBounce = inBounce,
outBounce = outBounce,
inOutBounce = inOutBounce,
outInBounce = outInBounce
}
-- private stuff
local function copyTables(destination, keysTable, valuesTable)
valuesTable = valuesTable or keysTable
local mt = getmetatable(keysTable)
if mt and getmetatable(destination) == nil then
setmetatable(destination, mt)
end
for k, v in pairs(keysTable) do
if type(v) == "table" then
destination[k] = copyTables({}, v, valuesTable[k])
else
destination[k] = valuesTable[k]
end
end
return destination
end
local function checkSubjectAndTargetRecursively(subject, target, path)
path = path or {}
local targetType, newPath
for k, targetValue in pairs(target) do
targetType, newPath = type(targetValue), copyTables({}, path)
table.insert(newPath, tostring(k))
if targetType == "number" then
assert(type(subject[k]) == "number",
"Parameter '" .. table.concat(newPath, "/") .. "' is missing from subject or isn't a number")
elseif targetType == "table" then
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
else
assert(targetType == "number",
"Parameter '" .. table.concat(newPath, "/") .. "' must be a number or table of numbers")
end
end
end
local function checkNewParams(duration, subject, target, easing)
assert(type(duration) == "number" and duration > 0,
"duration must be a positive number. Was " .. tostring(duration))
local tsubject = type(subject)
assert(tsubject == "table" or tsubject == "userdata",
"subject must be a table or userdata. Was " .. tostring(subject))
assert(type(target) == "table", "target must be a table. Was " .. tostring(target))
assert(type(easing) == "function", "easing must be a function. Was " .. tostring(easing))
checkSubjectAndTargetRecursively(subject, target)
end
local function getEasingFunction(easing)
easing = easing or "linear"
if type(easing) == "string" then
local name = easing
easing = Tween.easing[name]
if type(easing) ~= "function" then
error("The easing function name '" .. name .. "' is invalid")
end
end
return easing
end
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
local t, b, c, d
for k, v in pairs(target) do
if type(v) == "table" then
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
else
t, b, c, d = clock, initial[k], v - initial[k], duration
subject[k] = easing(t, b, c, d)
end
end
end
-- Tween methods
---@param clock number
---@return boolean # true if the tween has expired
function Tween:set(clock)
assert(type(clock) == "number", "clock must be a positive number or 0")
self.initial = self.initial or copyTables({}, self.target, self.subject)
self.clock = clock
if self.clock <= 0 then
self.clock = 0
copyTables(self.subject, self.initial)
elseif self.clock >= self.duration then -- the tween has expired
self.clock = self.duration
copyTables(self.subject, self.target)
else
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
end
return self.clock >= self.duration
end
function Tween:reset()
return self:set(0)
end
---@param dt number
---@return boolean # true if the tween has expired
function Tween:update(dt)
assert(type(dt) == "number", "dt must be a number")
return self:set(self.clock + dt)
end
-- Public interface
function Tween.new(duration, subject, target, easing)
easing = getEasingFunction(easing)
checkNewParams(duration, subject, target, easing)
return setmetatable({
duration = duration,
subject = subject,
target = target,
easing = easing,
clock = 0
}, Tween_mt)
end
return Tween
end

175
utils/utils.lua Normal file
View File

@ -0,0 +1,175 @@
include "globals"
include "utils.tables"
include "utils.rendering"
--- Print with outline
function print_border(text, x, y, color, outline, fixed, scale, smallfont)
local outline = outline == nil
and ((color == 0 or color == nil) and 12 or 0)
or outline
-- diagonals
print(text, x + 1, y + 1, outline, fixed, scale, smallfont)
print(text, x + 1, y - 1, outline, fixed, scale, smallfont)
print(text, x - 1, y + 1, outline, fixed, scale, smallfont)
print(text, x - 1, y - 1, outline, fixed, scale, smallfont)
-- cardinals
print(text, x + 1, y, outline, fixed, scale, smallfont)
print(text, x - 1, y, outline, fixed, scale, smallfont)
print(text, x, y + 1, outline, fixed, scale, smallfont)
print(text, x, y - 1, outline, fixed, scale, smallfont)
print(text, x, y, color, fixed, scale, smallfont)
end
--- Print debug
-- function printd(text, x, y)
-- print_border(text, x, y, 12, 2)
-- end
--- Like print(), but with font() and alignment
---@param txt string
---@param x number
---@param y number
---@param color number
---@param align? number
---@param scale? number
---@return number
function prn(txt, x, y, color, align, scale)
align = align or ALIGN.Left
scale = scale or 1
set1bpp()
if align == ALIGN.Right then
x = x - prn_len(txt, scale)
elseif align == ALIGN.Center then
x = x - prn_len(txt, scale) / 2
end
if color ~= nil then
swap_color(1, color)
end
local len = font(txt, x, y, 0, 4, 0, false, scale)
swap_color(1, 1)
set4bpp()
return len
end
---Like prn() but with an outline
---@param any string
---@param x number
---@param y number
---@param color number
---@param align? number
---@param border_color? number
---@param scale? number
---@return number
function prn_border(txt, x, y, color, align, border_color, scale)
if not border_color then
return prn(txt, x, y, color, align, scale)
end
align = align or ALIGN.Left
border_color = border_color or 0
scale = scale or 1
-- diagonals
prn(txt, x + 1, y + 1, border_color, align, scale)
prn(txt, x + 1, y - 1, border_color, align, scale)
prn(txt, x - 1, y + 1, border_color, align, scale)
prn(txt, x - 1, y - 1, border_color, align, scale)
-- cardinals
prn(txt, x + 1, y, border_color, align, scale)
prn(txt, x - 1, y, border_color, align, scale)
prn(txt, x, y + 1, border_color, align, scale)
prn(txt, x, y - 1, border_color, align, scale)
return prn(txt, x, y, color, align, scale)
end
---like prn_border() but with floaty text
---@param txt any
---@param x any
---@param y any
---@param color any
---@param align any
---@param border_color? any
---@param scale? any
function prn_border_floaty(txt, x, y, color, align, border_color, scale, delay)
delay = delay or 0
align = align or ALIGN.Left
border_color = border_color or 0
local len = prn_len(txt, scale)
prn_len(txt, scale)
if align == ALIGN.Right then
x = x - len
elseif align == ALIGN.Center then
x = x - len / 2
end
for i = 1, #txt do
local c = txt:sub(i, i)
local y = y + math.sin(((i+delay) * 100 + time()) / 200) * 3
x = x + prn_border(c, x, y, color, ALIGN.Left, border_color, scale)
end
return len
end
function swap_color(index, color)
poke4(PALETTE_MAP * 2 + index, color)
end
function reset_colors(...)
args = { ... }
if #args > 0 then
for _, c in ipairs(args) do
swap_color(c, c)
end
else
for i = 0, 16 do
poke4(PALETTE_MAP * 2 + i, i)
end
end
end
local _cachedPrintLen = {}
function print_len(txt)
if _cachedPrintLen[txt] then return _cachedPrintLen[txt] end
local len = print(txt, -1000, -1000)
_cachedPrintLen[txt] = len
return len
end
local _cachedFontLen = {}
function prn_len(txt, scale)
scale = scale or 1
if scale == 0 then scale = 1 end
local bpp = peek4(2 * 0x3ffc) -- get the current bpp value to reset it after
set1bpp()
if _cachedFontLen[txt] then return _cachedFontLen[txt] end
local len = font(txt, -1000, -1000, 0, 4, 0, false, scale)
_cachedFontLen[txt] = len
poke4(2 * 0x3ffc, bpp)
return len
end
function set1bpp()
poke4(2 * 0x3ffc, 8) -- 0b1000
end
function set4bpp()
poke4(2 * 0x3ffc, 2) -- 0b0010
end
function point_in_rect(x, y, rx, ry, rw, rh)
return x >= rx and x <= rx + rw and y >= ry and y <= ry + rh
end
function mouse_in_rect(rect)
local mx, my = mouse()
return point_in_rect(mx, my, rect.x, rect.y, rect.width, rect.height)
end
function clamp(min, val, max)
return math.max(math.min(max, val), min)
end
function normalize(val, min, max)
min = min or 0
max = max or val
return (val - min) / (max - min)
end