work in progress

This commit is contained in:
bryanthaboi
2026-07-21 05:50:40 -04:00
parent 7a1b4e5c45
commit 36188ef18a
32 changed files with 9336 additions and 177 deletions
+5 -1
View File
@@ -41,7 +41,11 @@ local CONSTANT_DEFAULTS = {
-- field.boot is the total-conversion override point for the new game; the
-- values match what SaveData.newGame and the Oak speech used to inline.
local BOOT_DEFAULTS = {
startMap = "PALLET_TOWN", startX = 5, startY = 6, startFacing = "down",
-- special_warps.asm NewGameWarp: REDS_HOUSE_2F, 3, 6 -- the bedroom, not
-- the tile outside the house. lastHeal is deliberately absent: SaveData
-- derives the vanilla blackout point, and seeding it here would leak into
-- total conversions that patch the spawn without naming a heal point.
startMap = "REDS_HOUSE_2F", startX = 3, startY = 6, startFacing = "down",
playerName = "RED", rivalName = "BLUE",
startMoney = 3000,
screens = { splash = "IntroMovie", title = "TitleState", newGame = "OakSpeech" },
+26 -2
View File
@@ -148,14 +148,38 @@ function Game:step(dt)
self.stack:update(dt)
-- play time for the trainer card / save screen
self.save.playTime = (self.save.playTime or 0) + dt
require("src.core.Music").update(Data)
-- Music.update is NOT serviced here: it decrements fade counters and
-- drives ChipAudio once per call, so running it inside the logic step
-- would pitch music and sfx up under fast-forward. Game:update advances
-- it on its own real-time 60Hz accumulator instead.
end
-- The logic multiplier for this frame. Read live rather than cached so the
-- Options row takes effect immediately; speedOverride is the --speed /
-- POKEPORT_SPEED run argument, which wins over the saved option so a bot
-- or screenshot run does not depend on whatever the player last chose.
function Game:logicSpeed()
local GameSpeed = require("src.core.GameSpeed")
if self.speedOverride then return GameSpeed.clamp(self.speedOverride) end
local opts = self.save and self.save.options
return GameSpeed.clamp(opts and opts.speed or GameSpeed.DEFAULT)
end
function Game:update(dt)
-- Touch timers / prior-frame auto-releases before the fixed step so
-- deferred A and edge pulses land in Input's press queue for this step.
TouchInput:update(dt)
FixedStep:update(dt)
-- Fast-forward scales only the logic clock (see src/core/GameSpeed.lua).
FixedStep:update(dt * self:logicSpeed())
-- Audio runs off real time at a fixed 60Hz regardless of game speed or
-- display refresh, so fades and chip synthesis keep their intended tempo
-- whether we are at 1X, 10X, or running with vsync disabled.
local step = FixedStep.STEP
self.audioAccum = math.min((self.audioAccum or 0) + dt, 0.25)
while self.audioAccum >= step do
self.audioAccum = self.audioAccum - step
require("src.core.Music").update(Data)
end
-- Overworld tilt toggle tween: presentational, so it runs on the real
-- frame dt (not the fixed logic step) for a smooth ~0.25s glide.
require("src.render.Tilt").update(dt)
+54
View File
@@ -0,0 +1,54 @@
-- Fast-forward multiplier for game logic.
--
-- Speeding up means running the 1/60 fixed step N times per real frame
-- (Game:update), so everything driven by the step -- movement, text,
-- battle timing, scripts -- advances N times faster while staying
-- deterministic. Audio deliberately does NOT scale: Music.update drives
-- fade counters and ChipAudio synthesis off its own real-time 60Hz
-- accumulator in Game:update, so music and sfx play at normal pitch and
-- tempo at every speed.
--
-- Vsync still caps how much work a real frame can do, so 10X is a target
-- rather than a promise on a slow machine -- the logic simply runs as many
-- steps as the frame budget allows.
local GameSpeed = {}
-- 20X exists for the bot runs (tests/drivers/route.lua): a full-route
-- attempt is long enough that the iteration loop, not the engine, is the
-- bottleneck. Vsync caps how much a real frame can do, so past 10X the
-- multiplier is increasingly a ceiling rather than a rate.
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75 }
GameSpeed.DEFAULT = 1
function GameSpeed.levelLabel(v)
v = tonumber(v) or GameSpeed.DEFAULT
if v == 1 then return "NORMAL" end
return tostring(v) .. "X"
end
-- nearest valid level for an arbitrary value (a hand-edited options.lua or
-- a --speed argument), so a bad number degrades to something sane
function GameSpeed.clamp(v)
v = tonumber(v)
if not v then return GameSpeed.DEFAULT end
local best, bestDiff = GameSpeed.DEFAULT, math.huge
for _, level in ipairs(GameSpeed.LEVELS) do
local diff = math.abs(level - v)
if diff < bestDiff then best, bestDiff = level, diff end
end
return best
end
-- cycle to the next/previous level, wrapping (the options row idiom)
function GameSpeed.cycle(v, dir)
local levels = GameSpeed.LEVELS
local cur = 1
for i, level in ipairs(levels) do
if level == GameSpeed.clamp(v) then cur = i break end
end
local nextIdx = (cur - 1 + (dir or 1)) % #levels + 1
return levels[nextIdx]
end
return GameSpeed
+33 -5
View File
@@ -41,6 +41,8 @@ function SaveData.defaultOptions()
musicVol = 7,
sfxVol = 7,
musicFilter = 0,
-- logic fast-forward multiplier; audio is unaffected (GameSpeed.lua)
speed = 1,
-- port display options (OptionsMenu / hotkeys 2/3/5)
colors = "gbc",
tilt = 0,
@@ -516,8 +518,8 @@ end
local function scrubMaps(save, data, report)
local boot = (data.field and data.field.boot) or {}
local spawn = { map = boot.startMap or "PALLET_TOWN",
x = boot.startX or 5, y = boot.startY or 6 }
local spawn = { map = boot.startMap or "REDS_HOUSE_2F",
x = boot.startX or 3, y = boot.startY or 6 }
-- heal point first, so the player fallback below always lands somewhere
-- valid; boot's heal cell (threaded from field.boot) is the last resort
if save.lastHeal and not known(data.maps, save.lastHeal.map) then
@@ -627,11 +629,31 @@ end
-- boot is Data.field.boot, threaded in by Game: this module must not reach
-- into Data itself. Every read falls back to the Red literal it replaced,
-- so an absent or partial config still produces the vanilla new game.
-- Where blackouts and ESCAPE ROPE return to for a given boot config.
--
-- In vanilla this is NOT the spawn. wLastBlackoutMap is zero-filled at new
-- game and PALLET_TOWN is map 0, so the player starts in the bedroom
-- (special_warps.asm NewGameWarp) but blacks out to Pallet Town's fly_warp
-- cell (5, 6). A world that moves the spawn without naming a heal point
-- keeps the two together -- it may have no Pallet Town at all.
--
-- Shared with the Hall of Fame reset, which pokered writes as a literal
-- (HallOfFameResetEventsAndSaveScript: wLastBlackoutMap := PALLET_TOWN)
-- rather than deriving from the spawn.
function SaveData.defaultHeal(boot)
boot = type(boot) == "table" and boot or {}
local h = boot.lastHeal
if h then return { map = h.map, x = h.x, y = h.y } end
local map = boot.startMap or "REDS_HOUSE_2F"
if map == "REDS_HOUSE_2F" then return { map = "PALLET_TOWN", x = 5, y = 6 } end
return { map = map, x = boot.startX or 3, y = boot.startY or 6 }
end
function SaveData.newGame(boot)
boot = type(boot) == "table" and boot or {}
local map = boot.startMap or "PALLET_TOWN"
local x, y = boot.startX or 5, boot.startY or 6
local heal = boot.lastHeal or {}
local map = boot.startMap or "REDS_HOUSE_2F"
local x, y = boot.startX or 3, boot.startY or 6
local heal = SaveData.defaultHeal(boot)
local save = {
meta = { format = Version.saveFormat, mods = {} },
player = {
@@ -655,6 +677,12 @@ function SaveData.newGame(boot)
-- where blackouts and ESCAPE ROPE return to (updated by nurses);
-- copied, never aliased, so a save never writes back into Data
lastHeal = { map = heal.map or map, x = heal.x or x, y = heal.y or y },
-- Interiors inherit the SGB palette of the last outdoor map. wLastMap
-- is zero-filled at new game and PALLET_TOWN is map 0, so before the
-- player has ever been outdoors that palette is Pallet Town's -- which
-- matters because the vanilla spawn (REDS_HOUSE_2F) is itself indoors.
-- Without this the palette falls through to the ROUTE default.
lastOutdoor = { id = heal.map or map, x = heal.x or x, y = heal.y or y },
repelSteps = 0,
-- per-mod persistence (mod.save) lives under here, keyed by mod id
modData = {},