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:
bryanthaboi
2026-08-16 08:55:40 -04:00
parent 65128e13a4
commit 1151c188a7
50 changed files with 2835 additions and 276 deletions
+20 -10
View File
@@ -583,7 +583,8 @@ end
local function stampOT(save, mon)
save.player.id = save.player.id or math.random(0, 65535)
mon.ot = mon.ot or save.player.name
mon.otId = mon.otId or save.player.id
-- engine/battle/experience.asm:69
if not mon.traded then mon.otId = mon.otId or save.player.id end
end
BattleState.stampOT = stampOT
@@ -5923,10 +5924,16 @@ function BattleState:drawTextArea()
Font.drawCode(Font.BORDER.h, 32, 96)
Font.drawCode(Font.BORDER.br, 80, 96)
love.graphics.setColor(0, 0, 0, 1)
for i, mv in ipairs(self.player.curMoves) do
-- unknown ids (mod-injected moves) print raw instead of crashing
local def = self.data.moves[mv.id]
Font.draw(def and def.name or tostring(mv.id), 48, 96 + i * 8)
-- engine/battle/misc.asm:37
for i = 1, 4 do
local mv = self.player.curMoves[i]
if mv then
-- unknown ids (mod-injected moves) print raw instead of crashing
local def = self.data.moves[mv.id]
Font.draw(def and def.name or tostring(mv.id), 48, 96 + i * 8)
else
Font.draw("-", 48, 96 + i * 8)
end
end
-- Swap cursor: SelectMenuItem parks the hollow arrow on the marked row
-- (core.asm:2600-2607), then HandleMenuInput's PlaceMenuCursor writes the
@@ -5953,13 +5960,16 @@ function BattleState:drawTextArea()
end
end
elseif self.phase == "mimicSelect" then
-- Mimic's copy menu (MoveSelectionMenu .mimicmenu, core.asm:
-- 2506-2517): the enemy's move list in a 16x6 box at (0,7), names
-- single-spaced from (2,8), cursor at column 1
-- Mimic's copy menu (MoveSelectionMenu .mimicmenu, core.asm:2506-2517):
-- 16x6 box at (0,7), names from (2,8), cursor at column 1
Font.drawBox(0, 7, 16, 6)
love.graphics.setColor(0, 0, 0, 1)
for i, m in ipairs(self.mimicMoves) do
Font.draw(self.data.moves[m.id].name, 16, (7 + i) * 8)
-- engine/battle/misc.asm:37
for i = 1, 4 do
local m = self.mimicMoves[i]
local def = m and self.data.moves[m.id]
Font.draw(m and (def and def.name or tostring(m.id)) or "-",
16, (7 + i) * 8)
end
Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8)
Font.draw(Strings("WHICH TECHNIQUE?"), 8, 112)
+5 -5
View File
@@ -232,15 +232,15 @@ function EffectRegistry.runDamaging(battle, ctx, record)
hitSfx = { sound = "Damage", pitch = 0x20 }
end
-- GetPlayerAnimationType / GetEnemyAnimationType (engine/battle/core.asm
-- :3159 / :5555): wAnimationType is 4 (blink the enemy pic) or 1 (shake
-- the screen vertically) for a damaging move with no added effect, and
-- 5 / 2 (a horizontal shake) as soon as the move HAS one -- which is why
-- Bubblebeam and Confusion shake instead of blinking (#354)
-- :3159 / :5555): 4 blinks the enemy pic, 1 shakes vertically, 5 / 2 once
-- the move has an added effect (#354)
local added = move.effect ~= nil and move.effect ~= "NO_ADDITIONAL_EFFECT"
-- PlayApplyingAttackAnimation runs on both arms of the wOptions check
-- (engine/battle/animations.asm:424-437), so the blink is not gated (#1384)
local hitFx = { sfx = hitSfx,
animType = user.isPlayer and (added and 5 or 4)
or (added and 2 or 1),
blink = battle:animationsOn() and target or nil }
blink = target }
local totalDealt = 0
local landed, brokeSub = 0, false
+6 -12
View File
@@ -192,14 +192,8 @@ function Runner:loadGfx(names)
end
end
-- BattleAnimCmd_BattlerGFX_1Row / _2Row. The battlers' pic tiles are
-- APPENDED after whatever the script already loaded rather than replacing it,
-- and they always land on the same two fixed tile ids.
--
-- (pokegold's jumptable has these two labels the other way round from the
-- macro names -- $d9 dispatches to BattleAnimCmd_BattlerGFX_1Row while
-- anim_battlergfx_2row is $d9 -- so the names below follow the MACRO, which
-- is what a script actually writes.)
-- engine/battle_anims/anim_commands.asm:755. The jumptable crosses the macro
-- names: $d9 (anim_battlergfx_2row) dispatches to _1Row (#1401)
function Runner:loadBattlerGfx(rows)
local tiles = rows == 2 and BATTLER_TILES.twoRow or BATTLER_TILES.oneRow
local slot = 1
@@ -210,11 +204,11 @@ function Runner:loadBattlerGfx(rows)
self.tileDict[slot] = { gfx = "BATTLE_ANIM_GFX_PLAYERHEAD", tile = tiles.player }
self.tileDict[slot + 1] = { gfx = "BATTLE_ANIM_GFX_ENEMYFEET", tile = tiles.enemy }
self.loaded[#self.loaded + 1] =
{ gfx = "BATTLE_ANIM_GFX_PLAYERHEAD", tile = tiles.player, tiles = rows * 6,
battler = "player", rows = rows }
self.loaded[#self.loaded + 1] =
{ gfx = "BATTLE_ANIM_GFX_ENEMYFEET", tile = tiles.enemy, tiles = rows * 7,
{ gfx = "BATTLE_ANIM_GFX_PLAYERHEAD", tile = tiles.player, tiles = rows * 7,
battler = "enemy", rows = rows }
self.loaded[#self.loaded + 1] =
{ gfx = "BATTLE_ANIM_GFX_ENEMYFEET", tile = tiles.enemy, tiles = rows * 6,
battler = "player", rows = rows }
end
--------------------------------------------------------------------------
+80 -31
View File
@@ -80,6 +80,7 @@ Battle.SECONDARY_EFFECTS = {
EFFECT_PARALYZE_HIT = "paralyze",
EFFECT_SLEEP_HIT = "sleep",
EFFECT_CONFUSE_HIT = "confuse",
EFFECT_SACRED_FIRE = "burn", -- data/moves/effects.asm:1696
}
local function rand(random, n)
@@ -1320,6 +1321,18 @@ function Battle:markMissed()
if self.moveEvent then self.moveEvent.missed = true end
end
-- engine/battle/effect_commands.asm:3615
Battle.AI_FAIL_STATUSES = {
sleep = true, poison = true, toxic = true, paralyze = true,
}
-- engine/battle/effect_commands.asm:3615
function Battle:aiRandomFail(attacker, defender)
if self:sideOf(attacker) ~= "enemy" then return false end
if self:volatile(defender).lockOn then return false end
return rand(self.random, 256) < 64
end
-- One attack, start to finish.
function Battle:useMove(attacker, defender, moveId)
local move = self:findMove(attacker, moveId)
@@ -1478,10 +1491,10 @@ function Battle:useMove(attacker, defender, moveId)
if charge and not charging then
state.chargeMove = moveId
state.vanished = charge.vanish or nil
-- DIG and FLY are the same effect in Gen 2 (both EFFECT_FLY), so the table
-- keyed by effect cannot tell them apart and DIG announced itself with
-- "flew up high!". BattleCommand_Fly picks the line off the MOVE, not the
-- effect: `cp DIG` and then the burrow text.
-- engine/battle/effect_commands.asm:5458
if self.moveEvent then self.moveEvent.animParam = 1 end
-- BattleCommand_Charge picks the line off the MOVE, not the shared
-- EFFECT_FLY (`cp DIG`, effect_commands.asm:5464).
local text = charge.text
if moveId == "DIG" then text = "%s dug a hole!" end
self:emit({ kind = "message", text = text:format(name) })
@@ -1811,20 +1824,20 @@ function Battle:useMove(attacker, defender, moveId)
-- Defense Curl arms Rollout as well as raising Defense.
if def.effect == "EFFECT_DEFENSE_CURL" then state.curled = true end
-- Stat changes: the primary ones always land, the *_HIT ones roll the
-- move's effect chance after a hit that connected.
--
-- A refused primary change is a failure the cart detects BEFORE its anim
-- command: RaiseStat's `.cant_raise_stat` and StatDown's `.CantLower` /
-- `.Mist` all write wAttackMissed (effect_commands.asm:4191, :4380-4400),
-- and `statupanim` / `statdownanim` read it (:2022) from a slot AFTER
-- `attackup` / `attackdown` in the effect list (data/moves/effects.asm,
-- AttackUp). The *_HIT twins must NOT be marked: their `attackdown` runs
-- after `moveanim` (AttackDownHit), so the animation has already played.
-- A refused primary change writes wAttackMissed (effect_commands.asm:4191,
-- :4380-4400); the *_HIT twins animate first and must stay unmarked.
local change = Effects.STAT_CHANGES[def.effect]
if change then
local target = change[3] == "self" and attacker or defender
if not self:changeStageAgainstMist(attacker, target, change[1], change[2])
-- CheckMist first (effect_commands.asm:4290), then .ComputerMiss (:4318)
local misted = target ~= attacker and (change[2] or 0) < 0
and self:volatile(target).mist
if not misted and change[3] == "foe"
and def.effect ~= "EFFECT_ACCURACY_DOWN_HIT"
and self:aiRandomFail(attacker, target) then
self:markMissed()
self:emit({ kind = "message", text = "But it failed!" })
elseif not self:changeStageAgainstMist(attacker, target, change[1], change[2])
then
self:markMissed()
end
@@ -1853,19 +1866,21 @@ function Battle:useMove(attacker, defender, moveId)
local record = Battle.moveEffectRecordFor(self.data, def.effect)
local status = record and record.kind == "primary" and record.status or nil
if status and (def.power or 0) == 0 then
-- Every status command's already-statused / immune arm ends on
-- AnimateFailedMove (BattleCommand_Poison's `.failed`,
-- effect_commands.asm:3748-3750, and :6656-6659): LowerSub, MoveDelay,
-- RaiseSub and no LoadMoveAnim. AnimateCurrentMove only runs on the
-- success path (:3752), and the effect scripts carry no moveanim of their
-- own (data/moves/effects.asm, Toxic / DoPoison). The SECONDARY_EFFECTS
-- branch below is the opposite case: that move already hit and already
-- animated, so a refused secondary must leave the event unmarked.
if not self:applyStatus(defender, status, attacker) then self:markMissed() end
-- A refused primary status is a failed move (effect_commands.asm:3748,
-- :6656); a refused secondary already animated and stays unmarked (:3752).
if Battle.AI_FAIL_STATUSES[status]
and self:aiRandomFail(attacker, defender) then
self:markMissed()
self:emit({ kind = "message", text = "But it failed!" })
elseif not self:applyStatus(defender, status, attacker) then
self:markMissed()
end
else
local secondary = record and record.kind == "secondary"
and record.status or nil
if secondary and (defender.hp or 0) > 0 then
-- engine/battle/effect_commands.asm:6325
if secondary and (defender.hp or 0) > 0
and not self:safeguarded(defender) then
local chance = def.effectChance or 0
if chance > 0 and rand(self.random, 100) < chance then
self:applyStatus(defender, secondary, attacker)
@@ -2425,6 +2440,15 @@ Battle.MOVE_EFFECTS.EFFECT_REFLECT = function(self, attacker)
text = self:monName(attacker) .. "'s DEFENSE rose!" })
end
-- engine/battle/move_effects/safeguard.asm:1
Battle.MOVE_EFFECTS.EFFECT_SAFEGUARD = function(self, attacker)
local side = self.screens[self:sideOf(attacker)]
if (side.safeguard or 0) > 0 then return fail(self) end
side.safeguard = Battle.SCREEN_TURNS
self:emit({ kind = "message",
text = self:monName(attacker) .. "'s covered by a veil!" })
end
-- BattleCommand_Curse (engine/battle/move_effects/curse.asm): two moves in
-- one body. A non-Ghost user trades a stage of Speed for one each of Attack
-- and Defense, refused only when BOTH raises are already capped; a Ghost
@@ -2910,6 +2934,11 @@ function Battle.statusPenaltyFor(data, mon, stat, value)
return math.max(1, math.floor(value / math.max(1, penalty.div or 1)))
end
-- engine/battle/effect_commands.asm:6325
function Battle:safeguarded(mon)
return (self.screens[self:sideOf(mon)].safeguard or 0) > 0
end
-- `source` is the battler that inflicted it, carried only so
-- battle.status_inflicted can name it the way Gen 1's does.
function Battle:applyStatus(mon, status, source)
@@ -2917,7 +2946,14 @@ function Battle:applyStatus(mon, status, source)
-- Confusion is SUBSTATUS_CONFUSED on the cart, not a status byte: it lives
-- in the volatile beside the major status, so a confused mon can still be
-- burned and a switch shakes the confusion off.
if status == "confuse" then return self:applyConfusion(mon) end
if status == "confuse" then return self:applyConfusion(mon, nil, source) end
-- engine/battle/effect_commands.asm:6338
if source and self:sideOf(source) ~= self:sideOf(mon)
and self:safeguarded(mon) then
self:emit({ kind = "message",
text = self:monName(mon) .. " is protected by SAFEGUARD!" })
return false
end
-- One major status at a time.
if mon.status then
self:emit({ kind = "message",
@@ -2953,8 +2989,15 @@ end
-- as 256 turns. HELD_PREVENT_CONFUSE on the target blocks it outright.
Battle.BERSERK_GENE_CONFUSE_TURNS = 256
function Battle:applyConfusion(mon, turns)
function Battle:applyConfusion(mon, turns, source)
if (mon.hp or 0) <= 0 then return false end
-- engine/battle/effect_commands.asm:6338
if source and self:sideOf(source) ~= self:sideOf(mon)
and self:safeguarded(mon) then
self:emit({ kind = "message",
text = self:monName(mon) .. " is protected by SAFEGUARD!" })
return false
end
local state = self:volatile(mon)
if (state.substitute or 0) > 0 then return false end
local held = self:heldEffect(mon, "confuse")
@@ -4434,14 +4477,20 @@ Battle.SCREEN_FALL_TEXT = {
function Battle:tickScreens()
for _, side in ipairs({ "player", "enemy" }) do
local screens = self.screens[side]
for _, field in ipairs({ "lightScreen", "reflect" }) do
for _, field in ipairs({ "lightScreen", "reflect", "safeguard" }) do
if (screens[field] or 0) > 0 then
screens[field] = screens[field] - 1
if screens[field] <= 0 then
screens[field] = nil
self:emit({ kind = "message",
text = Battle.SCREEN_SIDE_LABEL[side]
.. Battle.SCREEN_FALL_TEXT[field] })
if field == "safeguard" then
-- engine/battle/core.asm:1527
self:emit({ kind = "message",
text = self:monName(self[side]) .. "'s SAFEGUARD faded!" })
else
self:emit({ kind = "message",
text = Battle.SCREEN_SIDE_LABEL[side]
.. Battle.SCREEN_FALL_TEXT[field] })
end
end
end
end