Files
gen1recomp/src/core/game3/battle/engine.lua
T

2596 lines
89 KiB
Lua

-- Owned turn resolver: moves, effects, residuals, commands.
local Damage = require("src.core.game3.battle.damage")
local Moves = require("src.core.game3.battle.moves")
local State = require("src.core.game3.battle.state")
local Effects = require("src.core.game3.battle.effects")
local EffectIds = require("src.core.game3.battle.effect_ids")
local Residuals = require("src.core.game3.battle.residuals")
local rollWarned = false
local badgeWarned = false
local ResidualHandlers = require("src.core.game3.battle.residual_handlers")
local Commands = require("src.core.game3.battle.commands")
local Types = require("src.core.game3.battle.types")
local Rules = require("src.core.game3.battle.rules")
local Secondary = require("src.core.game3.battle.effects.secondary")
local HeldItems = require("src.core.game3.battle.held_items")
local Abilities = require("src.core.game3.battle.abilities")
local Oak = require("src.core.game3.battle.oak_advice")
local ModRuntime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local Engine = {}
ResidualHandlers.registerAll()
local E = EffectIds
-- pokefirered/include/constants/pokemon.h:238
local FLAG_PROTECT_AFFECTED = 2
local FLAG_MAGIC_COAT_AFFECTED = 4
local FLAG_SNATCH_AFFECTED = 8
local FLAG_MIRROR_MOVE_AFFECTED = 16
local MOVE_TARGET_SELECTED = 0
local MOVE_TARGET_DEPENDS = 1
local MOVE_TARGET_USER_OR_SELECTED = 2
local MOVE_TARGET_RANDOM = 4
local MOVE_TARGET_BOTH = 8
local MOVE_TARGET_USER = 16
local MOVE_TARGET_FOES_AND_ALLY = 32
local MOVE_TARGET_OPPONENTS_FIELD = 64
Engine.MOVE_TARGET = {
SELECTED = MOVE_TARGET_SELECTED, DEPENDS = MOVE_TARGET_DEPENDS,
USER_OR_SELECTED = MOVE_TARGET_USER_OR_SELECTED, RANDOM = MOVE_TARGET_RANDOM,
BOTH = MOVE_TARGET_BOTH, USER = MOVE_TARGET_USER, FOES_AND_ALLY = MOVE_TARGET_FOES_AND_ALLY,
OPPONENTS_FIELD = MOVE_TARGET_OPPONENTS_FIELD,
}
local MOVE_SNORE, MOVE_SLEEP_TALK, MOVE_STRUGGLE, MOVE_CURSE = 173, 214, 165, 174
local MOVE_SKY_ATTACK, MOVE_BOUNCE, MOVE_FLY, MOVE_DIG, MOVE_DIVE = 143, 340, 19, 91, 291
-- pokefirered/src/battle_script_commands.c:707
local FORBIDDEN_TO_COPY = {
118, 165, 166, 102, -1, 68, 243, 182, 197, 203, 194, 214, 168, 266, 289, 270, 343, 271, 264,
}
local function forbidden(move, list_end_at_mimic)
for _, m in ipairs(FORBIDDEN_TO_COPY) do
if m == -1 then
if list_end_at_mimic then return false end
elseif m == move then
return true
end
end
return false
end
Engine.isForbiddenToCopy = forbidden
-- pokefirered/src/battle_script_commands.c:741
Engine.NATURE_POWER_MOVES = {
[0] = 78, [1] = 75, [2] = 89, [3] = 56, [4] = 57, [5] = 61, [6] = 157, [7] = 247, [8] = 129, [9] = 129,
}
local function move_num(moveId, move)
local n = tonumber(move and move.numId) or tonumber(moveId)
if n then return n end
if type(moveId) == "string" then return Moves.numForName(Moves.normalizeId(moveId)) end
return nil
end
Engine.moveNum = move_num
local function has_flag(move, flag)
local f = move and move.flags
if f == nil or (move.numId == nil and (tonumber(f) or 0) == 0) then
if flag == FLAG_PROTECT_AFFECTED then
return (tonumber(move and move.target) or 0) ~= MOVE_TARGET_USER
end
return false
end
f = tonumber(f) or 0
return math.floor(f / flag) % 2 == 1
end
Engine.hasFlag = has_flag
local function roll(adapter, lo, hi)
if adapter and adapter.rng then
local ok, v = pcall(adapter:rng(), lo, hi)
if ok and type(v) == "number" then return v end
if not rollWarned then
rollWarned = true
print("[game3/engine] adapter rng failed: " .. tostring(v))
end
end
local okR, Rng = pcall(require, "src.core.game3.rng")
if okR and Rng and Rng.compat then
return Rng.compat(lo, hi)
end
return math.random(lo, hi)
end
Engine.roll = roll
local function terrain_of(st)
if st and st.terrain ~= nil then return tonumber(st.terrain) or 8 end
local Bg = package.loaded["src.core.game3.battle.bg"]
if Bg and Bg.terrainId then return tonumber(Bg.terrainId()) or 8 end
return 8
end
Engine.terrainOf = terrain_of
-- pokefirered/src/battle_util.c:203
function Engine.cancelMultiTurnMoves(b)
if not b then return end
b.expLockedMove = nil
b.expLockedSlot = nil
b.expRampageTurns = nil
b.expUproarTurns = nil
b.uproar = nil
b.bideTurns = nil
b.twoTurnMove = nil
b.twoTurnTarget = nil
b.semiInvulnerable = nil
b.onAir, b.underground, b.underwater = nil, nil, nil
b.expRolloutTimer = nil
b.expFuryCutter = nil
end
-- pokefirered/src/battle_main.c:2373
function Engine.refreshLinks(st)
if not st then return end
local onField = {}
for id = 0, 3 do
local b = State.battler(st, id)
if b then onField[b] = true end
end
for id = 0, 3 do
local b = State.battler(st, id)
if b then
local foe = (not st.double) and State.battler(st, State.OPPOSITE(id)) or nil
local function gone(src)
if st.double then return not onField[src] end
return src ~= foe
end
if b.expInfatuatedWith and gone(b.expInfatuatedWith) then
b.expInfatuated, b.expInfatuatedWith = nil, nil
end
if b.expTrapSource and gone(b.expTrapSource) then
b.expTrapTurns, b.expTrapSource, b.expTrapMove, b.wrapped = nil, nil, nil, nil
end
if b.expTrappedBy and gone(b.expTrappedBy) then
b.expTrapped, b.expTrappedBy, b.escapePrevention = nil, nil, nil
end
end
end
end
-- pokefirered/src/pokemon.c:2381
function Engine.hasBadge(st, n)
if not st or st.link then return false end
local b = st.badges
if type(b) == "table" then return b[n] == true end
if type(b) == "number" then return math.floor(b / 2 ^ (n - 1)) % 2 == 1 end
local Space = package.loaded["src.core.game3.scripting.space"]
local Flags = package.loaded["src.core.game3.scripting.flags"]
if Space and Space.store and Flags and Flags.hasBadge then
local ok, v = pcall(Flags.hasBadge, Space.store, n)
if not ok then
if not badgeWarned then
badgeWarned = true
print("[game3/battle] hasBadge failed: " .. tostring(v))
end
return false
end
return v == true
end
return false
end
-- pokefirered/src/battle_controllers.c:163 InitLinkBtlControllers
local function link_seat_swap(st)
if not (st and st.link) then return false end
return st.linkMaster == false
end
Engine.linkSeatSwap = link_seat_swap
-- pokefirered/src/battle_main.c:3399
local function speed_of(battler, st, adapter)
local mon = battler and battler.mon
local spe = tonumber(mon and (mon.speed or mon.spe)) or 50
if battler and battler.expTransform and battler.expTransform.speed then
spe = battler.expTransform.speed
end
local weather = adapter and Rules.weather.effective(st, adapter) or Rules.weather.kind(st and st.weather)
local ab = adapter and adapter:abilityOf(battler)
local mul = 1
if (ab == "SWIFT_SWIM" and weather == "RAIN") or (ab == "CHLOROPHYLL" and weather == "SUN") then
mul = 2
end
local stage = battler and battler.stages and battler.stages.speed or 0
spe = Damage.applyStage(spe * mul, stage)
if battler and battler.side == "player" and Engine.hasBadge(st, 3) then
spe = math.floor(spe * 110 / 100)
end
local he, param = HeldItems.of(battler)
if he == HeldItems.HOLD.MACHO_BRACE then spe = math.floor(spe / 2) end
local st1 = battler and (battler.status or (battler.mon and battler.mon.status))
if st1 == "PAR" then spe = math.floor(spe / 4) end
if he == HeldItems.HOLD.QUICK_CLAW and st and (tonumber(st.randomTurnNumber) or 0xFFFF)
< math.floor(0xFFFF * param / 100) then
spe = 0xFFFFFFFF
end
return spe
end
Engine.speedOf = speed_of
local function clear_turn_flags(battler)
if not battler then return end
-- pokefirered/src/battle_main.c:3623
battler.expProtected = nil
battler.expEnduring = nil
battler.expMagicCoat = nil
battler.expSnatch = nil
battler.expHelpingHand = nil
battler.flinched = nil
battler.damageTakenThisTurn = 0
battler.lastPhysicalDamageTaken = nil
battler.lastSpecialDamageTaken = nil
battler.expHurtBy = nil
battler.expHurtById = nil
battler.lastPhysicalById = nil
battler.lastSpecialById = nil
battler.expLightningRodRedirected = nil
battler.expMovedThisTurn = nil
battler.expTurnOrder = nil
battler.expUnableToMove = nil
battler._statLoweredMsg = nil
if (battler.isFirstTurn or 0) > 0 then battler.isFirstTurn = battler.isFirstTurn - 1 end
if battler.expRechargeTurns then
battler.expRechargeTurns = battler.expRechargeTurns - 1
if battler.expRechargeTurns <= 0 then
battler.expRechargeTurns = nil
battler.expMustRecharge = nil
battler.recharge = nil
end
end
end
Engine.clearTurnFlags = clear_turn_flags
local function effectiveness_line(flags, eff)
if flags then
if flags.super then return Strings("It's super effective!") end
if flags.notVery then return Strings("It's not very effective…") end
return nil
end
if eff >= 2 then return Strings("It's super effective!") end
if eff > 0 and eff < 1 then return Strings("It's not very effective…") end
return nil
end
Engine.effectivenessLine = effectiveness_line
local Ctx = {}
Ctx.__index = Ctx
function Ctx:say(text) self.adapter:say(text) end
-- pokefirered/src/battle_script_commands.c:1108
function Ctx:attackString()
if self.printedUsed then return end
self.printedUsed = true
self:say(Strings("%s used\n%s!", self.uname, self.moveName))
if ModRuntime.wants("battle.move_used") then
local G3 = require("src.mods.Gen3Compat")
local view = G3.moveView(self.move)
ModRuntime.emit("battle.move_used", {
battle = self.st, user = self.user, target = self.target, move = view,
isCalled = self.opts.called and true or false,
moveId = view and view.id, moveNum = self.mnum, side = self.user and self.user.side,
userId = self.user and State.idOf(self.user),
targetId = self.target and State.idOf(self.target),
})
end
end
-- pokefirered/src/battle_script_commands.c:1122
function Ctx:ppReduce()
if self.ppDone then return end
self.ppDone = true
if self.noPP or not self.slot then return end
local mon = self.user.mon
if not mon then return end
mon.pp = mon.pp or {}
local pp = tonumber(mon.pp[self.slot])
if not pp then return end
local cost = 1
local target = self.target
local ttype = tonumber(self.move.target) or 0
if self.st and self.st.double and (ttype == MOVE_TARGET_FOES_AND_ALLY or ttype == MOVE_TARGET_BOTH
or ttype == MOVE_TARGET_OPPONENTS_FIELD) then
for _, b in ipairs(State.present(self.st)) do
if b ~= self.user and self.adapter:abilityOf(b) == "PRESSURE"
and (ttype == MOVE_TARGET_FOES_AND_ALLY or b.side ~= self.user.side) then
cost = cost + 1
end
end
elseif target and target ~= self.user and ttype ~= MOVE_TARGET_USER
and self.adapter:abilityOf(target) == "PRESSURE" then
cost = 2
end
if pp > cost then mon.pp[self.slot] = pp - cost else mon.pp[self.slot] = 0 end
end
-- pokefirered/src/battle_controller_player.c:2330
function Ctx:attackAnimation(turn, multihitLeft)
local ad, user = self.adapter, self.user
if self.st and self.st.double and (self.animTargetsHit or 0) > 0 then
local ttype = tonumber(self.move.target) or 0
-- pokefirered/src/battle_script_commands.c:1677
if ttype == MOVE_TARGET_BOTH or ttype == MOVE_TARGET_FOES_AND_ALLY or ttype == MOVE_TARGET_DEPENDS then
return nil
end
end
local behindSub = user and (user.substituteHP or 0) > 0
if behindSub and not self._subLowered then
self._subLowered = true
ad:playAnim("special", "SUBSTITUTE_TO_MON", user, user)
end
local ev = ad:pushEvent({
kind = "move",
moveId = self.moveId,
attacker = user and user.side,
target = self.target and self.target.side,
attackerId = user and State.idOf(user),
targetId = self.target and State.idOf(self.target),
turn = turn or self.animTurn or 0,
damage = self._animDmg,
power = self._animPower,
})
self.animTurn = (self.animTurn or 0) + 1
self.animTargetsHit = (self.animTargetsHit or 0) + 1
if behindSub and (multihitLeft or 0) < 2 then
self._subLowered = false
ad:playAnim("special", "MON_TO_SUBSTITUTE", user, user)
end
return ev
end
function Ctx:isProtected()
local t = self.target
if not t or t == self.user or not t.expProtected then return false end
if not has_flag(self.move, FLAG_PROTECT_AFFECTED) then return false end
if self.mnum == MOVE_CURSE and not (self.adapter:hasType(self.user, Types.ID.GHOST)) then return false end
return true
end
function Ctx:lockOnActive()
local t = self.target
if not (t and (t.expLockedOn or 0) ~= 0 and t.expLockedOn ~= false) then return false end
if self.st and self.st.double and t.expLockedOnById ~= nil then
return t.expLockedOnById == State.idOf(self.user)
end
return t.expLockedOnBy == nil or t.expLockedOnBy == self.user.side
end
-- pokefirered/src/battle_script_commands.c:1003
function Ctx:accuracyCheck(mode, printFail)
local ad, user, target = self.adapter, self.user, self.target
local failMsg = function(reason)
self.missReason = reason
if not printFail then return end
if reason == "protected" then
self:say(Strings("%s\nprotected itself!", ad:displayName(target)))
elseif reason == "fail" then
self.failed = true
ad:sayFail()
else
self:say(Strings("%s's\nattack missed!", ad:displayName(user)))
end
end
if mode == "noacc" or mode == "lockon" then
if mode == "lockon" and self:lockOnActive() then return true end
if target and target.semiInvulnerable and target ~= user then
failMsg("fail")
return false
end
if self:isProtected() then
failMsg("protected")
return false
end
return true
end
-- pokefirered/src/battle_script_commands.c:896
if self:isProtected() then
failMsg("protected")
return false
end
-- pokefirered/src/battle_script_commands.c:1007
if Oak.active(self.st) and user and user.side == "player" then
local power = tonumber(self.move and self.move.power) or 0
local mask = (power > 0) and Oak.FLAG_INFLICT_DMG or Oak.FLAG_STAT_CHG
if not Oak.testFlag(self.st, mask) then return not self:absorbed() end
end
if self:lockOnActive() then return not self:absorbed() end
local semi = target and target ~= user and target.semiInvulnerable
if semi == "ON_AIR" and not self.ignoreOnAir then failMsg("miss"); return false end
if semi == "UNDERGROUND" and not self.ignoreUnderground then failMsg("miss"); return false end
if semi == "UNDERWATER" and not self.ignoreUnderwater then failMsg("miss"); return false end
local weather = Rules.weather.effective(self.st, ad)
local eff = self.effect
if (weather == "RAIN" and eff == E.THUNDER) or eff == E.ALWAYS_HIT or eff == E.VITAL_THROW then
return not self:absorbed()
end
local accStage = user.stages and user.stages.accuracy or 0
local evaStage = target and target.stages and target.stages.evasion or 0
local buff
if target and target.expIdentified then
buff = accStage
else
buff = accStage - evaStage
end
if buff < -6 then buff = -6 elseif buff > 6 then buff = 6 end
local moveAcc = tonumber(self.accOverride or self.move.accuracy) or 100
if weather == "SUN" and eff == E.THUNDER then moveAcc = 50 end
local ratio = Rules.ACCURACY_STAGE[buff]
local calc = math.floor(ratio[1] * moveAcc / ratio[2])
if ad:abilityOf(user) == "COMPOUND_EYES" then calc = math.floor(calc * 130 / 100) end
if weather == "SAND" and ad:abilityOf(target) == "SAND_VEIL" then calc = math.floor(calc * 80 / 100) end
if ad:abilityOf(user) == "HUSTLE" and Types.isPhysical(self.moveType or self.move.type) then
calc = math.floor(calc * 80 / 100)
end
local tHe, tParam = HeldItems.of(target)
if tHe == HeldItems.HOLD.EVASION_UP then calc = math.floor(calc * (100 - tParam) / 100) end
local hit
if ModRuntime.wantsHook("battle.accuracy") then
local G3 = require("src.mods.Gen3Compat")
local view = G3.moveView(self.move)
hit = ModRuntime.call("battle.accuracy", function(c)
return roll(ad, 1, 100) <= c.accuracy
end, { battle = self.st, move = view, moveId = view and view.id, moveNum = self.mnum,
user = user, target = target, accuracy = calc, rng = ad:rng() })
else
hit = roll(ad, 1, 100) <= calc
end
if not hit then
failMsg("miss")
return false
end
return not self:absorbed()
end
-- pokefirered/src/battle_script_commands.c:912
function Ctx:absorbed()
if Abilities.absorb(self) then
self.missReason = "absorbed"
return true
end
return false
end
function Ctx:faintMessage(battler)
local ad = self.adapter
if not battler or battler._faintAnnounced then return false end
if not ad:isFainted(battler) then return false end
battler._faintAnnounced = true
ad:pushEvent({ kind = "faint", side = battler.side, battler = State.idOf(battler) })
self:say(Strings("%s fainted!", ad:displayName(battler)))
self.anim.fainted = true
self.anim.faints[#self.anim.faints + 1] = { side = battler.side or "enemy", battler = State.idOf(battler) }
ad:emitFaint(battler)
return true
end
-- pokefirered/src/battle_script_commands.c:2831
function Ctx:tryFaintTarget()
local ad, user, target = self.adapter, self.user, self.target
if not target or target == user then return end
if not ad:isFainted(target) or target._faintAnnounced then return end
self:faintMessage(target)
if target.expDestinyBond and user.side ~= target.side and not ad:isFainted(user) then
self:say(Strings("%s took\n%s with it!", ad:displayName(target), ad:displayName(user)))
ad:applyHpLoss(user, ad:hp(user))
self:faintMessage(user)
end
if target.expGrudge and self.slot and user.mon and user.mon.pp and not ad:isFainted(user) then
user.mon.pp[self.slot] = 0
self:say(Strings("%s's %s lost\nall its PP due to the GRUDGE!", ad:displayName(user), self.moveName))
end
end
function Ctx:tryFaintUser()
if self.adapter:isFainted(self.user) then self:faintMessage(self.user) end
end
local function new_ctx(user, target, moveId, slot, adapter, st, out, anim, opts)
local move = Moves.get(moveId)
local M = setmetatable({
isMoveContext = true,
adapter = adapter,
st = st,
user = user,
target = target,
moveId = moveId,
move = move,
mnum = move_num(moveId, move),
effect = tonumber(move.effect) or 0,
slot = slot,
out = out,
anim = anim,
opts = opts or {},
moveName = Moves.displayName(moveId),
uname = adapter:displayName(user),
tname = adapter:displayName(target),
animTurn = 0,
}, Ctx)
return M
end
Engine.newContext = new_ctx
-- pokefirered/data/battle_scripts_1.s:3741
function Engine.selfHit(M, dmg)
local ad, user = M.adapter, M.user
local r = roll(ad, 85, 100)
dmg = math.floor(dmg * r / 100)
if dmg == 0 then dmg = 1 end
local banded = HeldItems.rollFocusBand(ad, user)
local hung = nil
if (user.substituteHP or 0) <= 0 and (user.expEnduring or banded) and dmg >= ad:hp(user) then
dmg = ad:hp(user) - 1
hung = user.expEnduring and "endured" or "band"
end
user.expFocusBanded = nil
M:say(Strings("It hurt itself in its\nconfusion!"))
ad:applyHpLoss(user, dmg)
if hung == "endured" then
M:say(Strings("%s ENDURED\nthe hit!", M.uname))
elseif hung == "band" then
HeldItems.focusBandMessage(ad, user)
end
M:tryFaintUser()
end
-- pokefirered/src/battle_util.c:1253
local function canceller(M)
local ad, user = M.adapter, M.user
local uname = M.uname
user.expDestinyBond = nil
user.destinyBond = nil
user.expGrudge = nil
local st = ad:status(user)
if st == "SLP" then
local up = ad:uproarActive()
if up and ad:abilityOf(user) ~= "SOUNDPROOF" then
ad:clearStatus(user)
user.expNightmare = nil
M:say(Strings("%s woke up\nin the UPROAR!", uname))
else
local toSub = (ad:abilityOf(user) == "EARLY_BIRD") and 2 or 1
local turns = tonumber(user.sleepTurns) or tonumber(user.mon and (user.mon.sleepTurns or user.mon.sleep))
if not turns or turns <= 0 then turns = ad:rollSleepTurns() end
if turns < toSub then turns = 0 else turns = turns - toSub end
user.sleepTurns = turns
if turns > 0 then
if M.mnum ~= MOVE_SNORE and M.mnum ~= MOVE_SLEEP_TALK then
M:say(Strings("%s is fast\nasleep.", uname))
ad:statusAnim(user, "SLP")
return false
end
else
ad:clearStatus(user)
user.expNightmare = nil
M:say(Strings("%s woke up!", uname))
end
end
end
if ad:status(user) == "FRZ" then
if roll(ad, 0, 4) ~= 0 then
if M.effect ~= E.THAW_HIT then
M:say(Strings("%s is\nfrozen solid!", uname))
ad:statusAnim(user, "FRZ")
return false
end
else
ad:clearStatus(user)
M:say(Strings("%s was\ndefrosted!", uname))
end
end
if Abilities.truantLoafs(ad, user) then
-- pokefirered/src/battle_util.c:1337
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s is\nloafing around!", uname))
user.expUnableToMove = true
return false
end
if user.expMustRecharge then
user.expMustRecharge = nil
user.expRechargeTurns = nil
user.recharge = nil
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s must\nrecharge!", uname))
return false
end
if user.flinched then
user.flinched = nil
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s flinched!", uname))
user.expUnableToMove = true
return false
end
if user.expDisabledMove and M.mnum == user.expDisabledMove then
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s's %s\nis disabled!", uname, M.moveName))
user.expUnableToMove = true
return false
end
if (user.expTauntedTurns or 0) > 0 and (tonumber(M.move.power) or 0) == 0 then
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s can't use\n%s after the TAUNT!", uname, M.moveName))
user.expUnableToMove = true
return false
end
if M.mnum and Engine.isImprisoned(ad, user, M.mnum) then
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s can't use the\nsealed %s!", uname, M.moveName))
user.expUnableToMove = true
return false
end
if (user.confusionTurns or 0) > 0 then
user.confusionTurns = user.confusionTurns - 1
if user.confusionTurns > 0 then
M:say(Strings("%s is\nconfused!", uname))
ad:playAnim("status", "CONFUSION", user, user)
if roll(ad, 0, 1) == 0 then
Engine.cancelMultiTurnMoves(user)
-- pokefirered/src/battle_util.c:1424
local dmg = Damage.base(user, user, { power = 40, type = 0, effect = 0 }, {
power = 40, moveType = 0, adapter = ad,
})
Engine.selfHit(M, dmg)
user.expUnableToMove = true
return false
end
else
user.confusionTurns = nil
M:say(Strings("%s snapped\nout of confusion!", uname))
end
end
if ad:status(user) == "PAR" and roll(ad, 0, 3) == 0 then
M:say(Strings("%s is paralyzed!\nIt can't move!", uname))
ad:statusAnim(user, "PAR")
user.expUnableToMove = true
return false
end
-- pokefirered/src/battle_util.c:1451
if M.st and M.st.ghostBattle and not M.st.ghostUnveiled then
if user.side == "player" then
-- pokefirered/data/battle_scripts_1.s:3809
M:say(Strings("%s is too scared to move!", uname))
ad:playAnim("general", "MON_SCARED", user, ad:foeOf(user))
else
-- pokefirered/data/battle_scripts_1.s:3815
ad:pushEvent({ kind = "msg", text = Strings("GHOST: Get out…… Get out……"), wait = 0 })
ad._say(Strings("GHOST: Get out…… Get out……"))
ad:playAnim("general", "GHOST_GET_OUT", user, ad:foeOf(user))
end
return "ghost"
end
if user.expInfatuated then
local lover = (M.st and M.st.double and user.expInfatuatedWith) or ad:foeOf(user)
M:say(Strings("%s is in love\nwith %s!", uname, ad:displayName(lover)))
ad:playAnim("status", "INFATUATION", user, user)
if roll(ad, 0, 1) == 0 then
Engine.cancelMultiTurnMoves(user)
M:say(Strings("%s is\nimmobilized by love!", uname))
user.expUnableToMove = true
return false
end
end
if (user.bideTurns or 0) > 0 then
user.bideTurns = user.bideTurns - 1
if user.bideTurns > 0 then
M:say(Strings("%s is storing\nenergy!", uname))
return false
end
return "bide"
end
if ad:status(user) == "FRZ" and M.effect == E.THAW_HIT then
ad:clearStatus(user)
M:say(Strings("%s was\ndefrosted by %s!", uname, M.moveName))
end
return true
end
Engine.canceller = canceller
-- pokefirered/data/battle_scripts_1.s:3251
local function bide_attack(M)
local ad, user = M.adapter, M.user
local dmgStored = user.expBideDamage or 0
local src = user.expBideTarget
user.expBideDamage = nil
user.expBideTarget = nil
user.bideTurns = nil
user.expLockedMove = nil
user.expLockedSlot = nil
M:say(Strings("%s unleashed\nenergy!", M.uname))
if dmgStored <= 0 then
M.failed = true
ad:sayFail()
return
end
if src and src.side and M.st and not M.st.double and M.st[src.side] then M.target = M.st[src.side] end
if src and M.st and M.st.double then
local sid = State.idOf(src)
-- pokefirered/src/battle_util.c:1500
if State.isPresent(M.st, sid) then
M.target = State.battler(M.st, sid)
else
M.target = State.battler(M.st, Engine.getMoveTarget(M.st, ad, user, M.moveId, MOVE_TARGET_SELECTED + 1))
end
M.tname = ad:displayName(M.target)
end
local target = M.target
if not M:accuracyCheck("normal", true) then return end
local _, flags = Types.typeCalc(M.move.type, target.type1, target.type2, nil, target.expIdentified)
if flags.immune then
M:say(Strings("It doesn't affect\n%s…", ad:displayName(target)))
return
end
local Hit = require("src.core.game3.battle.effects.hit")
Hit.applySetDamage(M, dmgStored * 2)
M:tryFaintTarget()
end
local function run_called(M, calledId, opts)
opts = opts or {}
local sub = {}
for k, v in pairs(M.opts or {}) do sub[k] = v end
sub.called = true
sub.anim = M.anim
sub.noAttackString = nil
sub.calledBy = M.moveId
M.anim.calledBy = M.anim.calledBy or M.moveId
local target = M.target
local cmove = Moves.get(calledId)
if M.st and M.st.double then
target = State.battler(M.st, Engine.getMoveTarget(M.st, M.adapter, M.user, calledId))
elseif tonumber(cmove.target) == MOVE_TARGET_USER then target = M.user else target = M.adapter:foeOf(M.user) end
return Engine.resolveMove(M.user, target, calledId, opts.slot, M.adapter, M.st, M.out, sub)
end
-- pokefirered/src/battle_script_commands.c:7519
local function pick_metronome(M)
for _ = 1, 64 do
local m = roll(M.adapter, 1, 511)
if m < 355 and not forbidden(m, false) then return m end
end
local list = {}
for m = 1, 354 do if not forbidden(m, false) then list[#list + 1] = m end end
return list[roll(M.adapter, 1, #list)]
end
local function invalid_sleep_talk(move)
-- pokefirered/src/battle_script_commands.c:7834
return move == nil or move == 0 or move == MOVE_SLEEP_TALK or move == 274 or move == 119 or move == 118
end
local function is_two_turn(move)
local m = Moves.get(move)
local e = tonumber(m and m.effect)
return e == E.SKULL_BASH or e == E.RAZOR_WIND or e == E.SKY_ATTACK or e == E.SOLAR_BEAM
or e == E.SEMI_INVULNERABLE or e == E.BIDE
end
Engine.isTwoTurnMove = is_two_turn
local function call_moves(M)
local ad, user, eff = M.adapter, M.user, M.effect
if eff == E.METRONOME then
M:attackString()
M:ppReduce()
M:attackAnimation()
return run_called(M, pick_metronome(M))
elseif eff == E.SLEEP_TALK then
-- pokefirered/data/battle_scripts_1.s:1307
if ad:status(user) ~= "SLP" then
M:attackString()
M:ppReduce()
M.failed = true
ad:sayFail()
return
end
M:say(Strings("%s is fast\nasleep.", M.uname))
ad:statusAnim(user, "SLP")
M:attackString()
M:ppReduce()
local mon = user.mon or {}
local valid = {}
for i = 1, 4 do
local mv = move_num(mon.moves and mon.moves[i])
local pp = mon.pp and mon.pp[i]
if mv and not invalid_sleep_talk(mv) and mv ~= 264 and mv ~= 253 and not is_two_turn(mv)
and not (user.expDisabledMove and mv == user.expDisabledMove)
and not (user.expEncoreMove and move_num(user.expEncoreMove) ~= mv and (user.expEncoreTurns or 0) > 0) then
valid[#valid + 1] = { move = mv, slot = i, pp = pp }
end
end
if #valid == 0 then
M.failed = true
ad:sayFail()
return
end
local pick = valid[roll(ad, 1, #valid)]
M:attackAnimation()
return run_called(M, pick.move)
elseif eff == E.MIRROR_MOVE then
-- pokefirered/src/battle_script_commands.c:6350
M:attackString()
local mv = user.expLastTakenMove
if mv and mv ~= 0 then
M:ppReduce()
return run_called(M, mv)
end
M:ppReduce()
M.failed = true
M:say(Strings("The MIRROR MOVE failed!"))
return
elseif eff == E.ASSIST then
-- pokefirered/src/battle_script_commands.c:9091
M:attackString()
local list = {}
for i, mon in ipairs(ad:partyMons(user)) do
if i ~= user.partyIndex and mon and (mon.species or 0) ~= 0 and not mon.isEgg then
for j = 1, 4 do
local mv = move_num(mon.moves and mon.moves[j])
if mv and mv ~= 0 and not invalid_sleep_talk(mv) and not forbidden(mv, false) then
list[#list + 1] = mv
end
end
end
end
M:ppReduce()
if #list == 0 then
M.failed = true
ad:sayFail()
return
end
M:attackAnimation()
return run_called(M, list[roll(ad, 1, #list)])
elseif eff == E.NATURE_POWER then
-- pokefirered/src/battle_script_commands.c:8718
M:attackString()
local called = Engine.NATURE_POWER_MOVES[terrain_of(M.st)] or 129
M:say(Strings("NATURE POWER turned into\n%s!", Moves.displayName(called)))
return run_called(M, called, { slot = M.slot })
end
end
-- Templates, translated at the time they are said (Strings.source only marks
-- the key: this table is built before a translation catalog exists).
local CHARGE_TEXT = {
[E.RAZOR_WIND] = Strings.source("%s whipped\nup a whirlwind!"),
[E.SOLAR_BEAM] = Strings.source("%s took\nin sunlight!"),
[E.SKULL_BASH] = Strings.source("%s lowered\nits head!"),
[E.SKY_ATTACK] = Strings.source("%s is glowing!"),
}
local SEMI_TEXT = {
[MOVE_FLY] = { Strings.source("%s flew\nup high!"), "ON_AIR" },
[MOVE_BOUNCE] = { Strings.source("%s sprang up!"), "ON_AIR" },
[MOVE_DIG] = { Strings.source("%s dug a hole!"), "UNDERGROUND" },
[MOVE_DIVE] = { Strings.source("%s hid\nunderwater!"), "UNDERWATER" },
}
local function set_semi(b, kind)
b.semiInvulnerable = kind
b.onAir = kind == "ON_AIR" or nil
b.underground = kind == "UNDERGROUND" or nil
b.underwater = kind == "UNDERWATER" or nil
end
Engine.setSemiInvulnerable = set_semi
-- pokefirered/data/battle_scripts_1.s:794
local function charge_turn(M)
local ad, user, eff = M.adapter, M.user, M.effect
local isCharge = CHARGE_TEXT[eff] or eff == E.SEMI_INVULNERABLE
if not isCharge then return false end
if user.twoTurnMove ~= nil and move_num(user.twoTurnMove) == M.mnum then
user.twoTurnMove = nil
user.twoTurnTarget = nil
user.expLockedMove = nil
user.expLockedSlot = nil
M.noPP = true
M.secondTurn = true
M.animTurn = 1
if M.mnum == MOVE_SKY_ATTACK then M.extraEffect = { eff = "FLINCH" } end
if M.mnum == MOVE_BOUNCE then M.extraEffect = { eff = "PARALYSIS" } end
return false
end
if eff == E.SOLAR_BEAM and Rules.weather.effective(M.st, ad) == "SUN" then
M:ppReduce()
M.noPP = true
return false
end
if ModRuntime.wantsHook("battle.charge_required") then
local required = ModRuntime.call("battle.charge_required", function(c)
return c.charge
end, { battle = M.st, user = user, target = M.target,
move = require("src.mods.Gen3Compat").moveView(M.move),
charge = true, isCalled = M.opts.called and true or false })
if required == false then
M:ppReduce()
M.noPP = true
return false
end
end
M:ppReduce()
M:attackAnimation(0)
user.twoTurnMove = M.moveId
user.twoTurnTarget = M.target
user.expLockedMove = M.moveId
user.expLockedSlot = M.slot
if eff == E.SEMI_INVULNERABLE then
local row = SEMI_TEXT[M.mnum]
if not row then
local n = tostring(M.moveName):upper()
if n:find("FLY") then row = SEMI_TEXT[MOVE_FLY]
elseif n:find("DIVE") then row = SEMI_TEXT[MOVE_DIVE]
elseif n:find("BOUNCE") then row = SEMI_TEXT[MOVE_BOUNCE]
else row = SEMI_TEXT[MOVE_DIG] end
end
M:say(Strings(row[1], M.uname))
set_semi(user, row[2])
else
M:say(Strings(CHARGE_TEXT[eff], M.uname))
if eff == E.SKULL_BASH then
Secondary.changeStat(ad, user, "defense", 1, { user = true, allowPtr = true, noMsg = (user.stages.defense or 0) >= 6 })
end
end
M.anim.statusOnly = true
M.anim.charging = true
return true
end
-- pokefirered/data/battle_scripts_1.s:761
local function ohko(M)
local ad, user, target = M.adapter, M.user, M.target
M:attackString()
M:ppReduce()
if not M:accuracyCheck("lockon", true) then
M.anim.missed = true
return
end
local _, flags = Types.typeCalc(M.move.type, target.type1, target.type2, nil, target.expIdentified)
if flags.immune or (ad:abilityOf(target) == "LEVITATE" and tonumber(M.move.type) == Types.ID.GROUND) then
M.anim.missed = true
M:say(Strings("It doesn't affect\n%s…", M.tname))
return
end
local banded = HeldItems.rollFocusBand(ad, target)
target.expFocusBanded = nil
if ad:abilityOf(target) == "STURDY" then
M.anim.missed = true
M:say(Strings("%s was protected\nby STURDY!", M.tname))
return
end
local uLvl = tonumber(user.mon and user.mon.level) or 1
local tLvl = tonumber(target.mon and target.mon.level) or 1
local hit
-- pokefirered/src/battle_script_commands.c:7104
if M:lockOnActive() and uLvl >= tLvl then
hit = true
else
local chance = (tonumber(M.move.accuracy) or 30) + (uLvl - tLvl)
hit = roll(ad, 1, 100) < chance and uLvl >= tLvl
end
if not hit then
M.anim.missed = true
if uLvl >= tLvl then
M:say(Strings("%s's\nattack missed!", M.uname))
else
M:say(Strings("%s is\nunaffected!", M.tname))
end
return
end
local Hit = require("src.core.game3.battle.effects.hit")
local hpBefore = ad:hp(target)
local dmg = hpBefore
local endured = target.expEnduring and true or false
local hung = not endured and banded
if endured or hung then dmg = math.max(0, hpBefore - 1) end
Hit.dealDamage(M, dmg, { physical = Types.isPhysical(M.move.type) })
if endured then
M:say(Strings("%s ENDURED\nthe hit!", M.tname))
elseif hung then
HeldItems.focusBandMessage(ad, target)
else
M:say(Strings("It's a one-hit KO!"))
end
M:tryFaintTarget()
end
-- pokefirered/src/battle_script_commands.c:866
local function try_bounce(M)
local ad, user, target = M.adapter, M.user, M.target
if M.opts.bounced then return false end
if target and target ~= user and target.expMagicCoat and has_flag(M.move, FLAG_MAGIC_COAT_AFFECTED) then
target.expMagicCoat = nil
M:attackString()
M:ppReduce()
M:say(Strings("%s's %s\nwas bounced back by MAGIC COAT!", M.uname, M.moveName))
M.user, M.target = target, user
M.uname, M.tname = M.tname, M.uname
M.slot = nil
M.noPP = true
M.opts.bounced = true
return true
end
local foe = ad:foeOf(user)
if M.st and M.st.double then
foe = nil
for _, id in ipairs(Engine.turnOrderIds(M.st)) do
local b = State.battler(M.st, id)
if b and b ~= user and b.expSnatch and State.isPresent(M.st, id) then foe = b break end
end
end
if foe and foe ~= user and foe.expSnatch and has_flag(M.move, FLAG_SNATCH_AFFECTED) then
foe.expSnatch = nil
M:attackString()
M:ppReduce()
ad:playAnim("general", "SNATCH_MOVE", user, foe)
M:say(Strings("%s SNATCHED\n%s's move!", ad:displayName(foe), M.uname))
M.user, M.target = foe, user
M.uname, M.tname = ad:displayName(foe), M.uname
M.slot = nil
M.noPP = true
M.opts.bounced = true
return true
end
return false
end
-- pokefirered/src/battle_util.c:427
function Engine.isImprisoned(ad, b, num)
if not (ad and b and num) then return false end
local st = ad._st
local foes = (st and st.double) and State.foes(st, b) or { ad:foeOf(b) }
for _, foe in ipairs(foes) do
if foe and foe.expImprison then
local fm = foe.mon and foe.mon.moves or {}
for j = 1, 4 do
if fm[j] and move_num(fm[j]) == num then return true end
end
end
end
return false
end
function Engine.turnOrderIds(st)
if st and st.turnOrder and #st.turnOrder > 0 then return st.turnOrder end
return State.presentIds(st)
end
-- pokefirered/src/battle_script_commands.c:2092
function Engine.turnOrderNum(st, id)
local order = Engine.turnOrderIds(st)
for i, v in ipairs(order) do
if v == id then return i - 1 end
end
return 4
end
local function has_bit(v, b) return math.floor((tonumber(v) or 0) / b) % 2 == 1 end
local function follow_me_id(st, ad, attacker)
local side = ad:foeSide(attacker)
if not side then return nil end
if side.expFollowMeId ~= nil then return side.expFollowMeId end
if side.expFollowMe then return State.idOf(side.expFollowMe) end
return nil
end
Engine.followMeId = follow_me_id
local function foe_left(aid) return (aid % 2 == 0) and 1 or 0 end
local function random_foe_flank(st, ad, aid)
if roll(ad, 0, 1) == 1 then return foe_left(aid) end
return foe_left(aid) + 2
end
-- pokefirered/src/battle_util.c:3054
function Engine.getMoveTarget(st, ad, attacker, moveId, setTarget)
local aid = State.idOf(attacker)
local mv = Moves.get(moveId)
local ttype = setTarget and (setTarget - 1) or (tonumber(mv and mv.target) or 0)
local count = (st and st.double) and 4 or 2
local target
if ttype == MOVE_TARGET_SELECTED then
local fm = follow_me_id(st, ad, attacker)
if fm ~= nil and ad:hp(State.battler(st, fm)) > 0 then
target = fm
else
for _ = 1, 256 do
target = roll(ad, 0, count - 1) % count
if target ~= aid and target % 2 ~= aid % 2 and State.isPresent(st, target) then break end
target = nil
end
target = target or foe_left(aid)
local lr = 0
for id = 0, count - 1 do
local b = State.battler(st, id)
if b and id % 2 ~= aid % 2 and ad:abilityOf(b) == "LIGHTNING_ROD" then lr = lr + 1 end
end
if tonumber(mv and mv.type) == Types.ID.ELECTRIC and lr > 0
and ad:abilityOf(State.battler(st, target)) ~= "LIGHTNING_ROD" then
target = State.PARTNER(target)
local rb = State.battler(st, target)
if rb then rb.expLightningRodRedirected = true end
end
end
elseif ttype == MOVE_TARGET_DEPENDS or ttype == MOVE_TARGET_BOTH or ttype == MOVE_TARGET_FOES_AND_ALLY
or ttype == MOVE_TARGET_OPPONENTS_FIELD then
target = foe_left(aid)
if not State.isPresent(st, target) and count == 4 then target = State.PARTNER(target) end
elseif ttype == MOVE_TARGET_RANDOM then
local fm = follow_me_id(st, ad, attacker)
if fm ~= nil and ad:hp(State.battler(st, fm)) > 0 then
target = fm
elseif st and st.double then
target = random_foe_flank(st, ad, aid)
if not State.isPresent(st, target) then target = State.PARTNER(target) end
else
target = foe_left(aid)
end
else
target = aid
end
if st then
st.moveTarget = st.moveTarget or {}
st.moveTarget[aid] = target
end
return target
end
-- pokefirered/src/battle_main.c:4024
function Engine.resolveTarget(st, ad, attacker, moveId, chosenId, info)
info = info or {}
local aid = State.idOf(attacker)
local mv = Moves.get(moveId)
local ttype = tonumber(mv and mv.target) or 0
local chosenMv = info.chosenMove and Moves.get(info.chosenMove) or mv
local chosenRandom = has_bit(chosenMv and chosenMv.target, MOVE_TARGET_RANDOM)
local mt = chosenId
if mt == nil or info.recompute then mt = Engine.getMoveTarget(st, ad, attacker, moveId) end
st.moveTarget = st.moveTarget or {}
st.moveTarget[aid] = mt
local function absent_fix(t)
if not State.isPresent(st, t) then
if t % 2 ~= aid % 2 then
t = State.PARTNER(t)
else
t = foe_left(aid)
if not State.isPresent(st, t) then t = State.PARTNER(t) end
end
end
return t
end
local fm = follow_me_id(st, ad, attacker)
if fm ~= nil and ttype == MOVE_TARGET_SELECTED and fm % 2 ~= aid % 2
and ad:hp(State.battler(st, fm)) > 0 then
return fm
end
if st.double and fm == nil and ((tonumber(mv and mv.power) or 0) ~= 0 or ttype ~= MOVE_TARGET_USER)
and ad:abilityOf(State.battler(st, mt)) ~= "LIGHTNING_ROD"
and tonumber(mv and mv.type) == Types.ID.ELECTRIC then
local best, bestNum = nil, 4
for id = 0, 3 do
local b = State.battler(st, id)
if b and id % 2 ~= aid % 2 and id ~= mt and ad:abilityOf(b) == "LIGHTNING_ROD" then
local n = Engine.turnOrderNum(st, id)
if n < bestNum then best, bestNum = id, n end
end
end
if best == nil then
local t = mt
if chosenRandom then t = random_foe_flank(st, ad, aid) end
return absent_fix(t)
end
State.battler(st, best).expLightningRodRedirected = true
return best
end
if st.double and chosenRandom then
local t = random_foe_flank(st, ad, aid)
if not State.isPresent(st, t) then t = State.PARTNER(t) end
return t
end
return absent_fix(mt)
end
-- pokefirered/src/battle_util.c:361
function Engine.moveLimitations(b, ad)
local bad = {}
local mon = b and b.mon or {}
local he = HeldItems.of(b)
local foe = ad and ad:foeOf(b)
for i = 1, 4 do
local mv = mon.moves and mon.moves[i]
local n = (mv ~= nil and mv ~= "" and mv ~= 0) and move_num(mv) or nil
if not n or n == 0 then
bad[i] = true
else
if tonumber(mon.pp and mon.pp[i]) == 0 then bad[i] = true end
if b.expDisabledMove and n == b.expDisabledMove then bad[i] = true end
if b.expTormented and n == move_num(b.lastMoveId) then bad[i] = true end
if (b.expTauntedTurns or 0) > 0 and (tonumber(Moves.get(mv).power) or 0) == 0 then bad[i] = true end
if ad and ad._st and ad._st.double then
if Engine.isImprisoned(ad, b, n) then bad[i] = true end
elseif foe and foe.expImprison then
local fm = foe.mon and foe.mon.moves or {}
for j = 1, 4 do
if fm[j] and move_num(fm[j]) == n then bad[i] = true end
end
end
if (b.expEncoreTurns or 0) > 0 and b.expEncoreMove and move_num(b.expEncoreMove) ~= n then bad[i] = true end
if he == HeldItems.HOLD.CHOICE_BAND and b.choicedMove and b.choicedMove ~= n then bad[i] = true end
end
end
return bad
end
local function player_identity(st)
local id, nm = st.playerTrainerId, st.playerOtName or st.playerName
if id == nil then
local Runtime = package.loaded["src.core.game3.runtime"]
local ok, sess = pcall(function() return Runtime and Runtime.getSession and Runtime.getSession() end)
if ok and sess then
id = sess.trainerId or sess.id or sess.playerId or 12345
nm = sess.name or sess.playerName or nm or "RED"
end
end
return id, nm
end
-- pokefirered/src/pokemon.c:5965
function Engine.isTradedMon(st, mon)
if not mon or mon.otId == nil then return false end
local pid, pname = player_identity(st or {})
if pid == nil then return false end
return tonumber(mon.otId) ~= tonumber(pid)
or (mon.otName ~= nil and pname ~= nil and tostring(mon.otName) ~= tostring(pname))
end
local LOAF_TEXT = {
Strings.source("%s is\nloafing around!"), Strings.source("%s won't\nobey!"),
Strings.source("%s turned away!"), Strings.source("%s pretended\nnot to notice!"),
}
-- pokefirered/src/battle_util.c:3143
local function disobedient(M)
local ad, user, st = M.adapter, M.user, M.st
if not st or st.link or st.pokedude or not user or user.side ~= "player" then return nil end
local mon = State.partyMon(user) or user.mon or {}
local species = tonumber(user.species)
local ob = 0
if not ((species == 151 or species == 410) and mon.fatefulEncounter == false) then
if not Engine.isTradedMon(st, mon) or Engine.hasBadge(st, 8) then return nil end
ob = 10
if Engine.hasBadge(st, 2) then ob = 30 end
if Engine.hasBadge(st, 4) then ob = 50 end
if Engine.hasBadge(st, 6) then ob = 70 end
end
local lvl = tonumber(user.mon and user.mon.level) or tonumber(mon.level) or 1
if lvl <= ob then return nil end
if math.floor((lvl + ob) * roll(ad, 0, 255) / 256) < ob then return nil end
if M.mnum == 99 then user.rage = nil end
if ad:status(user) == "SLP" and (M.mnum == MOVE_SNORE or M.mnum == MOVE_SLEEP_TALK) then
M:say(Strings("%s ignored\norders while asleep!", M.uname))
return "stop"
end
if math.floor((lvl + ob) * roll(ad, 0, 255) / 256) < ob and M.mnum ~= 264 then
local bad = Engine.moveLimitations(user, ad)
if M.slot then bad[M.slot] = true end
if bad[1] and bad[2] and bad[3] and bad[4] then
M:say(Strings(LOAF_TEXT[roll(ad, 0, 3) % 4 + 1], M.uname))
return "stop"
end
local slot
repeat slot = roll(ad, 0, 3) % 4 + 1 until not bad[slot]
M:say(Strings("%s ignored\norders!", M.uname))
return "called", slot
end
local diff = lvl - ob
local r = roll(ad, 0, 255)
local ab = ad:abilityOf(user)
if r < diff and not ad:status(user) and ab ~= "VITAL_SPIRIT" and ab ~= "INSOMNIA" and not ad:uproarActive() then
M:say(Strings("%s began to nap!", M.uname))
ad:applyStatus(user, "SLP", user, { force = true, ignoreSafeguard = true })
ad:statusAnim(user, "SLP")
M:say(Strings("%s\nfell asleep!", M.uname))
return "stop"
end
r = r - diff
if r < diff then
M:say(Strings("%s won't\nobey!", M.uname))
Engine.cancelMultiTurnMoves(user)
local dmg = Damage.base(user, user, { power = 40, type = 0, effect = 0 }, {
power = 40, moveType = 0, adapter = ad,
})
Engine.selfHit(M, dmg)
return "stop"
end
M:say(Strings(LOAF_TEXT[roll(ad, 0, 3) % 4 + 1], M.uname))
return "stop"
end
Engine.disobedient = disobedient
-- pokefirered/src/battle_script_commands.c:4121
function Engine.moveEndEffects(M)
local ad, user, target = M.adapter, M.user, M.target
if target and target ~= user then Abilities.synchronize(M, target, user) end
Abilities.onDamage(M)
Abilities.immunityCure(ad)
if target and target ~= user then Abilities.synchronize(M, user, target) end
local chosenNum = move_num(M.opts.calledBy or M.moveId)
if not M.notObeyed and HeldItems.has(user, HeldItems.HOLD.CHOICE_BAND) and chosenNum ~= MOVE_STRUGGLE
and not user.choicedMove then
local cm = Moves.get(M.opts.calledBy or M.moveId)
if not (tonumber(cm and cm.effect) == E.BATON_PASS and not M.failed) then
user.choicedMove = chosenNum
end
end
if user.choicedMove then
local H = require("src.core.game3.battle.effects._helpers")
if not H.slotOf(user, user.choicedMove) then user.choicedMove = nil end
end
HeldItems.moveEnd(ad)
HeldItems.kingsRockShellBell(M)
end
function Engine.moveEndLite(M)
Abilities.immunityCure(M.adapter)
HeldItems.moveEnd(M.adapter)
end
-- pokefirered/src/battle_util.c:1208
function Engine.afterAction(st, ad)
if not st or st.over then return end
for _ = 1, 4 do
local did = Abilities.runIntimidate(ad) or Abilities.runTrace(ad)
or HeldItems.normal(ad, st.player, true) or Abilities.forecast(ad)
if not did then break end
end
end
-- pokefirered/src/battle_script_commands.c:4056
local function move_end(M)
local ad, user, target = M.adapter, M.user, M.target
if target and target ~= user and target.rage and not ad:isFainted(target)
and user.side ~= target.side and not M.noEffect and (M.hitsLanded or 0) > 0
and (tonumber(M.move.power) or 0) > 0 and (target.stages.attack or 0) < 6 then
target.stages.attack = target.stages.attack + 1
M:say(Strings("%s's RAGE\nis building!", ad:displayName(target)))
end
if target and target ~= user and ad:status(target) == "FRZ" and not ad:isFainted(target)
and (M.specialHit or false) and not M.noEffect and tonumber(M.moveType or M.move.type) == Types.ID.FIRE then
ad:clearStatus(target)
M:say(Strings("%s was\ndefrosted!", ad:displayName(target)))
end
Engine.moveEndEffects(M)
local chosen = M.opts.calledBy or M.moveId
local chosenMove = M.opts.calledBy and Moves.get(M.opts.calledBy) or M.move
if tonumber(chosenMove.effect) ~= E.BATON_PASS then
user.lastMoveId = chosen
user.lastMove = chosen
user.expLastResulting = M.moveId
end
if M.printedUsed then user.expLastPrinted = chosen end
if M.notObeyed then
user.lastMoveId = nil
user.lastMove = nil
end
if target and target ~= user and not M.noEffect and not M.anim.missed
and has_flag(chosenMove, FLAG_MIRROR_MOVE_AFFECTED) and not ad:isFainted(target) then
target.expLastTakenMove = chosen
end
if target and target ~= user and not M.noEffect and not M.anim.missed and (M.hitsLanded or 0) > 0 then
target.expLastLandedMove = M.mnum
target.expLastHitByType = tonumber(M.moveType or M.move.type)
end
end
local function run(M)
local ad, user = M.adapter, M.user
local eff = M.effect
if not M.opts.called then
user.expMovedThisTurn = true
local c = canceller(M)
if c == false then
M.anim.statusOnly = true
M.anim.cancelled = true
user.lastMoveId = nil
user.lastMove = nil
Engine.moveEndLite(M)
return
end
if c == "bide" then
M.anim.statusOnly = false
bide_attack(M)
move_end(M)
return
end
if c == "ghost" then
M.anim.statusOnly = true
move_end(M)
return
end
if Abilities.soundproofBlocks(M) then
M.notObeyed = true
move_end(M)
return
end
if M.slot and user.mon and user.mon.pp and tonumber(user.mon.pp[M.slot]) == 0
and not user.expLockedMove and M.mnum ~= MOVE_STRUGGLE then
M:attackString()
M:say(Strings("But there was no PP left\nfor the move!"))
M.anim.statusOnly = true
return
end
if not user.expLockedMove then
local d, slot = disobedient(M)
if d == "stop" then
M.anim.statusOnly = true
M.anim.cancelled = true
user.lastMoveId = nil
user.lastMove = nil
Engine.moveEndLite(M)
return
elseif d == "called" then
M.anim.statusOnly = true
return run_called(M, user.mon.moves[slot], { slot = slot })
end
end
end
if eff ~= E.PROTECT and eff ~= E.ENDURE then
user.expProtectStreak = 0
end
if charge_turn(M) then
move_end(M)
return
end
if eff == E.METRONOME or eff == E.SLEEP_TALK or eff == E.MIRROR_MOVE or eff == E.ASSIST
or eff == E.NATURE_POWER then
M.anim.statusOnly = true
return call_moves(M)
end
if eff == E.FOCUS_PUNCH and ((user.lastPhysicalDamageTaken or 0) > 0 or (user.lastSpecialDamageTaken or 0) > 0) then
-- pokefirered/data/battle_scripts_1.s:2255
M:ppReduce()
M:say(Strings("%s lost its\nfocus and couldn't move!", M.uname))
M.anim.statusOnly = true
return
end
local function body()
try_bounce(M)
Engine.lightningRodTook(M)
if eff == E.OHKO then
ohko(M)
move_end(M)
return
end
if eff == E.BIDE then
-- pokefirered/data/battle_scripts_1.s:575
M:attackString()
M:ppReduce()
M:attackAnimation()
user.bideTurns = 2
user.expBideDamage = 0
user.expBideTarget = nil
user.expLockedMove = M.moveId
user.expLockedSlot = M.slot
M.anim.statusOnly = true
move_end(M)
return
end
local power = tonumber(M.move.power) or 0
if power > 0 then
local Hit = require("src.core.game3.battle.effects.hit")
Hit.run(M)
move_end(M)
return
end
M.anim.statusOnly = true
M:attackString()
M:ppReduce()
local handled = Effects.runForMove(ad, M.user, M.target, M.moveId, M)
if not handled then
M:attackAnimation()
M:say(Strings("But nothing happened!"))
end
M:tryFaintTarget()
M:tryFaintUser()
move_end(M)
end
Engine.forEachTarget(M, body)
end
-- pokefirered/src/battle_script_commands.c:888
function Engine.lightningRodTook(M)
local t = M.target
if not (t and t.expLightningRodRedirected) then return false end
t.expLightningRodRedirected = nil
M:attackString()
M:say(Strings("%s's %s\ntook the attack!", M.adapter:displayName(t), Abilities.name("LIGHTNING_ROD")))
return true
end
-- pokefirered/src/battle_script_commands.c:3469
function Engine.resetTargetValues(M)
M.noEffect, M.missReason, M.failed, M.hitSubstitute = nil, nil, nil, nil
M.hpDealt, M.firstDmg, M.hitsLanded, M.targetDamaged, M.specialHit = nil, nil, nil, nil, nil
M.dmgMultiplier = 1
M.ignoreOnAir, M.ignoreUnderground, M.ignoreUnderwater = nil, nil, nil
M.brokeWall, M._animDmg, M._animPower = nil, nil, nil
if M.anim then M.anim.missed = false end
end
local function set_target(M, b)
M.target = b
M.tname = M.adapter:displayName(b)
if b then b._statLoweredMsg = nil end
end
-- pokefirered/src/battle_script_commands.c:4308
function Engine.forEachTarget(M, body)
local st, ad = M.st, M.adapter
local ttype = tonumber(M.move and M.move.target) or 0
M.targets = { M.target and State.idOf(M.target) or nil }
M.targetIndex = 1
if not (st and st.double) or M.noSpread then return body(M) end
if ttype == MOVE_TARGET_FOES_AND_ALLY then
-- pokefirered/src/battle_script_commands.c:8532
local uid = State.idOf(M.user)
local ids = {}
for id = 0, 3 do
if id ~= uid and State.isPresent(st, id) then ids[#ids + 1] = id end
end
if #ids == 0 then return body(M) end
M.targets = ids
M.deferUserFaint = (M.effect == E.EXPLOSION)
local anyLanded = false
for i, id in ipairs(ids) do
if i > 1 then Engine.resetTargetValues(M) end
M.targetIndex = i
set_target(M, State.battler(st, id))
body(M)
if not M.anim.missed then anyLanded = true end
if M.stopTargets then break end
end
M.anim.missed = not anyLanded
if M.deferUserFaint then
M.deferUserFaint = nil
M:tryFaintUser()
end
return
end
if ttype == MOVE_TARGET_BOTH then
body(M)
local firstMissed = M.anim.missed
if M.stopTargets or not M.target then return end
local pid = State.PARTNER(State.idOf(M.target))
local nb = State.battler(st, pid)
if nb and State.isPresent(st, pid) and ad:hp(nb) > 0 then
Engine.resetTargetValues(M)
M.targets[2] = pid
M.targetIndex = 2
set_target(M, nb)
body(M)
M.anim.missed = firstMissed and M.anim.missed
end
return
end
return body(M)
end
--- Resolve a move. Returns message list.
-- Also fills out._anim (presentation meta for AnimSeq): moveId, user, target,
function Engine.resolveMove(user, target, moveId, slot, adapter, st, out, opts)
out = out or {}
opts = opts or {}
local chosenTargetId = (target ~= nil and type(target) ~= "string") and State.idOf(target) or nil
if st then
user = State.occupant(st, user)
target = State.occupant(st, target)
end
local nested = opts.called and opts.anim ~= nil
local prevSay = adapter._say
local mark = adapter:eventMark()
if not nested then
adapter._say = function(text) out[#out + 1] = text end
end
local absentUser = st and st.double and user and State.isAbsent(st, State.idOf(user))
if not opts.called and st and (st.over or absentUser) then
local anim = { moveId = moveId, user = user, target = target, hits = {}, heals = {}, faints = {},
missed = false, statusOnly = true, msgs = {}, events = {} }
out._anim = anim
adapter._say = prevSay
return out
end
local chosenMoveId = moveId
local retarget = false
local locked = false
if not opts.called and user then
if user.expLockedMove and not opts.pursuitSwitch then
moveId = user.expLockedMove
slot = user.expLockedSlot
locked = true
if user.twoTurnTarget and st and State.occupant(st, user.twoTurnTarget) then
target = State.occupant(st, user.twoTurnTarget)
end
elseif user.expEncoreMove and (user.expEncoreTurns or 0) > 0 then
local H = require("src.core.game3.battle.effects._helpers")
local es = H.slotOf(user, user.expEncoreMove)
if es then
if move_num(user.mon.moves[es]) ~= move_num(moveId) then retarget = true end
moveId = user.mon.moves[es]
slot = es
end
end
end
-- pokefirered/src/battle_main.c:3963
if st and st.double and user and not opts.called and not opts.pursuitSwitch and not opts.noRetarget
and not absentUser then
for id = 0, 3 do
local b = State.battler(st, id)
if b then b.expLightningRodRedirected = nil end
end
local uid = State.idOf(user)
local tid = chosenTargetId
if locked then
tid = (st.moveTarget and st.moveTarget[uid]) or (user.twoTurnTarget and State.idOf(user.twoTurnTarget)) or tid
end
local rid = Engine.resolveTarget(st, adapter, user, moveId, tid, {
recompute = retarget or tid == nil, chosenMove = locked and moveId or chosenMoveId,
})
target = State.battler(st, rid) or target
end
local anim = opts.anim or {
moveId = moveId,
user = user,
target = target,
hits = {},
heals = {},
faints = {},
missed = false,
statusOnly = false,
}
if nested then
anim.moveId = moveId
anim.target = target
end
if not opts.called then Engine.refreshLinks(st) end
if not opts.called and st and st._focusPunchSetup then
local list = st._focusPunchSetup
st._focusPunchSetup = nil
for _, b in ipairs(list) do
if not adapter:isFainted(b) then
adapter:playAnim("general", "FOCUS_PUNCH_SETUP", b, b)
adapter:say(Strings("%s is tightening\nits focus!", adapter:displayName(b)))
end
end
end
local M = new_ctx(user, target, moveId, slot, adapter, st, out, anim, opts)
if opts.called then M.printedUsed = false end
if user then user._statLoweredMsg = nil end
if target then target._statLoweredMsg = nil end
if not opts.called then adapter._syncEffect = nil end
if nested or not (st and st.interactiveChoices) then
run(M)
else
local co = coroutine.create(run)
local ok, req = coroutine.resume(co, M)
if not ok then error(debug.traceback(co, req), 0) end
if coroutine.status(co) == "suspended" then
st.pendingChoice = { co = co, req = req, M = M }
return Engine.finishResolve(adapter, st, out, anim, mark, prevSay, true)
end
end
if nested then return out end
return Engine.finishResolve(adapter, st, out, anim, mark, prevSay, opts.called)
end
function Engine.finishResolve(adapter, st, out, anim, mark, prevSay, skipAfter)
if not skipAfter then Engine.afterAction(st, adapter) end
anim.events = adapter:eventsSince(mark)
anim.heals = {}
for _, ev in ipairs(anim.events) do
if ev.kind == "hp" then
anim.heals[#anim.heals + 1] = { side = ev.side, from = ev.from, to = ev.to, maxHp = ev.maxHp }
end
end
anim.msgs = {}
for i = 1, #out do anim.msgs[i] = out[i] end
out.events = anim.events
out._anim = anim
out.pendingChoice = st and st.pendingChoice and st.pendingChoice.req or nil
adapter._say = prevSay
return out
end
-- pokefirered/src/battle_script_commands.c:4626
function Engine.resumeChoice(st, adapter, value)
local pend = st and st.pendingChoice
if not pend then return nil end
st.pendingChoice = nil
local M = pend.M
local out = {}
local anim = {
moveId = M.moveId, user = M.user, target = M.target, hits = {}, heals = {}, faints = {},
missed = false, statusOnly = true,
}
M.out = out
local prevSay = adapter._say
local mark = adapter:eventMark()
adapter._say = function(text) out[#out + 1] = text end
local ok, req = coroutine.resume(pend.co, value)
if not ok then
adapter._say = prevSay
error(debug.traceback(pend.co, req), 0)
end
if coroutine.status(pend.co) == "suspended" then
st.pendingChoice = { co = pend.co, req = req, M = M }
return Engine.finishResolve(adapter, st, out, anim, mark, prevSay, true)
end
return Engine.finishResolve(adapter, st, out, anim, mark, prevSay, false)
end
-- pokefirered/src/battle_ai_switch_items.c:428
function Engine.mostSuitableMon(st, adapter, side)
local id = (type(side) == "number") and side or State.idOf(side)
if type(side) == "table" then side = side.side end
if type(side) ~= "string" then side = State.sideOf(id) end
local party = (side == "player") and st.playerParty or st.foeParty
local active = State.battler(st, id)
local opp = State.battler(st, State.OPPOSITE(id))
local pending = st.monToSwitchInto or {}
if pending[id] then return pending[id] end
local in2 = id
if st.double then
if State.isPresent(st, State.PARTNER(id)) then in2 = State.PARTNER(id) end
-- pokefirered/src/battle_ai_switch_items.c:448
local oid = (math.floor(roll(adapter, 0, 65535) / 2) % 2) * 2 + (1 - id % 2)
if not State.isPresent(st, oid) then oid = State.PARTNER(oid) end
opp = State.battler(st, oid)
end
if not party or not opp then return nil end
local activeIdx = active and active.partyIndex
local partner = State.battler(st, in2)
local in2Idx = partner and partner.partyIndex
local function valid(i, mon)
return mon and not mon.isEgg and (tonumber(mon.hp) or 0) > 0 and i ~= activeIdx and i ~= in2Idx
and i ~= pending[id] and i ~= pending[in2]
and (tonumber(mon.species or mon.speciesId) or 0) ~= 0
end
-- pokefirered/src/battle_ai_switch_items.c:404
local function modulate(atk, d1, d2, v)
local t = Types.TABLE
for i = 1, #t, 3 do
if t[i] ~= -1 and t[i] == atk then
if t[i + 1] == d1 then v = math.floor(v * t[i + 2] / 10) end
if t[i + 1] == d2 and d1 ~= d2 then v = math.floor(v * t[i + 2] / 10) end
end
end
return v
end
local function types_of(mon)
local b = State.makeBattler(mon, side, {})
return b.type1, b.type2 or b.type1
end
local oT1, oT2 = opp.type1, opp.type2 or opp.type1
local invalid = {}
while true do
local bestDmg, bestId = 0, nil
for i = 1, 6 do
local mon = party[i]
if valid(i, mon) and not invalid[i] then
local t1, t2 = types_of(mon)
local v = modulate(oT1, t1, t2, 10)
v = modulate(oT2, t1, t2, v)
if bestDmg < v then bestDmg, bestId = v, i end
else
invalid[i] = true
end
end
if not bestId then break end
local mon = party[bestId]
for j = 1, 4 do
local mv = mon.moves and mon.moves[j]
local n = move_num(mv)
if n and n ~= 0 then
local _, flags = Types.typeCalc(Moves.get(mv).type, opp.type1, opp.type2)
if flags.super then return bestId end
end
end
invalid[bestId] = true
end
-- pokefirered/src/battle_ai_switch_items.c:510
local bestDmg, bestId = 0, nil
for i = 1, 6 do
local mon = party[i]
if valid(i, mon) then
for j = 1, 4 do
local mv = mon.moves and mon.moves[j]
local n = move_num(mv)
local dmg = 0
if n and n ~= 0 then
local m = Moves.get(mv)
if (tonumber(m.power) or 0) ~= 1 then
local mt = tonumber(m.type) or 0
dmg = 2
if active and (active.type1 == mt or active.type2 == mt) then dmg = math.floor(dmg * 15 / 10) end
dmg = Types.typeCalc(mt, opp.type1, opp.type2, dmg) or 0
end
end
if bestDmg < dmg then bestDmg, bestId = dmg, i end
end
end
end
return bestId
end
-- pokefirered/src/battle_main.c:4150
function Engine.performEnemyItem(st, adapter, act)
local BattleItems = require("src.core.game3.battle.items")
return BattleItems.enemyUse(st, adapter, act)
end
-- pokefirered/src/battle_script_commands.c:4467
local function switched_event(st, adapter, id, nb, old, opts)
if not ModRuntime.wants("battle.battler_switched") then return end
local mon = nb and nb.mon
local sp = mon and tonumber(mon.species or mon.speciesId)
ModRuntime.emit("battle.battler_switched", {
battle = st, side = adapter.ownSide and adapter:ownSide(nb), sideName = nb and nb.side, battler = nb,
previous = old, battlerId = id, reason = opts and opts.reason,
species = require("src.mods.Gen3Compat").speciesName(sp), speciesId = sp,
partyIndex = nb and nb.partyIndex,
})
end
function Engine.performSwitch(st, adapter, side, slot, opts)
opts = opts or {}
local id = (type(side) == "number") and side or State.idOf(side)
side = State.sideOf(id)
local old = State.battler(st, id)
local party = (side == "player") and st.playerParty or st.foeParty
if not old or not party or not party[slot] then return nil end
local oldSlot = old.partyIndex
if side == "player" and not st.double then
if not opts.isShift and opts.reason ~= "shift" then
State.trackParticipant(st, st.enemy, oldSlot or 1)
end
end
Engine.switchOutEffects(st, adapter, old)
State.syncBattlerToParty(old, party)
local nb = State.makeBattler(party[slot], side, { partyIndex = slot, id = id, st = st })
if opts.batonPass then
-- pokefirered/src/battle_main.c:2350
for k, v in pairs(old.stages or {}) do nb.stages[k] = v end
nb.confusionTurns = old.confusionTurns
nb.expFocusEnergy = old.expFocusEnergy
nb.focusEnergy = old.focusEnergy
nb.substituteHP = old.substituteHP
nb.expSeeded = old.expSeeded
nb.expSeedSource = old.expSeedSource
nb.expIngrain = old.expIngrain
nb.expPerishTurns = old.expPerishTurns
nb.expCursed = old.expCursed
nb.expTrapped = old.expTrapped
nb.escapePrevention = old.escapePrevention
nb.expLockedOn = old.expLockedOn
nb.expLockedOnBy = old.expLockedOnBy
end
if st.battlers then st.battlers[id] = nb else st[side] = nb end
if st.absent then st.absent[id] = nil end
if st.monToSwitchInto then st.monToSwitchInto[id] = nil end
Engine.cancelPendingAction(st, id)
if st.double then
-- pokefirered/src/battle_script_commands.c:4966
State.updateSentPokes(st, nb)
for oid = 0, 3 do
local b = State.battler(st, oid)
if b and oid ~= id then
if b.expSeedSource == old then b.expSeedSource = nb end
if b.expTrapSource == old then
b.expTrapTurns, b.expTrapSource, b.wrapped = nil, nil, nil
end
if b.expTrappedBy == old then
b.expTrapped, b.expTrappedBy, b.escapePrevention = nil, nil, nil
end
if b.expInfatuatedWith == old then
b.expInfatuated, b.expInfatuatedWith, b.expInfatuatedBy = nil, nil, nil
end
if b.expLockedOnById == id then b.expLockedOn, b.expLockedOnById, b.expLockedOnBy = nil, nil, nil end
end
end
adapter:pushEvent({ kind = "switch", side = side, battler = id, from = oldSlot, to = slot, reason = opts.reason })
switched_event(st, adapter, id, nb, old, opts)
return nb
end
local foe = (side == "player") and st.enemy or st.player
if side == "player" then
State.trackParticipant(st, st.enemy, slot)
elseif st.player then
-- pokefirered/src/battle_util.c:254
State.opponentSwitchInResetSentPokes(st, nb)
end
if foe then
if foe.expSeedSource == old then foe.expSeedSource = nb end
if foe.expTrapSource == old then
foe.expTrapTurns = nil
foe.expTrapSource = nil
foe.wrapped = nil
end
if foe.expTrappedBy == old then
foe.expTrapped = nil
foe.escapePrevention = nil
end
foe.expInfatuated = nil
if foe.expLockedOnBy == side then foe.expLockedOn = nil end
end
adapter:pushEvent({ kind = "switch", side = side, battler = id, from = oldSlot, to = slot, reason = opts.reason })
switched_event(st, adapter, id, nb, old, opts)
return nb
end
-- pokefirered/src/battle_script_commands.c:5013
function Engine.cancelPendingAction(st, id)
for _, act in ipairs(st and st.turnActions or {}) do
if act.battler == id and not act.done then act.finished = true end
end
end
function Engine.actionRunnable(st, act)
if not act or act.finished or act.done then return false end
if act.battler ~= nil and State.isAbsent(st, act.battler) then return false end
return true
end
-- pokefirered/src/battle_script_commands.c:9197
function Engine.switchOutEffects(st, adapter, battler)
if not battler or not adapter then return false end
return Abilities.switchOut(adapter, battler)
end
-- pokefirered/src/battle_script_commands.c:4960
function Engine.switchInEffects(st, adapter, battler, opts)
opts = opts or {}
if not battler or not adapter or (st and st.over) then return false end
if opts.spikes then
local Hazards = require("src.core.game3.battle.effects.hazards")
local layers = Hazards.layers(adapter:ownSide(battler))
if layers > 0 and not adapter:hasType(battler, Types.ID.FLYING) and adapter:abilityOf(battler) ~= "LEVITATE" then
local denom = ({ 8, 6, 4 })[math.min(layers, 3)]
local dmg = math.max(1, math.floor(adapter:maxHp(battler) / denom))
adapter:applyHpLoss(battler, dmg)
adapter:say(Strings("%s is hurt\nby SPIKES!", adapter:displayName(battler)))
if adapter:isFainted(battler) then return true end
end
end
if adapter:isFainted(battler) then return false end
if adapter:abilityOf(battler) == "TRUANT" then battler.expTruantCounter = 1 end
local did = Abilities.switchIn(adapter, battler)
if not did then did = HeldItems.onSwitchIn(adapter, battler) end
if not opts.deferIntimidate then
for _ = 1, 4 do
if not (Abilities.runIntimidate(adapter) or Abilities.runTrace(adapter)) then break end
did = true
end
end
return did
end
-- pokefirered/src/battle_main.c:2856
function Engine.battleStartEffects(st, adapter)
if not st or not adapter then return false end
local did = false
local w = Rules.weather.kind(st.overworldWeather)
if w and not st._overworldWeatherDone then
st._overworldWeatherDone = true
if Rules.weather.kind(st.weather) ~= w then
st.weather, st.weatherTurns = w, 0
local line = (w == "SAND" and Strings("A sandstorm is raging."))
or (w == "SUN" and Strings("The sunlight is strong.")) or Strings("It is raining.")
adapter:say(line)
adapter:playAnim("general", w == "SAND" and "SANDSTORM_CONTINUES" or (w == "SUN" and "SUN_CONTINUES")
or "RAIN_CONTINUES", st.player, st.player)
did = true
end
end
local order = Residuals.sortedBattlers(adapter)
-- pokefirered/src/battle_util.c:1678
if not st.safari then
for _, b in ipairs(order) do
if Abilities.switchIn(adapter, b) then did = true end
end
for _ = 1, 4 do
if not (Abilities.runIntimidate(adapter) or Abilities.runTrace(adapter)) then break end
did = true
end
end
for _, b in ipairs(order) do
if HeldItems.onSwitchIn(adapter, b) then did = true end
end
return did
end
-- pokefirered/src/battle_main.c:3002
function Engine.canRun(st, adapter, battler)
battler = battler or (st and st.player)
if not st or not battler then return false, nil end
-- pokefirered/src/battle_main.c:3240
if st.link then return true end
if not st.wild then return false, Strings("No! There's no running\nfrom a TRAINER battle!") end
if HeldItems.has(battler, HeldItems.HOLD.CAN_ALWAYS_RUN) or adapter:abilityOf(battler) == "RUN_AWAY" then
return true
end
local holder, ab = Abilities.escapeBlocker(adapter, battler)
if holder then
return false, Strings("%s prevents\nescape with %s!", adapter:displayName(holder), Abilities.name(ab))
end
if battler.expTrapped or battler.escapePrevention or (battler.expTrapTurns or 0) > 0 or battler.expIngrain then
return false, Strings("Can't escape!")
end
return true
end
-- pokefirered/src/battle_main.c:3196
function Engine.canSwitch(st, adapter, battler)
battler = battler or (st and st.player)
if not battler then return true end
if battler.expTrapped or battler.escapePrevention or (battler.expTrapTurns or 0) > 0 or battler.expIngrain then
return false, Strings("%s can't be switched\nout!", adapter:displayName(battler))
end
local holder, ab = Abilities.escapeBlocker(adapter, battler)
if holder then
return false, Strings("%s's %s\nprevents switching!", adapter:displayName(holder), Abilities.name(ab))
end
return true
end
-- pokefirered/src/battle_main.c:4229
function Engine.tryFlee(st, adapter, battler)
battler = battler or (st and st.player)
if not st.wild then
adapter:say(Strings("No! There's no running\nfrom a TRAINER battle!"))
return false
end
local foe = adapter:foeOf(battler)
if HeldItems.has(battler, HeldItems.HOLD.CAN_ALWAYS_RUN) then
adapter:playAnim("general", "SMOKEBALL_ESCAPE", battler, battler)
adapter:say(Strings("%s fled\nusing its %s!", adapter:displayName(battler), HeldItems.name(HeldItems.itemOf(battler))))
return true, "item"
end
if adapter:abilityOf(battler) == "RUN_AWAY" then
adapter:say(Strings("%s fled\nusing %s!", adapter:displayName(battler), Abilities.name("RUN_AWAY")))
return true, "ability"
end
if st.ghostBattle and battler.side == "player" then
adapter:say(Strings("Got away safely!"))
return true
end
local ok = false
local mySpe = tonumber(battler.mon and battler.mon.speed) or 0
local foeSpe = tonumber(foe and foe.mon and foe.mon.speed) or 0
local function vanilla_run(pSpd, eSpd)
if st.double then
-- pokefirered/src/battle_main.c:4259
return false
elseif pSpd < eSpd then
local speedVar = (math.floor(pSpd * 128 / math.max(1, eSpd)) + (st.fleeAttempts or 0) * 30) % 256
return speedVar > roll(adapter, 0, 255)
end
return true
end
if ModRuntime.wantsHook("battle.run") then
ok = ModRuntime.call("battle.run", function(c)
return vanilla_run(c.pSpd, c.eSpd)
end, { battle = st, pSpd = mySpe, eSpd = foeSpe, attempts = st.fleeAttempts or 0,
rng = adapter:rng(), battler = battler }) and true or false
else
ok = vanilla_run(mySpe, foeSpe)
end
st.fleeAttempts = (st.fleeAttempts or 0) + 1
if ok then
adapter:say(Strings("Got away safely!"))
return true
end
battler.expDestinyBond, battler.destinyBond, battler.expGrudge, battler.expFuryCutter = nil, nil, nil, 0
adapter:say(Strings("Can't escape!"))
return false
end
-- pokefirered/src/battle_script_commands.c:4536
function Engine.switchCandidates(st, side)
local id = (type(side) == "number") and side or State.idOf(side)
if type(side) ~= "string" then side = State.sideOf(id) end
local party = (side == "player") and st.playerParty or st.foeParty
local excl = {}
local ids = st.double and State.positionsOnSide(side) or { id }
for _, bid in ipairs(ids) do
local b = State.battler(st, bid)
if b and b.partyIndex then excl[b.partyIndex] = true end
local pend = st.monToSwitchInto and st.monToSwitchInto[bid]
if pend and bid ~= id then excl[pend] = true end
end
local out = {}
for i, mon in ipairs(party or {}) do
if not excl[i] and mon and (tonumber(mon.hp) or 0) > 0 and not mon.isEgg then out[#out + 1] = i end
end
return out
end
-- pokefirered/src/party_menu.c:5916
function Engine.replacementCandidates(st, id)
local side = State.sideOf(id)
local party = (side == "player") and st.playerParty or st.foeParty
local excl = {}
for _, bid in ipairs(st.double and State.positionsOnSide(side) or { id }) do
local b = State.battler(st, bid)
if b and b.partyIndex then excl[b.partyIndex] = true end
local pend = st.monToSwitchInto and st.monToSwitchInto[bid]
if pend then excl[pend] = true end
end
local out = {}
for i, mon in ipairs(party or {}) do
if not excl[i] and mon and not mon.isEgg and (tonumber(mon.hp) or 0) > 0
and (tonumber(mon.species or mon.speciesId) or 0) ~= 0 then
out[#out + 1] = i
end
end
return out
end
function Engine.faintedBattlers(st)
local out = {}
for id = 0, 3 do
local b = State.battler(st, id)
if b and not State.isAbsent(st, id) and State.isFainted(b) then out[#out + 1] = id end
end
return out
end
-- pokefirered/src/battle_script_commands.c:4870
function Engine.markAbsent(st, id)
st.absent = st.absent or {}
st.absent[id] = true
Engine.cancelPendingAction(st, id)
end
-- pokefirered/src/battle_util.c:1156
function Engine.refreshAbsent(st)
local back = {}
if not (st and st.double and st.absent) then return back end
for id = 0, 3 do
if st.absent[id] and State.battler(st, id) and #Engine.replacementCandidates(st, id) > 0 then
st.absent[id] = nil
back[#back + 1] = id
end
end
return back
end
-- pokefirered/src/battle_util.c:1144
function Engine.pendingReplacements(st)
local out = {}
for _, id in ipairs(Engine.faintedBattlers(st)) do
local cands = Engine.replacementCandidates(st, id)
out[#out + 1] = { battler = id, side = State.sideOf(id), candidates = cands, noMons = #cands == 0 }
end
return out
end
-- pokefirered/src/battle_script_commands.c:3113
function Engine.expAwardOrder(st)
local out = {}
for _, id in ipairs(Engine.faintedBattlers(st)) do
if State.sideOf(id) == "enemy" then out[#out + 1] = id end
end
return out
end
local function is_chosen_map(t)
if type(t) ~= "table" or t.kind ~= nil then return false end
for id = 0, 3 do
if t[id] ~= nil then return true end
end
return next(t) == nil
end
local function forced_move(b, mv, slot)
if not b then return mv, slot end
if b.expLockedMove then return b.expLockedMove, slot end
if b.expEncoreMove and (b.expEncoreTurns or 0) > 0 then return b.expEncoreMove, slot end
if b.choicedMove and HeldItems.has(b, HeldItems.HOLD.CHOICE_BAND) and move_num(mv) ~= b.choicedMove
and move_num(mv) ~= MOVE_STRUGGLE then
local H = require("src.core.game3.battle.effects._helpers")
local cs = H.slotOf(b, b.choicedMove)
if cs and (tonumber(b.mon.pp and b.mon.pp[cs]) or 1) > 0 then return b.mon.moves[cs], cs end
end
return mv, slot
end
local function is_meta_first(kind) return kind == "bag" or kind == "switch" or kind == "item" end
local function order_hook(st, adapter, a, aMove, b, bMove, vanilla)
local G3 = require("src.mods.Gen3Compat")
return ModRuntime.call("battle.turn_order", function()
return vanilla()
end, a, aMove and G3.moveView(aMove) or nil, b, bMove and G3.moveView(bMove) or nil,
{ battle = st, rng = adapter:rng(), playerMove = aMove and G3.moveName(move_num(aMove)),
enemyMove = bMove and G3.moveName(move_num(bMove)),
firstId = State.idOf(a), secondId = State.idOf(b) }) and true or false
end
-- pokefirered/src/battle_main.c:3532
function Engine.planTurnActions(st, adapter, chosen)
chosen = chosen or {}
st.chosen = chosen
st.monToSwitchInto = st.monToSwitchInto or {}
for id = 0, 3 do clear_turn_flags(State.battler(st, id)) end
Engine.refreshLinks(st)
if st.playerSide then st.playerSide.expFollowMe, st.playerSide.expFollowMeId = nil, nil end
if st.enemySide then st.enemySide.expFollowMe, st.enemySide.expFollowMeId = nil, nil end
local rows = {}
for id = 0, 3 do
local act = chosen[id]
local b = State.battler(st, id)
if b and State.isAbsent(st, id) then
-- pokefirered/src/battle_main.c:3115
rows[id] = { battler = id, user = b, kind = "nothing", finished = true }
elseif act and b then
local row = {}
for k, v in pairs(act) do row[k] = v end
row.battler = id
row.user = b
row.kind = act.kind or "move"
if row.kind == "move" then
row.move, row.slot = forced_move(b, act.move, act.slot)
local mv = Moves.get(row.move)
row.targetType = tonumber(mv and mv.target) or 0
end
rows[id] = row
end
end
-- pokefirered/src/battle_util.c:1223
for id, row in pairs(rows) do
local b = row.user
local mv = b.expLockedMove or (row.kind == "move" and row.move) or nil
if b.rage and move_num(mv) ~= 99 then b.rage = nil end
end
local order = {}
local sortable = 0
-- pokefirered/src/battle_controllers.c:163 the non-master owns the odd battler ids
local ids = link_seat_swap(st) and { 1, 0, 3, 2 } or { 0, 1, 2, 3 }
if rows[0] and rows[0].kind == "run" then
order[1] = 0
for id = 1, 3 do if rows[id] then order[#order + 1] = id end end
else
for _, id in ipairs(ids) do
if rows[id] and is_meta_first(rows[id].kind) then order[#order + 1] = id end
end
for _, id in ipairs(ids) do
if rows[id] and not is_meta_first(rows[id].kind) then
order[#order + 1] = id
sortable = sortable + 1
end
end
end
if sortable >= 2 then
-- pokefirered/src/battle_main.c:2926
st.randomTurnNumber = roll(adapter, 0, 0xFFFF)
for i = 1, #order - 1 do
for j = i + 1, #order do
local r1, r2 = rows[order[i]], rows[order[j]]
if not is_meta_first(r1.kind) and not is_meta_first(r2.kind) then
local p1 = r1.kind == "move" and Moves.priority(r1.move) or 0
local p2 = r2.kind == "move" and Moves.priority(r2.move) or 0
local s1 = speed_of(r1.user, st, adapter)
local s2 = speed_of(r2.user, st, adapter)
local function vanilla_swap()
-- pokefirered/src/battle_main.c:3505
if p1 ~= p2 then
return p1 < p2
elseif s1 == s2 then
return roll(adapter, 0, 1) == 1
end
return s1 < s2
end
local swap
if ModRuntime.wantsHook("battle.turn_order") then
swap = not order_hook(st, adapter, r1.user, r1.kind == "move" and r1.move or nil,
r2.user, r2.kind == "move" and r2.move or nil,
function() return not vanilla_swap() end)
else
swap = vanilla_swap()
end
if swap then order[i], order[j] = order[j], order[i] end
end
end
end
end
-- pokefirered/src/battle_main.c:3669
local focus = {}
for id = 0, 3 do
local row = rows[id]
if row and row.kind == "move" then
local mv = Moves.get(row.move)
if tonumber(mv and mv.effect) == E.FOCUS_PUNCH and not row.user.expLockedMove
and not adapter:hasStatus(row.user, "SLP") then
focus[#focus + 1] = row.user
end
end
end
st._focusPunchSetup = (#focus > 0) and focus or nil
local actions = {}
for i, id in ipairs(order) do
rows[id].user.expTurnOrder = i
actions[i] = rows[id]
end
st.turnOrder = order
st.turnActions = actions
return actions, nil
end
function Engine.planTurnFromActions(st, adapter, playerAct, enemyAct)
if is_chosen_map(playerAct) and enemyAct == nil then
return Engine.planTurnActions(st, adapter, playerAct)
end
if st and st.double then
return Engine.planTurnActions(st, adapter, {
[0] = playerAct or Commands.playerAction(st, 1, 1),
[1] = enemyAct or Commands.enemyAction(st, 1),
})
end
playerAct = playerAct or Commands.playerAction(st, 1, 1)
enemyAct = enemyAct or Commands.enemyAction(st)
clear_turn_flags(st.player)
clear_turn_flags(st.enemy)
Engine.refreshLinks(st)
if st.playerSide then st.playerSide.expFollowMe = nil end
if st.enemySide then st.enemySide.expFollowMe = nil end
-- pokefirered/src/battle_util.c:1223
local function chosen(b, act)
if not b then return nil end
if b.expLockedMove then return b.expLockedMove end
return act and act.kind == "move" and act.move or nil
end
local pChosen = chosen(st.player, playerAct)
local eChosen = chosen(st.enemy, enemyAct)
if st.player and st.player.rage and move_num(pChosen) ~= 99 then st.player.rage = nil end
if st.enemy and st.enemy.rage and move_num(eChosen) ~= 99 then st.enemy.rage = nil end
local actions = {}
local enemyMeta = enemyAct.kind == "switch" or enemyAct.kind == "item"
local function enemy_meta_row()
local row = {}
for k, v in pairs(enemyAct) do row[k] = v end
row.user, row.battler = st.enemy, 1
return row
end
-- pokefirered/src/battle_main.c:3537
if st.safari then
if st.player then st.player.expTurnOrder = 1 end
if st.enemy then st.enemy.expTurnOrder = 2 end
-- pokefirered/src/battle_controller_opponent.c:1364
actions[#actions + 1] = {
user = st.enemy, battler = 1,
kind = (enemyAct.kind == "run") and "run" or "watch",
}
st.turnOrder = { 0, 1 }
st.turnActions = actions
return actions, playerAct
end
if playerAct.kind == "run" or playerAct.kind == "bag" or playerAct.kind == "switch" then
if st.player then st.player.expTurnOrder = 1 end
if enemyMeta then
-- pokefirered/src/battle_main.c:3586
if st.enemy then st.enemy.expTurnOrder = 2 end
actions[#actions + 1] = enemy_meta_row()
elseif enemyAct.kind == "move" then
if st.enemy then st.enemy.expTurnOrder = 2 end
actions[#actions + 1] = {
user = st.enemy, target = st.player,
move = enemyAct.move, slot = enemyAct.slot,
battler = 1, kind = "move",
}
end
st.turnOrder = { 0, 1 }
st.turnActions = actions
return actions, playerAct
end
local pMove, pSlot = playerAct.move, playerAct.slot
if enemyMeta then
-- pokefirered/src/battle_main.c:3586
pMove, pSlot = forced_move(st.player, pMove, pSlot)
if st.enemy then st.enemy.expTurnOrder = 1 end
if st.player then st.player.expTurnOrder = 2 end
actions[1] = enemy_meta_row()
actions[2] = { user = st.player, target = st.enemy, move = pMove, slot = pSlot, battler = 0, kind = "move" }
local mv = Moves.get(pMove)
st._focusPunchSetup = nil
if tonumber(mv and mv.effect) == E.FOCUS_PUNCH and not st.player.expLockedMove
and not adapter:hasStatus(st.player, "SLP") then
st._focusPunchSetup = { st.player }
end
st.turnOrder = { 1, 0 }
st.turnActions = actions
return actions, nil
end
local eMove, eSlot = enemyAct.move, enemyAct.slot
local function forced(b, mv, slot)
if not b then return mv, slot end
if b.expLockedMove then return b.expLockedMove, slot end
if b.expEncoreMove and (b.expEncoreTurns or 0) > 0 then return b.expEncoreMove, slot end
if b.choicedMove and HeldItems.has(b, HeldItems.HOLD.CHOICE_BAND) and move_num(mv) ~= b.choicedMove
and move_num(mv) ~= MOVE_STRUGGLE then
local H = require("src.core.game3.battle.effects._helpers")
local cs = H.slotOf(b, b.choicedMove)
if cs and (tonumber(b.mon.pp and b.mon.pp[cs]) or 1) > 0 then return b.mon.moves[cs], cs end
end
return mv, slot
end
pMove, pSlot = forced(st.player, pMove, pSlot)
eMove, eSlot = forced(st.enemy, eMove, eSlot)
-- pokefirered/src/battle_main.c:2926
st.randomTurnNumber = roll(adapter, 0, 0xFFFF)
local pPri = Moves.priority(pMove)
local ePri = Moves.priority(eMove)
local pSpe = speed_of(st.player, st, adapter)
local eSpe = speed_of(st.enemy, st, adapter)
-- pokefirered/src/battle_main.c:3400
local function vanilla_first()
if pPri ~= ePri then
return pPri > ePri
elseif pSpe ~= eSpe then
return pSpe > eSpe
end
-- pokefirered/src/battle_controllers.c:163 the non-master reads battler1/battler2 swapped
return (roll(adapter, 0, 1) == 1) == link_seat_swap(st)
end
local playerFirst
if ModRuntime.wantsHook("battle.turn_order") then
playerFirst = order_hook(st, adapter, st.player, pMove, st.enemy, eMove, vanilla_first)
else
playerFirst = vanilla_first()
end
-- pokefirered/src/battle_main.c:3682
local focus = {}
for _, row in ipairs({ { st.player, pMove }, { st.enemy, eMove } }) do
local mv = Moves.get(row[2])
if tonumber(mv and mv.effect) == E.FOCUS_PUNCH and not row[1].expLockedMove
and not adapter:hasStatus(row[1], "SLP") then
focus[#focus + 1] = row[1]
end
end
st._focusPunchSetup = (#focus > 0) and focus or nil
if playerFirst then
st.player.expTurnOrder, st.enemy.expTurnOrder = 1, 2
actions[#actions + 1] = { user = st.player, target = st.enemy, move = pMove, slot = pSlot, battler = 0, kind = "move" }
actions[#actions + 1] = { user = st.enemy, target = st.player, move = eMove, slot = eSlot, battler = 1, kind = "move" }
st.turnOrder = { 0, 1 }
else
st.player.expTurnOrder, st.enemy.expTurnOrder = 2, 1
actions[#actions + 1] = { user = st.enemy, target = st.player, move = eMove, slot = eSlot, battler = 1, kind = "move" }
actions[#actions + 1] = { user = st.player, target = st.enemy, move = pMove, slot = pSlot, battler = 0, kind = "move" }
st.turnOrder = { 1, 0 }
end
st.turnActions = actions
return actions, nil
end
function Engine.planTurn(st, adapter)
return Engine.planTurnFromActions(st, adapter, nil, nil)
end
function Engine.collectResidualEvents(_st, adapter)
return Residuals.collectEvents(adapter)
end
function Engine.hasLivingMons(party)
if not party then return false end
for _, mon in ipairs(party) do
if mon and (tonumber(mon.hp) or 0) > 0 then
return true
end
end
return false
end
function Engine.nextLivingMonIndex(party, currentIdx)
if not party then return nil end
for i, mon in ipairs(party) do
if i ~= currentIdx and mon and (tonumber(mon.hp) or 0) > 0 then
return i
end
end
return nil
end
function Engine.isPursuit(move)
if not move then return false end
if type(move) == "string" and move:upper() == "PURSUIT" then return true end
if type(move) == "number" and move == 228 then return true end
if type(move) == "table" and (move.id == 228 or move.effect == 128 or (move.name and move.name:upper() == "PURSUIT")) then
return true
end
return false
end
function Engine.checkEnd(st, adapter)
if not st then return nil end
if st.over then return st.result end
if st.player and st.playerParty then
State.syncBattlerToParty(st.player, st.playerParty)
end
if st.enemy and st.foeParty then
State.syncBattlerToParty(st.enemy, st.foeParty)
end
if st.double then
local b2, b3 = State.battler(st, 2), State.battler(st, 3)
if b2 and st.playerParty then State.syncBattlerToParty(b2, st.playerParty) end
if b3 and st.foeParty then State.syncBattlerToParty(b3, st.foeParty) end
end
local playerAlive = Engine.hasLivingMons(st.playerParty)
if not playerAlive then
st.over = true
st.result = "lose"
return "lose"
end
local foeAlive = Engine.hasLivingMons(st.foeParty)
if not foeAlive then
st.over = true
st.result = "win"
return "win"
end
return nil
end
return Engine