mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
CLOSES #1474, CLOSES #1475, CLOSES #1477, CLOSES #1482, CLOSES #1512, CLOSES #1516, CLOSES #1558, CLOSES #1569, CLOSES #1578, CLOSES #1579
This commit is contained in:
+87
-23
@@ -1364,7 +1364,7 @@ function BattleState:updateQueue()
|
||||
-- subanimation (or just the coarse fx when animations are off).
|
||||
-- item.hit carries the target's blink + damage sound, applied when
|
||||
-- the animation ends (hitRow rows carry a hit with no animation --
|
||||
-- thrash/rage continuation turns that skip the announcement).
|
||||
-- Mimic, whose animation waits on a successful copy).
|
||||
if item.anim or item.hitRow then
|
||||
-- PlayMoveAnimation writes wAnimationID, calls Delay3, and only then
|
||||
-- jumps to MoveAnimation (core.asm:6635-6640), so three frames pass
|
||||
@@ -2708,9 +2708,11 @@ function BattleState:resolveSwitch(newMon)
|
||||
self.afterQueue = "menu"
|
||||
self:act(function()
|
||||
-- SwitchPlayerMon (core.asm:2419-2423): RetreatMon prints over the
|
||||
-- outgoing pic and holds 50 frames before the mon is recalled
|
||||
-- outgoing pic and holds 50 frames, then AnimateRetreatingPlayerMon
|
||||
-- runs before the mon is recalled
|
||||
self:sayNextAuto(self:withdrawText(self.player.name),
|
||||
Timing.SWITCH_PLAYER_MON)
|
||||
self:queueRetreatAnim()
|
||||
self:actNext(function()
|
||||
self:restoreMimicked(self.player) -- the battle copy leaves with it
|
||||
local previous = self.player
|
||||
@@ -3322,6 +3324,23 @@ function BattleState:queueSendOutAnim(append)
|
||||
if append then self:act(fn) else self:actNext(fn) end
|
||||
end
|
||||
|
||||
-- AnimateRetreatingPlayerMon (core.asm:1769-1796); the Yellow starter Pikachu
|
||||
-- slides off instead (pokeyellow core.asm:1862-1866, animations.asm:1259)
|
||||
function BattleState:queueRetreatAnim()
|
||||
if self:starterPikachuSendOut() then
|
||||
self:actNext(function() self:slidePic("playerMon", 0, -64, 8, 3) end)
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
table.insert(self.queue, self.nextInsert, { wait = 24 })
|
||||
self:actNext(function() self:slidePic("playerMon") end)
|
||||
else
|
||||
self:actNext(function()
|
||||
self.shrinkOut = { battler = self.player, frame = 0 }
|
||||
end)
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
table.insert(self.queue, self.nextInsert, { wait = 7 })
|
||||
end
|
||||
end
|
||||
|
||||
-- Should the low-health alarm sound this frame? pokered keys it off
|
||||
-- the drawn bar color: DrawPlayerHUDAndHPBar (core.asm:1846-1875) sets
|
||||
-- wLowHealthAlarm bit 7 when GetHealthBarColor says the player bar is
|
||||
@@ -3538,6 +3557,12 @@ function BattleState:updateFx()
|
||||
self.growIn.frame = self.growIn.frame + 1
|
||||
if self.growIn.frame >= 12 then self.growIn = nil end
|
||||
end
|
||||
-- the retreat shrink (AnimateRetreatingPlayerMon): 4+3 frames, then the
|
||||
-- 7x7 area is cleared for good
|
||||
if self.shrinkOut then
|
||||
self.shrinkOut.frame = self.shrinkOut.frame + 1
|
||||
if self.shrinkOut.frame >= 7 then self.shrinkOut = nil end
|
||||
end
|
||||
-- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren
|
||||
-- loops while the player's bar is red; see lowHealthAlarmActive
|
||||
local Sound = require("src.core.Sound")
|
||||
@@ -3802,6 +3827,8 @@ function BattleState:statusInterrupt(user, target, selectedId)
|
||||
{ rng = self.rng, forceCrit = false, typeless = true,
|
||||
screens = target })
|
||||
self:sayNext(self:romText("_HurtItselfText", "It hurt itself in\nits confusion!"))
|
||||
-- HandleSelfConfusionDamage (core.asm:3706-3714, enemy side :5807-5811)
|
||||
self:animNext("POUND", not user.isPlayer)
|
||||
self:clearVolatiles(user, true)
|
||||
self:applyDamage(user, dmg)
|
||||
if user.mon.hp <= 0 then self:onFaint(user) end
|
||||
@@ -3894,18 +3921,26 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
||||
end
|
||||
|
||||
self.moveAnimRow = nil
|
||||
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
|
||||
self:sayNextAuto(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
|
||||
-- the move's animation plays right after the announcement; the
|
||||
-- damage path attaches the target's hit blink to this row so the
|
||||
-- blink follows the animation (pokered's order). Mimic is the
|
||||
-- exception (announceAnim = false): PlayCurrentMoveAnimation runs
|
||||
-- only after a successful copy, never on a miss -- applyMimic queues it
|
||||
if not (record and record.announceAnim == false) then
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
self.moveAnimRow = { anim = move.id, attackerIsPlayer = user.isPlayer }
|
||||
table.insert(self.queue, self.nextInsert, self.moveAnimRow)
|
||||
local thrashing = user.thrashTurns and moveInst == user.thrashMove
|
||||
and user.thrashAnnounced or false
|
||||
if thrashing then
|
||||
-- .ThrashingAboutCheck (core.asm:3531-3552)
|
||||
self:sayNextAuto(self:romText("_ThrashingAboutText", "%s's\nthrashing about!",
|
||||
displayName(user)))
|
||||
user.thrashTurns = user.thrashTurns - 1
|
||||
if user.thrashTurns <= 0 then
|
||||
user.thrashTurns, user.thrashMove, user.thrashAnnounced = nil, nil, nil
|
||||
if not user.confusedTurns then user.confusedTurns = self.rng(2, 5) end
|
||||
end
|
||||
else
|
||||
self:sayNextAuto(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
|
||||
end
|
||||
-- PlayCurrentMoveAnimation follows the announcement; Mimic (announceAnim
|
||||
-- = false) queues it from applyMimic after a successful copy
|
||||
if not (record and record.announceAnim == false) then
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
self.moveAnimRow = { anim = move.id, attackerIsPlayer = user.isPlayer }
|
||||
table.insert(self.queue, self.nextInsert, self.moveAnimRow)
|
||||
end
|
||||
Runtime.emit("battle.move_used", {
|
||||
battle = self, user = user, target = target, move = move,
|
||||
@@ -3913,6 +3948,9 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
||||
})
|
||||
|
||||
local ctx = EffectRegistry.makeCtx(self, user, target, move, moveInst, isCalled)
|
||||
-- .ThrashingAboutCheck jumps past JumpMoveEffect into PlayerCalcMoveDamage
|
||||
-- (core.asm:3540), so SpecialEffectsCont never re-runs on a locked turn
|
||||
ctx.thrashing = thrashing
|
||||
|
||||
-- Metronome / Mirror Move re-entry; a nil pick means the record
|
||||
-- already said its failure text
|
||||
@@ -4185,8 +4223,9 @@ function BattleState:awardExp()
|
||||
end
|
||||
local function applyShare(mon, split, announce)
|
||||
local playerId = self.game.save.player and self.game.save.player.id
|
||||
local traded = mon.traded == true
|
||||
or (mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId)
|
||||
-- GainExperience (engine/battle/experience.asm:69-88) compares the
|
||||
-- stored MON_OTID against wPlayerID every award; no persistent flag
|
||||
local traded = mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId
|
||||
local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
|
||||
self.enemy.mon.level, self.kind == "trainer",
|
||||
split, traded)
|
||||
@@ -4338,21 +4377,35 @@ function BattleState:enemyMonFainted()
|
||||
-- the battle queue's own \f handling (not TextBox.lua's) does not
|
||||
-- page a sayChoice string the same way -- left as two calls.
|
||||
self:say(Strings("%s is\nabout to use\v%s!", self.trainer.name, nextName))
|
||||
-- EnemySendOutFirstMon .next9/.next8 (core.asm:1390-1409) and
|
||||
-- HasMonFainted's NoWillText (core.asm:1473-1488)
|
||||
self:sayChoice(
|
||||
Strings("Will %s\nchange POKéMON?", self.game.save.player.name),
|
||||
function(yes)
|
||||
if not yes then return end
|
||||
local game = self.game
|
||||
Screens.push(game, "PartyMenu", {
|
||||
local shiftOpts, reopenShift
|
||||
reopenShift = function(text)
|
||||
table.insert(self.queue, 1, { ui = function()
|
||||
return self:buildScreen("PartyMenu", shiftOpts)
|
||||
end })
|
||||
table.insert(self.queue, 1, { text = text })
|
||||
end
|
||||
shiftOpts = {
|
||||
battle = self,
|
||||
party = self:playerPartyView(),
|
||||
forceSwitch = true,
|
||||
onSwitch = function(mon)
|
||||
if mon ~= self.player.mon and mon.hp > 0 then
|
||||
if mon == self.player.mon then
|
||||
reopenShift(Strings("%s is\nalready out!", self.player.name))
|
||||
elseif mon.hp <= 0 then
|
||||
reopenShift(self:romText("_NoWillText", "There's no will\nto fight!"))
|
||||
else
|
||||
shiftSwitchMon = mon
|
||||
end
|
||||
end,
|
||||
})
|
||||
}
|
||||
Screens.push(game, "PartyMenu", shiftOpts)
|
||||
end, { box = Theme.trainerSwitchBox })
|
||||
end
|
||||
self:act(function()
|
||||
@@ -4395,10 +4448,11 @@ function BattleState:enemyMonFainted()
|
||||
local mon = shiftSwitchMon
|
||||
if not mon then return end
|
||||
-- SwitchPlayerMon (core.asm:2419-2423): RetreatMon, the 50-frame
|
||||
-- hold, then the recall and the send-out
|
||||
-- hold, AnimateRetreatingPlayerMon, then the recall and the send-out
|
||||
self.nextInsert = 0
|
||||
self:sayNextAuto(self:withdrawText(self.player.name),
|
||||
Timing.SWITCH_PLAYER_MON)
|
||||
self:queueRetreatAnim()
|
||||
self:actNext(function()
|
||||
local previous = self.player
|
||||
self.player = makeBattler(self.data, mon, true, self.game.save)
|
||||
@@ -5088,10 +5142,13 @@ function BattleState:openParty()
|
||||
battle = self,
|
||||
party = self:playerPartyView(),
|
||||
onSwitch = function(mon)
|
||||
-- PartyMenuOrRockOrRun's SWITCH .partyMonDeselected (core.asm:2396-2408)
|
||||
if mon == self.player.mon then
|
||||
self:say(Strings("%s is\nalready out!", self.player.name))
|
||||
self:act(function() self:openParty() end)
|
||||
elseif mon.hp <= 0 then
|
||||
self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
|
||||
self:act(function() self:openParty() end)
|
||||
else
|
||||
self:resolveSwitch(mon)
|
||||
end
|
||||
@@ -5235,6 +5292,14 @@ function BattleState:growInScale(battler)
|
||||
return f < 3 and 0 or f < 7 and 3 / 7 or 5 / 7
|
||||
end
|
||||
|
||||
-- AnimateRetreatingPlayerMon's CopyDownscaledMonTiles stages
|
||||
-- (core.asm:1769-1796)
|
||||
function BattleState:shrinkOutScale(battler)
|
||||
local shrink = self.shrinkOut
|
||||
if not shrink or shrink.battler ~= battler then return nil end
|
||||
return shrink.frame < 4 and 5 / 7 or 3 / 7
|
||||
end
|
||||
|
||||
-- battler hidden this frame? (damage blink)
|
||||
--
|
||||
-- AnimationBlinkMon hides the pic, waits DelayFrames 5, shows it, waits
|
||||
@@ -5871,11 +5936,10 @@ function BattleState:drawPicsLayer(slide, sx, sy, onlySide, skipMenuClip)
|
||||
local s = BattleState.resolveBattleScale(self.data, "back",
|
||||
imagePathOf(self.player.sprite),
|
||||
self.player.mon and self.player.mon.species)
|
||||
local gs = self:growInScale(self.player)
|
||||
local gs = self:growInScale(self.player) or self:shrinkOutScale(self.player)
|
||||
if gs then
|
||||
-- the player-side AnimateSendingOutMon grow (after the poof,
|
||||
-- core.asm:1757-1762): feet pinned at y=96, horizontal centre
|
||||
-- pinned, mod scale composed with the grow stage
|
||||
-- the player-side AnimateSendingOutMon grow (core.asm:1757-1762) and
|
||||
-- the AnimateRetreatingPlayerMon shrink (core.asm:1769-1796)
|
||||
local eff = s * gs
|
||||
if eff > 0 then
|
||||
love.graphics.draw(img,
|
||||
|
||||
@@ -221,7 +221,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
|
||||
-- replay PlayMoveAnimation per strike (pokered: GetPlayerAnimationType
|
||||
-- / GetEnemyAnimationType loop on wNumAttacksLeft); hit 1 reuses the
|
||||
-- announcement-time moveAnimRow, later hits queue fresh anim rows.
|
||||
-- Thrash/rage continuations have no announcement anim -- a bare
|
||||
-- Mimic queues no announcement anim (announceAnim = false) -- a bare
|
||||
-- hitRow carries the blink instead.
|
||||
-- PlayApplyingAttackSound (engine/battle/animations.asm, the routine after
|
||||
-- PlayApplyingAttackAnimation) picks the sound off wDamageMultipliers -- 10
|
||||
|
||||
@@ -582,30 +582,15 @@ MoveEffects.full = {
|
||||
},
|
||||
THRASH_PETAL_DANCE_EFFECT = {
|
||||
-- ThrashPetalDanceEffect (effects.asm:791-808) runs before damage
|
||||
-- (data/battle/special_effects.asm:22) and animates the setup turn
|
||||
-- (data/battle/special_effects.asm:22, core.asm:3531-3552)
|
||||
beforeAccuracy = function(ctx)
|
||||
local user = ctx.user
|
||||
if not user.thrashTurns then
|
||||
ctx.battle:animBeforeMove(
|
||||
user.isPlayer and "SHRINKING_SQUARE_ANIM" or "ANIM_B1", user.isPlayer)
|
||||
end
|
||||
end,
|
||||
afterDamage = function(ctx)
|
||||
local user = ctx.user
|
||||
if not user.thrashTurns then
|
||||
user.thrashTurns = ctx.rng(2, 3) -- 3-4 attacks total, then confusion
|
||||
user.thrashMove = ctx.moveInst
|
||||
user.thrashAnnounced = true
|
||||
else
|
||||
user.thrashTurns = user.thrashTurns - 1
|
||||
if user.thrashTurns <= 0 then
|
||||
user.thrashTurns, user.thrashMove, user.thrashAnnounced = nil, nil, nil
|
||||
if not user.confusedTurns then
|
||||
user.confusedTurns = ctx.rng(2, 5)
|
||||
ctx.say(romText(ctx.battle.data, "_BecameConfusedText", "%s\nbecame confused!", displayName(user)))
|
||||
end
|
||||
end
|
||||
end
|
||||
if ctx.thrashing or user.thrashTurns then return end
|
||||
user.thrashTurns = ctx.rng(2, 3) -- 3-4 attacks total, then confusion
|
||||
user.thrashMove = ctx.moveInst
|
||||
user.thrashAnnounced = true
|
||||
ctx.battle:animBeforeMove(
|
||||
user.isPlayer and "SHRINKING_SQUARE_ANIM" or "ANIM_B1", user.isPlayer)
|
||||
end,
|
||||
},
|
||||
JUMP_KICK_EFFECT = {
|
||||
|
||||
+35
-13
@@ -166,6 +166,9 @@ Battle.SUBSTATUS_ITEMS = {
|
||||
-- be run from or Roared away.
|
||||
Battle.BATTLETYPE_FORCESHINY = 7
|
||||
Battle.BATTLETYPE_TRAP = 9
|
||||
-- LostBattle's .canlose arm (engine/battle/core.asm:2766): the only battle
|
||||
-- type whose loss still prints the trainer's own line instead of a whiteout.
|
||||
Battle.BATTLETYPE_CANLOSE = 1
|
||||
|
||||
-- BadgeStatBoosts (engine/battle/core.asm:6534): each of these Johto badges
|
||||
-- raises the PLAYER's in-battle stat by 1/8. The routine walks every other
|
||||
@@ -889,10 +892,11 @@ Battle.PRIORITY = {
|
||||
EFFECT_ENDURE = 3,
|
||||
EFFECT_COUNTER = -1,
|
||||
EFFECT_MIRROR_COAT = -1,
|
||||
EFFECT_VITAL_THROW = -1,
|
||||
}
|
||||
|
||||
function Battle:movePriority(moveId)
|
||||
-- engine/battle/core.asm:786 GetMovePriority, `cp VITAL_THROW / ld a, 0`.
|
||||
if moveId == "VITAL_THROW" then return -1 end
|
||||
local def = self:moveDef(moveId)
|
||||
return (def and Battle.PRIORITY[def.effect]) or 0
|
||||
end
|
||||
@@ -2403,10 +2407,10 @@ Battle.MOVE_EFFECTS.EFFECT_BATON_PASS = function(self, attacker)
|
||||
self.enemy = party[target]
|
||||
self.enemy.volatile = carried
|
||||
end
|
||||
self:emit({ kind = "send", side = side,
|
||||
mon = side == "player" and self.player or self.enemy,
|
||||
text = "Go! " .. self:monName(side == "player" and self.player
|
||||
or self.enemy) .. "!" })
|
||||
local sent = side == "player" and self.player or self.enemy
|
||||
self:emit({ kind = "send", side = side, mon = sent,
|
||||
hp = sent.hp or 0, status = sent.status or false,
|
||||
text = "Go! " .. self:monName(sent) .. "!" })
|
||||
end
|
||||
|
||||
-- BattleCommand_TrapTarget's .Traps table, one line per move: target first,
|
||||
@@ -2665,6 +2669,7 @@ Battle.MOVE_EFFECTS.EFFECT_FORCE_SWITCH = function(self, attacker, defender,
|
||||
self.stages.enemy = Battle.newStages()
|
||||
end
|
||||
self:emit({ kind = "send", side = self:sideOf(incoming), mon = incoming,
|
||||
hp = incoming.hp or 0, status = incoming.status or false,
|
||||
text = self:monName(incoming) .. " was dragged out!" })
|
||||
self:breakTrapsOnSend(incoming)
|
||||
self:spikesDamage(incoming)
|
||||
@@ -3084,6 +3089,7 @@ function Battle:resolveFaints()
|
||||
if self.trainer then
|
||||
self:emit({ kind = "message",
|
||||
text = (self.trainer.name or "TRAINER") .. " was defeated!" })
|
||||
self:printWinLossText("win")
|
||||
self:awardPrizeMoney()
|
||||
end
|
||||
-- CheckPayDay, on the win arm only (engine/battle/core.asm:7971-7976,
|
||||
@@ -3110,6 +3116,7 @@ function Battle:resolveFaints()
|
||||
-- can offer a shift on (engine/battle/core.asm:2241-2278).
|
||||
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
|
||||
replacement = true,
|
||||
hp = self.enemy.hp or 0, status = self.enemy.status or false,
|
||||
text = (self.trainer and self.trainer.name or "Foe") .. " sent out "
|
||||
.. self:monName(self.enemy) .. "!" })
|
||||
Runtime.emit("battle.battler_switched", {
|
||||
@@ -3152,6 +3159,11 @@ function Battle:resolveFaints()
|
||||
local nextIndex = Battle.firstHealthy(self.party)
|
||||
if not nextIndex then
|
||||
self:emit({ kind = "message", text = "You have no more POKéMON!" })
|
||||
-- LostBattle (engine/battle/core.asm:2763-2782): only BATTLETYPE_CANLOSE
|
||||
-- reaches PrintWinLossText on a loss; every other loss whites out.
|
||||
if self.battleType == Battle.BATTLETYPE_CANLOSE then
|
||||
self:printWinLossText("lose")
|
||||
end
|
||||
self:endBattle("lose")
|
||||
return true
|
||||
end
|
||||
@@ -3177,14 +3189,22 @@ function Battle:resolveFaints()
|
||||
return false
|
||||
end
|
||||
|
||||
-- WinTrainerBattle's money arm, which runs after BattleText_EnemyWasDefeated
|
||||
-- and the frontpic slide: the four quarters are dealt between the wallet and
|
||||
-- Mom's savings and then one StdBattleTextbox names the figure.
|
||||
--
|
||||
-- The `ld a, [wDebugFlags] / bit DEBUG_BATTLE_F` skip in front of
|
||||
-- PrintWinLossText is the trainer's own after-battle line, which this port
|
||||
-- runs from the script on the way out of the battle rather than from here.
|
||||
-- The payout is not gated on it either way.
|
||||
-- WinTrainerBattle (engine/battle/core.asm:2310-2323), LostBattle's .canlose
|
||||
-- arm (:2769-2782), PrintWinLossText (home/trainers.asm:230)
|
||||
function Battle:printWinLossText(result)
|
||||
local trainer = self.trainer
|
||||
if not trainer then return end
|
||||
-- The DEBUG_BATTLE_F skip sits in front of PrintWinLossText alone, behind
|
||||
-- the slide (engine/battle/core.asm:2310, :2320-2323).
|
||||
self:emit({ kind = "trainer-return" })
|
||||
local text = (result == "lose") and trainer.lossText or trainer.winText
|
||||
if type(text) ~= "string" or text == "" then return end
|
||||
-- FarPrintText prints the pointer alone: no trainer-name tag in front of it,
|
||||
-- unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:341).
|
||||
self:emit({ kind = "win-text", text = text })
|
||||
end
|
||||
|
||||
-- WinTrainerBattle's money arm (engine/battle/core.asm:2310-2323)
|
||||
function Battle:awardPrizeMoney()
|
||||
local save = self.save
|
||||
if not (save and save.player) then return nil end
|
||||
@@ -3505,6 +3525,7 @@ function Battle:switch(index)
|
||||
self.participants[index] = true
|
||||
self.stages.player = Battle.newStages()
|
||||
self:emit({ kind = "send", side = "player", mon = mon,
|
||||
hp = mon.hp or 0, status = mon.status or false,
|
||||
text = "Go! " .. self:monName(mon) .. "!" })
|
||||
-- battle.battler_switched, the payload BattleState:resolveSwitch emits on
|
||||
-- Gen 1: the side record, whoever walked in, and whoever walked out.
|
||||
@@ -3969,6 +3990,7 @@ function Battle:enemyTrySwitchOrItem()
|
||||
self:clearVolatile(self.enemy)
|
||||
self.stages.enemy = Battle.newStages()
|
||||
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
|
||||
hp = self.enemy.hp or 0, status = self.enemy.status or false,
|
||||
text = (self.trainer.name or "TRAINER") .. " sent out "
|
||||
.. self:monName(self.enemy) .. "!" })
|
||||
Runtime.emit("battle.battler_switched", {
|
||||
|
||||
@@ -3439,6 +3439,18 @@ function RomExtractorGen2:extractScriptsAndText(maps, stdScripts)
|
||||
elseif info.name == "givepoke" then
|
||||
cmd.species, cmd.level, cmd.item, cmd.trainer =
|
||||
args[1], args[2], args[3], args[4]
|
||||
-- Script_givepoke (engine/overworld/scripting.asm:1806)
|
||||
if size == 8 then
|
||||
local function readAt(lo, hi)
|
||||
local addr = (args[lo] or 0) + (args[hi] or 0) * 0x100
|
||||
if not romAddrOk(bank, addr) then return nil end
|
||||
local okStr, str = pcall(self.rom.readString, self.rom,
|
||||
bank, addr, charmap, 0x50, 16)
|
||||
return okStr and str or nil
|
||||
end
|
||||
cmd.name = readAt(5, 6)
|
||||
cmd.otName = readAt(7, 8)
|
||||
end
|
||||
elseif info.name == "pokepic" or info.name == "disappear" then
|
||||
cmd.species = args[1] -- pokepic
|
||||
cmd.object = args[1] -- disappear (same byte)
|
||||
@@ -5231,6 +5243,20 @@ function RomExtractorGen2:extractMenuGfx()
|
||||
end
|
||||
if eggHatch.egg or eggHatch.shell then out.eggHatch = eggHatch end
|
||||
|
||||
-- StatsScreenPageTilesGFX (gfx/font.asm:23), the 17 tiles
|
||||
-- LoadStatsScreenPageTilesGFX lands at vTiles2 $31 (engine/gfx/load_font.asm:90).
|
||||
local hpBarBorder = self.symbols["EnemyHPBarBorderGFX"]
|
||||
if hpBarBorder then
|
||||
local address = hpBarBorder[2] - 17 * 16
|
||||
self:write2bpp(self.rom:bytes(hpBarBorder[1], address, 17 * 16),
|
||||
17 * 8, 8, "menu/stats_tiles.png")
|
||||
out.stats = {
|
||||
sheet = "assets/generated/menu/stats_tiles.png",
|
||||
tiles = 17,
|
||||
firstTile = 0x31,
|
||||
}
|
||||
end
|
||||
|
||||
-- Goldenrod Game Corner: Slot Machine graphics assets
|
||||
if self.symbols["Slots1LZ"] then
|
||||
local raw1 = self:decompressLz3Symbol("Slots1LZ")
|
||||
|
||||
@@ -93,6 +93,12 @@ function ItemEffects.healsHP(id)
|
||||
or id == "REVIVE" or id == "MAX_REVIVE"
|
||||
end
|
||||
|
||||
-- .useRareCandy prints over the still-drawn party menu
|
||||
-- (engine/items/item_effects.asm:1392-1418)
|
||||
function ItemEffects.keepsPartyMenuOpen(id)
|
||||
return ItemEffects.healsHP(id) or id == "RARE_CANDY"
|
||||
end
|
||||
|
||||
function ItemEffects.isBattleMedicine(id)
|
||||
return HEAL_AMOUNT[id] ~= nil or STATUS_HEAL[id] ~= nil
|
||||
or id == "MAX_POTION" or id == "FULL_RESTORE"
|
||||
|
||||
+19
-11
@@ -19,6 +19,9 @@ local romText = require("src.core.RomText")
|
||||
|
||||
local Evolution = {}
|
||||
|
||||
-- engine/pokemon/evos_moves.asm:122-123 (ld c, 50 / call DelayFrames)
|
||||
local EVOLVING_TEXT_FRAMES = 50
|
||||
|
||||
Evolution.METHODS = {
|
||||
LEVEL = {
|
||||
check = function(game, mon, evo, trigger)
|
||||
@@ -157,27 +160,32 @@ end
|
||||
-- Play the evolution movie (flashing forms), then apply + text.
|
||||
-- Headless (no real graphics) falls back to the plain text flow.
|
||||
function Evolution.evolve(game, mon, newSpecies, onDone, via)
|
||||
local oldName = mon.nickname or game.data.pokemon[mon.species].name
|
||||
-- IsEvolvingText, DelayFrames 50, then ClearScreenArea before EvolveMon
|
||||
-- (engine/pokemon/evos_moves.asm:120-134)
|
||||
local isEvolving = romText(game.data, "_IsEvolvingText",
|
||||
"What?\n%s is\nevolving!", oldName)
|
||||
if love.image and love.image.newImageData then
|
||||
-- forward `via` so EvolutionState can keep trade evolutions
|
||||
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213)
|
||||
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via)
|
||||
game.stack:push(TextBox.new(game, isEvolving, function()
|
||||
-- forward `via` so EvolutionState can keep trade evolutions
|
||||
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213)
|
||||
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via)
|
||||
end, { auto = { delay = EVOLVING_TEXT_FRAMES } }))
|
||||
return
|
||||
end
|
||||
Music.play(game.data, Music.special(game.data, "evolution"))
|
||||
local oldName = mon.nickname or game.data.pokemon[mon.species].name
|
||||
Evolution.apply(game, mon, newSpecies, via)
|
||||
-- the congrats page keeps the engine wording: _EvolvedText extracts
|
||||
-- truncated (it stops at a dynamic marker the decoder does not follow)
|
||||
local msg = romText(game.data, "_IsEvolvingText",
|
||||
"What?\n%s is\nevolving!", oldName)
|
||||
.. "\f" .. Strings("Congratulations!\nYour %s\nevolved into\n%s!",
|
||||
oldName, game.data.pokemon[newSpecies].name)
|
||||
-- EvolvedText then IntoText in the same box (evos_moves.asm:136-150)
|
||||
local msg = isEvolving .. "\f"
|
||||
.. romText(game.data, "_EvolvedText", "%s evolved", oldName)
|
||||
.. romText(game.data, "_IntoText", "\ninto %s!",
|
||||
game.data.pokemon[newSpecies].name)
|
||||
game.stack:push(TextBox.new(game, msg, function()
|
||||
Music.restoreMap(game.data)
|
||||
-- re-run the evolved species' level-up learn check before onDone
|
||||
-- (evos_moves.asm EvolveMon -> learn_move.asm LearnMoveFromLevelUp, #12)
|
||||
Evolution.learnEvolutionMoves(game, mon, onDone)
|
||||
end))
|
||||
end, TextBox.soundOpts(game, "Get_Item2")))
|
||||
end
|
||||
|
||||
-- Entry point for mods whose methods fire outside the vanilla moments
|
||||
|
||||
@@ -543,10 +543,14 @@ local function runCmd(self, cmd, op)
|
||||
local species = cmd.species or arg1(cmd)
|
||||
local level = cmd.level or (cmd.args and cmd.args[2]) or 5
|
||||
local item = cmd.item or (cmd.args and cmd.args[3]) or 0
|
||||
local trainer = cmd.trainer or (cmd.args and cmd.args[4]) or 0
|
||||
if self.givePokeFn then
|
||||
local mon = self.givePokeFn(species, level, item)
|
||||
-- engine/pokemon/move_mon.asm:1695-1736: the trainer arm copies the
|
||||
-- script's own nickname and OT name in instead of asking for one.
|
||||
local named = trainer ~= 0
|
||||
and { nickname = cmd.name, otName = cmd.otName } or nil
|
||||
local mon = self.givePokeFn(species, level, item, named)
|
||||
-- engine/pokemon/move_mon.asm:1753-1757
|
||||
local trainer = cmd.trainer or (cmd.args and cmd.args[4]) or 0
|
||||
if mon and trainer == 0 then
|
||||
Specials.askNickname(self, mon)
|
||||
end
|
||||
|
||||
+13
-6
@@ -17,10 +17,13 @@ local function buildItems(game)
|
||||
local items = {}
|
||||
for _, id in ipairs(Bag.order(game.save)) do
|
||||
local def = game.data.items[id]
|
||||
-- PrintListMenuEntries skips the quantity for anything IsKeyItem_ owns:
|
||||
-- the KeyItemFlags bitfield plus the HMs (item_effects.asm:2616-2641)
|
||||
local unsellable = (def and def.keyItem) or id:find("^HM_") ~= nil
|
||||
table.insert(items, {
|
||||
value = id,
|
||||
label = def and def.name or id,
|
||||
right = "x" .. game.save.inventory[id],
|
||||
right = (not unsellable) and ("x" .. game.save.inventory[id]) or nil,
|
||||
})
|
||||
end
|
||||
return items
|
||||
@@ -334,6 +337,9 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
|
||||
local Evolution = require("src.pokemon.Evolution")
|
||||
local evoTo, evo = Evolution.pendingFor(game, target,
|
||||
{ kind = "levelup" })
|
||||
-- the party menu stays up through TryEvolvingMon and only
|
||||
-- comes down at RemoveUsedItem (item_effects.asm:1392-1418)
|
||||
closePicker()
|
||||
if evoTo then
|
||||
Evolution.evolve(game, target, evoTo, nil, evo and evo.method)
|
||||
end
|
||||
@@ -399,10 +405,9 @@ local function pickTargetAndUse(game, battle, id, list)
|
||||
local opts = {
|
||||
pickOnly = true,
|
||||
battle = battle,
|
||||
-- HP medicine animates its bar with the picker still up (#252). Only
|
||||
-- out of battle: the in-battle tail closes the bag list underneath
|
||||
-- first, which needs the picker already gone.
|
||||
keepOpen = (not battle) and ItemEffects.healsHP(id),
|
||||
-- HP medicine animates with the picker up (#252), RARE CANDY prints over
|
||||
-- the party menu (item_effects.asm:1392-1418)
|
||||
keepOpen = (not battle) and ItemEffects.keepsPartyMenuOpen(id),
|
||||
onSwitch = function(mon, picker)
|
||||
if not wantsMove then
|
||||
useOn(game, battle, id, mon, list, nil, picker)
|
||||
@@ -470,7 +475,9 @@ function BagMenu.new(game, opts)
|
||||
local list
|
||||
list = ListMenu.new(game, "ITEMS", buildItems(game), {
|
||||
kind = "bag",
|
||||
footer = ("¥%d"):format(game.save.money),
|
||||
-- StartMenu_Item zeroes wPrintItemPrices and draws no money box: the
|
||||
-- LIST_MENU_BOX floats over the map (engine/menus/start_sub_menus.asm)
|
||||
itemBox = true,
|
||||
-- B returns to the start menu when the bag was opened from it
|
||||
onCancel = opts.onCancel,
|
||||
-- SELECT reorders items like the original bag (swap_items.asm)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
-- The evolution movie (engine/movie/evolution.asm): the mon's pic
|
||||
-- flashes back and forth with the evolved form, speeding up, then the
|
||||
-- new form appears with its cry and the congratulations text.
|
||||
-- new form appears with its cry and the "evolved into" text
|
||||
-- (engine/pokemon/evos_moves.asm:120-128).
|
||||
-- pokered engine/movie/evolution.asm (Evolution_CheckForCancel) polls the
|
||||
-- joypad during the flash: a fresh B press aborts the evolution -- the mon
|
||||
-- keeps its species and _StoppedEvolvingText ("Huh? MON stopped evolving!")
|
||||
@@ -9,9 +10,7 @@
|
||||
-- and stone evolutions, where the B press is read but thrown away because
|
||||
-- ItemUseEvoStone left wForceEvolution set (#290).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Music = require("src.core.Music")
|
||||
local Strings = require("src.core.Strings")
|
||||
local romText = require("src.core.RomText")
|
||||
|
||||
local EvolutionState = {}
|
||||
@@ -127,11 +126,11 @@ function EvolutionState:update(dt)
|
||||
require("src.core.Sound").playCry(game.data, self.newSpecies)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local newName = game.data.pokemon[self.newSpecies].name
|
||||
-- _EvolvedText extracts truncated (it stops at a dynamic marker the
|
||||
-- decoder does not follow), so the engine's wording stands here
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("Congratulations!\nYour %s\nevolved into\n%s!",
|
||||
self.oldName, newName),
|
||||
-- EvolvedText then IntoText in the same box (PrintText_NoCreatingTextBox),
|
||||
-- then SFX_GET_ITEM_2 (engine/pokemon/evos_moves.asm:136-153)
|
||||
local msg = romText(game.data, "_EvolvedText", "%s evolved", self.oldName)
|
||||
.. romText(game.data, "_IntoText", "\ninto %s!", newName)
|
||||
game.stack:push(TextBox.new(game, msg,
|
||||
function()
|
||||
Music.restoreMap(game.data)
|
||||
game.stack:pop() -- the evolution screen itself
|
||||
@@ -141,7 +140,8 @@ function EvolutionState:update(dt)
|
||||
-- first so the "learned MOVE!" text / forget prompt push onto the
|
||||
-- overworld / battle-return, not this state.
|
||||
Evolution.learnEvolutionMoves(game, self.mon, self.onDone)
|
||||
end))
|
||||
end,
|
||||
TextBox.soundOpts(game, "Get_Item2")))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,13 +172,6 @@ function EvolutionState:draw()
|
||||
require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions())
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if not self.done then
|
||||
Font.draw(Strings("What?"), 8, 104)
|
||||
Font.draw(self.oldName .. " is", 8, 114)
|
||||
Font.draw(Strings("evolving!"), 8, 124)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
+55
-1
@@ -17,6 +17,14 @@ function ListMenu:sgbPalettes(game)
|
||||
end
|
||||
|
||||
local ROWS = 7
|
||||
-- LIST_MENU_BOX 4,2 - 19,12 (data/text_boxes.asm:13); 4 names from
|
||||
-- hlcoord 6,4 two rows apart (home/list_menu.asm:51-52, 364-365, 471-479)
|
||||
local ITEM_BOX = { tx = 4, ty = 2, tw = 16, th = 11 }
|
||||
local ITEM_ROWS = 4
|
||||
local ITEM_NAME_X, ITEM_TOP_Y = 48, 32
|
||||
local ITEM_CURSOR_X = 40
|
||||
local ITEM_QTY_X, ITEM_QTY_END = 112, 136
|
||||
local ITEM_MORE_X, ITEM_MORE_Y = 144, 88
|
||||
-- frames to wait before key-repeat kicks in, then between repeats
|
||||
local REPEAT_DELAY = 16
|
||||
local REPEAT_RATE = 4
|
||||
@@ -83,7 +91,12 @@ function ListMenu.new(game, title, items, opts)
|
||||
-- for their whole run (engine/menus/pc.asm, engine/menus/players_pc.asm),
|
||||
-- so their lists opt out of the A/B beep the same way Menu's noSound does
|
||||
self.noSound = opts.noSound or false
|
||||
self.rows = opts.rows or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
|
||||
-- the bag's item list: a partial box the map stays visible around, not a
|
||||
-- screen of its own (home/list_menu.asm:29-31)
|
||||
self.itemBox = opts.itemBox or false
|
||||
if self.itemBox then self.isOpaque = false end
|
||||
self.rows = opts.rows or (self.itemBox and ITEM_ROWS)
|
||||
or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -206,7 +219,48 @@ function ListMenu:close()
|
||||
if top == self then self.game.stack:pop() end
|
||||
end
|
||||
|
||||
-- PrintListMenuEntries, minus the price column StartMenu_Item never asks for
|
||||
-- (wPrintItemPrices = 0, engine/menus/start_sub_menus.asm)
|
||||
function ListMenu:drawItemBox()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
Font.drawBox(ITEM_BOX.tx, ITEM_BOX.ty, ITEM_BOX.tw, ITEM_BOX.th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if #self.items == 0 then
|
||||
Font.draw(Strings("Nothing here."), ITEM_NAME_X, ITEM_TOP_Y)
|
||||
end
|
||||
local shown = 0
|
||||
for row = 1, self.rows do
|
||||
local i = self.scroll + row
|
||||
local item = self.items[i]
|
||||
if not item then break end
|
||||
shown = shown + 1
|
||||
local y = ITEM_TOP_Y + (row - 1) * 16
|
||||
Font.draw(item.label, ITEM_NAME_X, y)
|
||||
if item.right then
|
||||
-- '×' at column 14, PrintNumber's two right-aligned digits after it
|
||||
-- (home/list_menu.asm:479-490)
|
||||
local count = item.right:sub(2)
|
||||
Font.draw(item.right:sub(1, 1), ITEM_QTY_X, y + 8)
|
||||
Font.draw(count, ITEM_QTY_END - Font.width(count), y + 8)
|
||||
end
|
||||
if i == self.index then
|
||||
Font.drawCode(self.hollowIndex == i
|
||||
and Theme.cursorHollow or Theme.cursor, ITEM_CURSOR_X, y)
|
||||
end
|
||||
if self.swapIndex == i and i ~= self.index then
|
||||
Font.drawCode(Theme.cursorHollow, ITEM_CURSOR_X, y)
|
||||
end
|
||||
end
|
||||
-- the terminator prints CANCEL and returns before the '▼'
|
||||
-- (home/list_menu.asm:372, 518-524)
|
||||
if shown == self.rows then
|
||||
Font.drawCode(Theme.moreArrow, ITEM_MORE_X, ITEM_MORE_Y)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function ListMenu:draw()
|
||||
if self.itemBox then return self:drawItemBox() end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
|
||||
+15
-5
@@ -34,6 +34,9 @@ function Menu.new(game, items, opts)
|
||||
if self.tx + self.tw > 20 then self.tx = math.max(0, 20 - self.tw) end
|
||||
end
|
||||
self.rowStep = opts.rowStep or 2
|
||||
-- engine/movie/oak_speech/oak_speech2.asm:162 (DisplayIntroNameTextBox)
|
||||
self.title = opts.title
|
||||
self.itemY = opts.itemY
|
||||
-- maxVisible: cap the box to this many rows and scroll the rest instead
|
||||
-- of growing past it (e.g. the start menu, whose row count varies with
|
||||
-- save state and mod hooks); nil/unset keeps every caller's old
|
||||
@@ -117,6 +120,10 @@ function Menu:draw()
|
||||
end
|
||||
Font.drawBox(self.tx, self.ty, self.tw, self.th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
-- PlaceString at hlcoord 3,0 writes over the border row it was just drawn on
|
||||
if self.title then
|
||||
Font.draw(self.title, (self.tx + 3) * 8, self.ty * 8)
|
||||
end
|
||||
local visible = (self.maxVisible and math.min(self.maxVisible, #self.items))
|
||||
or #self.items
|
||||
-- Row Y: pokered's boxed menus anchor the choices to the BOTTOM interior
|
||||
@@ -130,15 +137,18 @@ function Menu:draw()
|
||||
-- USE/TOSS is th = 5 for two choices (#284, matching text_boxes.asm's
|
||||
-- USE_TOSS_MENU_TEMPLATE rows 10..14), and a top anchor pushed TOSS onto
|
||||
-- the bottom border (#564, #572).
|
||||
local function rowY(row)
|
||||
if self.itemY then
|
||||
return (self.ty + self.itemY + (row - 1) * self.rowStep) * 8
|
||||
end
|
||||
return (self.ty + self.th - 2 - (visible - row) * self.rowStep) * 8
|
||||
end
|
||||
for row = 1, visible do
|
||||
local item = self.items[self.scroll + row]
|
||||
if not item then break end
|
||||
Font.draw(item.label, (self.tx + 2) * 8,
|
||||
(self.ty + self.th - 2 - (visible - row) * self.rowStep) * 8)
|
||||
Font.draw(item.label, (self.tx + 2) * 8, rowY(row))
|
||||
end
|
||||
local cursorRow = self.index - self.scroll
|
||||
Font.drawCode(Theme.cursor, (self.tx + 1) * 8,
|
||||
(self.ty + self.th - 2 - (visible - cursorRow) * self.rowStep) * 8)
|
||||
Font.drawCode(Theme.cursor, (self.tx + 1) * 8, rowY(self.index - self.scroll))
|
||||
-- moreArrow ($EE): the same "more below" glyph OptionRows/ManagerState
|
||||
-- use, sat on the bottom border like TextBox's page-advance cursor. It
|
||||
-- has to be the border row, not ty + th - 2: that is the last interior
|
||||
|
||||
+13
-3
@@ -67,6 +67,7 @@ function NamingScreen.new(game, opts)
|
||||
self.game = game
|
||||
self.title = opts.title or Strings("YOUR NAME?")
|
||||
self.presets = opts.presets
|
||||
self.introBox = opts.introBox
|
||||
self.maxLen = opts.maxLen or 7
|
||||
self.default = opts.default
|
||||
self.onDone = opts.onDone
|
||||
@@ -99,9 +100,18 @@ function NamingScreen:enter()
|
||||
end,
|
||||
})
|
||||
end
|
||||
self.game.stack:push(Menu.new(self.game, items, {
|
||||
tx = 4, ty = 0, tw = 12, th = #items * 2 + 2, cancelable = false,
|
||||
}))
|
||||
if self.introBox then
|
||||
-- DisplayIntroNameTextBox (oak_speech2.asm:162): TextBoxBorder at
|
||||
-- hlcoord 0,0 with b=$a c=$9, "NAME" at hlcoord 3,0, list at hlcoord 2,2
|
||||
self.game.stack:push(Menu.new(self.game, items, {
|
||||
tx = 0, ty = 0, tw = 11, th = #items * 2 + 4,
|
||||
itemY = 2, title = Strings("NAME"), cancelable = false,
|
||||
}))
|
||||
else
|
||||
self.game.stack:push(Menu.new(self.game, items, {
|
||||
tx = 4, ty = 0, tw = 12, th = #items * 2 + 2, cancelable = false,
|
||||
}))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+69
-16
@@ -35,6 +35,23 @@ OakSpeech.letterboxWhite = true
|
||||
local FADE_FRAMES = 24
|
||||
local WIPE_FRAMES = 32
|
||||
|
||||
-- OakSpeechSlidePicRight / OakSpeechSlidePicLeft (oak_speech2.asm:67-89)
|
||||
local SLIDE_TILES = 6
|
||||
local SLIDE_FRAMES = 3
|
||||
|
||||
local PicSlide = {}
|
||||
PicSlide.__index = PicSlide
|
||||
|
||||
function PicSlide:update(dt)
|
||||
self.t = self.t + 1
|
||||
local tiles = math.min(SLIDE_TILES, math.floor(self.t / SLIDE_FRAMES))
|
||||
self.speech.picSlide = (self.dir > 0 and tiles or (SLIDE_TILES - tiles)) * 8
|
||||
if tiles >= SLIDE_TILES then
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
end
|
||||
end
|
||||
|
||||
-- naming presets are boot config (field.boot.namePresets), which a total
|
||||
-- conversion replaces; the Red/Blue lists remain the fallback
|
||||
local function namePresets(game, who, fallback)
|
||||
@@ -155,6 +172,10 @@ function OakSpeech.defaultSteps(speech)
|
||||
kind = "say",
|
||||
textKey = "_IntroducePlayerText",
|
||||
pic = "player",
|
||||
-- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText ends in
|
||||
-- text_end, so PrintText returns with the box still up under the names
|
||||
reveal = "wipe",
|
||||
stay = true,
|
||||
},
|
||||
{
|
||||
id = "name_player",
|
||||
@@ -177,6 +198,10 @@ function OakSpeech.defaultSteps(speech)
|
||||
kind = "say",
|
||||
textKey = "_IntroduceRivalText",
|
||||
pic = "rival",
|
||||
-- oak_speech.asm:98-101: FadeInIntroPic, then IntroduceRivalText's
|
||||
-- text_end leaves the box up for ChooseRivalName
|
||||
reveal = "fade",
|
||||
stay = true,
|
||||
},
|
||||
{
|
||||
id = "name_rival",
|
||||
@@ -378,7 +403,14 @@ function OakSpeech:runStep(step)
|
||||
self:applyPic(step)
|
||||
self:afterReveal(step, function()
|
||||
self:runCry(step)
|
||||
self:sayText(self:stepText(step), function() self:advance() end)
|
||||
if step.stay then
|
||||
local box = TextBox.new(self.game, self:stepText(step), nil,
|
||||
{ stay = { onShown = function() self:advance() end } })
|
||||
self.holdBox = box
|
||||
self.game.stack:push(box)
|
||||
else
|
||||
self:sayText(self:stepText(step), function() self:advance() end)
|
||||
end
|
||||
end)
|
||||
elseif kind == "demo" then
|
||||
-- NIDORINO show-off: mirrored front sprite + wipe + cry + text 2A
|
||||
@@ -394,20 +426,26 @@ function OakSpeech:runStep(step)
|
||||
local presets = step.presets
|
||||
or namePresets(self.game, step.presetsWho or who,
|
||||
step.presetsFallback or { "RED" })
|
||||
require("src.ui.Screens").push(self.game, "NamingScreen", {
|
||||
title = step.title or (who == "rival" and "HIS NAME?" or Strings("YOUR NAME?")),
|
||||
presets = presets,
|
||||
maxLen = step.maxLen or self.nameLen,
|
||||
onDone = function(name)
|
||||
if who == "rival" then
|
||||
self.game.save.player.rival = name
|
||||
else
|
||||
self.game.save.player.name = name
|
||||
end
|
||||
self:recordAnswer(step, 1, name, name)
|
||||
self:advance()
|
||||
end,
|
||||
})
|
||||
local function openNaming()
|
||||
require("src.ui.Screens").push(self.game, "NamingScreen", {
|
||||
title = step.title or (who == "rival" and "HIS NAME?" or Strings("YOUR NAME?")),
|
||||
presets = presets,
|
||||
introBox = true,
|
||||
maxLen = step.maxLen or self.nameLen,
|
||||
onDone = function(name)
|
||||
if who == "rival" then
|
||||
self.game.save.player.rival = name
|
||||
else
|
||||
self.game.save.player.name = name
|
||||
end
|
||||
self:recordAnswer(step, 1, name, name)
|
||||
-- YourNameIsText/HisNameIsText print into the box this one held
|
||||
self:closeHoldBox()
|
||||
self:slidePic(-1, function() self:advance() end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
self:slidePic(1, openNaming)
|
||||
elseif kind == "choice" then
|
||||
self:applyPic(step)
|
||||
self:afterReveal(step, function()
|
||||
@@ -518,6 +556,21 @@ function OakSpeech:revealPic(kind, next)
|
||||
}
|
||||
end
|
||||
|
||||
-- ..(engine/movie/oak_speech/oak_speech2.asm ln 67)
|
||||
function OakSpeech:slidePic(dir, onDone)
|
||||
self.picSlide = (dir > 0 and 0 or SLIDE_TILES * 8)
|
||||
self.game.stack:push(setmetatable({
|
||||
game = self.game, speech = self, dir = dir, t = 0, onDone = onDone,
|
||||
}, PicSlide))
|
||||
end
|
||||
|
||||
-- IntroducePlayerText's text_end box (oak_speech.asm:90) is ours to close
|
||||
function OakSpeech:closeHoldBox()
|
||||
local box = self.holdBox
|
||||
self.holdBox = nil
|
||||
if box and self.game.stack:top() == box then self.game.stack:pop() end
|
||||
end
|
||||
|
||||
function OakSpeech:advance()
|
||||
self.step = self.step + 1
|
||||
-- picFlip belongs to the pic, not to the step: OakSpeechText2 prints 2A
|
||||
@@ -615,7 +668,7 @@ function OakSpeech:draw()
|
||||
-- it like the sprite buffer does ((8 - w) >> 1) tiles across,
|
||||
-- bottom-aligned
|
||||
local w, h = self.pic:getDimensions()
|
||||
local x = 48 + math.floor((8 - w / 8) / 2) * 8
|
||||
local x = 48 + math.floor((8 - w / 8) / 2) * 8 + (self.picSlide or 0)
|
||||
local y = 32 + (7 - h / 8) * 8
|
||||
local reveal = self.picReveal
|
||||
local off = 0
|
||||
|
||||
+30
-7
@@ -59,14 +59,37 @@ function StartMenu.new(game)
|
||||
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
|
||||
owned = owned + 1
|
||||
end
|
||||
local t = math.floor(game.save.playTime or 0)
|
||||
local panel = Strings("PLAYER %s\nBADGES %d\nPOKéDEX %3d\nTIME %6d:%02d",
|
||||
game.save.player.name or "RED", badges, owned,
|
||||
math.floor(t / 3600), math.floor(t / 60) % 60)
|
||||
-- PrintSaveScreenText draws its own border at hlcoord 4,0 (b=8, c=$e) and
|
||||
-- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
|
||||
local panel = {
|
||||
update = function() end,
|
||||
draw = function()
|
||||
local t = math.floor(game.save.playTime or 0)
|
||||
Font.drawBox(4, 0, 16, 10)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8)
|
||||
Font.draw(game.save.player.name or "RED", 12 * 8, 2 * 8)
|
||||
Font.draw(Strings("BADGES"), 5 * 8, 4 * 8)
|
||||
Font.draw(("%2d"):format(badges), 17 * 8, 4 * 8)
|
||||
Font.draw(Strings("POKéDEX"), 5 * 8, 6 * 8)
|
||||
Font.draw(("%3d"):format(owned), 16 * 8, 6 * 8)
|
||||
Font.draw(Strings("TIME"), 5 * 8, 8 * 8)
|
||||
Font.draw(("%3d:%02d"):format(math.floor(t / 3600),
|
||||
math.floor(t / 60) % 60), 13 * 8, 8 * 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end,
|
||||
}
|
||||
local function closePanel()
|
||||
if game.stack:top() == panel then game.stack:pop() end
|
||||
end
|
||||
game.stack:push(panel)
|
||||
game.stack:push(TextBox.new(game,
|
||||
panel .. Strings("\fWould you like to\nSAVE the game?"), nil, {
|
||||
Strings("Would you like to\nSAVE the game?"), nil, {
|
||||
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 rather
|
||||
-- than the shared right-hand one -- engine/menus/save.asm:186-192
|
||||
choiceBox = { tx = 0, ty = 7, tw = 6, th = 5 },
|
||||
choice = function(yes)
|
||||
if not yes then return end
|
||||
if not yes then closePanel() return end
|
||||
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
|
||||
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
|
||||
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
|
||||
@@ -78,7 +101,7 @@ function StartMenu.new(game)
|
||||
game:writeSave()
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
|
||||
nil, { auto = {
|
||||
closePanel, { auto = {
|
||||
sound = function()
|
||||
return require("src.core.Sound").play(game.data, "Save")
|
||||
end,
|
||||
|
||||
+30
-7
@@ -511,9 +511,12 @@ function TitleState:openMenu()
|
||||
table.insert(items, { label = Strings("NEW GAME"), onSelect = function()
|
||||
if self.onNewGame then self.onNewGame() end
|
||||
end })
|
||||
table.insert(items, { label = Strings("OPTION"), onSelect = function()
|
||||
require("src.ui.Screens").push(game, "OptionsMenu")
|
||||
end })
|
||||
-- DisplayOptionMenu returns to .mainMenuLoop, which redraws the box
|
||||
-- (engine/menus/main_menu.asm ln 87-90)
|
||||
table.insert(items, { label = Strings("OPTION"), keepOpen = true,
|
||||
onSelect = function()
|
||||
require("src.ui.Screens").push(game, "OptionsMenu")
|
||||
end })
|
||||
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
|
||||
if self.onExit then
|
||||
self.onExit()
|
||||
@@ -530,6 +533,12 @@ function TitleState:openMenu()
|
||||
end
|
||||
local th = #items * 2 + 2
|
||||
local menu = Menu.new(game, items, { tx = 0, ty = 0, tw = 13, th = th })
|
||||
-- .mainMenuLoop's B branch jumps back to DisplayTitleScreen, which opens
|
||||
-- with GBPalWhiteOut (engine/menus/main_menu.asm:69, title.asm:29)
|
||||
menu.onCancel = function()
|
||||
game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
|
||||
function() self.menuOpen = false end))
|
||||
end
|
||||
-- full-width title LOGO zones would recolor this box; see sgbPalettes.
|
||||
-- Menu.new may have grown tw for longer (e.g. localized) labels, so the
|
||||
-- recolor zone follows the box's real width instead of the vanilla 13.
|
||||
@@ -537,6 +546,17 @@ function TitleState:openMenu()
|
||||
game.stack:push(menu)
|
||||
end
|
||||
|
||||
-- .finishedWaiting: GBPalWhiteOutWithDelay3 then ClearScreen before MainMenu,
|
||||
-- which clears again itself (engine/movie/title.asm ln 243, main_menu.asm ln 26)
|
||||
function TitleState:toMenu()
|
||||
local game = self.game
|
||||
game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
|
||||
function()
|
||||
self.menuOpen = true
|
||||
self:openMenu()
|
||||
end))
|
||||
end
|
||||
|
||||
-- ..(engine/movie/title.asm ln 271)
|
||||
function TitleState:pickNewMon()
|
||||
if #self.cycleSpecies < 2 then return end
|
||||
@@ -602,7 +622,7 @@ function TitleState:update(dt)
|
||||
if not Sound.playPikaCry(self.game.data, 11) then
|
||||
Sound.playCry(self.game.data, "PIKACHU")
|
||||
end
|
||||
self:openMenu()
|
||||
self:toMenu()
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -616,20 +636,23 @@ function TitleState:update(dt)
|
||||
require("src.core.Sound").playCry(self.game.data,
|
||||
self.yellowLayout and "PIKACHU"
|
||||
or self.cycleSpecies[self.cycleIndex])
|
||||
self:openMenu()
|
||||
self:toMenu()
|
||||
end
|
||||
end
|
||||
|
||||
-- ..(engine/movie/title.asm ln 28)
|
||||
function TitleState:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
-- MainMenu's own ClearScreen wipes the logo, mon and sprites before the
|
||||
-- CONTINUE / NEW GAME border is drawn (engine/menus/main_menu.asm ln 26)
|
||||
if self.menuOpen then return end
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local playerImage = self.player
|
||||
if playerImage and PaletteFX.usesSpriteObp() then
|
||||
playerImage = require("src.render.SpriteRenderer").obpImage(
|
||||
self.playerPath, PaletteFX.ogObj())
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
local scrollY = -(self.scy or 0)
|
||||
-- ..(engine/movie/title.asm ln 28)
|
||||
local preRibbon = not self.yellowLayout
|
||||
|
||||
+108
-14
@@ -38,6 +38,9 @@ local Sound = require("src.core.Sound")
|
||||
-- Only for playerPic: the player.sprite raiser both generations share.
|
||||
local Sprites = require("src.pokemon.Sprites")
|
||||
local Strings = require("src.core.Strings")
|
||||
-- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text
|
||||
-- carries into the battle box (PrintWinLossText, home/trainers.asm:230).
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Unown = require("src.core.gen2.Unown")
|
||||
|
||||
local BattleState = {}
|
||||
@@ -87,6 +90,20 @@ local TRAINER_SLIDE_STEPS = 8
|
||||
local TRAINER_SLIDE_FRAMES_PER_STEP = 2
|
||||
local TRAINER_SLIDE_FRAMES = TRAINER_SLIDE_STEPS * TRAINER_SLIDE_FRAMES_PER_STEP
|
||||
|
||||
-- BattleWinSlideInEnemyTrainerFrontpic (engine/battle/core.asm:6279-6318) and
|
||||
-- WinTrainerBattle's DelayFrames 40 (:2311)
|
||||
local WIN_SLIDE_STEPS = 6
|
||||
local WIN_SLIDE_FRAMES_PER_STEP = 4
|
||||
local WIN_SLIDE_FRAMES = WIN_SLIDE_STEPS * WIN_SLIDE_FRAMES_PER_STEP
|
||||
local WIN_SLIDE_REST_TILES = 2
|
||||
local WIN_SLIDE_DELAY_FRAMES = 40
|
||||
|
||||
local function winSlideTiles(frames)
|
||||
local step = math.min(WIN_SLIDE_STEPS,
|
||||
math.floor(frames / WIN_SLIDE_FRAMES_PER_STEP) + 1)
|
||||
return WIN_SLIDE_STEPS + WIN_SLIDE_REST_TILES - step
|
||||
end
|
||||
|
||||
-- MonFaintedAnimation (engine/battle/core.asm), which PlayerMonFaintedAnimation
|
||||
-- and EnemyMonFaintedAnimation both fall into with the fainted side's pic
|
||||
-- corner: the pic's tilemap rows are copied DOWN one row per step and the row
|
||||
@@ -141,6 +158,10 @@ local MENU_COL_SPACING = 6
|
||||
-- the count PrintNum writes after it (two digits, leading zeros) at (13,16).
|
||||
local CONTEST_MENU_BOX_X = 2
|
||||
local CONTEST_MENU_COL_SPACING = 12
|
||||
|
||||
-- PrintMoveType prints the type table's own names; only these two differ from
|
||||
-- the constant (data/types/names.asm).
|
||||
local TYPE_NAMES = { PSYCHIC_TYPE = "PSYCHIC", CURSE_TYPE = "???" }
|
||||
-- charmap.asm's quantity glyph, spelled the way MartMenu spells it.
|
||||
local CONTEST_BALL_LABEL = "PARKBALL\xc3\x97"
|
||||
|
||||
@@ -508,6 +529,14 @@ function BattleState:pushAll(events)
|
||||
for _, event in ipairs(events or {}) do self:push(event) end
|
||||
end
|
||||
|
||||
-- LearnMove returns before HandleEnemyMonFaint's send-out/prize arms
|
||||
-- (engine/battle/core.asm:1959-2010).
|
||||
function BattleState:pushFront(events)
|
||||
for i = #(events or {}), 1, -1 do
|
||||
table.insert(self.queue, 1, events[i])
|
||||
end
|
||||
end
|
||||
|
||||
function BattleState:pic(mon, back)
|
||||
local def = self.pokemon and mon and self.pokemon[mon.species]
|
||||
local path = def and (back and def.spriteBack or def.spriteFront)
|
||||
@@ -686,6 +715,8 @@ function BattleState:drawPic(mon, back)
|
||||
-- One tile per two frames to the right, SlideBattlePicOut's own step.
|
||||
if enemyTrainer and self.trainerSlide then
|
||||
px = px + math.floor(self.trainerSlide / TRAINER_SLIDE_FRAMES_PER_STEP) * 8
|
||||
elseif enemyTrainer and self.winSlide then
|
||||
px = px + winSlideTiles(self.winSlide) * 8
|
||||
end
|
||||
-- The pic's own scale (battle_sprite_scales, then the species record, then
|
||||
-- 1x) composed with whatever square BattleBGEffect_RunPicResizeScript has
|
||||
@@ -1371,7 +1402,7 @@ function BattleState:advanceQueue()
|
||||
and self.shownHp[event.side] ~= event.hp then
|
||||
self.hpAnim = { side = event.side, to = event.hp }
|
||||
elseif event.kind == "send" and event.side and event.mon and self.shownHp then
|
||||
self.shownHp[event.side] = event.mon.hp or 0
|
||||
self.shownHp[event.side] = event.hp or event.mon.hp or 0
|
||||
if self.hpAnim and self.hpAnim.side == event.side then self.hpAnim = nil end
|
||||
end
|
||||
-- And the same lag for the status tag (home/battle.asm:150); a send snaps it
|
||||
@@ -1379,8 +1410,9 @@ function BattleState:advanceQueue()
|
||||
if self.shownStatus and event.side
|
||||
and (event.kind == "status"
|
||||
or (event.kind == "send" and event.mon)) then
|
||||
self.shownStatus[event.side] =
|
||||
(event.kind == "send" and event.mon.status or event.status) or false
|
||||
local shown = event.status
|
||||
if event.kind == "send" and shown == nil then shown = event.mon.status end
|
||||
self.shownStatus[event.side] = shown or false
|
||||
end
|
||||
-- AnimateExpBar (engine/battle/core.asm:7191) is called from INSIDE
|
||||
-- GiveExperiencePoints before the exp is committed (the call at :6888 sits
|
||||
@@ -1435,6 +1467,24 @@ function BattleState:advanceQueue()
|
||||
self.trainerSlide = 0
|
||||
return
|
||||
end
|
||||
-- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it
|
||||
-- (engine/battle/core.asm:2310-2312)
|
||||
if event.kind == "trainer-return" then
|
||||
if not self.enemyTrainerImage then return self:advanceQueue() end
|
||||
self.showEnemyTrainer = true
|
||||
self.picHidden.enemy = false
|
||||
self.winSlide = 0
|
||||
self.winSliding = true
|
||||
return
|
||||
end
|
||||
-- PrintWinLossText (home/trainers.asm:230): one FarPrintText of the trainer
|
||||
-- struct's own line, paged and held for A/B like any other map text.
|
||||
if event.kind == "win-text" then
|
||||
local text = event.text
|
||||
if self.game then text = TextBox.substitute(self.game, text) end
|
||||
self:showPages(text)
|
||||
return
|
||||
end
|
||||
-- The shiny sparkle: hBattleTurn 1 and wBattleAnimParam 1 pick
|
||||
-- BattleAnim_SendOutMon's `.Shiny` arm on the enemy (core.asm:8708-8715).
|
||||
if event.kind == "shiny-flash" then
|
||||
@@ -1917,6 +1967,17 @@ function BattleState:update(_dt)
|
||||
return
|
||||
end
|
||||
|
||||
-- BattleWinSlideInEnemyTrainerFrontpic plus WinTrainerBattle's DelayFrames
|
||||
-- 40 (engine/battle/core.asm:6279-6318, :2310-2312)
|
||||
if self.winSliding then
|
||||
self.winSlide = self.winSlide + 1
|
||||
if self.winSlide >= WIN_SLIDE_FRAMES + WIN_SLIDE_DELAY_FRAMES then
|
||||
self.winSliding = nil
|
||||
self:advanceQueue()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- SlideBattlePicOut is a plain loop with DelayFrames in it, so it owns the
|
||||
-- screen the same way (engine/battle/core.asm:2882).
|
||||
if self.trainerSlide then
|
||||
@@ -2004,6 +2065,8 @@ function BattleState:update(_dt)
|
||||
self.phase = "stats-box"
|
||||
return
|
||||
end
|
||||
-- PrintWinLossText's line pages like any map text (home/text.asm:403-448)
|
||||
if self:nextPage() then return end
|
||||
self:advanceQueue()
|
||||
return
|
||||
end
|
||||
@@ -2292,7 +2355,7 @@ function BattleState:update(_dt)
|
||||
learn.move, learn.moveName)
|
||||
self.pendingLearn = nil
|
||||
self.phase = "resolving"
|
||||
self:pushAll(self.battle:takeEvents())
|
||||
self:pushFront(self.battle:takeEvents())
|
||||
self:advanceQueue()
|
||||
end
|
||||
return
|
||||
@@ -2872,7 +2935,7 @@ function BattleState:finishDecline()
|
||||
self.pendingLearn = nil
|
||||
self.phase = "resolving"
|
||||
if learn then self.battle:declineForget(learn.index, learn.moveName) end
|
||||
self:pushAll(self.battle:takeEvents())
|
||||
self:pushFront(self.battle:takeEvents())
|
||||
self:advanceQueue()
|
||||
end
|
||||
|
||||
@@ -3475,6 +3538,23 @@ function BattleState:printMessage()
|
||||
end
|
||||
end
|
||||
|
||||
-- MoveInfoBox (engine/battle/core.asm:5403-5478): "TYPE/" at (1,9), the type
|
||||
-- at (2,10), cur/max PP at (5,11), or "Disabled!" at (1,10).
|
||||
function BattleState:drawMoveInfoBox(move)
|
||||
if not move then return end
|
||||
local fighter = self.battle and self.battle.player
|
||||
if fighter and self.battle:moveDisabled(fighter, move.id) then
|
||||
Chrome.print("Disabled!", 1, 10)
|
||||
return
|
||||
end
|
||||
local def = self.game and self.game.data and self.game.data.moves
|
||||
and self.game.data.moves[move.id]
|
||||
Chrome.print("TYPE/", 1, 9)
|
||||
local moveType = def and def.type
|
||||
Chrome.print(moveType and (TYPE_NAMES[moveType] or moveType) or "", 2, 10)
|
||||
Chrome.print(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0), 5, 11)
|
||||
end
|
||||
|
||||
function BattleState:drawPanel()
|
||||
Chrome.clear()
|
||||
-- A tutorial battle legitimately has no player mon, so only the enemy is
|
||||
@@ -3494,7 +3574,15 @@ function BattleState:drawPanel()
|
||||
-- Message box across the bottom, with the menu window over its right half --
|
||||
-- the cart draws the prompt into the full-width box and then opens the menu
|
||||
-- on top, so the tail of a long name is simply covered.
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
-- MoveSelectionScreen type 0 is two boxes: the name-only list
|
||||
-- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410).
|
||||
local moveMenu = self.phase == "moves"
|
||||
if moveMenu then
|
||||
Chrome.box(0, 8, 11, 5)
|
||||
Chrome.box(4, 12, 16, 6)
|
||||
else
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
end
|
||||
if self.phase == "menu" then
|
||||
self:printMessage()
|
||||
local boxX = self.contest and CONTEST_MENU_BOX_X or MENU_BOX_X
|
||||
@@ -3520,24 +3608,30 @@ function BattleState:drawPanel()
|
||||
moves = (mon and mon.moves) or moves
|
||||
end
|
||||
local cursorRow = forgetting and self.forgetIndex or self.moveIndex
|
||||
-- w2DMenuCursorInitX 5 with the names at hlcoord 6 (core.asm:5086-5107).
|
||||
local cursorCol = moveMenu and 5 or 1
|
||||
local nameCol = moveMenu and 6 or 2
|
||||
for i, move in ipairs(moves) do
|
||||
local ty = 13 + (i - 1)
|
||||
-- Cursor in the box's own gutter, not clipped against the border.
|
||||
if i == cursorRow then Chrome.cursor(1, ty) end
|
||||
if i == cursorRow then Chrome.cursor(cursorCol, ty) end
|
||||
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
|
||||
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
|
||||
-- picked up for a swap is visible while the cursor moves off it. It
|
||||
-- sits a column right of the cursor gutter, where the cart puts it
|
||||
-- (hlcoord 5, 13 against the cursor's own column), and only while the
|
||||
-- move list itself is up -- the forget picker has no swapping.
|
||||
if not forgetting and self.moveSwapIndex == i then
|
||||
Chrome.print("\u{25B7}", 0, ty)
|
||||
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
|
||||
-- the marker on the cursor's row.
|
||||
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
|
||||
Chrome.print("\u{25B7}", cursorCol, ty)
|
||||
end
|
||||
local def = self.game and self.game.data and self.game.data.moves
|
||||
and self.game.data.moves[move.id]
|
||||
Chrome.print((def and def.name) or move.id, 2, ty)
|
||||
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0), 19, ty)
|
||||
Chrome.print((def and def.name) or move.id, nameCol, ty)
|
||||
if not moveMenu then
|
||||
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
|
||||
19, ty)
|
||||
end
|
||||
end
|
||||
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
|
||||
else
|
||||
-- Battle messages wrap inside the box rather than running off the frame.
|
||||
self:printMessage()
|
||||
|
||||
@@ -337,10 +337,15 @@ function PackMenu:useSelected()
|
||||
return
|
||||
end
|
||||
local world = self.world
|
||||
local result = world and world.useFieldItem and world:useFieldItem(row.id)
|
||||
local result, extra = nil, nil
|
||||
if world and world.useFieldItem then result, extra = world:useFieldItem(row.id) end
|
||||
if result then
|
||||
if result == "nowhere" then
|
||||
self.message = OAK_THIS_ISNT_THE_TIME
|
||||
elseif result == "coin_case" then
|
||||
-- _CoinCaseCountText (data/text/common_3.asm:336): "Coins:" then the
|
||||
-- count, text_decimal 4 digits with PRINTNUM_LEFTALIGN_F so no padding.
|
||||
self.message = { "Coins:", tostring(extra or 0) }
|
||||
elseif result == "repel_used" then
|
||||
-- ItemUsedText (data/text/common_3.asm): "<PLAYER> used the\n<ITEM>."
|
||||
-- World already wrote the counter and took the item out of the bag, so
|
||||
|
||||
@@ -150,7 +150,11 @@ function PokedexMenu.new(game, opts)
|
||||
self.pokemon = opts.pokemon or data.pokemon
|
||||
self.palettes = opts.palettes or data.gen2Palettes
|
||||
self.onClose = opts.onClose
|
||||
-- InitPokedex: wLastDexMode -> wCurDexMode (engine/pokedex/pokedex.asm:97).
|
||||
self.modeIndex = 1
|
||||
for i, name in ipairs(MODES) do
|
||||
if self.save and name == self.save.lastDexMode then self.modeIndex = i end
|
||||
end
|
||||
self.index = 1
|
||||
self.scroll = 0
|
||||
self.view = "list" -- list | entry | area | option | search | results | unown
|
||||
@@ -317,6 +321,13 @@ function PokedexMenu:cursorVisible()
|
||||
return ((self.entryBlink or 0) % 32) < 20
|
||||
end
|
||||
|
||||
-- Pokedex: wCurDexMode -> wLastDexMode on the way out
|
||||
-- (engine/pokedex/pokedex.asm:60), which lives in the saved game data.
|
||||
function PokedexMenu:close()
|
||||
if self.save then self.save.lastDexMode = MODES[self.modeIndex] end
|
||||
if self.onClose then self.onClose() end
|
||||
end
|
||||
|
||||
function PokedexMenu:update(_dt)
|
||||
self.entryBlink = (self.entryBlink or 0) + 1
|
||||
local input = self.game and self.game.input
|
||||
@@ -328,8 +339,8 @@ function PokedexMenu:update(_dt)
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
if self.page == 1 then
|
||||
self.page = 2
|
||||
elseif self.onClose then
|
||||
self.onClose()
|
||||
else
|
||||
self:close()
|
||||
end
|
||||
end
|
||||
return
|
||||
@@ -367,7 +378,7 @@ function PokedexMenu:update(_dt)
|
||||
if self.view == "search" then return self:updateSearch(input) end
|
||||
if self.view == "unown" then return self:updateUnown(input) end
|
||||
if input:wasPressed("b") then
|
||||
if self.onClose then self.onClose() end
|
||||
self:close()
|
||||
return
|
||||
elseif input:wasPressed("select") then
|
||||
-- Pokedex_UpdateMainScreen: SELECT opens the OPTION screen and START the
|
||||
|
||||
@@ -2280,7 +2280,9 @@ function Pokegear:drawPanel()
|
||||
self:drawClock()
|
||||
end
|
||||
-- Last: the arrow is an OBJ and composites over whatever the card drew.
|
||||
self:drawModeArrow()
|
||||
-- _FlyMap has no card strip and never animates it
|
||||
-- (engine/pokegear/pokegear.asm:1999).
|
||||
if not self.fly then self:drawModeArrow() end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
@@ -36,9 +36,8 @@
|
||||
-- $3f the shiny ⁂ icon (stats_tiles tile 14)
|
||||
-- $40 / $41 the left and right HP/exp bar end caps
|
||||
--
|
||||
-- The extractor does not carry that sheet yet, so `pageTile` draws those seven
|
||||
-- shapes directly and takes the sheet the moment menu_gfx grows a `stats`
|
||||
-- entry. Everything that IS a glyph goes through the font: ◀ ($71), ▶ ($ed),
|
||||
-- The extractor writes that sheet as menu_gfx.stats, which `pageTile` draws.
|
||||
-- Everything that IS a glyph goes through the font: ◀ ($71), ▶ ($ed),
|
||||
-- № ($74), <ID> ($73), <LV> ($6e) and the row-7 rule's $62 (the empty HP/exp
|
||||
-- bar cell, which is FontBattleExtra's -- hence Font.useBattleExtra(true)
|
||||
-- around the whole screen, exactly as the party menu does).
|
||||
@@ -95,6 +94,14 @@ local TILE_BAR_CAP_RIGHT = 0x41
|
||||
-- made of (StatsScreen_PlaceHorizontalDivider).
|
||||
local TILE_HORIZONTAL_DIVIDER = 0x62
|
||||
|
||||
-- gfx/stats/pages.pal, the three palettes _CGB_StatsScreenHPPals copies to
|
||||
-- wBGPals1 slots 3-5 (engine/gfx/cgb_layouts.asm:199-212)
|
||||
local PAGE_PALETTES = {
|
||||
{ { 255, 255, 255 }, { 255, 156, 255 }, { 255, 123, 255 }, { 0, 0, 0 } },
|
||||
{ { 255, 255, 255 }, { 173, 255, 115 }, { 140, 255, 0 }, { 0, 0, 0 } },
|
||||
{ { 255, 255, 255 }, { 140, 255, 255 }, { 140, 255, 255 }, { 0, 0, 0 } },
|
||||
}
|
||||
|
||||
-- PrintTempMonStats' .StatNames, and the wTempMon fields it prints beside
|
||||
-- them. <NEXT> steps two rows, so the five labels are 2 rows apart and the
|
||||
-- values start one row below the first label.
|
||||
@@ -821,12 +828,37 @@ end
|
||||
|
||||
-- ----------------------------------------------------------------- drawing
|
||||
|
||||
-- A tile out of StatsScreenPageTilesGFX. The extractor does not carry that
|
||||
-- sheet, so each of the seven shapes it needs is drawn here; the moment
|
||||
-- menu_gfx grows a `stats` entry this can take the real tiles instead.
|
||||
-- menu_gfx.stats, the 17 tiles LoadStatsScreenPageTilesGFX lands at vTiles2
|
||||
-- tile $31 (engine/gfx/load_font.asm:90-95)
|
||||
function SummaryMenu:statsTiles()
|
||||
if self.statsSheet ~= nil then return self.statsSheet or nil end
|
||||
local gfx = (self.menuGfx or {}).stats
|
||||
local image = gfx and self:picImage(gfx.sheet)
|
||||
if not image then
|
||||
self.statsSheet = false
|
||||
return nil
|
||||
end
|
||||
local w, h = image:getDimensions()
|
||||
local quads = {}
|
||||
for index = 0, (gfx.tiles or 17) - 1 do
|
||||
quads[(gfx.firstTile or 0x31) + index] =
|
||||
love.graphics.newQuad(index * 8, 0, 8, 8, w, h)
|
||||
end
|
||||
self.statsSheet = { image = image, quads = quads }
|
||||
return self.statsSheet
|
||||
end
|
||||
|
||||
-- A tile out of StatsScreenPageTilesGFX. The fallback arm draws each of the
|
||||
-- seven shapes by hand for a cache built before menu_gfx.stats existed.
|
||||
function SummaryMenu:pageTile(id, tx, ty)
|
||||
local G = love.graphics
|
||||
local px, py = tx * 8, ty * 8
|
||||
local sheet = self:statsTiles()
|
||||
if sheet and sheet.quads[id] then
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(sheet.image, sheet.quads[id], px, py)
|
||||
return
|
||||
end
|
||||
G.setColor(0, 0, 0, 1)
|
||||
if id == TILE_VERTICAL_DIVIDER then
|
||||
G.rectangle("fill", px + 3, py, 2, 8)
|
||||
@@ -847,11 +879,29 @@ end
|
||||
-- (17,5), all small ($36) first, then the one for this page redrawn large
|
||||
-- ($3a). The routine writes the four tiles as [hli]/[hld], a row down, then
|
||||
-- [hli]/[hl] -- which is why it is a 2x2 block and not a 2x1 strip.
|
||||
function SummaryMenu:drawPageSquare(tx, ty, large)
|
||||
function SummaryMenu:drawPageSquare(tx, ty, large, colors)
|
||||
local G = love.graphics
|
||||
local px, py = tx * 8, ty * 8
|
||||
-- $3a..$3d for the page that is up, $36..$39 for the other two.
|
||||
local first = large and TILE_SQUARE_LARGE or TILE_SQUARE_SMALL
|
||||
local sheet = self:statsTiles()
|
||||
if sheet and sheet.quads[first] then
|
||||
-- [hli] / [hld], a row down, [hli] / [hl]: the four tiles in that order.
|
||||
local function body()
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(sheet.image, sheet.quads[first], px, py)
|
||||
G.draw(sheet.image, sheet.quads[first + 1], px + 8, py)
|
||||
G.draw(sheet.image, sheet.quads[first + 2], px, py + 8)
|
||||
G.draw(sheet.image, sheet.quads[first + 3], px + 8, py + 8)
|
||||
end
|
||||
if colors and GbcPalette.available() then
|
||||
GbcPalette.with(colors, body)
|
||||
else
|
||||
body()
|
||||
end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
local inset = first == TILE_SQUARE_LARGE and 2 or 5
|
||||
local size = 16 - inset * 2
|
||||
G.setColor(0, 0, 0, 1)
|
||||
@@ -862,7 +912,7 @@ end
|
||||
function SummaryMenu:drawPageIndicators()
|
||||
local columns = { 13, 15, 17 }
|
||||
for i, tx in ipairs(columns) do
|
||||
self:drawPageSquare(tx, 5, i == self.page)
|
||||
self:drawPageSquare(tx, 5, i == self.page, PAGE_PALETTES[i])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2969,6 +2969,25 @@ function OverworldState:openPC(onDone)
|
||||
})
|
||||
end
|
||||
|
||||
-- PKMN LEAGUE sits between PROF.OAK's PC and LOG OFF once wNumHoFTeams
|
||||
-- is nonzero (engine/pokemon/bills_pc.asm:5, :49)
|
||||
if #(Game.save.hallOfFame or {}) > 0 then
|
||||
table.insert(items, {
|
||||
label = Strings("<PK><MN>LEAGUE"),
|
||||
keepOpen = true,
|
||||
onSelect = function()
|
||||
-- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints
|
||||
-- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2)
|
||||
require("src.core.Sound").play(Game.data, "Enter_PC")
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
romText(Game.data, "_AccessedHoFPCText",
|
||||
"Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."),
|
||||
function() Screens.push(Game, "LeaguePC") end))
|
||||
done()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local hooked = Runtime.call("ui.pc.items", sameItems, Game, items)
|
||||
if type(hooked) == "table" then
|
||||
items = hooked
|
||||
@@ -3366,7 +3385,8 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
||||
-- checkVictoryRewards pushes the badge/prize box and starts the map's
|
||||
-- onVictory script UNDER whatever runs next, so the player still sees
|
||||
-- EndBattle (now inside the battle), then the reward, then AfterBattle
|
||||
self:checkVictoryRewards(d.trainerClass, d.trainerParty)
|
||||
self:checkVictoryRewards(d.trainerClass, d.trainerParty,
|
||||
endBattleText ~= nil)
|
||||
self:afterBattle(result, battle)
|
||||
if onDone then onDone() end
|
||||
else
|
||||
@@ -3477,7 +3497,10 @@ end
|
||||
-- SetEvent / SetEventRange do after the leader victory.
|
||||
-- `hide` is { { mapId, objName }, ... } -- HideObject on those toggles
|
||||
-- (e.g. Brock victory clears PEWTERCITY_YOUNGSTER / ROUTE22_RIVAL1).
|
||||
function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
|
||||
-- `shownOnBattleScreen`: `dialogue` already rode the battle screen as the
|
||||
-- armed end-battle line (scripts/CeruleanGym.asm:113)
|
||||
function OverworldState:checkVictoryRewards(trainerClass, partyIndex,
|
||||
shownOnBattleScreen)
|
||||
local victories = require("data.scripts.victories")
|
||||
local reward = victories[trainerClass .. "#" .. tostring(partyIndex or 1)]
|
||||
if not reward then return self:runVictoryHook() end
|
||||
@@ -3510,7 +3533,9 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
|
||||
end
|
||||
local chain = rewardChain()
|
||||
if reward.dialogue then
|
||||
chain.add(reward.dialogue, reward.badgeSound)
|
||||
if not shownOnBattleScreen then
|
||||
chain.add(reward.dialogue, reward.badgeSound)
|
||||
end
|
||||
if reward.item then
|
||||
chain.add(reward.tmPre)
|
||||
if tmGiven then
|
||||
@@ -4423,7 +4448,8 @@ function OverworldState:restoreBattleContinuation(battle, origin)
|
||||
if result == "win" then
|
||||
game.save.defeatedTrainers[origin.npcId] = true
|
||||
if origin.event then game.save.flags[origin.event] = true end
|
||||
self:checkVictoryRewards(battle.oppClass, battle.partyIndex)
|
||||
self:checkVictoryRewards(battle.oppClass, battle.partyIndex,
|
||||
battle.endBattleText ~= nil)
|
||||
end
|
||||
self:afterBattle(result, battle)
|
||||
self.engaging = false
|
||||
|
||||
@@ -391,15 +391,19 @@ end
|
||||
-- Gen 2 moveset lives in `levelMoves` (EvosAttacks). So every scripted gift,
|
||||
-- the STARTER included, arrived knowing nothing: FIGHT listed no moves and the
|
||||
-- battle had no legal action left in it.
|
||||
local function givePokeMon(data, speciesIndex, level, itemIndex)
|
||||
local function givePokeMon(data, speciesIndex, level, itemIndex, opts)
|
||||
local id = speciesByIndex(data.pokemon, speciesIndex)
|
||||
if not id then return nil end
|
||||
return Mon.new(data, id, level or 5, {
|
||||
item = itemIndex and itemIndex ~= 0
|
||||
and itemByIndex(data.items, itemIndex) or nil,
|
||||
nickname = opts and opts.nickname or nil,
|
||||
})
|
||||
end
|
||||
|
||||
-- GivePoke's trainer arm (engine/pokemon/move_mon.asm:1698-1736)
|
||||
local RANDY_OT_ID = 1001
|
||||
|
||||
local function loadGenerated(path)
|
||||
-- Same NX gold/ fallback Game2 uses. World:load is what surfaces
|
||||
-- "Gold cache incomplete" when maps.lua is invisible at the unprefixed path.
|
||||
@@ -997,13 +1001,18 @@ function World:load()
|
||||
setStringBuffer = function(value)
|
||||
if self.game then self.game.stringBuffer = value end
|
||||
end,
|
||||
givePoke = function(speciesIndex, level, item)
|
||||
givePoke = function(speciesIndex, level, item, opts)
|
||||
local data = self.game and self.game.data
|
||||
local save = self.game and self.game.save
|
||||
if not (data and save) then return end
|
||||
save.party = save.party or {}
|
||||
local mon = givePokeMon(data, speciesIndex, level, item)
|
||||
local mon = givePokeMon(data, speciesIndex, level, item, opts)
|
||||
if mon then
|
||||
if opts and opts.otName then
|
||||
mon.ot = opts.otName
|
||||
mon.otName = opts.otName
|
||||
mon.otId = RANDY_OT_ID
|
||||
end
|
||||
-- GivePoke -> TryAddMonToParty -> AddPartyMon (move_mon.asm:44-56, :143-149).
|
||||
Mon.stampOT(save, mon)
|
||||
Party.add(save.party, mon)
|
||||
@@ -1458,13 +1467,11 @@ function World:mapSceneOf(group, mapNum)
|
||||
end
|
||||
|
||||
-- wTimeOfDay (constants/ram_constants.asm): MORN_F 0, DAY_F 1, NITE_F 2,
|
||||
-- DARKNESS_F 3. Palettes.daytimeFor has already resolved the clock and the
|
||||
-- map's own PALETTE_* override into one of four names, so this is a rename
|
||||
-- rather than a second clock.
|
||||
-- DARKNESS_F 3, off the RTC hour (engine/tilesets/timeofday_pals.asm:5-11)
|
||||
local TIME_OF_DAY_ID = { MORN = 0, DAY = 1, NITE = 2, DARK = 3 }
|
||||
|
||||
function World:timeOfDayId()
|
||||
return TIME_OF_DAY_ID[self.daytime or "DAY"] or 1
|
||||
return TIME_OF_DAY_ID[self.tod or self.daytime or "DAY"] or 1
|
||||
end
|
||||
|
||||
-- GetWeekday -> wCurDay, which the RTC counts SUNDAY 0 .. SATURDAY 6 -- the
|
||||
@@ -4369,6 +4376,8 @@ function World:useFieldItem(itemId)
|
||||
if itemId == "SACRED_ASH" then return self:useSacredAsh() end
|
||||
if itemId == "ESCAPE_ROPE" then return self:useEscapeRope(itemId) end
|
||||
if itemId == "SQUIRTBOTTLE" then return self:useSquirtbottle() end
|
||||
-- CoinCaseEffect (engine/items/item_effects.asm:2243).
|
||||
if itemId == "COIN_CASE" then return "coin_case", self:coins() end
|
||||
if REPEL_STEPS[itemId] then return self:useRepel(itemId) end
|
||||
if TROPHY_BOXES[itemId] then return self:openTrophyBox(itemId) end
|
||||
if not World.isRod(itemId, items) then return nil end
|
||||
@@ -5802,7 +5811,9 @@ function World:battleMusicContext(opts)
|
||||
members = trainer and trainer.classId and members
|
||||
and members[trainer.classId] or nil,
|
||||
landmark = self.map and self.map.def and self.map.def.landmark,
|
||||
daytime = self.daytime,
|
||||
-- PlayBattleMusic reads wTimeOfDay (engine/battle/start_battle.asm:24),
|
||||
-- not the map's pinned palette set.
|
||||
daytime = self.tod,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -5999,6 +6010,18 @@ function World:startBattle(opts, onDone)
|
||||
return true
|
||||
end
|
||||
|
||||
-- wWinTextPointer / wLossTextPointer (home/trainers.asm:120), overwritten by
|
||||
-- `winlosstext` (engine/overworld/scripting.asm:651)
|
||||
function World:trainerWinLossText()
|
||||
local vm = self.vm
|
||||
if not vm then return nil, nil end
|
||||
local obj = vm.trainerObject or {}
|
||||
local text = self.text or {}
|
||||
local winKey = vm.winTextOverride or obj.winText
|
||||
local lossKey = vm.lossTextOverride or obj.lossText
|
||||
return winKey and text[winKey] or nil, lossKey and text[lossKey] or nil
|
||||
end
|
||||
|
||||
-- `startbattle` from a script: a trainer record (class + member) or a
|
||||
-- loadwildmon pair. The VM is parked on the yield until onDone fires, so the
|
||||
-- rest of the trainer script (flag set, after-battle text) runs on return.
|
||||
@@ -6057,6 +6080,9 @@ function World:startScriptedBattle(record, wild, onDone)
|
||||
attributes = record.attributes,
|
||||
items = record.items,
|
||||
}
|
||||
-- wWinTextPointer / wLossTextPointer, read by PrintWinLossText on the
|
||||
-- battle screen (home/trainers.asm:230) (#1512)
|
||||
opts.trainer.winText, opts.trainer.lossText = self:trainerWinLossText()
|
||||
elseif wild and wild.species then
|
||||
local id, def = speciesByIndex(data and data.pokemon, wild.species)
|
||||
-- InitEnemyMon `.NotRoaming` / BATTLETYPE.FORCESHINY: the DV pair is
|
||||
@@ -9194,8 +9220,10 @@ function World:stepContext()
|
||||
local def = self.map and self.map.def
|
||||
return {
|
||||
data = self.game and self.game.data,
|
||||
-- CheckTime reads wTimeOfDay (engine/events/checktime.asm:2), so the
|
||||
-- caller windows follow the clock even inside a pinned-palette room.
|
||||
phone = {
|
||||
map = def, maps = self.maps, daytime = self.daytime,
|
||||
map = def, maps = self.maps, daytime = self.tod,
|
||||
clock = self.game and self.game.clock,
|
||||
},
|
||||
-- GetMapPhoneService: zero means the map HAS service, which maps.lua has
|
||||
|
||||
Reference in New Issue
Block a user