mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
The new experience (#201)
* new launcher and save converts and pipeline * fixing bugs
This commit is contained in:
@@ -116,6 +116,16 @@ FieldDefaults.FIELD = {
|
||||
badgeGates = { ROUTE_22_GATE = { passedFlag = "PASSED_ROUTE22_GATE" } },
|
||||
-- VermilionGymSetDoorTile opens the motorized door once both locks are hit
|
||||
hiddenExtras = {
|
||||
-- PrintTrashText bins (#188); seeds stale caches missing the key
|
||||
printTrash = {
|
||||
SS_ANNE_KITCHEN = {
|
||||
{ x = 13, y = 5, facing = "down" },
|
||||
{ x = 13, y = 7, facing = "down" },
|
||||
},
|
||||
VERMILION_GYM = {
|
||||
{ x = 6, y = 1, facing = "down" },
|
||||
},
|
||||
},
|
||||
trashCans = { map = "VERMILION_GYM",
|
||||
doorBlock = { bx = 2, by = 2, block = 5 } },
|
||||
},
|
||||
|
||||
@@ -223,6 +223,12 @@ function Map:setBlock(bx, by, block)
|
||||
self.def.blocks[by * self.def.width + bx + 1] = block
|
||||
end
|
||||
|
||||
-- true if the cell's collision tile is a door tile
|
||||
-- (pokered IsPlayerStandingOnDoorTile)
|
||||
function Map:isDoorTileCell(cx, cy)
|
||||
return self.doorTiles[self:cellTile(cx, cy)] or false
|
||||
end
|
||||
|
||||
-- true if the cell's collision tile is a door or warp-activating tile
|
||||
function Map:isWarpTileCell(cx, cy)
|
||||
local t = self:cellTile(cx, cy)
|
||||
|
||||
+11
-2
@@ -101,9 +101,18 @@ function NPC:walkPhase()
|
||||
return (p >= 4 and p < 12) and 1 or 0
|
||||
end
|
||||
|
||||
-- Same contract as Player:pose -- the sheet, position, facing and step
|
||||
-- phase this frame renders to -- so a render pipeline can pose an NPC
|
||||
-- without caring which kind of entity it is. An NPC never hops, so the
|
||||
-- trailing hop flag is always false.
|
||||
function NPC:pose()
|
||||
return self.sprite, self.px, self.py, self.facing,
|
||||
self:walkPhase(), self.stepFlip, false
|
||||
end
|
||||
|
||||
function NPC:draw(camX, camY)
|
||||
self.sprite:draw(self.px, self.py, camX, camY, self.facing,
|
||||
self:walkPhase(), self.stepFlip)
|
||||
local sprite, px, py, facing, phase, flip = self:pose()
|
||||
sprite:draw(px, py, camX, camY, facing, phase, flip)
|
||||
end
|
||||
|
||||
return NPC
|
||||
|
||||
@@ -12,6 +12,7 @@ local Map = require("src.world.Map")
|
||||
local MapLoader = require("src.world.MapLoader")
|
||||
local NPC = require("src.world.NPC")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local Player = require("src.world.Player")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Screens = require("src.ui.Screens")
|
||||
@@ -20,6 +21,7 @@ local Tilt = require("src.render.Tilt")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Transition = require("src.render.Transition")
|
||||
local Warp = require("src.world.Warp")
|
||||
local Zoom = require("src.render.Zoom")
|
||||
|
||||
-- isOverworld marks the live world state for WorldAPI's stack scan
|
||||
local OverworldState = { isOpaque = true, isOverworld = true }
|
||||
@@ -716,16 +718,13 @@ function OverworldState:update(dt)
|
||||
end
|
||||
if self.healAnim then
|
||||
local ha = self.healAnim
|
||||
local Music = require("src.core.Music")
|
||||
if ha.jinglePlaying and not ha.jingleDone then
|
||||
ha.jingleDone = not Music.oneShotPlaying()
|
||||
end
|
||||
local ev = OverworldState.stepHealAnim(ha)
|
||||
if ev == "ball" then
|
||||
require("src.core.Sound").play(Game.data, "Healing_Machine")
|
||||
elseif ev == "jingle" then
|
||||
ha.jinglePlaying = Music.playOnce(Game.data, "Music_PkmnHealed")
|
||||
ha.jingleDone = not ha.jinglePlaying
|
||||
-- playOnce restores the map theme when the jingle ends; we no longer
|
||||
-- block the fighting-fit text on that (#157)
|
||||
require("src.core.Music").playOnce(Game.data, "Music_PkmnHealed")
|
||||
elseif ev == "done" then
|
||||
local done = ha.onDone
|
||||
self.healAnim = nil
|
||||
@@ -1509,6 +1508,15 @@ function OverworldState:tryHiddenObject(fx, fy)
|
||||
end
|
||||
end
|
||||
|
||||
-- PrintTrashText: SS Anne kitchen + Vermilion Gym non-puzzle can
|
||||
for _, h in ipairs(extras.printTrash and extras.printTrash[self.map.id] or {}) do
|
||||
if h.x == fx and h.y == fy then
|
||||
Game.stack:push(TextBox.new(Game, txt._VermilionGymTrashText
|
||||
or "Nope, there's\nonly trash here."))
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- the Vermilion Gym trash can lock puzzle
|
||||
if self.map.id == "VERMILION_GYM" then
|
||||
for _, h in ipairs(extras.trashCans.cans or {}) do
|
||||
@@ -2188,13 +2196,10 @@ function OverworldState:dexRating()
|
||||
Game.stack:push(TextBox.new(Game, completion .. "\f" .. rating))
|
||||
end
|
||||
|
||||
-- AnimateHealingMachine (engine/overworld/healing_machine.asm): the
|
||||
-- monitor lights, then one ball per party mon appears every 30 frames
|
||||
-- (SFX_HEALING_MACHINE each); the healed jingle plays while the machine
|
||||
-- sprites flash 8 times (an OBP1 xor every 10 frames), then a 32-frame
|
||||
-- beat once the jingle ends. Pure per-frame step over the ha table
|
||||
-- ({ balls, lit, timer, visible, jingleDone }); returns "ball"/"jingle"/
|
||||
-- "done" when the caller must fire the matching side effect.
|
||||
-- AnimateHealingMachine (engine/overworld/healing_machine.asm): balls
|
||||
-- every 30 frames, then jingle + FlashSprite8Times (8 x 10). #157: skip
|
||||
-- pokered's post-flash .waitLoop2 / DelayFrames 32 so fighting-fit is
|
||||
-- immediate; jingle still plays and restoreMap runs when it ends.
|
||||
function OverworldState.stepHealAnim(ha)
|
||||
ha.timer = ha.timer + 1
|
||||
ha.phase = ha.phase or "balls"
|
||||
@@ -2217,17 +2222,11 @@ function OverworldState.stepHealAnim(ha)
|
||||
ha.visible = not ha.visible
|
||||
ha.flashes = ha.flashes + 1
|
||||
if ha.flashes >= 8 then
|
||||
ha.phase = "wait"
|
||||
ha.visible = true
|
||||
ha.phase = "done"
|
||||
return "done"
|
||||
end
|
||||
end
|
||||
elseif ha.phase == "wait" then
|
||||
-- .waitLoop2: hold until the jingle ends, then 32 more frames
|
||||
if not ha.jingleDone then
|
||||
ha.timer = 0
|
||||
elseif ha.timer >= 32 then
|
||||
return "done"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2403,19 +2402,34 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
|
||||
Commands.hide_object(ctx, entry[1], entry[2])
|
||||
end
|
||||
end
|
||||
local lines = {}
|
||||
if reward.badge then
|
||||
Game.save.inventory[reward.badge] = 1
|
||||
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
|
||||
or reward.badge
|
||||
table.insert(lines, ("%s received\nthe %s!"):format(Game.save.player.name, name))
|
||||
end
|
||||
if reward.item then
|
||||
local inv = Game.save.inventory
|
||||
inv[reward.item] = (inv[reward.item] or 0) + 1
|
||||
local name = Game.data.items[reward.item] and Game.data.items[reward.item].name
|
||||
or reward.item
|
||||
table.insert(lines, ("%s received\n%s!"):format(Game.save.player.name, name))
|
||||
local idef = Game.data.items[reward.item]
|
||||
-- GiveItem -> CopyToStringBuffer for "{RAM:wStringBuffer}" received texts
|
||||
Game.stringBuffer = idef and idef.name or reward.item
|
||||
end
|
||||
local lines = {}
|
||||
if reward.dialogue then
|
||||
local text = Game.data.text or {}
|
||||
for _, label in ipairs(reward.dialogue) do
|
||||
if text[label] and text[label] ~= "" then
|
||||
table.insert(lines, text[label])
|
||||
end
|
||||
end
|
||||
elseif reward.badge or reward.item then
|
||||
if reward.badge then
|
||||
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
|
||||
or reward.badge
|
||||
table.insert(lines, ("%s received\nthe %s!"):format(Game.save.player.name, name))
|
||||
end
|
||||
if reward.item then
|
||||
local name = Game.stringBuffer or reward.item
|
||||
table.insert(lines, ("%s received\n%s!"):format(Game.save.player.name, name))
|
||||
end
|
||||
end
|
||||
if #lines > 0 then
|
||||
Game.stack:push(TextBox.new(Game, table.concat(lines, "\f")))
|
||||
@@ -2437,6 +2451,8 @@ end
|
||||
-- interposed NPCs / walls -- but unsigned 8-bit Y makes a sprite exactly 4
|
||||
-- tiles north of the player sit at Y=$fc, so |$3c-$fc|=$c0 and a range-4
|
||||
-- DOWN trainer does not engage that tile (Route 9 Bug Catcher / issue #76).
|
||||
-- Off-screen sprites (IMAGEINDEX=$ff) never engage: without that gate, the
|
||||
-- same 8-bit wrap makes far same-row trainers look in-range (#153/#183).
|
||||
local PLAYER_SCREEN_X, PLAYER_SCREEN_Y = 0x40, 0x3c
|
||||
local function u8(n) return n % 256 end
|
||||
local function calcDiff(a, b)
|
||||
@@ -2452,6 +2468,14 @@ local function trainerSightPixelDist(npc, player, horizontal)
|
||||
u8(PLAYER_SCREEN_Y + (npc.cellY - player.cellY) * 16))
|
||||
end
|
||||
|
||||
-- CheckSpriteAvailability (movement.asm): wXCoord/wYCoord = player - 4;
|
||||
-- visible when sprite is in [wCoord, wCoord + SCREEN_*/2 - 1] (GB 10x9).
|
||||
local function trainerSpriteOnScreen(npc, player)
|
||||
local dx = npc.cellX - player.cellX
|
||||
local dy = npc.cellY - player.cellY
|
||||
return dx >= -4 and dx <= 5 and dy >= -4 and dy <= 4
|
||||
end
|
||||
|
||||
-- STAY trainers with a facing spot the player crossing their line of
|
||||
-- sight (range from the extracted trainer headers), walk up and battle.
|
||||
function OverworldState:checkTrainerSight()
|
||||
@@ -2464,7 +2488,8 @@ function OverworldState:checkTrainerSight()
|
||||
-- walkers included (they sight between steps)
|
||||
if d.trainerClass and not npc.moving
|
||||
and not self:trainerDefeated(npc)
|
||||
and not mapScripts.talkScript(self.map.id, d.text) then
|
||||
and not mapScripts.talkScript(self.map.id, d.text)
|
||||
and trainerSpriteOnScreen(npc, p) then
|
||||
local header = Game.data:trainerHeader(self.map.def.label, d.index)
|
||||
local range = header and header.range or 0
|
||||
local vec = DIRVEC[npc.facing]
|
||||
@@ -3208,7 +3233,7 @@ function OverworldState:takeWarp(warpDef)
|
||||
self:startWarpTo(destMap, x, y, facing)
|
||||
return
|
||||
end
|
||||
self.doorWarp = true -- door SFX + outdoor walk-out step
|
||||
self.doorWarp = true -- door SFX + PlayerStepOutFromDoor walk-out
|
||||
self:startWarpTo(destMap, x, y, facing)
|
||||
end
|
||||
|
||||
@@ -3293,12 +3318,12 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
|
||||
local outdoor = Map.isOutdoor(self.map.def)
|
||||
require("src.core.Sound").play(Game.data,
|
||||
outdoor and "Go_Outside" or "Go_Inside")
|
||||
-- stepping out of an outdoor door/cave entrance (the original's
|
||||
-- walk-out). Auto-walk leaves the mat, so the arrival disable
|
||||
-- PlayerStepOutFromDoor (engine/overworld/auto_movement.asm): any
|
||||
-- warp that lands on a door tile auto-steps south once, indoor or
|
||||
-- outdoor. Auto-walk leaves the mat, so the arrival disable
|
||||
-- (warpEntryCell / justWarped) is unnecessary -- and would let you
|
||||
-- stand on the door without re-entering if you hold back into it.
|
||||
if outdoor and self.player.facing == "down"
|
||||
and self.map:isWarpTileCell(self.player.cellX, self.player.cellY) then
|
||||
if self.map:isDoorTileCell(self.player.cellX, self.player.cellY) then
|
||||
self.warpEntryCell = nil
|
||||
self.justWarped = false
|
||||
self:scriptMove(self.player, "down", 1)
|
||||
@@ -3539,11 +3564,22 @@ function OverworldState:drawWorld()
|
||||
-- tilt is active). So the ground draw calls below never change with tilt;
|
||||
-- only the sprite/FX draw path below them branches. The sorts below only
|
||||
-- reorder (no draws), so they run once for both paths.
|
||||
local tilt = Tilt.active()
|
||||
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
|
||||
self.map.renderer:draw(cam.x, bgY, vw, vh)
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
|
||||
-- A render pipeline (src/render/Pipelines.lua) replaces the ground draw
|
||||
-- entirely with geometry of its own, so it is decided before tilt and
|
||||
-- wins over it. It falls back to the tilt/flat path whenever it cannot
|
||||
-- run this frame -- headless, a driver with no depth canvas, or a mod
|
||||
-- that threw -- so no caller ever sees a blank frame.
|
||||
local pipelineId = Pipelines.worldPipeline()
|
||||
local tilt = (not pipelineId) and Tilt.active()
|
||||
-- the pipeline's finished world image, once it has run; nil keeps every
|
||||
-- path below on the vanilla flat/tilt draw
|
||||
local override
|
||||
if not pipelineId then
|
||||
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
|
||||
self.map.renderer:draw(cam.x, bgY, vw, vh)
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
|
||||
end
|
||||
end
|
||||
-- per-billboard SGB palette source; only needed (and only paid for) when
|
||||
-- tilting. nil headless / on stale palettes -> billboards go uncolorized.
|
||||
@@ -3774,7 +3810,114 @@ function OverworldState:drawWorld()
|
||||
end
|
||||
end
|
||||
|
||||
if not tilt then
|
||||
if pipelineId then
|
||||
-- === PIPELINE PATH: a mod owns the world pass. ======================
|
||||
-- It renders terrain and characters however it likes and hands back one
|
||||
-- window-resolution image; the field FX stay ordinary 2D draws
|
||||
-- composited on top by ctx.drawFx, each anchored to where its ground
|
||||
-- point projects under the pipeline's own camera. That is the direct
|
||||
-- analogue of what :billboard does for tilt, and it keeps exactly one
|
||||
-- copy of every effect: the closures above are the ones that run.
|
||||
local pw, ph = love.graphics.getDimensions()
|
||||
local pscale = Zoom.scale(Game.renderer:fitScale())
|
||||
local ctx = {
|
||||
state = self, cam = cam, vw = vw, vh = vh, bgY = bgY,
|
||||
width = pw, height = ph, scale = pscale,
|
||||
level = Pipelines.level(pipelineId),
|
||||
-- the SGB world palette a map draws under; nil in the true-colour
|
||||
-- modes, whose art is already baked (and must not be re-mapped)
|
||||
paletteFor = function(map)
|
||||
return PaletteFX.pal(Game.data, self:paletteNameFor(map or self.map))
|
||||
end,
|
||||
spriteColors = function(map)
|
||||
if PaletteFX.usesGbcPack() then return nil end
|
||||
return PaletteFX.pal(Game.data, self:paletteNameFor(map or self.map))
|
||||
end,
|
||||
fx = { heal = fxHeal, dust = fxDust, cutTree = fxCutTree,
|
||||
emote = fxEmote, dark = fxDark, bird = fxBird, rod = fxRod },
|
||||
}
|
||||
-- Draw every active field FX into the finished scene. `project(wx, wy)`
|
||||
-- maps a world point to canvas pixels (nil when it is behind the
|
||||
-- camera) and `scale` is canvas pixels per world pixel; the pipeline
|
||||
-- owns the camera, this owns where each effect belongs and how the
|
||||
-- closures' flat coordinates are slid onto the projected anchor.
|
||||
-- Deliberately unscaled by depth, like :billboard: an effect keeps its
|
||||
-- crisp authored size and only its anchor moves.
|
||||
ctx.drawFx = function(project, scale)
|
||||
scale = scale or pscale
|
||||
local colors = ctx.spriteColors()
|
||||
local function at(drawFn, wx, wy)
|
||||
if not drawFn then return end
|
||||
local sx, sy = project(wx, wy)
|
||||
if not sx then return end -- behind the camera
|
||||
local shader = colors and PaletteFX.shader() or nil
|
||||
if shader then
|
||||
PaletteFX.sendColors(shader, colors)
|
||||
love.graphics.setShader(shader)
|
||||
end
|
||||
-- the closures draw relative to the flat foot; slide that onto the
|
||||
-- projected anchor, in world-pixel units inside the scaled transform
|
||||
local fx, fy = wx - cam.x, wy - cam.y
|
||||
love.graphics.push()
|
||||
love.graphics.scale(scale, scale)
|
||||
love.graphics.translate(sx / scale - fx, sy / scale - fy)
|
||||
drawFn()
|
||||
love.graphics.pop()
|
||||
if shader then love.graphics.setShader() end
|
||||
end
|
||||
-- ground-hugging effects sit on the cell they belong to
|
||||
if self.dustAnim then
|
||||
at(fxDust, self.dustAnim.x * 16 + 8, self.dustAnim.y * 16 + 8)
|
||||
end
|
||||
if self.cutAnim then
|
||||
at(fxCutTree, self.cutAnim.x * 16 + 8, self.cutAnim.y * 16 + 16)
|
||||
end
|
||||
if self.healAnim then
|
||||
at(fxHeal, self.healAnim.px + 8, self.healAnim.py + 16)
|
||||
end
|
||||
-- standing effects anchor at the foot of whoever they belong to
|
||||
if self.emote and self.emote.npc then
|
||||
at(fxEmote, self.emote.npc.px + 8, self.emote.npc.py + 16)
|
||||
end
|
||||
if self.flyAnim then
|
||||
at(fxBird, self.player.px + 8, self.player.py + 16)
|
||||
end
|
||||
if self.fishing then
|
||||
at(fxRod, self.player.px + 8, self.player.py + 16)
|
||||
end
|
||||
-- Rock Tunnel darkness is a screen-space light window, not a ground
|
||||
-- object: draw it flat over the finished scene like the tilt path.
|
||||
-- It fills the view in world-pixel units, so it only needs the scale.
|
||||
if self.dark then
|
||||
love.graphics.push()
|
||||
love.graphics.scale(scale, scale)
|
||||
fxDark()
|
||||
love.graphics.pop()
|
||||
end
|
||||
end
|
||||
override = Pipelines.drawWorld(pipelineId, ctx)
|
||||
-- world post-processes (a miniature-diorama blur, a colour grade) fold
|
||||
-- over the finished scene here, so they never touch the UI drawn on top
|
||||
if override then
|
||||
override = Pipelines.worldPresent(override, ctx)
|
||||
end
|
||||
Game.renderer:setWorldOverride(override)
|
||||
if not override then
|
||||
-- The pipeline declined this frame (nothing to draw, or it threw and
|
||||
-- was retired). The ground pass was skipped on its behalf above, so
|
||||
-- draw it now and fall through to the flat path below rather than
|
||||
-- compositing an empty canvas.
|
||||
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
|
||||
self.map.renderer:draw(cam.x, bgY, vw, vh)
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if override then
|
||||
-- the pipeline owns the whole frame; nothing else draws into the world
|
||||
elseif not tilt then
|
||||
-- === FLAT PATH: everything into the one world canvas, as before =====
|
||||
-- OBP-baked sprites replay after the zone pass in GBC mode, so their
|
||||
-- grass feet-overdraw must replay over them too, colorized with the
|
||||
|
||||
+34
-16
@@ -140,28 +140,29 @@ end
|
||||
|
||||
local SPIN_ORDER = { "down", "left", "up", "right" }
|
||||
|
||||
function Player:draw(camX, camY)
|
||||
-- What this frame renders to: the sheet, where it sits, which way it faces
|
||||
-- and how far through a step it is. Shared by the 2D draw below and by a
|
||||
-- render pipeline's own geometry (src/render/Pipelines.lua), so the two can
|
||||
-- never disagree about which sprite or facing is current.
|
||||
--
|
||||
-- The last return says the player is mid-ledge-hop, which is what the 2D
|
||||
-- path draws the ground shadow from and a 3D path turns into vertical lift.
|
||||
--
|
||||
-- This ADVANCES the surf-bob and spinner timers, so exactly one of pose()
|
||||
-- and draw() may run per frame -- and draw() is written in terms of pose()
|
||||
-- to keep that true by construction. (hopFrames counts down in
|
||||
-- Player:update, on the fixed step, so it is safe to read here.)
|
||||
function Player:pose()
|
||||
local py = self.py
|
||||
-- ledge hops arc (set for 2 cells by the ledge handler); surfing bobs.
|
||||
-- hopFrames counts down in Player:update (fixed step), never here.
|
||||
local hopping = false
|
||||
-- ledge hops arc (set for 2 cells by the ledge handler); surfing bobs
|
||||
if self.hopFrames and self.hopFrames > 0 then
|
||||
local total = self.hopTotal or 32
|
||||
-- update runs before draw, so remaining N means N steps already
|
||||
-- consumed this hop → t matches the old draw-side post-decrement phase
|
||||
local t = 1 - self.hopFrames / total
|
||||
py = py - math.floor(10 * math.sin(t * math.pi) + 0.5)
|
||||
-- the shadow stays on the ground under the jumper: one 8x8 tile
|
||||
-- mirrored into a 2x2 block (normal/XFLIP/YFLIP/both) whose top-left
|
||||
-- is 8px below the sprite's standing top-left (LoadHoppingShadowOAM +
|
||||
-- LedgeHoppingShadowOAMBlock, engine/overworld/ledges.asm)
|
||||
if self.shadowImg then
|
||||
local sx = math.floor(self.px - camX)
|
||||
local sy = math.floor(self.py - camY) - 4 + 8
|
||||
love.graphics.draw(self.shadowImg, sx, sy)
|
||||
love.graphics.draw(self.shadowImg, sx + 16, sy, 0, -1, 1)
|
||||
love.graphics.draw(self.shadowImg, sx, sy + 16, 0, 1, -1)
|
||||
love.graphics.draw(self.shadowImg, sx + 16, sy + 16, 0, -1, -1)
|
||||
end
|
||||
hopping = true
|
||||
elseif self.surfing then
|
||||
self.bobTimer = ((self.bobTimer or 0) + 1) % 32
|
||||
py = py + (self.bobTimer < 16 and 0 or 1)
|
||||
@@ -186,7 +187,24 @@ function Player:draw(camX, camY)
|
||||
end
|
||||
local sprite = (self.surfing and self.surfSprite)
|
||||
or (self.onBike and self.bikeSprite) or self.sprite
|
||||
sprite:draw(self.px, py, camX, camY, facing, phase, flip)
|
||||
return sprite, self.px, py, facing, phase, flip, hopping
|
||||
end
|
||||
|
||||
function Player:draw(camX, camY)
|
||||
local sprite, px, py, facing, phase, flip, hopping = self:pose()
|
||||
-- the shadow stays on the ground under the jumper: one 8x8 tile
|
||||
-- mirrored into a 2x2 block (normal/XFLIP/YFLIP/both) whose top-left
|
||||
-- is 8px below the sprite's standing top-left (LoadHoppingShadowOAM +
|
||||
-- LedgeHoppingShadowOAMBlock, engine/overworld/ledges.asm)
|
||||
if hopping and self.shadowImg then
|
||||
local sx = math.floor(self.px - camX)
|
||||
local sy = math.floor(self.py - camY) - 4 + 8
|
||||
love.graphics.draw(self.shadowImg, sx, sy)
|
||||
love.graphics.draw(self.shadowImg, sx + 16, sy, 0, -1, 1)
|
||||
love.graphics.draw(self.shadowImg, sx, sy + 16, 0, 1, -1)
|
||||
love.graphics.draw(self.shadowImg, sx + 16, sy + 16, 0, -1, -1)
|
||||
end
|
||||
sprite:draw(px, py, camX, camY, facing, phase, flip)
|
||||
end
|
||||
|
||||
return Player
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- S.S. Anne 1F cabins: ROM packs rooms so hallway doors map right→left
|
||||
-- onto the 3×2 rooms map (warp 1 = top-left = rightmost door). Survey
|
||||
-- zoom makes that look left/right flipped vs 2F/B1F. Reorder door→cell
|
||||
-- and slide objects so each door keeps its OG occupants while L→R doors
|
||||
-- land in reading order on the rooms map.
|
||||
|
||||
local SsAnneLayout = {}
|
||||
|
||||
local function leftmostRoomDoor(hall)
|
||||
local best
|
||||
for _, w in ipairs(hall.warps or {}) do
|
||||
if w.destMap == "SS_ANNE_1F_ROOMS" then
|
||||
if not best or w.x < best.x then best = w end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
function SsAnneLayout.apply(maps)
|
||||
if not maps then return false end
|
||||
local hall = maps.SS_ANNE_1F
|
||||
local rooms = maps.SS_ANNE_1F_ROOMS
|
||||
if not hall or not rooms then return false end
|
||||
local left = leftmostRoomDoor(hall)
|
||||
-- already hallway-ordered (leftmost door → rooms warp 1)
|
||||
if not left or left.destWarp == 1 then return false end
|
||||
|
||||
local doors = {}
|
||||
for _, w in ipairs(hall.warps) do
|
||||
if w.destMap == "SS_ANNE_1F_ROOMS" then doors[#doors + 1] = w end
|
||||
end
|
||||
table.sort(doors, function(a, b) return a.x < b.x end)
|
||||
for i, w in ipairs(doors) do w.destWarp = i end
|
||||
|
||||
-- rooms warps 1..6 return through hall warps 8..3 (L→R cabin doors)
|
||||
for i, w in ipairs(rooms.warps or {}) do
|
||||
if w.destMap == "SS_ANNE_1F" then w.destWarp = 9 - i end
|
||||
end
|
||||
|
||||
local warpPos = {}
|
||||
for i, w in ipairs(rooms.warps or {}) do
|
||||
warpPos[i] = { x = w.x, y = w.y }
|
||||
end
|
||||
for _, o in ipairs(rooms.objects or {}) do
|
||||
local col, row = math.floor(o.x / 10), math.floor(o.y / 10)
|
||||
local oldK
|
||||
for k, p in ipairs(warpPos) do
|
||||
if math.floor(p.x / 10) == col and math.floor(p.y / 10) == row then
|
||||
oldK = k
|
||||
break
|
||||
end
|
||||
end
|
||||
if oldK then
|
||||
local newK = 7 - oldK
|
||||
o.x = o.x + (warpPos[newK].x - warpPos[oldK].x)
|
||||
o.y = o.y + (warpPos[newK].y - warpPos[oldK].y)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return SsAnneLayout
|
||||
Reference in New Issue
Block a user