mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 04:06:10 +02:00
CLOSES #1211, CLOSES #1228, CLOSES #1229, CLOSES #1232, CLOSES #1251, CLOSES #1265, CLOSES #1267, CLOSES #1276, CLOSES #1279, CLOSES #1282, CLOSES #1293, CLOSES #1296, CLOSES #1303, CLOSES #1329, CLOSES #1338, CLOSES #1341, CLOSES #1343, CLOSES #1344, CLOSES #1368, CLOSES #1385, CLOSES #1388, CLOSES #1389, CLOSES #1391
This commit is contained in:
+16
-29
@@ -33,6 +33,7 @@ function NPC.new(data, mapId, objDef)
|
||||
self.facing = FACING_FROM_RANGE[objDef.range] or "down"
|
||||
self.moving = false
|
||||
self.progress = 0
|
||||
self.animClock = 0
|
||||
self.stepFlip = false
|
||||
self.frozen = false -- scripts freeze NPCs while talking
|
||||
self.wanders = objDef.movement == "WALK"
|
||||
@@ -52,31 +53,15 @@ function NPC:facePlayer(player)
|
||||
end
|
||||
|
||||
function NPC:update(map, entities)
|
||||
-- An NPC tile is 32 frames, half the player's rate: TryWalking loads
|
||||
-- WALKANIMATIONCOUNTER with $10 and UpdateSpriteInWalkingAnimation adds
|
||||
-- the 1px step vector once per call (engine/overworld/movement.asm), but
|
||||
-- UpdateSprites runs once per OverworldLoop pass and every pass opens
|
||||
-- with two DelayFrame calls (home/overworld.asm) -- so those 16 ticks
|
||||
-- cost 32 frames for one 16px cell, against AdvancePlayerSprite's 8
|
||||
-- ticks of 2px. That halving is why pokeyellow's NormalPikachuFollow
|
||||
-- needs TryDoubleAddPikachuStepVectorToScreenPixelCoords to keep up.
|
||||
--
|
||||
-- self.stepFrames overrides the shared walk for an object whose
|
||||
-- step has to stay in phase with something else: Yellow's follower
|
||||
-- Pikachu takes the player's own step length, halved while it is more
|
||||
-- than a cell behind (FastPikachuFollow, engine/pikachu/
|
||||
-- pikachu_follow.asm). self.hopStep is the same file's $5-$8 hop
|
||||
-- command: two cells of travel inside one step's frames
|
||||
-- (DoubleAddPikachuStepVectorToScreenPixelCoords), which is why the
|
||||
-- pixel span doubles while the frame count does not. Nothing else sets
|
||||
-- either field, so every other object keeps the constant (#410, #409).
|
||||
-- engine/overworld/movement.asm:301, 32 frames per NPC cell; stepFrames is
|
||||
-- the follower's own step length (#410, #409).
|
||||
local stepLen = self.stepFrames or STEP_FRAMES
|
||||
local span = self.hopStep and 2 or 1
|
||||
if self.moving then
|
||||
self.progress = self.progress + 1
|
||||
-- NPC_CHANGE_FACING: animate the walk cycle in place, no translation
|
||||
-- (movement.asm ChangeFacingDirection zeroes the delta); px/py stay
|
||||
-- pinned to the current cell while walkPhase() cycles.
|
||||
self.animClock = (self.animClock or 0) + 1
|
||||
-- NPC_CHANGE_FACING (movement.asm ChangeFacingDirection): walk cycle in
|
||||
-- place, no translation.
|
||||
if self.marching then
|
||||
if self.progress >= stepLen then
|
||||
self.progress = 0
|
||||
@@ -122,18 +107,20 @@ end
|
||||
|
||||
function NPC:walkPhase()
|
||||
if not self.moving then return 0 end
|
||||
local stepLen = self.stepFrames or STEP_FRAMES
|
||||
local p = self.progress % stepLen
|
||||
return (p >= stepLen / 4 and p < stepLen * 3 / 4) and 1 or 0
|
||||
-- engine/overworld/movement.asm:301
|
||||
local p = (self.animClock or 0) % 16
|
||||
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.
|
||||
-- Same contract as Player:pose; an NPC never hops, so the trailing hop
|
||||
-- flag is always false.
|
||||
function NPC:pose()
|
||||
local flip = self.stepFlip
|
||||
if self.moving then
|
||||
flip = math.floor((self.animClock or 0) / 16) % 2 == 1
|
||||
end
|
||||
return self.sprite, self.px, self.py, self.facing,
|
||||
self:walkPhase(), self.stepFlip, false
|
||||
self:walkPhase(), flip, false
|
||||
end
|
||||
|
||||
function NPC:draw(camX, camY)
|
||||
|
||||
@@ -3090,9 +3090,12 @@ function OverworldState:finishNurseHeal(bye, onDone, npc)
|
||||
end))
|
||||
end
|
||||
if not npc then farewell() return end
|
||||
npc.frameOverride = 3
|
||||
-- engine/events/pokecenter.asm:36-39; Yellow's walk-down pose when the
|
||||
-- sheet has it (pokeyellow engine/events/pokecenter.asm:82-88)
|
||||
local yellow = GameVersion.isYellow()
|
||||
npc.frameOverride = (yellow and npc.sprite.frames[3]) and 3 or 1
|
||||
-- bubble = false is the silent world hold, this port's DelayFrames
|
||||
self.emote = { npc = npc, frames = 20, bubble = false, onDone = function()
|
||||
self.emote = { npc = npc, frames = yellow and 40 or 20, bubble = false, onDone = function()
|
||||
npc.frameOverride = nil
|
||||
npc:facePlayer(self.player)
|
||||
farewell()
|
||||
|
||||
+59
-12
@@ -1074,11 +1074,12 @@ function World:load()
|
||||
save.pokedex = save.pokedex or { seen = {}, caught = {} }
|
||||
save.pokedex.seen[mon.species] = true
|
||||
save.pokedex.caught[mon.species] = true
|
||||
-- AddPartyMon's `.registerunowndex` runs on the same path, so a gifted
|
||||
-- Unown lands in the form list too (nothing in Gold gives one, but the
|
||||
-- cart's check is on the species, not on where it came from).
|
||||
-- AddPartyMon's `.registerunowndex` runs on the same path, so a
|
||||
-- gifted Unown lands in the form list too (move_mon.asm:347).
|
||||
Unown.registerCatch(save, mon)
|
||||
end
|
||||
-- engine/pokemon/move_mon.asm:1632-1645
|
||||
return mon
|
||||
end,
|
||||
giveItem = function(itemIndex, qty)
|
||||
local data = self.game and self.game.data
|
||||
@@ -2402,12 +2403,43 @@ function World:rollWild()
|
||||
return { species = def.index, level = roll.level }
|
||||
end
|
||||
|
||||
-- GetMapMusic (home/map.asm:2550)
|
||||
function World.mapMusicLabel(audio, musicByte, rocketsMahogany, rocketsRadioTower)
|
||||
if type(musicByte) ~= "number" then return nil end
|
||||
local MUSIC_MAHOGANY_MART = 100 -- constants/music_constants.asm:100
|
||||
local RADIO_TOWER_MUSIC = 0x80 -- constants/music_constants.asm:109
|
||||
local order = audio and audio.musicOrder
|
||||
local songs = audio and audio.songs
|
||||
local label
|
||||
if musicByte == MUSIC_MAHOGANY_MART then
|
||||
label = rocketsMahogany and "Music_RocketHideout" or "Music_CherrygroveCity"
|
||||
elseif musicByte >= RADIO_TOWER_MUSIC then
|
||||
label = rocketsRadioTower and "Music_RocketTheme"
|
||||
or (order and order[(musicByte - RADIO_TOWER_MUSIC) + 1])
|
||||
else
|
||||
return nil
|
||||
end
|
||||
if label and label ~= "Music_Nothing" and songs and songs[label] then
|
||||
return label
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function World:mapMusicSong(mapId)
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
local def = self.maps and self.maps[mapId]
|
||||
-- ENGINE_ROCKETS_IN_MAHOGANY / _RADIO_TOWER (data/events/engine_flags.asm:40,:36)
|
||||
return World.mapMusicLabel(audio, def and def.music,
|
||||
self:engineFlag(22), self:engineFlag(18))
|
||||
end
|
||||
|
||||
function World:playMapMusic()
|
||||
local data = self.game and self.game.data
|
||||
if data and data.audio and data.audio.runtime and self.map then
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
FieldMoves.isSurfing(self.playerState), nil,
|
||||
self:mapMusicSong(self.map.id))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3255,7 +3287,8 @@ function World:surfStartStep(mon)
|
||||
if audio and audio.runtime and self.map then
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(self.game.data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
FieldMoves.isSurfing(self.playerState), nil,
|
||||
self:mapMusicSong(self.map.id))
|
||||
end
|
||||
if p.scriptStep then p:scriptStep(p.facing) end
|
||||
self.fieldMove = { phase = "step" }
|
||||
@@ -3565,7 +3598,7 @@ function World:rareWildMon()
|
||||
local entry = self.encounters and self.encounters.grass
|
||||
and map and self.encounters.grass[map.id]
|
||||
if not entry or not entry.slots then return nil end
|
||||
local key = (self.daytime == "DARK") and "NITE" or (self.daytime or "DAY")
|
||||
local key = self.tod or "DAY"
|
||||
local slots = entry.slots[key] or entry.slots.DAY
|
||||
if not slots then return nil end
|
||||
local rare = slots[4 + math.random(3)]
|
||||
@@ -3958,7 +3991,7 @@ function World:rollEncounter(kind, terrain, tables, vanilla)
|
||||
-- Same guard World:rockRandom uses: a headless suite has no love global.
|
||||
rng = (love and love.math and love.math.random) or math.random,
|
||||
kind = kind,
|
||||
daytime = self.daytime,
|
||||
daytime = self.tod,
|
||||
environment = map and map.def and map.def.environment,
|
||||
tables = tables,
|
||||
data = self.game and self.game.data,
|
||||
@@ -4021,7 +4054,8 @@ function World:tryWildEncounter()
|
||||
if onWater then
|
||||
rate = Encounter.waterRate(tables, map.id)
|
||||
else
|
||||
rate = Encounter.grassRate(tables, map.id, self.daytime)
|
||||
-- engine/overworld/wildmons.asm:283
|
||||
rate = Encounter.grassRate(tables, map.id, self.tod)
|
||||
end
|
||||
-- ApplyMusicEffectOnEncounterRate runs first (wildmons.asm:213-215).
|
||||
rate = World.musicEncounterRate(rate, Music.mapSong())
|
||||
@@ -4315,6 +4349,14 @@ function World:rollFishing(rod)
|
||||
-- the flag and nothing about the map changes. Roamers.Swarm.fishing is the
|
||||
-- same store CheckSwarmFlag clears when the swarm expires.
|
||||
local swarm = Roamers.Swarm.fishing(game.save)
|
||||
-- engine/events/fish.asm:24-30
|
||||
local groupRow = self.encounters.fishGroups
|
||||
and self.encounters.fishGroups[
|
||||
Encounter.fishGroupFor(self.encounters, group, swarm)]
|
||||
if groupRow and groupRow.chance
|
||||
and not Encounter.triggers(groupRow.chance, nil) then
|
||||
return "nibble"
|
||||
end
|
||||
local roll
|
||||
if Runtime.wantsHook("encounter.fishing") then
|
||||
-- Gen 1's three arguments, in Gen 1's order: the rod, the map, and the
|
||||
@@ -5100,7 +5142,7 @@ function World:sweetScentEncounter()
|
||||
local tables = self:wildTables()
|
||||
local onWater = FieldMoves.encounterTable(collision) == "water"
|
||||
local rate = onWater and Encounter.waterRate(tables, map.id)
|
||||
or Encounter.grassRate(tables, map.id, self.daytime)
|
||||
or Encounter.grassRate(tables, map.id, self.tod)
|
||||
if not (rate and rate > 0) then return false end
|
||||
-- CheckEncounterRoamMon, the first thing ChooseWildEncounter itself does:
|
||||
-- a beast REPLACES the map's own slot rather than adding to it.
|
||||
@@ -5374,7 +5416,8 @@ function World:runSurf(result)
|
||||
if audio and audio.runtime and self.map then
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(self.game.data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
FieldMoves.isSurfing(self.playerState), nil,
|
||||
self:mapMusicSong(self.map.id))
|
||||
end
|
||||
if self.player and self.player.scriptStep then
|
||||
self.player:scriptStep(self.player.facing)
|
||||
@@ -5886,6 +5929,8 @@ function World:startBattle(opts, onDone)
|
||||
-- World:startCatchTutorial sets it.
|
||||
tutorial = opts.tutorial,
|
||||
onDone = function(outcome)
|
||||
-- WildBattleScript's reloadmapafterbattle (engine/overworld/events.asm:1158-1162)
|
||||
self.wildCooldown = 5
|
||||
self.battleActive = nil
|
||||
game.stack:pop()
|
||||
-- wBattleResult (constants/battle_constants.asm): WIN 0, LOSE 1, DRAW 2.
|
||||
@@ -8540,7 +8585,8 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
if audio and audio.runtime then
|
||||
if not (FieldMoves.isBiking(self.playerState) and self:playBikeMusic()) then
|
||||
Music.playMap(self.game.data, mapId, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
FieldMoves.isSurfing(self.playerState), nil,
|
||||
self:mapMusicSong(mapId))
|
||||
end
|
||||
end
|
||||
-- Fires with the map fully built and BEFORE the map's own scene script, so a
|
||||
@@ -8928,7 +8974,8 @@ function World:movePlayer(dir)
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
if audio and audio.runtime then
|
||||
Music.playMap(self.game.data, map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
FieldMoves.isSurfing(self.playerState), nil,
|
||||
self:mapMusicSong(map.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user