commit 7c537e3c4f85885588b32595731e961d0e85ed1d Author: Simon Cambier Date: Sat Aug 12 11:36:14 2023 +0200 First commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..42734d9 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa72e25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build.lua +html.zip +html/ diff --git a/.local/02fd0eb/config.tic b/.local/02fd0eb/config.tic new file mode 100644 index 0000000..bd52d41 Binary files /dev/null and b/.local/02fd0eb/config.tic differ diff --git a/.local/02fd0eb/default_lua.tic b/.local/02fd0eb/default_lua.tic new file mode 100644 index 0000000..32424d5 Binary files /dev/null and b/.local/02fd0eb/default_lua.tic differ diff --git a/.local/31ea041/config.tic b/.local/31ea041/config.tic new file mode 100644 index 0000000..bd52d41 Binary files /dev/null and b/.local/31ea041/config.tic differ diff --git a/.local/31ea041/default_lua.tic b/.local/31ea041/default_lua.tic new file mode 100644 index 0000000..32424d5 Binary files /dev/null and b/.local/31ea041/default_lua.tic differ diff --git a/.local/31ea041/options.dat b/.local/31ea041/options.dat new file mode 100644 index 0000000..cc30286 Binary files /dev/null and b/.local/31ea041/options.dat differ diff --git a/.local/ffd6965/config.tic b/.local/ffd6965/config.tic new file mode 100644 index 0000000..bd52d41 Binary files /dev/null and b/.local/ffd6965/config.tic differ diff --git a/.local/ffd6965/default_lua.tic b/.local/ffd6965/default_lua.tic new file mode 100644 index 0000000..32424d5 Binary files /dev/null and b/.local/ffd6965/default_lua.tic differ diff --git a/.local/ffd6965/options.dat b/.local/ffd6965/options.dat new file mode 100644 index 0000000..edfc55b Binary files /dev/null and b/.local/ffd6965/options.dat differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d3958d6 --- /dev/null +++ b/.vscode/settings.json @@ -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 + } +} \ No newline at end of file diff --git a/ai.lua b/ai.lua new file mode 100644 index 0000000..dabc0be --- /dev/null +++ b/ai.lua @@ -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 diff --git a/board.lua b/board.lua new file mode 100644 index 0000000..eef72df --- /dev/null +++ b/board.lua @@ -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 diff --git a/classes/die.lua b/classes/die.lua new file mode 100644 index 0000000..3782ac6 --- /dev/null +++ b/classes/die.lua @@ -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 diff --git a/coroutines.lua b/coroutines.lua new file mode 100644 index 0000000..0207153 --- /dev/null +++ b/coroutines.lua @@ -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 diff --git a/dev.bat b/dev.bat new file mode 100644 index 0000000..076d93b --- /dev/null +++ b/dev.bat @@ -0,0 +1 @@ +..\tq-bundler.exe run game.lua main.lua --tic=..\tic80.exe \ No newline at end of file diff --git a/events.lua b/events.lua new file mode 100644 index 0000000..a6cd76c --- /dev/null +++ b/events.lua @@ -0,0 +1,4 @@ +EVENT_CHANGE_TURN = "change_turn" +EVENT_SET_STEP = "set_step" +EVENT_REMOVE_DIE = "remove_die" +EVENT_RESET_DIE = "reset_die" diff --git a/game.lua b/game.lua new file mode 100644 index 0000000..14c725e --- /dev/null +++ b/game.lua @@ -0,0 +1,2523 @@ +-- +-- Bundle file +-- Code changes will be overwritten +-- + +-- 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 + +-- [TQ-Bundler: utils.utils] + +-- [TQ-Bundler: globals] + +-- [TQ-Bundler: events] + +EVENT_CHANGE_TURN = "change_turn" +EVENT_SET_STEP = "set_step" +EVENT_REMOVE_DIE = "remove_die" +EVENT_RESET_DIE = "reset_die" + + +-- [/TQ-Bundler: 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 +} + + +-- [/TQ-Bundler: globals] + +-- [TQ-Bundler: utils.tables] + +--- 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 + +-- [/TQ-Bundler: utils.tables] + +-- [TQ-Bundler: utils.rendering] + +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 + +-- [/TQ-Bundler: 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 + + +-- [/TQ-Bundler: utils.utils] + +-- [TQ-Bundler: utils.input] + +-- 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 + + +-- [/TQ-Bundler: utils.input] + +-- [TQ-Bundler: utils.tween] + +-- 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 + + +-- [/TQ-Bundler: utils.tween] + +-- [TQ-Bundler: coroutines] + +-- +-- 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 + + +-- [/TQ-Bundler: coroutines] + +-- [TQ-Bundler: states.state_manager] + +--- 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, +} + + +-- [/TQ-Bundler: states.state_manager] + +-- [TQ-Bundler: states.state_main_menu] + +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 + + +-- [/TQ-Bundler: states.state_main_menu] + +-- [TQ-Bundler: states.state_game] + +-- [TQ-Bundler: board] + +---@diagnostic disable: lowercase-global +-- [TQ-Bundler: scoring] + +function get_total_score(scores) + local sum = 0 + for col = 1, 3 do + local value = scores[col] + sum = sum + value + end + return sum +end + + +-- [/TQ-Bundler: 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 + + +-- [/TQ-Bundler: board] + +-- [TQ-Bundler: classes.die] + +--- @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 + + +-- [/TQ-Bundler: classes.die] + +-- [TQ-Bundler: states.game.loading] + +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 + + +-- [/TQ-Bundler: states.game.loading] + +-- [TQ-Bundler: states.game.rolling] + +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 + + +-- [/TQ-Bundler: states.game.rolling] + +-- [TQ-Bundler: states.game.placing] + +-- [TQ-Bundler: ai] + +--[[ +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 + + +-- [/TQ-Bundler: 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 + + +-- [/TQ-Bundler: states.game.placing] + +-- [TQ-Bundler: states.game.scoring] + +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 + + +-- [/TQ-Bundler: states.game.scoring] + +-- [TQ-Bundler: states.game.gameover] + +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 + + +-- [/TQ-Bundler: states.game.gameover] + +-- [TQ-Bundler: utils.event_bus] + +--- @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 +} + + +-- [/TQ-Bundler: 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 + + +-- [/TQ-Bundler: 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 + +-- +-- 001:0cccccccccccccccccdcccccccccccccccccdccccccccdccccccccc8cccccc88 +-- 002:cccccce0cccccccecccccdcecccccccecccdccceccdcccce8cccccce88ccccce +-- 003:0ccccccccccccccccccccccccccddcccccdccdccccccccdcccc88ccccc8888cc +-- 004:cccccce0cccccccecccccccecccddcceccdccdcecdccccceccc88ccecc8888ce +-- 005:0cccccccccccccddccc88ccccc8888cccc8888cdccc88ccccdccccc8cdccdc88 +-- 006:cccccce0ccccdccedccccdcecccdccceddccccceccdcccce8ccdccce88cdcdce +-- 007:0cccccccccccccdcccc88ccdcc8888cccc8888ccccc88ccccccccccdcdcccddc +-- 008:cccccce0cdcccccedcc88ccecc8888cecc8888ceccc88ccedccccccecddcccde +-- 009:0ccdddccccccccdcccc88ccdcc8888cccc8888ccccc88cdccccccdc8cccccc88 +-- 010:cccccce0ccccccceccc88ccecc8888cecc8888cecdc88cce8cdcccde88cccdce +-- 011:0cccccccccc88ccccc8888cccc8888ccccc88cccccccccdcccc88ccdcc8888cd +-- 012:cccccce0ccc88ccedc8888cedc8888cedcc88ccecdccccceccc88ccecc8888ce +-- 017:cdcddc88ccccccc8cccccdccccccdcccccccccccccdccccceccccccc0eeeeeee +-- 018:88cddcde8cccccceccdccccecccdcccecccccccecccccdcecccccceeeeeeeee0 +-- 019:cc8888ccccc88cccccccccdcccdccdcccccddccccccccccceccccccc0eeeeeee +-- 020:cc8888ceccc88ccecdccccceccdccdcecccddccecccccccecccccceeeeeeeee0 +-- 021:ccdcdc88ccccdcc8cccccdcccccdccddcdccccccccdccccdeccccccc0eeeeeee +-- 022:88cdccde8cccccdeccc88ccedc8888cecc8888ceccc88cceddcccceeeeeeeee0 +-- 023:cdccccccccccccccccc88ccdcc8888cccc8888ccccc88ccdeccccccc0eeeeeee +-- 024:ccccccdecccccccedcc88ccecc8888cecc8888cedcc88ccecccccceeeeeeeee0 +-- 025:ccdccc88cdcccdc8dcc88cdcdc8888ccdc8888ccccc88ccceccccccc0eeeeeee +-- 026:88ccccce8cdccccecdc88ccecc8888cecc8888cedcc88ccecdcccceeeeeeeee0 +-- 027:cc8888ccccc88cccccccccdcccc88ccdcc8888cdcc8888cdecc88ccc0eeeeeee +-- 028:dc8888cedcc88ccecdccccceccc88ccecc8888cecc8888ceccc88ceeeeeeeee0 +-- 032:ccccccccc0000000c0000000c0000000c0000000c0000000c0000000c0000000 +-- 033:cccccccc00000000000000000000000000000000000000000000000000000000 +-- 034:ccccc0000000c0000000c0000000c0000000c0000000c0000000c0000000c000 +-- 035:000cc0c0000cccc00000cc00cc00cc00ccc3cc3c0cc3cc3ccc00cc000000cc00 +-- 036:00000000000000000000000000000000cccccccccccccccc0000000000000000 +-- 037:0cccccd0ccc22ccdcc2cc2cdccccc2cdcccc2ccdcccccccddccc2cdd0dddddd0 +-- 048:c0000000c0000000c0000000c0000000c0000000c0000000c0000000c0000000 +-- 050:0000c0000000c0000000c0000000c0000000c0000000c0000000c0000000c000 +-- 051:000ccc00000ccc00000ccc00cc3ccc3ccc3ccc3ccc3ccc3c000ccc00000ccc00 +-- 052:000000000000000000000000cccccccccccccccccccccccc0000000000000000 +-- 064:c0000000cccccccc000000000000000000000000000000000000000000000000 +-- 065:00000000cccccccc000000000000000000000000000000000000000000000000 +-- 066:0000c000ccccc000000000000000000000000000000000000000000000000000 +-- + +-- +-- 000:00c3c3000024e763005abdf70018fff7005affe30099bdc10024668000c3c300 +-- 001:80808000c1c1c100e3a2e300f7f7f7c0e3a2f7c0c180800080c1c10000000000 +-- 002:ffc13e0fff22dd0cff14eb0a7e14ebe97e14eb11ff22dd11ffc13e11ff00ffe0 +-- 003:c1c0c380224142a22242c3c122424277c14142c1804072a2e370b38080308100 +-- 016:100880f3300c8021700e8021f00fe321700e8071300c8001100880a000008040 +-- 017:e3c100405120004051c000e06121c3404121c3e041c000404101004000e00000 +-- 018:ff000c30ff000c3000000c3000000c3000000c3000000c3000ff0c3000ff0c30 +-- 019:000000f7804000e380a000c1ff11008080a080008040c1000000e3000000f700 +-- 032:0080414100804141008041e300800041000000e3000000410080004100000000 +-- 033:80604001c362a080a0014040c180a20082402100e12323008003c00000000000 +-- 034:014080008080a2804001c180400180e34001c1808080a2800140800000000000 +-- 035:00000000000000800000008000c30040000000408000c0208000c02040000000 +-- 048:c180c1c122c0222223a00202a28001816280c00222802022c1e3e3c100000000 +-- 049:01e381e38120402241e020012101e180e30222800101228001e0c18000000000 +-- 050:c1c100002222000022228080c1c300002202000022018080c1c0008000000040 +-- 051:030060c18100c022c0e3810260000301c0e381808100c0000300608000000000 +-- 064:c180e1812241424202224220c222c120a2e34220a2224242c122e18100000000 +-- 065:e0e3e3c1412020224220202042e1e1a34220202241202022e0e320c100000000 +-- 066:22c1832222800121228001a0e3800160228021a02280212122c1c02200000000 +-- 067:202222c12063622220a2622220a2a2222022232220222322e32222c100000000 +-- 080:e1c1e1c12222222222222220e122e1c120a2a0022021212220c222c100000000 +-- 081:e32222228022222280222222802222a2802241a28022416380c1802200000000 +-- 082:2222e3c1222202404122014080c1804041804040228020402280e3c100000000 +-- 083:00c18000000141002001220040010000800100000101000002c100e300000000 +-- 096:400020008000200001c1a1c10002622200c322200022622200c3a1c100000000 +-- 097:0200010002008200c2c180c22322e32322e38023232080c2c2c18002000000c1 +-- 098:008001404000004040c08142c1800141428001c04280014142c121420000c000 +-- 099:c0000000800000008061a1c180a2622280a2222280a22222c1a222c100000000 +-- 112:0000000000000000a1c2a1c362236220622320e1a1c22002200220e120020000 +-- 113:4000000040000000e1212222402122a2402122a2422141a281c2804100000000 +-- 114:00000003000000802222e380412201408023808041c240802202e30300c10000 +-- 115:806040008080a2008080018000010041808000e3808000008060000000000000 +-- 128:c1210180220080412000c1c1202122022221e3c3c121202280c2c1c3c0000000 +-- 129:42808000000100c1c1c1c12002020220c3c3c320222222c1c3c3c301000000c0 +-- 130:8041804141000100c1c1c100222222c0e3e3e38020202080c1c1c1c100000000 +-- 131:804041804180000000008080c0c04141808022228080e3e3c1c1222200000000 +-- 144:01008fc080004121e3c62100200927c0e1cfe12120212121e3ce2fc000000000 +-- 145:214040400080a08000000000c0c0a0a02121a0a02121a0a0c0c0414100000000 +-- 146:214141800000008021c122c321222220a1222220412222c301c1c180e0000080 +-- 147:032260038441a0808080a080e3e362e380802780e8e322804780228000002640 +-- 160:0101808080804040c100000002c0c021c380212122802121c3c1c0c200000000 +-- 161:41820000a041c1c000000221a062c32161a222212123c3c0212200000000e3e1 +-- 162:80000024000000228000002140f1f1a22010014522100124c100000200000007 +-- 163:248000002200000021008421a280424243802184a28042428780842102000000 +-- 176:88ccee8044ccdd802233bb801133778088ccee8044ccdd802233bb8011337780 +-- 177:808041008080410080f04100f08071f180f04141808041418080414180804141 +-- 178:0041410000414100f07141f180014101f0714171804141418041414180414141 +-- 179:41418000414180007141f00001f180f0f100f080000000800000008000000080 +-- 192:8080008080800080808000808fffff8f00008080000080800000808000008080 +-- 193:008080410080804100808f41ffff804f00808f41008080410080804100808041 +-- 194:82004100820041008ecf7fff804000008f4fff7f004100410041004100410041 +-- 195:41004180410041804fff7fff400000004fff7fff410041004100410041004100 +-- 208:410000434100004141ff0041ff00ffcf00ff4100008041000080410000804100 +-- 209:80000041800000418f8f00418080cfff8f8f4141008041410080414100804141 +-- 210:808000ff808000ffff8000ff80f08fffff0080ff800080ff800080ff800080ff +-- 211:00f00fff00f00fff00f00fff00f00ffffff00f00fff00f00fff00f00fff00f00 +-- 224:0081e3e300422241c242204121c120412142204121422041c2c1202300600000 +-- 225:e30000002200414140c341a080214180402141802221c280e3c0208000002000 +-- 226:e380c18180412240c1222240a2e32280a2224141c141414180806341e3000080 +-- 227:000283c100c1402200a2202241a2e322a2a22022a2c140224120832200000000 +-- 240:00806003e38081c000e30220e38081c000806003e300000000e3e3e300000000 +-- 241:0180000082808041808000a08080e30080800041808080a080a0000080400000 +-- 242:0000008f0381008084c3008084c30080038181a0000000c00000008000000000 +-- 243:a040000041a08300418083004140830000e08300000083000000830000000000 +-- + +-- +-- 000:00000000ffffffff00000000ffffffff +-- 001:0123456789abcdeffedcba9876543210 +-- 002:0123456789abcdef0123456789abcdef +-- + +-- +-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000 +-- + +-- +-- 000:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 001:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 002:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 003:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 004:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 005:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 006:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 007:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 008:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 010:000000000000000000000000000000000000000000000000cc000000cc00cc000000cc00cc000000cc000000cccc0000cc000000cc00cc0000000000cccccccccc00cccccccc000000cccccc0000cc000000cc00cccccccccc0000cccccc0000000000000000000000000000000000000000000000000000 +-- 011:000000000000000000000000000000000000000000000000cc000000cc00cc000000cc00cc000000cc000000cccc0000cc000000cc00cc0000000000cccccccccc00cccccccc000000cccccc0000cc000000cc00cccccccccc0000cccccc0000000000000000000000000000000000000000000000000000 +-- 012:000000000000000000000000000000000000000000000000cc0000cc0000cccc0000cc00cc000000cc0000cc0000cc00cc0000cc0000cc0000000000cc000000000000cc0000cc00cc000000cc00cccc0000cc00cc0000000000cc000000cc00000000000000000000000000000000000000000000000000 +-- 013:000000000000000000000000000000000000000000000000cc0000cc0000cccc0000cc00cc000000cc0000cc0000cc00cc0000cc0000cc0000000000cc000000000000cc0000cc00cc000000cc00cccc0000cc00cc0000000000cc000000cc00000000000000000000000000000000000000000000000000 +-- 014:000000000000000000000000000000000000000000000000cc00cc000000cccc0000cc00cc000000cc00cc0000000000cc00cc000000cc0000000000cc000000000000cc0000cc00cc000000cc00cccc0000cc00cc0000000000cc0000000000000000000000000000000000000000000000000000000000 +-- 015:000000000000000000000000000000000000000000000000cc00cc000000cccc0000cc00cc000000cc00cc0000000000cc00cc000000cc0000000000cc000000000000cc0000cc00cc000000cc00cccc0000cc00cc0000000000cc0000000000000000000000000000000000000000000000000000000000 +-- 016:000000000000000000000000000000000000000000000000cccc00000000cc00cc00cc00cc000000cc00cc0000000000cccc00000000cc0000000000cccccccc000000cccccc0000cc000000cc00cc00cc00cc00cccccccc000000cccccc0000000000000000000000000000000000000000000000000000 +-- 017:000000000000000000000000000000000000000000000000cccc00000000cc00cc00cc00cc000000cc00cc0000000000cccc00000000cc0000000000cccccccc000000cccccc0000cc000000cc00cc00cc00cc00cccccccc000000cccccc0000000000000000000000000000000000000000000000000000 +-- 018:000000000000000000000000000000000000000000000000cc00cc000000cc0000cccc00cc000000cc00cc0000000000cc00cc000000cc0000000000cc000000000000cc0000cc00cc000000cc00cc0000cccc00cc000000000000000000cc00000000000000000000000000000000000000000000000000 +-- 019:000000000000000000000000000000000000000000000000cc00cc000000cc0000cccc00cc000000cc00cc0000000000cc00cc000000cc0000000000cc000000000000cc0000cc00cc000000cc00cc0000cccc00cc000000000000000000cc00000000000000000000000000000000000000000000000000 +-- 020:000000000000000000000000000000000000000000000000cc0000cc0000cc0000cccc00cc000000cc0000cc0000cc00cc0000cc0000cc0000000000cc000000000000cc0000cc00cc000000cc00cc0000cccc00cc0000000000cc000000cc00000000000000000000000000000000000000000000000000 +-- 021:000000000000000000000000000000000000000000000000cc0000cc0000cc0000cccc00cc000000cc0000cc0000cc00cc0000cc0000cc0000000000cc000000000000cc0000cc00cc000000cc00cc0000cccc00cc0000000000cc000000cc00000000000000000000000000000000000000000000000000 +-- 022:000000000000000000000000000000000000000000000000cc000000cc00cc000000cc0000cccccc00000000cccc0000cc000000cc00cccccccccc00cccccccccc00cccccccc000000cccccc0000cc00f000cc00cccccccccc0000cccccc0000000000000000000000000000000000000000000000000000 +-- 023:000000000000000000000000000000000000000000000000cc000000cc00cc000000cc0000cccccc00000000cccc0000cc000000cc00cccccccccc00cccccccccc00cccccccc000000cccccc0000cc0ffff0cc00cccccccccc0000cccccc0000000000000000000000000000000000000000000000000000 +-- 024:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 025:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000fffffff00000000000000000000000000000000000000000000000000000000000000000000 +-- 026:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000ffffffff0000000000000000000000000000000000000000000000000000000000000000000 +-- 027:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0fffffffff0000000000000000000000000000000000000000000000000000000000000000000 +-- 028:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000 +-- 029:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000ffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000 +-- 030:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff00000000000000000000000000000000000000000000000fffffffff0ffffffff00000000000000000000000000000000000000000000000000000000000000000 +-- 031:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffff0000000000000000000000000000000000000000000000ffffffff0000ffffff00000000000000000000000000000000000000000000000000000000000000000 +-- 032:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0ffff0000000000000000000000000000000000000000000000fffffff0000ffffff00000000000000000000000000000000000000000000000000000000000000000 +-- 033:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff000ffff00000000000000000000000000000000000000000000fffffff0000ffff0000000000000000000000000000000000000000000000000000000000000000000 +-- 034:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff000fffff00000000000000000000000000000000000000000000fffffffffffff00000000000000000000000000000000000000000000000000000000000000000000 +-- 035:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffff000ffffff0000000000000000000000000000000000000000000fffffffffff0000000000000000000000000000000000000000000000000000000000000000000000 +-- 036:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffff0000000000000000000000000000000000000000000ffffffff000000000000000000000000000000000000000000000000000000000000000000000000 +-- 037:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffff000000000000000000000000000000000000000000fffff00000000000000000000000000000000000000000000000000000000000000000000000000 +-- 038:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00fff0000ff000000000000000000000000000000000000000000fff0000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 039:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff000ff0000ff000ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 040:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff000ff0000ff000fff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 041:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000fff00ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 042:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 043:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffff0000000000000000000000000000000000088888800000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 044:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff000fffff000000000000000000000000000000000008d8dd880000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 045:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffff000ffff0000000000000000000000000000000000008dd88d88888800000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 046:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff000fff00000000000000000000000000000000000008d888888ddd888808880000000000000008880000000000000000000000000000000000000000000000000000 +-- 047:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0ff000000000000000000000000000000000000008d80008d888d8d888d88888800000000008d80000000000000000000000000000000000000000000000000000 +-- 048:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffff0000000000000000000000000000000000000008d80008ddddd8d8d8d88ddd888888808888d80000000000000000000000000000000000000000000000000000 +-- 049:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0000000000000000000000000000000000000008880008d88888d8d8d88888d8d8dd888dd8d80000000000000000000000000000ffffffffffffff0000000000 +-- 050:00000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000d0000000000f000000000000000000000000000000000000d000000000088ddd88d8d8d88dddd8dd88d8d88dd8000000000000000000000000000ffffffffffffffff000000000 +-- 051:0000000000000000000000000000000000000000000000000000d0d000000000000000000000000000000000000000000d0d0000000000000000000011100000000000000000000000d000000000008888888d8d88d888d8d88888d888d8000000000000000000000000000fff00fffffffffff000000000 +-- 052:000000000000000000000000000000000000000000000000000d000d000000dd0d00ddd00dd0d000ddd0000000ddd0000d00000000000000000000001d100000000ddd00d0dd000dd0d000000000000000008888888dddd8d80008d88dd8000000000000000000000000000ff0000ffffffffff000000000 +-- 053:000000000000000000000000000000000000000000000000000d000d00000d00dd00000d0d0d0d0d000d00000d000d0ddddd000000000001110111111d111100000000d0dd00d0d00dd00000000000000000000000888888d800088dd8d8000000000000000000000000000ff0000ffffffffff000000000 +-- 054:000000000000000000000000000000000000000000000000000ddddd00000d00dd00dddd0d0d0d0ddddd00000d000d000d00000000000001d111dddd1d11d100000dddd0d000d0d000d00000000000000000000000000008880000888888000000000000000000000000000fff00fffffffffff000000000 +-- 055:000000000000000000000000000000000000000000000000000d000d000000dd0d0d000d0d0d0d0d000000000d000d000d00000000000011111d11111d1d110000d000d0d000d0d00dd00000000000000000000000000000000000000000000000000000000000000000000fffffff00fffffff000000000 +-- 056:000000000000000000000000000000000000000000000000000d000d000000000d00dddd0d0d0d00ddd0000000ddd0000d0000001111111dd11dddd11dd11000000dddd0d000d00dd0d00000000000000000000000000000000000000000000000000000000000000000000ffffff0000ffffff000000000 +-- 057:00000000000000000000000000000000000000000000000000000000000000ddd0000000000000000000000000000000000000001d1dd111d111111d1d1d1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000ffffff000000000 +-- 058:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001dd11d11d11dddd11d11d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffff00fffffff000000000 +-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d111111d111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffff00fff000000000 +-- 060:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d10001ddd10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffff0000ff000000000 +-- 061:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d1000111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffff0000ff000000000 +-- 062:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffff00fff000000000 +-- 063:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff000000000 +-- 064:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff0000000000 +-- 073:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 074:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 075:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 076:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff0000ffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 077:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff0000ffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 078:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffff0fffffff0ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 079:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff000fff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 080:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff000ffffff0000ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 081:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff0000fffff0000ff000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 082:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff000fffffffffff000000000000000000000000000000000000ffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 083:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffff000ffff000000000000000000000000000000000000fffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 084:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000fffff0000fff0000000000000000000000000000000000000fff00ffffff0ffff00000000000000000000000000000000000000000000000000000000000000000000000000 +-- 085:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffffff000fff000000000000000000000000000000000000ff0000ffff000fff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 086:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff000ffffff00fff0000000000000000000000000000000000000fff000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 087:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0fffffff0fffff0000000000000000000000000000000000000fff00ffffff000ff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 088:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000fff00000000000000000000000000000000000000fffffff00fffffff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 089:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0000fff00000000000000000000000000000000000000ffffff0000ffffff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 090:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ff000000000000000000000000000000000000000ffffff0000ffffff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 091:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff000000000000000000000000000000000000000fffffff00fffffff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 092:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff0000000000000000000000000000000000000000ff000ffffff00fff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 093:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff000fff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 094:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff000ffff0000ff000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 095:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0ffffff00fff0000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 096:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 097:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 098:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 100:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dddd00dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 101:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000d00d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 102:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000d00d000ddd00d000d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 103:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dddd000d000000d0d000d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 104:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000000d000dddd0d00dd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffff +-- 105:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000000d00d000d00dd0d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 106:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d00000ddd00dddd00000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 107:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ddd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 108:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 109:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 110:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffff00fffff +-- 111:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000ffff +-- 112:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffff0000ffff +-- 113:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffff00fffff +-- 114:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 115:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 116:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 117:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 118:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff +-- 119:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffff +-- 123:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 124:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 125:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 126:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 127:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 128:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 129:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- 130:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff0000e000000e00000e000000000ee00ee0000000000e000000000000000e0e0eee0e0000000000ee00000ee000e0000000000e0000e00e000000000e00000000000e000e0e0 +-- 131:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff000e0e0e0e0000ee000ee00ee000e000e00e0e0000e00e0e00e00eee000e0e00e00ee000ee000e000e0e00e00eee0000e000e0000eee0ee000ee000e000ee00eee0ee00e0e0 +-- 132:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff000e0e0ee00e0e0e0e0e0e00ee00e000e00e0e000eee0ee00e0e0eee00000000e00e0e0e0e000e000e0e00e000e0000e0e0eee0000e00e0e0e0e000e0000ee0eee0e0e00000 +-- 133:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff000e0e0e000e0eee0e0e0e0e0e00e000e000ee0000e00e000e0e0e0e00000000e00e0e0ee0000e000e0e00e000e0000e0e00e00000e00e0e0ee0000e000e0e0e0e0e0e00000 +-- 134:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000fff00ffffff00fff0000e00e000e000e0e0e0e0eee0eee0eee000e0000e00e0000e00e0e00000000e00e0e00ee0000ee00ee0eee000e0000e000e000000e0e0e00ee000eee0eee0e0e0ee000000 +-- 135:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff0000ffff0000ff00000000000000e000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- + +-- +-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57 +-- + diff --git a/globals.lua b/globals.lua new file mode 100644 index 0000000..a7f5178 --- /dev/null +++ b/globals.lua @@ -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 +} diff --git a/kunckles.gif b/kunckles.gif new file mode 100644 index 0000000..89cdec6 Binary files /dev/null and b/kunckles.gif differ diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..82ca46e --- /dev/null +++ b/main.lua @@ -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 diff --git a/scoring.lua b/scoring.lua new file mode 100644 index 0000000..c17e3e9 --- /dev/null +++ b/scoring.lua @@ -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 diff --git a/states/game/gameover.lua b/states/game/gameover.lua new file mode 100644 index 0000000..42dc24c --- /dev/null +++ b/states/game/gameover.lua @@ -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 diff --git a/states/game/loading.lua b/states/game/loading.lua new file mode 100644 index 0000000..236a334 --- /dev/null +++ b/states/game/loading.lua @@ -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 diff --git a/states/game/placing.lua b/states/game/placing.lua new file mode 100644 index 0000000..30d0ae1 --- /dev/null +++ b/states/game/placing.lua @@ -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 diff --git a/states/game/rolling.lua b/states/game/rolling.lua new file mode 100644 index 0000000..8a75e20 --- /dev/null +++ b/states/game/rolling.lua @@ -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 diff --git a/states/game/scoring.lua b/states/game/scoring.lua new file mode 100644 index 0000000..f104142 --- /dev/null +++ b/states/game/scoring.lua @@ -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 diff --git a/states/state_game.lua b/states/state_game.lua new file mode 100644 index 0000000..b11361c --- /dev/null +++ b/states/state_game.lua @@ -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 diff --git a/states/state_main_menu.lua b/states/state_main_menu.lua new file mode 100644 index 0000000..014ac9e --- /dev/null +++ b/states/state_main_menu.lua @@ -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 diff --git a/states/state_manager.lua b/states/state_manager.lua new file mode 100644 index 0000000..ea2613c --- /dev/null +++ b/states/state_manager.lua @@ -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, +} diff --git a/tic.lua b/tic.lua new file mode 100644 index 0000000..975a960 --- /dev/null +++ b/tic.lua @@ -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 +-- 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 function’s 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 diff --git a/utils/event_bus.lua b/utils/event_bus.lua new file mode 100644 index 0000000..d35273e --- /dev/null +++ b/utils/event_bus.lua @@ -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 +} diff --git a/utils/input.lua b/utils/input.lua new file mode 100644 index 0000000..b1cafd4 --- /dev/null +++ b/utils/input.lua @@ -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 diff --git a/utils/rendering.lua b/utils/rendering.lua new file mode 100644 index 0000000..2c632d3 --- /dev/null +++ b/utils/rendering.lua @@ -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 \ No newline at end of file diff --git a/utils/tables.lua b/utils/tables.lua new file mode 100644 index 0000000..c7a5399 --- /dev/null +++ b/utils/tables.lua @@ -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 \ No newline at end of file diff --git a/utils/tween.lua b/utils/tween.lua new file mode 100644 index 0000000..d6ff644 --- /dev/null +++ b/utils/tween.lua @@ -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 diff --git a/utils/utils.lua b/utils/utils.lua new file mode 100644 index 0000000..e1e52d9 --- /dev/null +++ b/utils/utils.lua @@ -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