Compare commits
17 Commits
feature/ta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 649b73b3f8 | |||
| cd41cb3312 | |||
| ecd094e2d7 | |||
| 5b4f3052a6 | |||
| 83301a1a8e | |||
| 77bd76f6f9 | |||
| aa7d0e902d | |||
| cb19251556 | |||
| 83f801210e | |||
| a732b5cbfa | |||
| c39e0ece35 | |||
| 03b6567c08 | |||
| 41eea638c0 | |||
| 67de284598 | |||
| 8a7fa96bcd | |||
| 6a3ef5d81e | |||
| c334f644de |
@@ -3,6 +3,8 @@
|
||||
|
||||
globals = {
|
||||
"Focus",
|
||||
"Day",
|
||||
"Timer",
|
||||
"Util",
|
||||
"Decision",
|
||||
"Situation",
|
||||
|
||||
8
.vscode/tasks.json
vendored
8
.vscode/tasks.json
vendored
@@ -11,17 +11,19 @@
|
||||
{
|
||||
"label": "Build & Run TIC80",
|
||||
"type": "shell",
|
||||
"command": "make build && tic80 --fs=. impostor.lua"
|
||||
"command": "make build && tic80 --fs=. impostor.lua",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Export assets",
|
||||
"type": "shell",
|
||||
"command": "make export_assets"
|
||||
"command": "make export_assets",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Make build",
|
||||
"type": "shell",
|
||||
"command": "make build"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ steps:
|
||||
- name: update
|
||||
image: alpine
|
||||
environment:
|
||||
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||
UPDATE_SERVER: https://games.teletype.hu
|
||||
UPDATE_SECRET:
|
||||
from_secret: update_secret_key
|
||||
commands:
|
||||
|
||||
4
Makefile
4
Makefile
@@ -30,7 +30,7 @@ DROPAREA_HOST ?= vps.teletype.hu
|
||||
DROPAREA_PORT ?= 2223
|
||||
DROPAREA_TARGET_PATH ?= /home/drop
|
||||
DROPAREA_USER ?= drop
|
||||
UPDATE_SERVER ?= https://games.vps.teletype.hu
|
||||
UPDATE_SERVER ?= https://games.teletype.hu
|
||||
|
||||
all: build
|
||||
|
||||
@@ -164,6 +164,8 @@ export_assets:
|
||||
@$(call f_export_asset_awk,MAP,$(OUTPUT),$(ASSETS_LUA))
|
||||
@$(call f_export_asset_awk,SFX,$(OUTPUT),$(ASSETS_LUA))
|
||||
@$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA))
|
||||
@$(call f_export_asset_awk,PATTERNS,$(OUTPUT),$(ASSETS_LUA))
|
||||
@$(call f_export_asset_awk,TRACKS,$(OUTPUT),$(ASSETS_LUA))
|
||||
|
||||
clean:
|
||||
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(PROJECT)-*-docs.zip $(PROJECT)-docs.zip $(OUTPUT) $(OUTPUT_ORIGINAL)
|
||||
|
||||
@@ -2,12 +2,14 @@ meta/meta.header.lua
|
||||
init/init.module.lua
|
||||
init/init.config.lua
|
||||
init/init.minigame.lua
|
||||
init/init.meter.lua
|
||||
init/init.context.lua
|
||||
system/system.util.lua
|
||||
system/system.print.lua
|
||||
system/system.input.lua
|
||||
system/system.focus.lua
|
||||
logic/logic.meter.lua
|
||||
logic/logic.focus.lua
|
||||
logic/logic.day.lua
|
||||
logic/logic.timer.lua
|
||||
system/system.ui.lua
|
||||
audio/audio.manager.lua
|
||||
audio/audio.songs.lua
|
||||
@@ -27,6 +29,8 @@ decision/decision.play_rhythm.lua
|
||||
decision/decision.play_ddr.lua
|
||||
map/map.manager.lua
|
||||
map/map.bedroom.lua
|
||||
map/map.street.lua
|
||||
map/map.office.lua
|
||||
screen/screen.manager.lua
|
||||
screen/screen.home.lua
|
||||
screen/screen.toilet.lua
|
||||
|
||||
@@ -19,12 +19,11 @@ function Audio.music_play_room_street_1() end
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_street_2() end
|
||||
--- Plays room music.
|
||||
-- TODO: function name is incomplete, determine the correct room identifier
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_() end
|
||||
--- Plays room work music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_work() end
|
||||
function Audio.music_play_room_work() music(0) end
|
||||
|
||||
--- Plays select sound effect.
|
||||
--- @within Audio
|
||||
|
||||
@@ -98,3 +98,39 @@ function Decision.filter_available(decisions_list)
|
||||
end
|
||||
return available
|
||||
end
|
||||
|
||||
--- Draws decision selector.
|
||||
--- @within Decision
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The index of the selected decision.<br/>
|
||||
function Decision.draw(decisions, selected_decision_index)
|
||||
local bar_height = 16
|
||||
local bar_y = Config.screen.height - bar_height
|
||||
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
|
||||
if #decisions > 0 then
|
||||
local selected_decision = decisions[selected_decision_index]
|
||||
local decision_label = selected_decision.label
|
||||
local text_width = #decision_label * 4
|
||||
local text_y = bar_y + 4
|
||||
local text_x = (Config.screen.width - text_width) / 2
|
||||
Print.text("<", 2, text_y, Config.colors.light_blue)
|
||||
Print.text(decision_label, text_x, text_y, Config.colors.item)
|
||||
Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates decision selector.
|
||||
--- @within Decision
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The current index of the selected decision.<br/>
|
||||
--- @return number selected_decision_index The updated index of the selected decision.
|
||||
function Decision.update(decisions, selected_decision_index)
|
||||
if Input.left() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
|
||||
elseif Input.right() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
|
||||
end
|
||||
return selected_decision_index
|
||||
end
|
||||
|
||||
@@ -9,18 +9,19 @@ function Config.initial_data()
|
||||
height = 136
|
||||
},
|
||||
colors = {
|
||||
black = 2,
|
||||
light_grey = 13,
|
||||
dark_grey = 14,
|
||||
red = 0,
|
||||
light_blue = 7,
|
||||
blue = 9,
|
||||
white = 12,
|
||||
item = 12,
|
||||
meter_bg = 12
|
||||
black = 0,
|
||||
light_grey = 2,
|
||||
dark_grey = 1,
|
||||
red = 13,
|
||||
light_blue = 9,
|
||||
blue = 3,
|
||||
white = 4,
|
||||
item = 7,
|
||||
meter_bg = 1
|
||||
},
|
||||
timing = {
|
||||
splash_duration = 120
|
||||
splash_duration = 120,
|
||||
minigame_win_duration = 180
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -34,10 +34,12 @@ function Context.initial_data()
|
||||
minigame_button_mash = Minigame.get_default_button_mash(),
|
||||
minigame_rhythm = Minigame.get_default_rhythm(),
|
||||
meters = Meter.get_initial(),
|
||||
timer = Timer.get_initial(),
|
||||
game = {
|
||||
current_screen = "home",
|
||||
current_situation = nil,
|
||||
}
|
||||
},
|
||||
day_count = 1,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ function Minigame.get_default_ddr()
|
||||
current_song = nil,
|
||||
pattern_index = 1,
|
||||
use_pattern = false,
|
||||
return_window = nil
|
||||
return_window = nil,
|
||||
win_timer = 0
|
||||
}
|
||||
end
|
||||
|
||||
@@ -101,6 +102,7 @@ end
|
||||
--- * button_x (number) Button indicator X position.<br/>
|
||||
--- * button_y (number) Button indicator Y position.<br/>
|
||||
--- * button_size (number) Button indicator size.<br/>
|
||||
--- * win_timer (number) Timer for the win message overlay.<br/>
|
||||
function Minigame.get_default_button_mash()
|
||||
return {
|
||||
bar_fill = 0,
|
||||
@@ -120,7 +122,8 @@ function Minigame.get_default_button_mash()
|
||||
button_size = 12,
|
||||
focus_center_x = nil,
|
||||
focus_center_y = nil,
|
||||
focus_initial_radius = 0
|
||||
focus_initial_radius = 0,
|
||||
win_timer = 0
|
||||
}
|
||||
end
|
||||
|
||||
@@ -150,6 +153,7 @@ end
|
||||
--- * button_size (number) Button indicator size.<br/>
|
||||
--- * press_cooldown (number) Current cooldown timer.<br/>
|
||||
--- * press_cooldown_duration (number) Frames of press cooldown.<br/>
|
||||
--- * win_timer (number) Timer for the win message overlay.<br/>
|
||||
function Minigame.get_default_rhythm()
|
||||
return {
|
||||
line_position = 0,
|
||||
@@ -176,10 +180,28 @@ function Minigame.get_default_rhythm()
|
||||
press_cooldown_duration = 15,
|
||||
focus_center_x = nil,
|
||||
focus_center_y = nil,
|
||||
focus_initial_radius = 0
|
||||
focus_initial_radius = 0,
|
||||
win_timer = 0
|
||||
}
|
||||
end
|
||||
|
||||
--- Draws a unified win message overlay.
|
||||
--- @within Minigame
|
||||
function Minigame.draw_win_overlay()
|
||||
local text = "WELL DONE!"
|
||||
local tw = #text * 4
|
||||
local th = 6
|
||||
local padding = 4
|
||||
local box_w = tw + padding * 2
|
||||
local box_h = th + padding * 2
|
||||
local box_x = (Config.screen.width - box_w) / 2
|
||||
local box_y = (Config.screen.height - box_h) / 2
|
||||
|
||||
rect(box_x, box_y, box_w, box_h, Config.colors.dark_grey)
|
||||
rectb(box_x, box_y, box_w, box_h, Config.colors.white)
|
||||
Print.text_center(text, Config.screen.width / 2, box_y + padding, Config.colors.white)
|
||||
end
|
||||
|
||||
--- Configures DDR minigame.
|
||||
--- @within Minigame
|
||||
--- @param params table Optional parameters to override defaults (see Minigame.get_default_ddr).
|
||||
|
||||
@@ -12,3 +12,5 @@ Input = {}
|
||||
Sprite = {}
|
||||
Audio = {}
|
||||
Focus = {}
|
||||
Day = {}
|
||||
Timer = {}
|
||||
18
inc/logic/logic.day.lua
Normal file
18
inc/logic/logic.day.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local _day_increase_handlers = {}
|
||||
|
||||
function Day.increase()
|
||||
Context.day_count = Context.day_count + 1
|
||||
for _, handler in ipairs(_day_increase_handlers) do
|
||||
handler()
|
||||
end
|
||||
end
|
||||
function Day.register_handler(handler)
|
||||
table.insert(_day_increase_handlers, handler)
|
||||
end
|
||||
|
||||
Day.register_handler(function()
|
||||
local m = Context.meters
|
||||
m.ism = math.max(0, m.ism - METER_DECAY_PER_DAY)
|
||||
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_DAY)
|
||||
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
|
||||
end)
|
||||
@@ -2,34 +2,17 @@
|
||||
local METER_MAX = 1000
|
||||
local METER_DEFAULT = 500
|
||||
local METER_GAIN_PER_CHORE = 100
|
||||
local METER_DECAY_PER_DAY = 20
|
||||
local COMBO_BASE_BONUS = 0.02
|
||||
local COMBO_MAX_BONUS = 0.16
|
||||
local COMBO_TIMEOUT_FRAMES = 600
|
||||
|
||||
-- 1800 frames = 30 seconds (1800 ÷ 60 = 30)
|
||||
local meter_timer_duration = 1800
|
||||
local meter_timer_decay_per_revolution = 20
|
||||
|
||||
-- Internal meters for tracking game progress and player stats.
|
||||
Meter.COLOR_ISM = Config.colors.red
|
||||
Meter.COLOR_WPM = Config.colors.blue
|
||||
Meter.COLOR_BM = Config.colors.black
|
||||
Meter.COLOR_BG = Config.colors.meter_bg
|
||||
|
||||
--- Sets the number of frames for one full timer revolution.
|
||||
--- @within Meter
|
||||
--- @param frames number Frames per revolution (controls degradation speed).
|
||||
function Meter.set_timer_duration(frames)
|
||||
meter_timer_duration = frames
|
||||
end
|
||||
|
||||
--- Sets the degradation amount applied to all meters per revolution.
|
||||
--- @within Meter
|
||||
--- @param amount number Amount to subtract from each meter per revolution.
|
||||
function Meter.set_timer_decay(amount)
|
||||
meter_timer_decay_per_revolution = amount
|
||||
end
|
||||
|
||||
--- Gets initial meter values.
|
||||
--- @within Meter
|
||||
--- @return result table Initial meter values. </br>
|
||||
@@ -39,8 +22,7 @@ end
|
||||
--- * bm (number) Initial BM meter value.<br/>
|
||||
--- * combo (number) Current combo count.<br/>
|
||||
--- * combo_timer (number) Frames since last combo action.<br/>
|
||||
--- * hidden (boolean) Whether meters are hidden.<br/>
|
||||
--- * timer_progress (number) Clock timer revolution progress (0 to 1).
|
||||
--- * hidden (boolean) Whether meters are hidden.
|
||||
function Meter.get_initial()
|
||||
return {
|
||||
ism = METER_DEFAULT,
|
||||
@@ -49,7 +31,6 @@ function Meter.get_initial()
|
||||
combo = 0,
|
||||
combo_timer = 0,
|
||||
hidden = false,
|
||||
timer_progress = 0,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -72,6 +53,20 @@ function Meter.get_max()
|
||||
return METER_MAX
|
||||
end
|
||||
|
||||
--- Sets the decay amount applied to all meters per day.
|
||||
--- @within Meter
|
||||
--- @param amount number Amount to subtract from each meter.
|
||||
function Meter.set_decay(amount)
|
||||
METER_DECAY_PER_DAY = amount
|
||||
end
|
||||
|
||||
--- Gets the meter decay as a percentage of the max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The decay percentage per day.
|
||||
function Meter.get_decay_percentage()
|
||||
return math.floor(METER_DECAY_PER_DAY / METER_MAX * 100)
|
||||
end
|
||||
|
||||
--- Gets combo multiplier.
|
||||
--- @within Meter
|
||||
--- @return number The current combo multiplier.
|
||||
@@ -96,13 +91,6 @@ function Meter.update()
|
||||
m.combo_timer = 0
|
||||
end
|
||||
end
|
||||
m.timer_progress = m.timer_progress + (1 / meter_timer_duration)
|
||||
if m.timer_progress >= 1 then
|
||||
m.timer_progress = m.timer_progress - 1
|
||||
m.ism = math.max(0, m.ism - meter_timer_decay_per_revolution)
|
||||
m.wpm = math.max(0, m.wpm - meter_timer_decay_per_revolution)
|
||||
m.bm = math.max(0, m.bm - meter_timer_decay_per_revolution)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -118,13 +106,6 @@ function Meter.add(key, amount)
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets the timer decay as a percentage of the max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The decay percentage per revolution (e.g. 2 means -2%).
|
||||
function Meter.get_timer_decay_percentage()
|
||||
return math.floor(meter_timer_decay_per_revolution / METER_MAX * 100)
|
||||
end
|
||||
|
||||
--- Called on minigame completion.
|
||||
--- @within Meter
|
||||
function Meter.on_minigame_complete()
|
||||
@@ -136,3 +117,37 @@ function Meter.on_minigame_complete()
|
||||
m.combo = m.combo + 1
|
||||
m.combo_timer = 0
|
||||
end
|
||||
|
||||
--- Draws meters.
|
||||
--- @within Meter
|
||||
function Meter.draw()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
if Context.meters.hidden then return end
|
||||
|
||||
local m = Context.meters
|
||||
local max = Meter.get_max()
|
||||
local bar_w = 44
|
||||
local bar_h = 2
|
||||
local bar_x = 182
|
||||
local label_x = 228
|
||||
local line_h = 5
|
||||
local start_y = 11
|
||||
local bar_offset = math.floor((line_h - bar_h) / 2)
|
||||
|
||||
local meter_list = {
|
||||
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 },
|
||||
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 },
|
||||
{ key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 },
|
||||
}
|
||||
|
||||
for _, meter in ipairs(meter_list) do
|
||||
local label_y = start_y + meter.row * line_h
|
||||
local bar_y = label_y + bar_offset
|
||||
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
|
||||
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
|
||||
if fill_w > 0 then
|
||||
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
|
||||
end
|
||||
print(meter.label, label_x, label_y, meter.color, false, 1, true)
|
||||
end
|
||||
end
|
||||
88
inc/logic/logic.timer.lua
Normal file
88
inc/logic/logic.timer.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
--- @section Timer
|
||||
|
||||
local timer_duration = 1800
|
||||
|
||||
--- Gets initial timer values.
|
||||
--- @within Timer
|
||||
--- @return result table Initial timer values. </br>
|
||||
--- Fields: </br>
|
||||
--- * progress (number) Clock timer revolution progress (0 to 1).
|
||||
function Timer.get_initial()
|
||||
return {
|
||||
progress = 0,
|
||||
}
|
||||
end
|
||||
|
||||
--- Sets the number of frames for one full timer revolution.
|
||||
--- @within Timer
|
||||
--- @param frames number Frames per revolution.
|
||||
function Timer.set_duration(frames)
|
||||
timer_duration = frames
|
||||
end
|
||||
|
||||
--- Updates the timer and handles revolution events.
|
||||
--- @within Timer
|
||||
function Timer.update()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
local t = Context.timer
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
|
||||
if not in_minigame then
|
||||
t.progress = t.progress + (1 / timer_duration)
|
||||
if t.progress >= 1 then
|
||||
Day.increase()
|
||||
t.progress = t.progress - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the clock timer indicator as a circular progress bar.
|
||||
--- @within Timer
|
||||
function Timer.draw()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
if Context.meters.hidden and not Context.stat_screen_active then return end
|
||||
|
||||
local cx = 10
|
||||
local cy = 20
|
||||
local r_outer = 5
|
||||
local r_inner = 3
|
||||
local progress = Context.timer.progress
|
||||
|
||||
local fg_color
|
||||
if progress <= 0.25 then
|
||||
fg_color = Config.colors.white
|
||||
elseif progress <= 0.5 then
|
||||
fg_color = Config.colors.light_blue
|
||||
elseif progress <= 0.75 then
|
||||
fg_color = Config.colors.blue
|
||||
elseif progress <= 1 then
|
||||
fg_color = Config.colors.red
|
||||
end
|
||||
|
||||
local bg_color = Config.colors.dark_grey
|
||||
local start_angle = -math.pi * 0.5
|
||||
local progress_angle = progress * 2 * math.pi
|
||||
local r_outer_sq = r_outer * r_outer
|
||||
local r_inner_sq = r_inner * r_inner
|
||||
|
||||
for dy = -r_outer, r_outer do
|
||||
for dx = -r_outer, r_outer do
|
||||
local dist_sq = dx * dx + dy * dy
|
||||
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
|
||||
local angle = math.atan(dy, dx)
|
||||
local relative = angle - start_angle
|
||||
if relative < 0 then relative = relative + 2 * math.pi end
|
||||
if relative <= progress_angle then
|
||||
pix(cx + dx, cy + dy, fg_color)
|
||||
else
|
||||
pix(cx + dx, cy + dy, bg_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hand_angle = start_angle + progress_angle
|
||||
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
|
||||
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
|
||||
line(cx, cy, hand_x, hand_y, Config.colors.white)
|
||||
end
|
||||
9
inc/map/map.office.lua
Normal file
9
inc/map/map.office.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
Map.register({
|
||||
id = "office",
|
||||
from_x = 60,
|
||||
from_y = 0,
|
||||
width = 30,
|
||||
height = 17,
|
||||
to_x = 0,
|
||||
to_y = 0,
|
||||
})
|
||||
9
inc/map/map.street.lua
Normal file
9
inc/map/map.street.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
Map.register({
|
||||
id = "street",
|
||||
from_x = 30,
|
||||
from_y = 0,
|
||||
width = 30,
|
||||
height = 17,
|
||||
to_x = 0,
|
||||
to_y = 0,
|
||||
})
|
||||
@@ -305,13 +305,15 @@
|
||||
-- 033:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
|
||||
-- 044:0600f6000620f6000600f6000610f600f600f6000600f600f600f600f6000600060006000600060006000600060006000600060006000600060006004600000f0f00
|
||||
-- 045:0000f0000020f0000000f0000010f000f000f0000000f000f000f000f0000000000000000000000000000000000000000000000000000000000000004600000f0f00
|
||||
-- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
|
||||
-- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100100000080800
|
||||
-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000
|
||||
-- 048:090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900400000000000
|
||||
-- 056:4100510061406140717081709100b100c100d100e100e100e100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100580000000600
|
||||
-- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c40000000004
|
||||
-- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100400000080800
|
||||
-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000
|
||||
-- 060:220022002200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200100000000000
|
||||
-- 061:9f009f00bf00df00df00ef00ef00ef00ef00ef00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00400000000000
|
||||
-- 062:00000100010001000100510081008100910091009100a100a100a100a100a100b100b100b100b100c100c100c100d100d100d100e100e100e100f100484000000000
|
||||
-- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000405000000000
|
||||
-- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000200000000000
|
||||
-- </SFX>
|
||||
-- <WAVES>
|
||||
-- 000:bcceefceedddddc84333121268abaa99
|
||||
@@ -325,3 +327,11 @@
|
||||
-- 008:0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
|
||||
-- 009:fff000fff000fff000fff000fff000ff
|
||||
-- </WAVES>
|
||||
-- <PATTERNS>
|
||||
-- 000:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000008c10000000008c10000000000000000000000000000000000000000000000000000000000000000004008b50000000000000000001008c10000004008b50000001008c10000000008c1000000e008b30000004008b50000001008c10000000008c10000000008c10000000008c10000000008c10000000008c1000000000000000000000000000000
|
||||
-- 001:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007008b50000007008b50000001008c10000007008b50000001008c10000000008c10000007008b50000009008b50000001008c10000009008b50000001008c10000009008b50000009008b50000001008c10000009008b50000001008c1000000
|
||||
-- 003:4008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d9000000000000000000000000000000000000000000
|
||||
-- </PATTERNS>
|
||||
-- <TRACKS>
|
||||
-- 000:100001200001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
-- </TRACKS>
|
||||
|
||||
@@ -5,5 +5,5 @@ Screen.register({
|
||||
"go_to_toilet",
|
||||
"go_to_walking_to_office",
|
||||
},
|
||||
background = "bedroom",
|
||||
background = "bedroom"
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
Screen.register({
|
||||
id = "office",
|
||||
name = "Office",
|
||||
background_color = Config.colors.dark_grey,
|
||||
decisions = {
|
||||
"play_button_mash",
|
||||
"play_rhythm",
|
||||
@@ -12,4 +11,5 @@ Screen.register({
|
||||
situations = {
|
||||
"drink_coffee",
|
||||
},
|
||||
background = "office"
|
||||
})
|
||||
|
||||
@@ -30,8 +30,7 @@ Screen.register({
|
||||
local bar_x = math.floor((sw - bar_w) / 2)
|
||||
local bar_h = 4
|
||||
|
||||
-- TODO: Add day counter
|
||||
Print.text_center("day 1", cx, 10, Config.colors.white)
|
||||
Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white)
|
||||
|
||||
local narrative = "reflecting on my past and present\n...\nboth eventually flushed."
|
||||
local wrapped = UI.word_wrap(narrative, 38)
|
||||
@@ -43,7 +42,7 @@ Screen.register({
|
||||
|
||||
local m = Context.meters
|
||||
local max_val = Meter.get_max()
|
||||
local decay_pct = Meter.get_timer_decay_percentage()
|
||||
local decay_pct = Meter.get_decay_percentage()
|
||||
local decay_text = string.format("-%d%%", decay_pct)
|
||||
local combo_mult = Meter.get_combo_multiplier()
|
||||
local combo_pct = math.floor((combo_mult - 1) * 100)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Screen.register({
|
||||
id = "walking_to_home",
|
||||
name = "Walking to home",
|
||||
background_color = Config.colors.dark_grey,
|
||||
decisions = {
|
||||
"go_to_home",
|
||||
"go_to_office",
|
||||
}
|
||||
},
|
||||
background = "street"
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Screen.register({
|
||||
id = "walking_to_office",
|
||||
name = "Walking to office",
|
||||
background_color = Config.colors.dark_grey,
|
||||
decisions = {
|
||||
"go_to_home",
|
||||
"go_to_office",
|
||||
}
|
||||
},
|
||||
background = "street"
|
||||
})
|
||||
|
||||
@@ -22,8 +22,9 @@ function TIC()
|
||||
handler()
|
||||
end
|
||||
Meter.update()
|
||||
Timer.update()
|
||||
if Context.game_in_progress then
|
||||
UI.draw_meters()
|
||||
UI.draw_timer()
|
||||
Meter.draw()
|
||||
Timer.draw()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,12 +8,6 @@ function UI.draw_top_bar(title)
|
||||
Print.text(title, 3, 2, Config.colors.light_blue)
|
||||
end
|
||||
|
||||
--- Draws dialog window.
|
||||
--- @within UI
|
||||
function UI.draw_dialog()
|
||||
PopupWindow.draw()
|
||||
end
|
||||
|
||||
--- Draws a menu.
|
||||
--- @within UI
|
||||
--- @param items table A table of menu items.<br/>
|
||||
@@ -85,176 +79,3 @@ function UI.word_wrap(text, max_chars_per_line)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Creates a numeric stepper.
|
||||
--- @within UI
|
||||
--- @param label string The label for the stepper.<br/>
|
||||
--- @param value_getter function Function to get the current value.<br/>
|
||||
--- @param value_setter function Function to set the current value.<br/>
|
||||
--- @param min number The minimum value.<br/>
|
||||
--- @param max number The maximum value.<br/>
|
||||
--- @param step number The step increment.<br/>
|
||||
--- @param[opt] format string The format string for displaying the value.<br/>
|
||||
--- @return result table A numeric stepper control definition or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * label (string) The label for the stepper.<br/>
|
||||
--- * get (function) Function to get the current value.<br/>
|
||||
--- * set (function) Function to set the current value.<br/>
|
||||
--- * min (number) The minimum value.<br/>
|
||||
--- * max (number) The maximum value.<br/>
|
||||
--- * step (number) The step increment.<br/>
|
||||
--- * format (string) The format string for displaying the value.<br/>
|
||||
--- * type (string) Control type identifier ("numeric_stepper").<br/>
|
||||
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
|
||||
return {
|
||||
label = label,
|
||||
get = value_getter,
|
||||
set = value_setter,
|
||||
min = min,
|
||||
max = max,
|
||||
step = step,
|
||||
format = format or "%.1f",
|
||||
type = "numeric_stepper"
|
||||
}
|
||||
end
|
||||
|
||||
--- Creates an action item.
|
||||
--- @within UI
|
||||
--- @param label string The label for the action item.<br/>
|
||||
--- @param action function The function to execute when the item is selected.<br/>
|
||||
--- @return result table An action item control definition or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * label (string) The label for the action item.<br/>
|
||||
--- * action (function) The function to execute when the item is selected.<br/>
|
||||
--- * type (string) Control type identifier ("action_item").<br/>
|
||||
function UI.create_action_item(label, action)
|
||||
return {
|
||||
label = label,
|
||||
action = action,
|
||||
type = "action_item"
|
||||
}
|
||||
end
|
||||
|
||||
--- Draws decision selector.
|
||||
--- @within UI
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The index of the selected decision.<br/>
|
||||
function UI.draw_decision_selector(decisions, selected_decision_index)
|
||||
local bar_height = 16
|
||||
local bar_y = Config.screen.height - bar_height
|
||||
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
|
||||
if #decisions > 0 then
|
||||
local selected_decision = decisions[selected_decision_index]
|
||||
local decision_label = selected_decision.label
|
||||
local text_width = #decision_label * 4
|
||||
local text_y = bar_y + 4
|
||||
local text_x = (Config.screen.width - text_width) / 2
|
||||
Print.text("<", 2, text_y, Config.colors.light_blue)
|
||||
Print.text(decision_label, text_x, text_y, Config.colors.item)
|
||||
Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the clock timer indicator as a circular progress bar in the top-left area.
|
||||
--- Color transitions: white (0-50%), yellow (50-75%), red (75-100%).
|
||||
--- @within UI
|
||||
function UI.draw_timer()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
if Context.meters.hidden and not Context.stat_screen_active then return end
|
||||
|
||||
local m = Context.meters
|
||||
local cx = 10
|
||||
local cy = 20
|
||||
local r_outer = 5
|
||||
local r_inner = 3
|
||||
local progress = m.timer_progress
|
||||
|
||||
local fg_color
|
||||
if progress <= 0.25 then
|
||||
fg_color = Config.colors.white
|
||||
elseif progress <= 0.5 then
|
||||
fg_color = Config.colors.light_blue
|
||||
elseif progress <= 0.75 then
|
||||
fg_color = Config.colors.blue
|
||||
elseif progress <= 1 then
|
||||
fg_color = Config.colors.red
|
||||
end
|
||||
|
||||
|
||||
local bg_color = Config.colors.dark_grey
|
||||
local start_angle = -math.pi * 0.5
|
||||
local progress_angle = progress * 2 * math.pi
|
||||
local r_outer_sq = r_outer * r_outer
|
||||
local r_inner_sq = r_inner * r_inner
|
||||
|
||||
for dy = -r_outer, r_outer do
|
||||
for dx = -r_outer, r_outer do
|
||||
local dist_sq = dx * dx + dy * dy
|
||||
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
|
||||
local angle = math.atan(dy, dx)
|
||||
local relative = angle - start_angle
|
||||
if relative < 0 then relative = relative + 2 * math.pi end
|
||||
if relative <= progress_angle then
|
||||
pix(cx + dx, cy + dy, fg_color)
|
||||
else
|
||||
pix(cx + dx, cy + dy, bg_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hand_angle = start_angle + progress_angle
|
||||
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
|
||||
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
|
||||
line(cx, cy, hand_x, hand_y, Config.colors.white)
|
||||
end
|
||||
|
||||
--- Draws meters.
|
||||
--- @within UI
|
||||
function UI.draw_meters()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
if Context.meters.hidden then return end
|
||||
|
||||
local m = Context.meters
|
||||
local max = Meter.get_max()
|
||||
local bar_w = 44
|
||||
local bar_h = 2
|
||||
local bar_x = 182
|
||||
local label_x = 228
|
||||
local line_h = 5
|
||||
local start_y = 11
|
||||
local bar_offset = math.floor((line_h - bar_h) / 2)
|
||||
|
||||
local meter_list = {
|
||||
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 },
|
||||
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 },
|
||||
{ key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 },
|
||||
}
|
||||
|
||||
for _, meter in ipairs(meter_list) do
|
||||
local label_y = start_y + meter.row * line_h
|
||||
local bar_y = label_y + bar_offset
|
||||
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
|
||||
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
|
||||
if fill_w > 0 then
|
||||
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
|
||||
end
|
||||
print(meter.label, label_x, label_y, meter.color, false, 1, true)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates decision selector.
|
||||
--- @within UI
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The current index of the selected decision.<br/>
|
||||
--- @return number selected_decision_index The updated index of the selected decision.
|
||||
function UI.update_decision_selector(decisions, selected_decision_index)
|
||||
if Input.left() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
|
||||
elseif Input.right() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
|
||||
end
|
||||
return selected_decision_index
|
||||
end
|
||||
@@ -1,11 +1,9 @@
|
||||
--- @section AudioTestWindow
|
||||
AudioTestWindow = {
|
||||
index_menu = 1,
|
||||
index_func = 1,
|
||||
list_func = {},
|
||||
menuitems = {},
|
||||
last_pressed = false
|
||||
}
|
||||
AudioTestWindow.index_menu = 1
|
||||
AudioTestWindow.index_func = 1
|
||||
AudioTestWindow.list_func = {}
|
||||
AudioTestWindow.menuitems = {}
|
||||
AudioTestWindow.last_pressed = false
|
||||
|
||||
--- Generates menu items for audio test.
|
||||
--- @within AudioTestWindow
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
--- @section ConfigurationWindow
|
||||
ConfigurationWindow = {
|
||||
controls = {},
|
||||
selected_control = 1,
|
||||
}
|
||||
ConfigurationWindow.controls = {}
|
||||
ConfigurationWindow.selected_control = 1
|
||||
|
||||
--- Initializes configuration window.
|
||||
--- @within ConfigurationWindow
|
||||
function ConfigurationWindow.init()
|
||||
ConfigurationWindow.controls = {
|
||||
UI.create_action_item(
|
||||
"Save",
|
||||
function() Config.save() end
|
||||
),
|
||||
UI.create_action_item(
|
||||
"Restore Defaults",
|
||||
function() Config.reset() end
|
||||
),
|
||||
{
|
||||
label = "Save",
|
||||
action = function() Config.save() end,
|
||||
type = "action_item"
|
||||
},
|
||||
{
|
||||
label = "Restore Defaults",
|
||||
action = function() Config.reset() end,
|
||||
type = "action_item"
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ function GameWindow.draw()
|
||||
end
|
||||
UI.draw_top_bar(screen.name)
|
||||
if not Context.stat_screen_active and #_available_decisions > 0 then
|
||||
UI.draw_decision_selector(_available_decisions, _selected_decision_index)
|
||||
Decision.draw(_available_decisions, _selected_decision_index)
|
||||
end
|
||||
Sprite.draw()
|
||||
Focus.draw()
|
||||
@@ -53,7 +53,7 @@ function GameWindow.update()
|
||||
_selected_decision_index = 1
|
||||
end
|
||||
|
||||
local new_selected_decision_index = UI.update_decision_selector(
|
||||
local new_selected_decision_index = Decision.update(
|
||||
_available_decisions,
|
||||
_selected_decision_index
|
||||
)
|
||||
|
||||
@@ -108,18 +108,25 @@ end
|
||||
--- @within MinigameDDRWindow
|
||||
function MinigameDDRWindow.update()
|
||||
local mg = Context.minigame_ddr
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if mg.bar_fill >= mg.max_fill then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
return
|
||||
end
|
||||
mg.frame_counter = mg.frame_counter + 1
|
||||
if mg.use_pattern and mg.current_song and mg.current_song.end_frame then
|
||||
if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -265,4 +272,7 @@ function MinigameDDRWindow.draw()
|
||||
else
|
||||
Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue)
|
||||
end
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
@@ -25,6 +25,18 @@ end
|
||||
--- @within MinigameButtonMashWindow
|
||||
function MinigameButtonMashWindow.update()
|
||||
local mg = Context.minigame_button_mash
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if Input.select() then
|
||||
mg.bar_fill = mg.bar_fill + mg.fill_per_press
|
||||
mg.button_pressed_timer = mg.button_press_duration
|
||||
@@ -33,10 +45,7 @@ function MinigameButtonMashWindow.update()
|
||||
end
|
||||
end
|
||||
if mg.bar_fill >= mg.max_fill then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
Window.set_current(mg.return_window)
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
return
|
||||
end
|
||||
local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier)
|
||||
@@ -86,4 +95,8 @@ function MinigameButtonMashWindow.draw()
|
||||
Print.text_center("MASH Z!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
|
||||
local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100)
|
||||
Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,18 @@ end
|
||||
--- @within MinigameRhythmWindow
|
||||
function MinigameRhythmWindow.update()
|
||||
local mg = Context.minigame_rhythm
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction)
|
||||
if mg.line_position > 1 then
|
||||
mg.line_position = 1
|
||||
@@ -57,10 +69,7 @@ function MinigameRhythmWindow.update()
|
||||
end
|
||||
end
|
||||
if mg.score >= mg.max_score then
|
||||
Meter.on_minigame_complete()
|
||||
Meter.show()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
Window.set_current(mg.return_window)
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
return
|
||||
end
|
||||
if mg.button_pressed_timer > 0 then
|
||||
@@ -107,4 +116,8 @@ function MinigameRhythmWindow.draw()
|
||||
circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color)
|
||||
end
|
||||
Print.text_center("Z", mg.button_x - 2, mg.button_y - 3, button_color)
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
|
||||
289
infra.md
289
infra.md
@@ -1,289 +0,0 @@
|
||||
# Server
|
||||
```mermaid
|
||||
graph TD
|
||||
Internet --> Nginx
|
||||
|
||||
Nginx --> Traefik
|
||||
Traefik --> Gitea
|
||||
Gitea --> GiteaDB[(Gitea data / SQLite)]
|
||||
|
||||
Traefik --> WoodpeckerServer
|
||||
WoodpeckerServer --> WoodpeckerDB[(Woodpecker data / SQLite)]
|
||||
WoodpeckerServer --> WoodpeckerAgent
|
||||
WoodpeckerAgent --> DockerSocket[(Docker)]
|
||||
|
||||
Traefik --> WebApp
|
||||
WebApp --> MySQL[(MySQL)]
|
||||
WebApp --> Softwares[(Volume)]
|
||||
|
||||
Droparea --> Softwares
|
||||
|
||||
Nginx --> Discourse
|
||||
Discourse --> ForumDB[(Postgres)]
|
||||
Discourse --> Redis[(Redis)]
|
||||
|
||||
Nginx --> Wiki
|
||||
Wiki --> WikiDB[(Postgres)]
|
||||
```
|
||||
|
||||
# TIC-80 Pipeline
|
||||
|
||||
This document describes the Woodpecker CI pipeline used to build, export, upload, and publish a TIC-80 game project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The pipeline performs the following steps:
|
||||
|
||||
1. **Build** the TIC-80 project using a custom Docker image
|
||||
2. **Export** the game to `.tic` and HTML formats
|
||||
3. **Upload artifacts** to a remote server via SCP
|
||||
4. **Notify an update server** to publish the new version
|
||||
|
||||
The pipeline is driven by environment variables so it can be reused across projects.
|
||||
|
||||
---
|
||||
|
||||
## Global Environment
|
||||
|
||||
```yaml
|
||||
environment: &environment
|
||||
GAME_NAME: mranderson
|
||||
GAME_LANG: lua
|
||||
```
|
||||
|
||||
- **GAME_NAME**: Project name (used for all outputs)
|
||||
- **GAME_LANG**: Source language used by TIC-80 (Lua)
|
||||
|
||||
The anchor (`&environment`) allows reuse across steps.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Build & Export
|
||||
|
||||
```yaml
|
||||
- name: build
|
||||
image: git.teletype.hu/internal/tic80pro:latest
|
||||
environment:
|
||||
<<: *environment
|
||||
XDG_RUNTIME_DIR: /tmp
|
||||
commands:
|
||||
- make build
|
||||
- make export
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
|
||||
- Uses a custom TIC-80 Pro Docker image hosted in Gitea
|
||||
- Runs the Makefile `build` target to assemble source files
|
||||
- Runs the `export` target to generate:
|
||||
- `.tic` cartridge
|
||||
- `.html.zip` web build
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Artifact Upload
|
||||
|
||||
```yaml
|
||||
- name: artifact
|
||||
image: alpine
|
||||
environment:
|
||||
<<: *environment
|
||||
DROPAREA_HOST: vps.teletype.hu
|
||||
DROPAREA_PORT: 2223
|
||||
DROPAREA_TARGET_PATH: /home/drop
|
||||
DROPAREA_USER: drop
|
||||
DROPAREA_SSH_PASSWORD:
|
||||
from_secret: droparea_ssh_password
|
||||
commands:
|
||||
- apk add --no-cache openssh-client sshpass
|
||||
- mkdir -p /root/.ssh
|
||||
- sshpass -p $DROPAREA_SSH_PASSWORD scp -o StrictHostKeyChecking=no -P $DROPAREA_PORT \
|
||||
$GAME_NAME.$GAME_LANG \
|
||||
$GAME_NAME.tic \
|
||||
$GAME_NAME.html.zip \
|
||||
$DROPAREA_USER@$DROPAREA_HOST:$DROPAREA_TARGET_PATH
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
|
||||
- Installs SCP tooling in a minimal Alpine container
|
||||
- Uploads:
|
||||
- Source file
|
||||
- TIC-80 cartridge
|
||||
- HTML export ZIP
|
||||
- Uses secrets for SSH authentication
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Update Notification
|
||||
|
||||
```yaml
|
||||
- name: update
|
||||
image: alpine
|
||||
environment:
|
||||
<<: *environment
|
||||
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||
UPDATE_SECRET:
|
||||
from_secret: update_secret_key
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
|
||||
- Sends an HTTP request to the update server
|
||||
- Notifies that a new TIC-80 build is available
|
||||
- Uses a secret key to authorize the update
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
After a successful run:
|
||||
|
||||
- The game is built and exported
|
||||
- Artifacts are uploaded to the server
|
||||
- The public game index is updated automatically
|
||||
|
||||
This pipeline enables **fully automated TIC-80 releases** using open tools and infrastructure.
|
||||
|
||||
|
||||
# TIC-80 Makefile Project Builder
|
||||
|
||||
This Makefile provides a simple, reproducible workflow for building **TIC-80 Lua projects** from multiple source files. It is designed for small indie or experimental projects where the source code is split into logical parts and then merged into a single `.lua` cartridge.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The workflow is based on four core ideas:
|
||||
|
||||
- Source code is split into multiple Lua files inside an `inc/` directory
|
||||
- A project-specific `.inc` file defines the **build order**
|
||||
- All source files are concatenated into one final `.lua` file
|
||||
- TIC-80 is used in CLI mode to export runnable artifacts
|
||||
|
||||
This approach keeps the codebase modular while remaining compatible with TIC-80’s single-file cartridge model.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
project-root/
|
||||
├── inc/
|
||||
│ ├── core.lua
|
||||
│ ├── player.lua
|
||||
│ └── world.lua
|
||||
├── mranderson.inc
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- `inc/` contains all Lua source fragments
|
||||
- `<project>.inc` defines the order in which files are merged
|
||||
- `<project>.lua` is generated automatically
|
||||
|
||||
---
|
||||
|
||||
## The `.inc` File
|
||||
|
||||
The `.inc` file is a **plain text file** listing Lua source files in build order:
|
||||
|
||||
```text
|
||||
core.lua
|
||||
player.lua
|
||||
world.lua
|
||||
```
|
||||
|
||||
The order matters. Files listed earlier are concatenated first and must define any globals used later.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Build (default)
|
||||
|
||||
```sh
|
||||
make build
|
||||
```
|
||||
|
||||
- Reads `mranderson.inc`
|
||||
- Concatenates files from `inc/`
|
||||
- Produces `mranderson.lua`
|
||||
|
||||
### Export (TIC-80)
|
||||
|
||||
```sh
|
||||
make export
|
||||
```
|
||||
|
||||
- Loads the generated Lua file into TIC-80 (CLI mode)
|
||||
- Saves a `.tic` cartridge
|
||||
- Exports an HTML build
|
||||
|
||||
### Export Assets
|
||||
|
||||
```sh
|
||||
make export_assets
|
||||
```
|
||||
|
||||
- **Purpose**: Extracts asset sections (PALETTE, TILES, SPRITES, MAP, SFX, MUSIC) from the compiled `<project>.lua` file.
|
||||
- **Mechanism**: Uses `sed` to directly parse the generated `<project>.lua` and saves the extracted data into `inc/meta/meta.assets.lua`. This file can then be used to embed the asset data directly into other parts of the project or for version control of visual assets.
|
||||
|
||||
### Import Assets
|
||||
|
||||
The `import_assets` target was considered during development but is currently not part of the build workflow. Asset handling for TIC-80 projects within this Makefile relies solely on direct extraction (`export_assets`) from the built Lua cartridge, rather than importing external asset definitions. This target may be implemented in the future if a need for pre-build asset injection arises.
|
||||
|
||||
### Watch Mode
|
||||
|
||||
```sh
|
||||
make watch
|
||||
```
|
||||
|
||||
- Performs an initial build
|
||||
- Watches the `inc/` directory and `.inc` file
|
||||
- Rebuilds automatically on any change
|
||||
|
||||
Requires `fswatch` to be installed.
|
||||
|
||||
---
|
||||
|
||||
## Generated Artifacts
|
||||
|
||||
| File | Description |
|
||||
|-----|-------------|
|
||||
| `<project>.lua` | Merged Lua source (input for TIC-80) |
|
||||
| `<project>.tic` | TIC-80 cartridge |
|
||||
| `<project>.html` | Web export |
|
||||
| `<project>.html.zip` | Packaged HTML build |
|
||||
|
||||
---
|
||||
|
||||
## Design Goals
|
||||
|
||||
- Keep TIC-80 projects modular
|
||||
- Avoid manual copy-paste between files
|
||||
- Enable fast iteration and experimentation
|
||||
- Remain fully compatible with open-source tooling
|
||||
|
||||
This Makefile is intentionally minimal and transparent, favoring simplicity over abstraction.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- `make`
|
||||
- `tic80` available in PATH
|
||||
- `fswatch` (only for watch mode)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License — free to use, modify, and redistribute.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user