mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 21:46:51 +02:00
Pokemon Silver as a full launcher version, plus launcher mods-list and title-tempo fixes
Silver: derived import manifest (tools/make_silver_manifest.py re-resolves the Gold manifest's symbols from pokesilver.sym), silver GameVersion row, generation-keyed extractor routing, required-files override, edition save stamping (a Silver playthrough no longer writes into the Gold save), checkver-driven edition data, SILVER/KAMON/OSCAR/MAX presets, GOLD rival default, edition credits banner, Lugia title screen (OAM layouts, bob, trail, palettes as title.lua data keys with Gold defaults so old caches need no re-import), packaging for every build target, docs, and tests. Launcher: the installed-mods list is one continuous scroll (rows culled to the viewport) instead of a pager with an inner scroll viewport; the pad cursor's edge-scroll no longer runs it to the bottom. The game dropdown shows just the initial and caret. Find-tab behavior unchanged. Title tempo: a sprite-anim frame shows duration+1 ticks (engine/sprite_anims/core.asm GetSpriteAnimFrame), which locks both editions' 64-tick wing beat to the 64-tick sine bob; the title screens no longer run fast and out of phase.
This commit is contained in:
+12
-5
@@ -189,11 +189,18 @@ defineString("CREDIT_END", "END")
|
||||
|
||||
-- STAFF and everything after it is a heading. Anything BELOW this id is a
|
||||
-- person as far as ParseCredits is concerned.
|
||||
Credits.STAFF = defineString("STAFF", {
|
||||
" #MON",
|
||||
" GOLD VERSION",
|
||||
" PORT STAFF",
|
||||
})
|
||||
-- Credits_Staff differs per edition, including the centring spaces
|
||||
-- (data/credits_strings.asm:54-60).
|
||||
Credits.STAFF = defineString("STAFF",
|
||||
require("src.core.GameVersion").get() == "silver" and {
|
||||
" #MON",
|
||||
" SILVER VERSION",
|
||||
" PORT STAFF",
|
||||
} or {
|
||||
" #MON",
|
||||
" GOLD VERSION",
|
||||
" PORT STAFF",
|
||||
})
|
||||
defineString("DIRECTOR", " DIRECTOR")
|
||||
defineString("PROGRAMMING", " PROGRAMMING")
|
||||
defineString("ENGINE_DESIGN", " ENGINE DESIGN")
|
||||
|
||||
@@ -147,8 +147,8 @@ function GoldSilverIntro.new(game, opts)
|
||||
-- the movie's full 2335 frames over an empty screen and reads exactly
|
||||
-- like "the intro does not work". Say so instead: the fix is a
|
||||
-- re-import, and nothing else in the boot chain will mention it.
|
||||
Logger.warn("gold intro: no intro.lua in the cache -- re-import Gold "
|
||||
.. "or the movie plays blank")
|
||||
Logger.warn("gen2 intro: no intro.lua in the cache -- re-import "
|
||||
.. "this version or the movie plays blank")
|
||||
end
|
||||
self.images = {}
|
||||
self.sheets = {}
|
||||
|
||||
@@ -56,7 +56,7 @@ function MainMenu.new(game, opts)
|
||||
|
||||
self.save = opts.save
|
||||
if self.save == nil and opts.hasSave ~= false then
|
||||
local loaded = Save.load("gold")
|
||||
local loaded = Save.load()
|
||||
self.save = loaded
|
||||
end
|
||||
self.hasSave = opts.hasSave
|
||||
|
||||
@@ -34,8 +34,14 @@ local NamePick = {}
|
||||
NamePick.__index = NamePick
|
||||
NamePick.isOpaque = true
|
||||
|
||||
-- data/player_names.asm PlayerNameArray, Gold's half of the IF.
|
||||
-- data/player_names.asm PlayerNameArray, one half of the IF per edition.
|
||||
local PRESETS = { "GOLD", "HIRO", "TAYLOR", "KARL" }
|
||||
local PRESETS_SILVER = { "SILVER", "KAMON", "OSCAR", "MAX" }
|
||||
|
||||
local function presetsFor()
|
||||
local silver = require("src.core.GameVersion").get() == "silver"
|
||||
return silver and PRESETS_SILVER or PRESETS
|
||||
end
|
||||
|
||||
-- menu_coords 0, 0, 10, TEXTBOX_Y - 1 (TEXTBOX_Y = 12).
|
||||
local BOX_X1, BOX_Y1, BOX_X2, BOX_Y2 = 0, 0, 10, 11
|
||||
@@ -60,7 +66,7 @@ function NamePick.new(game, opts)
|
||||
self.game = game
|
||||
self.onDone = opts.onDone
|
||||
self.items = { "NEW NAME" }
|
||||
for _, name in ipairs(opts.presets or PRESETS) do
|
||||
for _, name in ipairs(opts.presets or presetsFor()) do
|
||||
self.items[#self.items + 1] = name
|
||||
end
|
||||
-- `db 1 ; default option`: the cursor starts on NEW NAME, not on a preset.
|
||||
@@ -106,7 +112,7 @@ function NamePick:openNaming()
|
||||
if name and #name > 0 then
|
||||
self:choose(name)
|
||||
else
|
||||
self:choose(self.items[2] or "GOLD")
|
||||
self:choose(self.items[2] or presetsFor()[1])
|
||||
end
|
||||
end,
|
||||
})
|
||||
@@ -146,7 +152,7 @@ function NamePick:update(_dt)
|
||||
if self.fontOk then
|
||||
self:openNaming()
|
||||
else
|
||||
self:choose(self.items[2] or "GOLD")
|
||||
self:choose(self.items[2] or presetsFor()[1])
|
||||
end
|
||||
else
|
||||
-- A preset returns through MovePlayerPicLeft, so the pic walks back
|
||||
@@ -227,5 +233,7 @@ function NamePick:drawWidescreen(winW, winH)
|
||||
end
|
||||
|
||||
NamePick.PRESETS = PRESETS
|
||||
NamePick.PRESETS_SILVER = PRESETS_SILVER
|
||||
NamePick.presetsFor = presetsFor
|
||||
|
||||
return NamePick
|
||||
|
||||
@@ -72,8 +72,8 @@ end
|
||||
|
||||
-- Naming presets are boot config the same way Gen 1 reads them
|
||||
-- (field.boot.namePresets), so a total conversion that replaces the list once
|
||||
-- replaces it for both games; NamePick.PRESETS (data/player_names.asm
|
||||
-- PlayerNameArray) is Gold's fallback.
|
||||
-- replaces it for both games; NamePick.presetsFor (data/player_names.asm
|
||||
-- PlayerNameArray) is the running edition's fallback.
|
||||
local function namePresets(game, who, fallback)
|
||||
local boot = game and game.data and game.data.field
|
||||
and game.data.field.boot
|
||||
@@ -351,9 +351,9 @@ function OakSpeech:openNamePick(step)
|
||||
picColors = self.playerColors,
|
||||
presets = step.presets
|
||||
or namePresets(self.game, step.presetsWho or step.who or "player",
|
||||
step.presetsFallback or NamePick.PRESETS),
|
||||
step.presetsFallback or NamePick.presetsFor()),
|
||||
onDone = function(name)
|
||||
name = name or "GOLD"
|
||||
name = name or NamePick.presetsFor()[1]
|
||||
self.game.save.player.name = name
|
||||
self.game.stack:pop() -- NamePick
|
||||
self.busy = false
|
||||
|
||||
@@ -181,7 +181,7 @@ function PcMenu:beginChangeBox(index)
|
||||
self.saveTimer = 0
|
||||
self.saved = nil
|
||||
local existed = self.saveExists
|
||||
if existed == nil then existed = Save.exists("gold") end
|
||||
if existed == nil then existed = Save.exists() end
|
||||
self.existed = existed
|
||||
end
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function SaveMenu.new(game, opts)
|
||||
self.onDone = opts.onDone
|
||||
self.writer = opts.writer or Save.save
|
||||
local existed = opts.existed
|
||||
if existed == nil then existed = Save.exists("gold") end
|
||||
if existed == nil then existed = Save.exists() end
|
||||
self.existed = existed
|
||||
-- confirm -> overwrite (only when a file exists) -> saving -> done
|
||||
self.phase = "confirm"
|
||||
|
||||
+60
-25
@@ -2,6 +2,7 @@
|
||||
-- wing-flap (Frameset_GSIntroHoOhLugia), spark trails, A/Start to continue.
|
||||
-- drawWidescreen fills the window with sky/clouds so widescreen has no
|
||||
-- pillarbox voids; the 160x144 art stays aspect-centered on top.
|
||||
-- Every Gold/Silver difference arrives as a title.lua key, defaulted to Gold.
|
||||
|
||||
-- src/render/Assets.lua is the mod-override choke point: a raw
|
||||
-- love.graphics.newImage skips overrides/ and AssetTransform output.
|
||||
@@ -16,7 +17,7 @@ local TitleState = {}
|
||||
TitleState.__index = TitleState
|
||||
TitleState.isOpaque = true
|
||||
|
||||
-- title_bg_gold.pal mid-sky shade (sampled from composed title_screen.png).
|
||||
-- title_bg_gold.pal mid-sky shade, for a cache built before title.sky existed.
|
||||
local SKY = { 123 / 255, 165 / 255, 255 / 255, 1 }
|
||||
-- ...and its grey stand-in, for when COLOR is not GBC. The title art is the
|
||||
-- one thing in the port baked with its colours in (see the extractor), so the
|
||||
@@ -61,6 +62,15 @@ function TitleState.new(game, opts)
|
||||
self.hoohY = tonumber(title.hoohY) or 56
|
||||
self.cloudY = tonumber(title.cloudY) or 88
|
||||
self.cloudScrollEvery = tonumber(title.cloudScrollEvery) or 8
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
|
||||
self.hoohBobAmplitude = tonumber(title.hoohBobAmplitude) or 2
|
||||
self.hoohBobStep = tonumber(title.hoohBobStep) or 1
|
||||
local sky = title.sky
|
||||
self.sky = (type(sky) == "table" and #sky >= 3)
|
||||
and { sky[1], sky[2], sky[3], 1 } or SKY
|
||||
local below = title.below
|
||||
self.below = (type(below) == "table" and #below >= 3)
|
||||
and { below[1], below[2], below[3], 1 } or { 1, 1, 1, 1 }
|
||||
|
||||
self.hoohColor, self.hoohGray = {}, {}
|
||||
local paths = title.hoohFrames
|
||||
@@ -80,8 +90,12 @@ function TitleState.new(game, opts)
|
||||
self.sequence = title.hoohSequence or {
|
||||
{ 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 },
|
||||
}
|
||||
-- A frame shows duration + 1 ticks: GetSpriteAnimFrame stores the byte on
|
||||
-- the advancing tick and only decrements on the ones after
|
||||
-- (engine/sprite_anims/core.asm:400-434). Both editions' framesets total
|
||||
-- 64 ticks with it, locking the wing beat to the 64-tick sine bob.
|
||||
self.seqIndex = 1
|
||||
self.seqLeft = self.sequence[1] and self.sequence[1][2] or 10
|
||||
self.seqLeft = (self.sequence[1] and self.sequence[1][2] or 10) + 1
|
||||
self.frame = 1
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia's SPRITEANIMSTRUCT_VAR1.
|
||||
@@ -89,12 +103,21 @@ function TitleState.new(game, opts)
|
||||
self.frameCounter = 0
|
||||
self.cloudScroll = 0
|
||||
self.trails = {}
|
||||
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm), in pixels.
|
||||
self.trailSpawns = {
|
||||
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm:1069-1124), in
|
||||
-- pixels.
|
||||
self.trailSpawns = title.trailSpawns or {
|
||||
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
|
||||
{ 120, 88 }, { 88, 88 },
|
||||
}
|
||||
self.trailSpawnIndex = 1
|
||||
-- AnimSeq_GSTitleTrail (engine/sprite_anims/functions.asm:720-818).
|
||||
self.trailMode = title.trailMode or "gold"
|
||||
self.trailSpawnEvery = tonumber(title.trailSpawnEvery) or 4
|
||||
self.trailStepX = tonumber(title.trailStepX) or 4
|
||||
self.trailStepY = tonumber(title.trailStepY) or 1
|
||||
self.trailBobAmplitude = tonumber(title.trailBobAmplitude) or 2
|
||||
self.trailPhaseStep = tonumber(title.trailPhaseStep) or 3
|
||||
self.trailPhase = tonumber(title.trailPhase)
|
||||
-- How far past the 160px frame trails may fly (GB pixels); set each draw.
|
||||
self.trailMaxX = 200
|
||||
self.musicStarted = false
|
||||
@@ -119,47 +142,56 @@ function TitleState:enter()
|
||||
end
|
||||
end
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm): VAR1 counts up
|
||||
-- one per frame and the struct's Y offset becomes `d * sin(VAR1 * pi/32)` with
|
||||
-- d = 2 on Gold (Silver counts DOWN with d = 8). Sprites_Sine hands back the
|
||||
-- byte the ASM leaves in a, so the down half of the wave arrives in two's
|
||||
-- complement and has to be read as a signed pixel delta here.
|
||||
function TitleState:hoohBob()
|
||||
local value = SpriteAnims.sine(self.hoohPhase, 2)
|
||||
if value >= 0x80 then value = value - 0x100 end
|
||||
-- Sprites_Sine hands back the byte the ASM leaves in a, so the down half of
|
||||
-- the wave arrives in two's complement and is a signed pixel delta here.
|
||||
local function signed(value)
|
||||
if value >= 0x80 then return value - 0x100 end
|
||||
return value
|
||||
end
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
|
||||
function TitleState:hoohBob()
|
||||
return signed(SpriteAnims.sine(self.hoohPhase, self.hoohBobAmplitude))
|
||||
end
|
||||
|
||||
function TitleState:advanceHooh()
|
||||
self.hoohPhase = (self.hoohPhase + 1) % 256
|
||||
self.hoohPhase = (self.hoohPhase + self.hoohBobStep) % 256
|
||||
self.seqLeft = self.seqLeft - 1
|
||||
if self.seqLeft > 0 then return end
|
||||
self.seqIndex = self.seqIndex + 1
|
||||
if self.seqIndex > #self.sequence then self.seqIndex = 1 end
|
||||
local step = self.sequence[self.seqIndex]
|
||||
self.frame = step[1]
|
||||
self.seqLeft = step[2]
|
||||
self.seqLeft = step[2] + 1
|
||||
end
|
||||
|
||||
function TitleState:spawnTrail()
|
||||
if not (self.trailColor or self.trailGray) then return end
|
||||
if self.frameCounter % 4 ~= 0 then return end
|
||||
if #self.trailSpawns == 0 then return end
|
||||
if self.frameCounter % self.trailSpawnEvery ~= 0 then return end
|
||||
local spawn = self.trailSpawns[self.trailSpawnIndex]
|
||||
self.trailSpawnIndex = self.trailSpawnIndex % #self.trailSpawns + 1
|
||||
if not spawn then return end
|
||||
self.trails[#self.trails + 1] = {
|
||||
x = spawn[1], y = spawn[2], phase = love.math.random(0, 255),
|
||||
x = spawn[1], y = spawn[2],
|
||||
phase = self.trailPhase or love.math.random(0, 255),
|
||||
}
|
||||
end
|
||||
|
||||
function TitleState:stepTrails()
|
||||
local alive = {}
|
||||
local maxX = self.trailMaxX or 200
|
||||
local silver = self.trailMode == "silver"
|
||||
for _, t in ipairs(self.trails) do
|
||||
t.x = t.x + 4
|
||||
t.y = t.y + 1
|
||||
t.phase = t.phase + 3
|
||||
t.drawY = t.y + math.floor(math.sin(t.phase / 16) * 2)
|
||||
t.x = t.x + self.trailStepX
|
||||
t.y = t.y + self.trailStepY
|
||||
t.phase = t.phase + self.trailPhaseStep
|
||||
if silver then
|
||||
t.drawY = t.y + signed(SpriteAnims.sine(t.phase, self.trailBobAmplitude))
|
||||
else
|
||||
t.drawY = t.y
|
||||
+ math.floor(math.sin(t.phase / 16) * self.trailBobAmplitude)
|
||||
end
|
||||
if t.x < maxX then alive[#alive + 1] = t end
|
||||
end
|
||||
self.trails = alive
|
||||
@@ -254,14 +286,17 @@ function TitleState:drawWidescreen(winW, winH)
|
||||
-- Let trails fly into the side bands.
|
||||
self.trailMaxX = math.ceil((winW - ox) / scale) + 16
|
||||
|
||||
-- Sky above the cloud line, paper white below : edge to edge. The fill has
|
||||
-- to match whichever baked set is showing, or the surround would stay blue
|
||||
-- around a grey screen.
|
||||
local sky = self:gray() and SKY_GRAY or SKY
|
||||
-- Sky above the cloud line, title.below under it (Gold's white cloud
|
||||
-- field, Silver's black sea) : edge to edge. The fill has to match
|
||||
-- whichever baked set is showing, or the surround would stay blue around a
|
||||
-- grey screen.
|
||||
local sky = self:gray() and SKY_GRAY or self.sky
|
||||
G.setColor(sky[1], sky[2], sky[3], 1)
|
||||
G.rectangle("fill", 0, 0, winW, math.max(0, cloudTop))
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local below = self.below
|
||||
G.setColor(below[1], below[2], below[3], 1)
|
||||
G.rectangle("fill", 0, cloudTop, winW, winH - cloudTop)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
|
||||
-- Clouds across the full window width, aligned to the GB cloud band.
|
||||
G.push()
|
||||
|
||||
Reference in New Issue
Block a user