Merge branch 'develop' into feature/ascension_7_8
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
# Conflicts: # impostor.inc # inc/decision/decision.have_a_coffee.lua # inc/decision/decision.sumphore_discussion.lua # inc/screen/screen.mysterious_man.lua # inc/screen/screen.walking_to_home.lua # inc/screen/screen.walking_to_office.lua # inc/window/window.menu.lua
This commit is contained in:
60
inc/logic/logic.codegenerator.lua
Normal file
60
inc/logic/logic.codegenerator.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
--- @section CodeGenerator
|
||||
|
||||
CodeGenerator = {}
|
||||
|
||||
local SALT = 27471
|
||||
local BASE36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
local NAME_LEN = 3
|
||||
|
||||
-- Per-position offsets derived from SALT so each character slot
|
||||
-- maps to a different region of the 2-char base-36 space.
|
||||
local SALTS = {
|
||||
SALT % 36,
|
||||
math.floor(SALT / 36) % 36,
|
||||
math.floor(SALT / 1296) % 36,
|
||||
}
|
||||
|
||||
--- Encodes a number (0–935) as exactly 2 base-36 characters.
|
||||
--- @within CodeGenerator
|
||||
function CodeGenerator.encode_pair(n)
|
||||
return BASE36:sub(math.floor(n / 36) + 1, math.floor(n / 36) + 1)
|
||||
.. BASE36:sub(n % 36 + 1, n % 36 + 1)
|
||||
end
|
||||
|
||||
--- Decodes 2 base-36 characters back to a number.
|
||||
--- @within CodeGenerator
|
||||
function CodeGenerator.decode_pair(s)
|
||||
local d1 = BASE36:find(s:sub(1, 1), 1, true) - 1
|
||||
local d2 = BASE36:find(s:sub(2, 2), 1, true) - 1
|
||||
return d1 * 36 + d2
|
||||
end
|
||||
|
||||
--- Encrypts a player name into a code twice its length.
|
||||
--- Each input character (A-Z, value 0-25) is encoded as
|
||||
--- c + SALTS[i] * 26, producing 2 base-36 output characters.
|
||||
--- @within CodeGenerator
|
||||
--- @param text string NAME_LEN-character uppercase player name.
|
||||
--- @return string Encrypted code (2 * NAME_LEN base-36 characters).
|
||||
function CodeGenerator.encrypt(text)
|
||||
local result = ""
|
||||
for i = 1, NAME_LEN do
|
||||
local c = math.max(0, (string.byte(text, i) or 65) - 65)
|
||||
result = result .. CodeGenerator.encode_pair(c + SALTS[i] * 26)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Decrypts a personal code back to the original player name.
|
||||
--- @within CodeGenerator
|
||||
--- @param encrypted_text string The code to decrypt (2 * NAME_LEN chars).
|
||||
--- @return string Original player name, or "???" if the code is invalid.
|
||||
function CodeGenerator.decrypt(encrypted_text)
|
||||
local t = encrypted_text:upper()
|
||||
if #t ~= NAME_LEN * 2 then return "???" end
|
||||
local result = ""
|
||||
for i = 1, NAME_LEN do
|
||||
local pair = CodeGenerator.decode_pair(t:sub((i - 1) * 2 + 1, i * 2))
|
||||
result = result .. string.char(pair % 26 + 65)
|
||||
end
|
||||
return result
|
||||
end
|
||||
@@ -8,6 +8,10 @@ function Day.increase()
|
||||
if Context.day_count == 3 then
|
||||
Context.should_ascend = true
|
||||
end
|
||||
if Context.day_count >= 100 and not Ascension.is_complete() then
|
||||
GameOverWindow.show("days")
|
||||
return
|
||||
end
|
||||
for _, handler in ipairs(_day_increase_handlers) do
|
||||
handler()
|
||||
end
|
||||
@@ -27,6 +31,15 @@ Day.register_handler(function()
|
||||
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
|
||||
end)
|
||||
|
||||
Day.register_handler(function()
|
||||
Context.toilet_meters_today_morning = false
|
||||
Context.toilet_meters_today_evening = false
|
||||
Context.coworker_discussion_meter_applied_today = false
|
||||
Context.sumphore_discussion_meter_applied_today = false
|
||||
Context.glitch_conversation_done_today = false
|
||||
Context.fast_food_eaten_today = 0
|
||||
end)
|
||||
|
||||
Day.register_handler(function()
|
||||
if Context.should_ascend then
|
||||
Ascension.increase()
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
--- @section Meter
|
||||
local METER_MAX = 1000
|
||||
local METER_DEFAULT = 500
|
||||
local BM_METER_DEFAULT = 200
|
||||
local ISM_METER_DEFAULT = 500
|
||||
local WPM_METER_DEFAULT = 200
|
||||
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
|
||||
local METER_FLASH_DURATION = 2.0
|
||||
local FLASH_COLOR = 4
|
||||
|
||||
-- Internal meters for tracking game progress and player stats.
|
||||
Meter.COLOR_ISM = Config.colors.orange
|
||||
@@ -14,6 +18,12 @@ Meter.COLOR_BM = Config.colors.red
|
||||
Meter.COLOR_BG = Config.colors.meter_bg
|
||||
Meter.COLOR_CONTOUR = Config.colors.white
|
||||
|
||||
local _flash = {
|
||||
wpm = { timer = 0, delta = 0 },
|
||||
ism = { timer = 0, delta = 0 },
|
||||
bm = { timer = 0, delta = 0 },
|
||||
}
|
||||
|
||||
--- Gets initial meter values.
|
||||
--- @within Meter
|
||||
--- @return result table Initial meter values. </br>
|
||||
@@ -26,9 +36,9 @@ Meter.COLOR_CONTOUR = Config.colors.white
|
||||
--- * hidden (boolean) Whether meters are hidden.
|
||||
function Meter.get_initial()
|
||||
return {
|
||||
ism = METER_DEFAULT,
|
||||
wpm = METER_DEFAULT,
|
||||
bm = METER_DEFAULT,
|
||||
ism = ISM_METER_DEFAULT,
|
||||
wpm = WPM_METER_DEFAULT,
|
||||
bm = BM_METER_DEFAULT,
|
||||
combo = 0,
|
||||
combo_timer = 0,
|
||||
hidden = false,
|
||||
@@ -93,6 +103,12 @@ function Meter.update()
|
||||
end
|
||||
end
|
||||
end
|
||||
local dt = Context.delta_time or 0
|
||||
for _, key in ipairs({ "wpm", "ism", "bm" }) do
|
||||
if _flash[key].timer > 0 then
|
||||
_flash[key].timer = _flash[key].timer - dt
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Adds amount to a meter.
|
||||
@@ -103,22 +119,140 @@ function Meter.add(key, amount)
|
||||
if not Context or not Context.meters then return end
|
||||
local m = Context.meters
|
||||
if m[key] ~= nil then
|
||||
m[key] = math.min(METER_MAX, m[key] + amount)
|
||||
if amount > 0 and (key == "ism" or key == "bm") and m[key] >= METER_MAX then
|
||||
GameOverWindow.show(key)
|
||||
return
|
||||
end
|
||||
local prev_wpm = (key == "wpm") and m.wpm or nil
|
||||
local old_val = m[key]
|
||||
m[key] = math.max(0, math.min(METER_MAX, m[key] + amount))
|
||||
local actual_delta = m[key] - old_val
|
||||
if actual_delta ~= 0 and _flash[key] then
|
||||
_flash[key].delta = actual_delta
|
||||
_flash[key].timer = METER_FLASH_DURATION
|
||||
end
|
||||
if prev_wpm and prev_wpm > 0 and m.wpm == 0 and Context.game_in_progress
|
||||
and Ascension.get_level() == 5 then
|
||||
Context.should_ascend = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Called on minigame completion.
|
||||
--- @within Meter
|
||||
function Meter.on_minigame_complete()
|
||||
--- @param is_work boolean If true (work-style minigame), apply combo to WPM/ISM/BM and advance combo. DDR uses `Meter.apply_ddr_reward` instead. Otherwise flat equal gain, combo unchanged.
|
||||
function Meter.on_minigame_complete(is_work)
|
||||
local m = Context.meters
|
||||
local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier())
|
||||
Meter.add("wpm", gain)
|
||||
Meter.add("ism", gain)
|
||||
Meter.add("bm", gain)
|
||||
if is_work then
|
||||
local mult = Meter.get_combo_multiplier()
|
||||
local wpm_delta = math.floor(METER_GAIN_PER_CHORE / mult)
|
||||
local ism_bm_delta = math.floor(METER_GAIN_PER_CHORE * mult)
|
||||
Meter.add("wpm", wpm_delta)
|
||||
Meter.add("ism", ism_bm_delta)
|
||||
Meter.add("bm", ism_bm_delta)
|
||||
m.combo = m.combo + 1
|
||||
m.combo_timer = 0
|
||||
else
|
||||
local flat = METER_GAIN_PER_CHORE
|
||||
Meter.add("wpm", flat)
|
||||
Meter.add("ism", flat)
|
||||
Meter.add("bm", flat)
|
||||
end
|
||||
end
|
||||
|
||||
--- Meter changes after DDR: uses max-meter percentages; combo advances like other work minigames.
|
||||
--- 0 mistakes: WPM −10%, ISM +5%, BM +5%. 1–3: WPM −5%, ISM +10%, BM +10%. More than 3: WPM unchanged, ISM +10%, BM +10%.
|
||||
--- @within Meter
|
||||
--- @param mistake_count number Total mistakes (missed arrows, wrong inputs, and special-mode rule violations).
|
||||
function Meter.apply_ddr_reward(mistake_count)
|
||||
if not Context or not Context.meters then return end
|
||||
local max = Meter.get_max()
|
||||
local m = Context.meters
|
||||
local wpm_pct, ism_pct, bm_pct
|
||||
if mistake_count == 0 then
|
||||
wpm_pct, ism_pct, bm_pct = -0.10, 0.05, 0.05
|
||||
elseif mistake_count <= 3 then
|
||||
wpm_pct, ism_pct, bm_pct = -0.05, 0.10, 0.10
|
||||
else
|
||||
wpm_pct, ism_pct, bm_pct = 0, 0.10, 0.10
|
||||
end
|
||||
if wpm_pct ~= 0 then
|
||||
Meter.add("wpm", math.floor(max * wpm_pct))
|
||||
end
|
||||
if ism_pct ~= 0 then
|
||||
Meter.add("ism", math.floor(max * ism_pct))
|
||||
end
|
||||
if bm_pct ~= 0 then
|
||||
Meter.add("bm", math.floor(max * bm_pct))
|
||||
end
|
||||
m.combo = m.combo + 1
|
||||
m.combo_timer = 0
|
||||
end
|
||||
|
||||
--- Meter changes for the wake-up button mash: faster completion is better for WPM.
|
||||
--- Perfect: under 2s — WPM +20%. Good: 2–3s — WPM +10%, ISM +5%, BM +5%. Bad: over 3s — WPM −5%, ISM +10%, BM +10%.
|
||||
--- @within Meter
|
||||
--- @param elapsed_sec number Seconds from minigame start until the bar was filled.
|
||||
function Meter.apply_wakeup_reward(elapsed_sec)
|
||||
if not Context or not Context.meters then return end
|
||||
local max = Meter.get_max()
|
||||
local wpm_pct, ism_pct, bm_pct
|
||||
if elapsed_sec < 2 then
|
||||
wpm_pct, ism_pct, bm_pct = 0.20, 0, 0
|
||||
elseif elapsed_sec <= 3 then
|
||||
wpm_pct, ism_pct, bm_pct = 0.10, 0.05, 0.05
|
||||
else
|
||||
wpm_pct, ism_pct, bm_pct = -0.05, 0.10, 0.10
|
||||
end
|
||||
if wpm_pct ~= 0 then
|
||||
Meter.add("wpm", math.floor(max * wpm_pct))
|
||||
end
|
||||
if ism_pct ~= 0 then
|
||||
Meter.add("ism", math.floor(max * ism_pct))
|
||||
end
|
||||
if bm_pct ~= 0 then
|
||||
Meter.add("bm", math.floor(max * bm_pct))
|
||||
end
|
||||
end
|
||||
|
||||
--- Random single meter shift after finishing a coworker discussion: ISM +10%, WPM −10%, or BM +10%.
|
||||
--- @within Meter
|
||||
function Meter.apply_coworker_discussion_reward()
|
||||
if not Context or not Context.meters then return end
|
||||
if Context.coworker_discussion_meter_applied_today then return end
|
||||
local max = Meter.get_max()
|
||||
local delta = math.floor(max * 0.10)
|
||||
local roll = math.random(1, 3)
|
||||
if roll == 1 then
|
||||
Meter.add("ism", delta)
|
||||
elseif roll == 2 then
|
||||
Meter.add("wpm", -delta)
|
||||
else
|
||||
Meter.add("bm", delta)
|
||||
end
|
||||
Context.coworker_discussion_meter_applied_today = true
|
||||
end
|
||||
|
||||
--- After finishing a sumphore discussion: reduce whichever of ISM / WPM / BM is highest by 10% of max (stable tie to ISM, then WPM, then BM).
|
||||
--- @within Meter
|
||||
function Meter.apply_sumphore_discussion_reward()
|
||||
if not Context or not Context.meters then return end
|
||||
if Context.sumphore_discussion_meter_applied_today then return end
|
||||
local m = Context.meters
|
||||
local max = Meter.get_max()
|
||||
local delta = math.floor(max * 0.10)
|
||||
local biggest_val_key = "ism"
|
||||
local biggest_val = m.ism
|
||||
for _, key in ipairs({ "wpm", "bm" }) do
|
||||
if m[key] > biggest_val then
|
||||
biggest_val = m[key]
|
||||
biggest_val_key = key
|
||||
end
|
||||
end
|
||||
Meter.add(biggest_val_key, -delta)
|
||||
Context.sumphore_discussion_meter_applied_today = true
|
||||
end
|
||||
|
||||
--- Draws meters.
|
||||
--- @within Meter
|
||||
function Meter.draw()
|
||||
@@ -149,12 +283,32 @@ function Meter.draw()
|
||||
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
|
||||
rect(bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2, Meter.COLOR_CONTOUR)
|
||||
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
|
||||
if fill_w > 0 then
|
||||
local flash = _flash[meter.key]
|
||||
if flash and flash.timer > 0 then
|
||||
local old_val = m[meter.key] - flash.delta
|
||||
local old_fill_w = math.max(0, math.floor((old_val / max) * bar_w))
|
||||
local stable_w = math.min(fill_w, old_fill_w)
|
||||
if stable_w > 0 then
|
||||
rect(bar_x, bar_y, stable_w, bar_h, meter.color)
|
||||
end
|
||||
if flash.delta > 0 then
|
||||
local hi_w = fill_w - stable_w
|
||||
if hi_w > 0 then
|
||||
rect(bar_x + stable_w, bar_y, hi_w, bar_h, FLASH_COLOR)
|
||||
end
|
||||
else
|
||||
local hi_w = old_fill_w - fill_w
|
||||
if hi_w > 0 then
|
||||
rect(bar_x + fill_w, bar_y, hi_w, bar_h, FLASH_COLOR)
|
||||
end
|
||||
end
|
||||
elseif 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
|
||||
|
||||
local ascension_y = start_y + 3 * line_h + 1
|
||||
Ascension.draw(bar_x, ascension_y, { spacing = 8 })
|
||||
Ascension.draw(bar_x - 4, ascension_y, { spacing = 8 })
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user