mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
CLOSES #1091, CLOSES #1097, CLOSES #1100, CLOSES #1104, CLOSES #1135, CLOSES #1155, CLOSES #1157, CLOSES #1158, CLOSES #1159, CLOSES #1160, CLOSES #1167, CLOSES #1208
This commit is contained in:
@@ -197,14 +197,19 @@ end
|
|||||||
local touchEditorHost
|
local touchEditorHost
|
||||||
local closeTouchControlsEditor -- forward declaration
|
local closeTouchControlsEditor -- forward declaration
|
||||||
|
|
||||||
local function openTouchControlsEditor()
|
-- `version` is the launcher tab the gear was opened on, and it decides which
|
||||||
|
-- option block the layout lands in (src/ui/TouchControlsEditor.lua persist).
|
||||||
|
local function openTouchControlsEditor(version)
|
||||||
touchEditorHost = Importer
|
touchEditorHost = Importer
|
||||||
if Importer and Importer.prepareOverlayHandoff then
|
if Importer and Importer.prepareOverlayHandoff then
|
||||||
Importer:prepareOverlayHandoff()
|
Importer:prepareOverlayHandoff()
|
||||||
end
|
end
|
||||||
Importer = nil
|
Importer = nil
|
||||||
TouchEditor = require("src.ui.TouchControlsEditor")
|
TouchEditor = require("src.ui.TouchControlsEditor")
|
||||||
TouchEditor.load({ onClose = function() closeTouchControlsEditor() end })
|
TouchEditor.load({
|
||||||
|
version = version,
|
||||||
|
onClose = function() closeTouchControlsEditor() end,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function closeTouchControlsEditor()
|
function closeTouchControlsEditor()
|
||||||
|
|||||||
@@ -1604,9 +1604,25 @@ function Battle:useMove(attacker, defender, moveId)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Damage that skips the formula entirely.
|
-- Damage that skips the formula entirely. The move's own power goes with
|
||||||
local fixed = Effects.fixedDamage(def.effect, attacker, defender, self.random)
|
-- it: EFFECT_STATIC_DAMAGE's arm of BattleCommand_ConstantDamage reads
|
||||||
|
-- BATTLE_VARS_MOVE_POWER as the damage (effect_commands.asm:3157-3161).
|
||||||
|
local fixed = Effects.fixedDamage(def.effect, attacker, defender, self.random,
|
||||||
|
def.power)
|
||||||
if fixed then
|
if fixed then
|
||||||
|
-- The constant-damage effect list carries `resettypematchup` instead of
|
||||||
|
-- `stab`, and that command misses the move outright when the matchup byte
|
||||||
|
-- is 0 (effect_commands.asm:1480-1493) -- an immune target is the one
|
||||||
|
-- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG.
|
||||||
|
local defenderTypes = (self:speciesDef(defender) or {}).types
|
||||||
|
or defender.types
|
||||||
|
local matchups = self.data.type_chart and self.data.type_chart.matchups
|
||||||
|
if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then
|
||||||
|
self:markMissed()
|
||||||
|
self:emit({ kind = "message",
|
||||||
|
text = "It doesn't affect " .. self:monName(defender) .. "..." })
|
||||||
|
return
|
||||||
|
end
|
||||||
self:dealDamage(attacker, defender, fixed, { move = def, moveId = moveId })
|
self:dealDamage(attacker, defender, fixed, { move = def, moveId = moveId })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -179,9 +179,9 @@ end
|
|||||||
|
|
||||||
-- --------------------------------------------------------------- fixed damage
|
-- --------------------------------------------------------------- fixed damage
|
||||||
|
|
||||||
-- BattleCommand_LevelDamage / SuperFang / Psywave, all of which skip the
|
-- BattleCommand_ConstantDamage (engine/battle/effect_commands.asm:3131-3205),
|
||||||
-- damage formula entirely.
|
-- one command for the whole SuperFang / Psywave / StaticDamage list.
|
||||||
function Effects.fixedDamage(effect, attacker, defender, random)
|
function Effects.fixedDamage(effect, attacker, defender, random, power)
|
||||||
if effect == "EFFECT_LEVEL_DAMAGE" then
|
if effect == "EFFECT_LEVEL_DAMAGE" then
|
||||||
return math.max(1, attacker.level or 1)
|
return math.max(1, attacker.level or 1)
|
||||||
end
|
end
|
||||||
@@ -189,9 +189,17 @@ function Effects.fixedDamage(effect, attacker, defender, random)
|
|||||||
return math.max(1, math.floor((defender.hp or 1) / 2))
|
return math.max(1, math.floor((defender.hp or 1) / 2))
|
||||||
end
|
end
|
||||||
if effect == "EFFECT_PSYWAVE" then
|
if effect == "EFFECT_PSYWAVE" then
|
||||||
-- 1..(level * 1.5), rerolled until it is in range; one roll is enough here.
|
-- .psywave rerolls until the byte is nonzero AND below level * 1.5, so the
|
||||||
local ceiling = math.max(1, math.floor((attacker.level or 1) * 3 / 2))
|
-- top of the range is that ceiling minus one (effect_commands.asm:3163).
|
||||||
return math.max(1, (random and random(ceiling) or 0) + 1)
|
local ceiling = math.max(2, math.floor((attacker.level or 1) * 3 / 2))
|
||||||
|
return math.max(1, (random and random(ceiling - 1) or 0) + 1)
|
||||||
|
end
|
||||||
|
-- SONIC BOOM and DRAGON RAGE share EFFECT_STATIC_DAMAGE, whose arm reads
|
||||||
|
-- BATTLE_VARS_MOVE_POWER straight into the damage word: their stored power
|
||||||
|
-- (20 and 40) IS the damage, never a formula input
|
||||||
|
-- (effect_commands.asm:3157-3161).
|
||||||
|
if effect == "EFFECT_STATIC_DAMAGE" then
|
||||||
|
return math.max(1, math.floor(power or 0))
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|||||||
+26
-3
@@ -818,9 +818,19 @@ end
|
|||||||
function SaveData.slotSummary(save)
|
function SaveData.slotSummary(save)
|
||||||
if type(save) ~= "table" then return nil, nil end
|
if type(save) ~= "table" then return nil, nil end
|
||||||
local name = save.player and save.player.name or nil
|
local name = save.player and save.player.name or nil
|
||||||
|
-- A Gen 2 slot carries no badge items and fills pokedex.caught, so both
|
||||||
|
-- counts come off wJohtoBadges/wPokedexCaught (engine/menus/intro_menu.asm:461).
|
||||||
|
local vinfo = type(save.version) == "string" and GameVersion.info(save.version)
|
||||||
|
local gen2 = save.generation == 2 or (vinfo and vinfo.generation == 2) or false
|
||||||
local dexCount = 0
|
local dexCount = 0
|
||||||
for _ in pairs((save.pokedex and save.pokedex.owned) or {}) do
|
if gen2 then
|
||||||
dexCount = dexCount + 1
|
for _, has in pairs((save.pokedex and save.pokedex.caught) or {}) do
|
||||||
|
if has then dexCount = dexCount + 1 end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _ in pairs((save.pokedex and save.pokedex.owned) or {}) do
|
||||||
|
dexCount = dexCount + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
-- playTime is a plain seconds count in a Gen 1 save but a
|
-- playTime is a plain seconds count in a Gen 1 save but a
|
||||||
-- { hours, minutes, seconds, frames } table in a Gen 2 (Gold) save, matching
|
-- { hours, minutes, seconds, frames } table in a Gen 2 (Gold) save, matching
|
||||||
@@ -838,8 +848,21 @@ function SaveData.slotSummary(save)
|
|||||||
end
|
end
|
||||||
local timeText = ("%d:%02d"):format(math.floor(t / 3600),
|
local timeText = ("%d:%02d"):format(math.floor(t / 3600),
|
||||||
math.floor(t / 60) % 60)
|
math.floor(t / 60) % 60)
|
||||||
|
local badges
|
||||||
|
if gen2 then
|
||||||
|
-- Continue_DisplayBadgeCount walks TWO bytes, Johto then Kanto
|
||||||
|
-- (engine/menus/intro_menu.asm:461-469).
|
||||||
|
badges = 0
|
||||||
|
local p = save.player or {}
|
||||||
|
for _, has in pairs(p.badges or {}) do if has then badges = badges + 1 end end
|
||||||
|
for _, has in pairs(p.kantoBadges or {}) do
|
||||||
|
if has then badges = badges + 1 end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
badges = Badges.count(nil, save)
|
||||||
|
end
|
||||||
return name, {
|
return name, {
|
||||||
badges = Badges.count(nil, save),
|
badges = badges,
|
||||||
timeText = timeText,
|
timeText = timeText,
|
||||||
dexCount = dexCount,
|
dexCount = dexCount,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -764,9 +764,14 @@ end
|
|||||||
function Save.summary(save)
|
function Save.summary(save)
|
||||||
if type(save) ~= "table" then return nil end
|
if type(save) ~= "table" then return nil end
|
||||||
local badges = 0
|
local badges = 0
|
||||||
|
-- Continue_DisplayBadgeCount counts TWO bytes, wJohtoBadges then wKantoBadges
|
||||||
|
-- (engine/menus/intro_menu.asm:461-469).
|
||||||
for _, has in pairs(save.player and save.player.badges or {}) do
|
for _, has in pairs(save.player and save.player.badges or {}) do
|
||||||
if has then badges = badges + 1 end
|
if has then badges = badges + 1 end
|
||||||
end
|
end
|
||||||
|
for _, has in pairs(save.player and save.player.kantoBadges or {}) do
|
||||||
|
if has then badges = badges + 1 end
|
||||||
|
end
|
||||||
local caught = 0
|
local caught = 0
|
||||||
for _, has in pairs(save.pokedex and save.pokedex.caught or {}) do
|
for _, has in pairs(save.pokedex and save.pokedex.caught or {}) do
|
||||||
if has then caught = caught + 1 end
|
if has then caught = caught + 1 end
|
||||||
|
|||||||
@@ -62,6 +62,68 @@ local FILTERS = { "OFF", "1X", "2X", "3X" }
|
|||||||
-- The core rows. Helper modules are required lazily under pcall: they are
|
-- The core rows. Helper modules are required lazily under pcall: they are
|
||||||
-- pure label/cycle tables, but the launcher must never die because a render
|
-- pure label/cycle tables, but the launcher must never die because a render
|
||||||
-- module grew a dependency on live game data.
|
-- module grew a dependency on live game data.
|
||||||
|
-- TOUCH PAD, VIBRATION and the layout editor, shared by both row sets.
|
||||||
|
--
|
||||||
|
-- Gold reads these out of its own `gold` block (src/core/gen2/Save.lua:297),
|
||||||
|
-- so `opts` is whichever table the gear is editing and the rows never have to
|
||||||
|
-- know which game they belong to. #1100 / #1135: the Gold gear carried none
|
||||||
|
-- of them, so a phone player could turn the pad and the buzz off in Red and
|
||||||
|
-- had no way to reach either in Gold.
|
||||||
|
local function addTouchRows(rows, add, opts, hooks)
|
||||||
|
-- TOUCH PAD only where the overlay can appear, mirroring OptionsMenu's
|
||||||
|
-- gate (mobile, or desktop forced by POKEPORT_TOUCH=1).
|
||||||
|
local env = os.getenv("POKEPORT_TOUCH")
|
||||||
|
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||||
|
local show = env == "1"
|
||||||
|
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
|
||||||
|
if show then
|
||||||
|
add(Strings("TOUCH PAD"),
|
||||||
|
function()
|
||||||
|
local tc = opts.touchControls
|
||||||
|
local on = not (type(tc) == "table" and tc.enabled == false)
|
||||||
|
return on and Strings("ON") or Strings("OFF")
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local tc = type(opts.touchControls) == "table" and opts.touchControls or {}
|
||||||
|
tc.enabled = tc.enabled == false
|
||||||
|
opts.touchControls = tc
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
-- VIBRATION sits with it (#806): same gate, same subsystem. Stepping
|
||||||
|
-- the row buzzes once at the level being selected.
|
||||||
|
local okTC, TC = pcall(require, "src.core.TouchControls")
|
||||||
|
if okTC then
|
||||||
|
add(Strings("VIBRATION"),
|
||||||
|
function() return Strings(TC.hapticLabel(opts.haptics)) end,
|
||||||
|
function(dir)
|
||||||
|
opts.haptics = TC.cycleHaptics(opts.haptics, dir)
|
||||||
|
TC.buzz(opts.haptics)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TOUCH CONTROLS, the on-screen pad's layout editor. It used to be a
|
||||||
|
-- button on the game panel, once per game -- but the overlay layout is
|
||||||
|
-- global (options.touchControls.layouts), so three tabs offered three
|
||||||
|
-- buttons that edited the same thing while crowding the column that has to
|
||||||
|
-- hold Play. It belongs with the other control rows, behind the gear.
|
||||||
|
-- The host owns the editor screen, so the row only fires when a hook was
|
||||||
|
-- supplied (the standalone save editor opens this model with none).
|
||||||
|
if hooks and hooks.editTouchControls then
|
||||||
|
rows[#rows + 1] = {
|
||||||
|
label = Strings("TOUCH CONTROLS"),
|
||||||
|
actionLabel = Strings("Edit"),
|
||||||
|
action = function()
|
||||||
|
hooks.editTouchControls()
|
||||||
|
-- The editor replaces the whole screen: nothing left to persist here
|
||||||
|
-- beyond what the caller already saved on the way out.
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function coreRows(opts, hooks)
|
local function coreRows(opts, hooks)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local function add(label, value, step)
|
local function add(label, value, step)
|
||||||
@@ -236,60 +298,7 @@ local function coreRows(opts, hooks)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TOUCH PAD only where the overlay can appear, mirroring OptionsMenu's
|
addTouchRows(rows, add, opts, hooks)
|
||||||
-- gate (mobile, or desktop forced by POKEPORT_TOUCH=1).
|
|
||||||
do
|
|
||||||
local env = os.getenv("POKEPORT_TOUCH")
|
|
||||||
local osName = love.system and love.system.getOS and love.system.getOS()
|
|
||||||
local show = env == "1"
|
|
||||||
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
|
|
||||||
if show then
|
|
||||||
add(Strings("TOUCH PAD"),
|
|
||||||
function()
|
|
||||||
local tc = opts.touchControls
|
|
||||||
local on = not (type(tc) == "table" and tc.enabled == false)
|
|
||||||
return on and Strings("ON") or Strings("OFF")
|
|
||||||
end,
|
|
||||||
function()
|
|
||||||
local tc = type(opts.touchControls) == "table" and opts.touchControls or {}
|
|
||||||
tc.enabled = tc.enabled == false
|
|
||||||
opts.touchControls = tc
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
-- VIBRATION sits with it (#806): same gate, same subsystem. Stepping
|
|
||||||
-- the row buzzes once at the level being selected.
|
|
||||||
local okTC, TC = pcall(require, "src.core.TouchControls")
|
|
||||||
if okTC then
|
|
||||||
add(Strings("VIBRATION"),
|
|
||||||
function() return Strings(TC.hapticLabel(opts.haptics)) end,
|
|
||||||
function(dir)
|
|
||||||
opts.haptics = TC.cycleHaptics(opts.haptics, dir)
|
|
||||||
TC.buzz(opts.haptics)
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- TOUCH CONTROLS, the on-screen pad's layout editor. It used to be a
|
|
||||||
-- button on the game panel, once per game -- but the overlay layout is
|
|
||||||
-- global (options.touchControls.layouts), so three tabs offered three
|
|
||||||
-- buttons that edited the same thing while crowding the column that has to
|
|
||||||
-- hold Play. It belongs with the other control rows, behind the gear.
|
|
||||||
-- The host owns the editor screen, so the row only fires when a hook was
|
|
||||||
-- supplied (the standalone save editor opens this model with none).
|
|
||||||
if hooks and hooks.editTouchControls then
|
|
||||||
rows[#rows + 1] = {
|
|
||||||
label = Strings("TOUCH CONTROLS"),
|
|
||||||
actionLabel = Strings("Edit"),
|
|
||||||
action = function()
|
|
||||||
hooks.editTouchControls()
|
|
||||||
-- The editor replaces the whole screen: nothing left to persist here
|
|
||||||
-- beyond what the caller already saved on the way out.
|
|
||||||
return false
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- RESET REBINDS, directly under the touch-pad row. Rebinds are additive
|
-- RESET REBINDS, directly under the touch-pad row. Rebinds are additive
|
||||||
-- (src/core/Input.lua:applyBindings layers options.bindings over the
|
-- (src/core/Input.lua:applyBindings layers options.bindings over the
|
||||||
@@ -462,7 +471,7 @@ end
|
|||||||
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
|
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
|
||||||
local GEN2_KEY = "gold"
|
local GEN2_KEY = "gold"
|
||||||
|
|
||||||
local function gen2Rows(opts)
|
local function gen2Rows(opts, hooks)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local function add(label, value, step)
|
local function add(label, value, step)
|
||||||
rows[#rows + 1] = { label = label, value = value, step = step }
|
rows[#rows + 1] = { label = label, value = value, step = step }
|
||||||
@@ -551,6 +560,8 @@ local function gen2Rows(opts)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
addTouchRows(rows, add, opts, hooks)
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -574,7 +585,7 @@ function LauncherSettings.open(hooks, version)
|
|||||||
opts[GEN2_KEY] = block
|
opts[GEN2_KEY] = block
|
||||||
end
|
end
|
||||||
sections = {
|
sections = {
|
||||||
{ title = Strings("OPTIONS"), rows = gen2Rows(block) },
|
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks) },
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
sections = {
|
sections = {
|
||||||
|
|||||||
@@ -2652,11 +2652,14 @@ function RomImporter:_openSettings()
|
|||||||
-- hook rather than reaching for main.lua's handler itself. Closing the
|
-- hook rather than reaching for main.lua's handler itself. Closing the
|
||||||
-- settings panel FIRST persists the pending edits (_closeSettings saves)
|
-- settings panel FIRST persists the pending edits (_closeSettings saves)
|
||||||
-- and leaves no modal behind the editor to return to.
|
-- and leaves no modal behind the editor to return to.
|
||||||
|
-- The tab rides along: the editor persists the layout into that game's own
|
||||||
|
-- option block, and Gold's is not the flat Gen 1 one (#1100).
|
||||||
local hooks = {}
|
local hooks = {}
|
||||||
if self.onEditTouchControls then
|
if self.onEditTouchControls then
|
||||||
|
local version = self.tab
|
||||||
hooks.editTouchControls = function()
|
hooks.editTouchControls = function()
|
||||||
self:_closeSettings()
|
self:_closeSettings()
|
||||||
self.onEditTouchControls()
|
self.onEditTouchControls(version)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- The tab the gear was opened on decides the row set: Gold reads a
|
-- The tab the gear was opened on decides the row set: Gold reads a
|
||||||
|
|||||||
+54
-11
@@ -106,6 +106,10 @@ local TEXT_ASK_FORGET_SLOT = Strings.source("Which move should\nbe forgotten?")
|
|||||||
local TEXT_CANT_FORGET_HM = Strings.source("HM moves can't be\nforgotten now.")
|
local TEXT_CANT_FORGET_HM = Strings.source("HM moves can't be\nforgotten now.")
|
||||||
local TEXT_STOP_LEARNING = Strings.source("Stop learning\n%s?")
|
local TEXT_STOP_LEARNING = Strings.source("Stop learning\n%s?")
|
||||||
|
|
||||||
|
-- BattleText_EnemyIsAboutToUseWillPlayerChangeMon (data/text/battle.asm:222-231).
|
||||||
|
local TEXT_ENEMY_ABOUT_TO_USE = Strings.source(
|
||||||
|
"%s\nis about to use\v%s.\fWill %s\nchange POKéMON?")
|
||||||
|
|
||||||
-- _AskForgetMoveText, all three paragraphs (data/text/common_3.asm:141-165).
|
-- _AskForgetMoveText, all three paragraphs (data/text/common_3.asm:141-165).
|
||||||
local TEXT_ASK_FORGET_MOVE = Strings.source(
|
local TEXT_ASK_FORGET_MOVE = Strings.source(
|
||||||
"%s is\ntrying to learn\v%s.\fBut %s\ncan't learn more\vthan four moves."
|
"%s is\ntrying to learn\v%s.\fBut %s\ncan't learn more\vthan four moves."
|
||||||
@@ -548,6 +552,14 @@ BattleState.PLAYER_PIC_TILES = 6
|
|||||||
-- box's own bottom centre.
|
-- box's own bottom centre.
|
||||||
local PIC_RESIZE_TILES = { [0] = 6, [1] = 4, [2] = 2, [3] = 7, [4] = 5, [5] = 3 }
|
local PIC_RESIZE_TILES = { [0] = 6, [1] = 4, [2] = 2, [3] = 7, [4] = 5, [5] = 3 }
|
||||||
|
|
||||||
|
-- SUBSTATUS_UNDERGROUND / SUBSTATUS_FLYING, which BattleCommand_Charge sets on
|
||||||
|
-- FLY and DIG only (engine/battle/effect_commands.asm:5478-5485); the port
|
||||||
|
-- carries both as the volatile `vanished` flag.
|
||||||
|
function BattleState.isVanished(mon)
|
||||||
|
local volatiles = mon and mon.volatile
|
||||||
|
return (volatiles and volatiles.vanished) and true or false
|
||||||
|
end
|
||||||
|
|
||||||
function BattleState:drawPic(mon, back)
|
function BattleState:drawPic(mon, back)
|
||||||
-- During the intro slide the player-side pic belongs to presentSlide's
|
-- During the intro slide the player-side pic belongs to presentSlide's
|
||||||
-- backpic overlay, not to the baked bands (see BattleAnimView).
|
-- backpic overlay, not to the baked bands (see BattleAnimView).
|
||||||
@@ -571,6 +583,13 @@ function BattleState:drawPic(mon, back)
|
|||||||
-- The box is empty either because the animation running right now has
|
-- The box is empty either because the animation running right now has
|
||||||
-- cleared it, or because the last one ENDED with it cleared (picHidden).
|
-- cleared it, or because the last one ENDED with it cleared (picHidden).
|
||||||
if (anim and anim.hidden) or self.picHidden[side] then return end
|
if (anim and anim.hidden) or self.picHidden[side] then return end
|
||||||
|
-- Mid FLY / DIG the box is empty: DisappearUser ClearBoxes it
|
||||||
|
-- (engine/battle/misc.asm:1-13), AppearUserRaiseSub puts it back on the
|
||||||
|
-- stored attack (engine/battle/effect_commands.asm:2113-2117).
|
||||||
|
if not (trainerBack or enemyTrainer) and BattleState.isVanished(mon)
|
||||||
|
and not (self.vanishAnim and self.vanishAnim == self.anim) then
|
||||||
|
return
|
||||||
|
end
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
local w, h = image:getDimensions()
|
local w, h = image:getDimensions()
|
||||||
local px, py
|
local px, py
|
||||||
@@ -582,10 +601,12 @@ function BattleState:drawPic(mon, back)
|
|||||||
py = BattleState.PLAYER_PIC_TILE_Y * 8 + (box - h)
|
py = BattleState.PLAYER_PIC_TILE_Y * 8 + (box - h)
|
||||||
boxTiles = BattleState.PLAYER_PIC_TILES
|
boxTiles = BattleState.PLAYER_PIC_TILES
|
||||||
else
|
else
|
||||||
-- Bottom-aligned and horizontally centred inside the 7x7 box.
|
-- PadFrontpic pads a short pic and never a long one
|
||||||
|
-- (engine/gfx/load_pics.asm:342-386), so an oversized mod pic pins to the
|
||||||
|
-- box's own corner at hlcoord 12, 0 rather than to a negative offset.
|
||||||
local box = BattleState.ENEMY_PIC_TILES * 8
|
local box = BattleState.ENEMY_PIC_TILES * 8
|
||||||
px = BattleState.ENEMY_PIC_TILE_X * 8 + math.floor((box - w) / 2)
|
px = BattleState.ENEMY_PIC_TILE_X * 8 + math.max(0, math.floor((box - w) / 2))
|
||||||
py = BattleState.ENEMY_PIC_TILE_Y * 8 + (box - h)
|
py = BattleState.ENEMY_PIC_TILE_Y * 8 + math.max(0, box - h)
|
||||||
boxTiles = BattleState.ENEMY_PIC_TILES
|
boxTiles = BattleState.ENEMY_PIC_TILES
|
||||||
end
|
end
|
||||||
-- One tile per two frames to the right, SlideBattlePicOut's own step.
|
-- One tile per two frames to the right, SlideBattlePicOut's own step.
|
||||||
@@ -903,11 +924,12 @@ function BattleState:startAnim(key, opts)
|
|||||||
param = opts.param or 0,
|
param = opts.param or 0,
|
||||||
sfxOrder = audio.sfxOrder,
|
sfxOrder = audio.sfxOrder,
|
||||||
ballPalette = opts.ballPalette,
|
ballPalette = opts.ballPalette,
|
||||||
|
-- BGEffect_CheckFlyDigStatus reads wPlayerSubStatus3 / wEnemySubStatus3
|
||||||
|
-- (engine/battle_anims/bg_effects.asm:2838-2851); the port keeps that bit
|
||||||
|
-- on the mon's volatile table, not on the mon itself.
|
||||||
flying = {
|
flying = {
|
||||||
player = self.battle and self.battle.player
|
player = BattleState.isVanished(self.battle and self.battle.player),
|
||||||
and self.battle.player.vanished or false,
|
enemy = BattleState.isVanished(self.battle and self.battle.enemy),
|
||||||
enemy = self.battle and self.battle.enemy
|
|
||||||
and self.battle.enemy.vanished or false,
|
|
||||||
},
|
},
|
||||||
hooks = {
|
hooks = {
|
||||||
-- anim_sound (engine/battle_anims/anim_commands.asm:1105) calls
|
-- anim_sound (engine/battle_anims/anim_commands.asm:1105) calls
|
||||||
@@ -1365,6 +1387,13 @@ function BattleState:advanceQueue()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- BattleCommand_Charge runs LoadMoveAnim BEFORE DisappearUser
|
||||||
|
-- (engine/battle/effect_commands.asm:5459-5470), so FLY / DIG still draw
|
||||||
|
-- the take-off or the burrow on the turn the substatus goes up; the box
|
||||||
|
-- only empties once THIS animation is done with.
|
||||||
|
if BattleState.isVanished(self:activeMon(event.side)) then
|
||||||
|
self.vanishAnim = self.anim
|
||||||
|
end
|
||||||
elseif event.kind == "damage" and event.side then
|
elseif event.kind == "damage" and event.side then
|
||||||
-- ANIM_x_DAMAGE is the MOVE's after-anim (effect_commands.asm:1963-1972),
|
-- ANIM_x_DAMAGE is the MOVE's after-anim (effect_commands.asm:1963-1972),
|
||||||
-- so only a move hit gets it; `animMove` is HandleWrap's (core.asm:1198-1203).
|
-- so only a move hit gets it; `animMove` is HandleWrap's (core.asm:1198-1203).
|
||||||
@@ -1864,6 +1893,20 @@ function BattleState:update(_dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The prompt's earlier pages: PlaceYesNoBox only follows the LAST one
|
||||||
|
-- (engine/battle/core.asm:3302-3305), so the mon is named and read first.
|
||||||
|
if self.phase == "shift-intro" then
|
||||||
|
if self.messageTimer > 0 then
|
||||||
|
if input:wasPressed("a") or input:wasPressed("b") then
|
||||||
|
self.messageTimer = 0
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:nextPage()
|
||||||
|
if not self.messagePages then self.phase = "ask-shift" end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- OfferSwitch's YesNoBox: YES opens PickSwitchMonInBattle, NO (and B) falls
|
-- OfferSwitch's YesNoBox: YES opens PickSwitchMonInBattle, NO (and B) falls
|
||||||
-- straight through to the enemy's send-out (engine/battle/core.asm:3305-3310).
|
-- straight through to the enemy's send-out (engine/battle/core.asm:3305-3310).
|
||||||
if self.phase == "ask-shift" then
|
if self.phase == "ask-shift" then
|
||||||
@@ -2464,13 +2507,13 @@ end
|
|||||||
-- (engine/battle/core.asm:3298-3304, data/text/battle.asm:222-231).
|
-- (engine/battle/core.asm:3298-3304, data/text/battle.asm:222-231).
|
||||||
function BattleState:offerShiftSwitch(mon)
|
function BattleState:offerShiftSwitch(mon)
|
||||||
self.shiftIndex = 1
|
self.shiftIndex = 1
|
||||||
self.phase = "ask-shift"
|
|
||||||
local trainer = (self.battle.trainer and self.battle.trainer.name) or "Foe"
|
local trainer = (self.battle.trainer and self.battle.trainer.name) or "Foe"
|
||||||
local player = (self.save and self.save.player and self.save.player.name)
|
local player = (self.save and self.save.player and self.save.player.name)
|
||||||
or "GOLD"
|
or "GOLD"
|
||||||
self.message = trainer .. " is about to use " .. self:name(mon)
|
-- The `para` splits this in two (data/text/battle.asm:222-231): the incoming
|
||||||
.. ". Will " .. player .. " change POKéMON?"
|
-- mon is NAMED on its own page, and only the second carries the yes/no box.
|
||||||
self.messageTimer = MESSAGE_FRAMES
|
self:showPages(Strings(TEXT_ENEMY_ABOUT_TO_USE, trainer, self:name(mon), player))
|
||||||
|
self.phase = self.messagePages and "shift-intro" or "ask-shift"
|
||||||
end
|
end
|
||||||
|
|
||||||
function BattleState:answerUseNextMon(yes)
|
function BattleState:answerUseNextMon(yes)
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
-- #1091: does GAME SPEED change the wild encounter rate per STEP?
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1091_test.lua love .
|
||||||
|
local Encounter = require("src.battle.gen2.Encounter")
|
||||||
|
local Player = require("src.world.gen2.Player")
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local rolls, hits, steps = 0, 0, 0
|
||||||
|
|
||||||
|
local realTriggers = Encounter.triggers
|
||||||
|
Encounter.triggers = function(rate, random)
|
||||||
|
rolls = rolls + 1
|
||||||
|
if realTriggers(rate, random) then hits = hits + 1 end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local World = require("src.world.gen2.World")
|
||||||
|
local ticks = 0
|
||||||
|
local realWorldStep = World.step
|
||||||
|
World.step = function(self)
|
||||||
|
ticks = ticks + 1
|
||||||
|
return realWorldStep(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
local realUpdate = Player.update
|
||||||
|
Player.update = function(self)
|
||||||
|
local landed = realUpdate(self)
|
||||||
|
if landed then steps = steps + 1 end
|
||||||
|
return landed
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wait(n) for _ = 1, n do coroutine.yield() end end
|
||||||
|
local function clearDirs()
|
||||||
|
for _, d in ipairs({ "up", "down", "left", "right" }) do
|
||||||
|
game.input.state[d] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(30)
|
||||||
|
local world = game.world
|
||||||
|
game.save.party = { Mon.new(game.data, "PIDGEY", 5) }
|
||||||
|
|
||||||
|
local function measure(speed, yields)
|
||||||
|
world:setMap("ROUTE_29", 44, 12, "right")
|
||||||
|
wait(20)
|
||||||
|
world.wildCooldown = 0
|
||||||
|
game.speedOverride = speed
|
||||||
|
rolls, hits, steps, ticks = 0, 0, 0, 0
|
||||||
|
local dir = "right"
|
||||||
|
for i = 1, yields do
|
||||||
|
if i % 8 == 0 then dir = (dir == "right") and "left" or "right" end
|
||||||
|
clearDirs()
|
||||||
|
game.input.state[dir] = true
|
||||||
|
table.insert(game.input.pressQueue, dir)
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
clearDirs()
|
||||||
|
game.speedOverride = 1
|
||||||
|
wait(5)
|
||||||
|
return rolls, hits, steps, ticks
|
||||||
|
end
|
||||||
|
|
||||||
|
local r1, h1, s1, t1 = measure(1, 900)
|
||||||
|
local r2, h2, s2, t2 = measure(200, 60)
|
||||||
|
print(("[driver] 1X frames=%d cells=%d rolls=%d hits=%d rolls/cell=%.3f")
|
||||||
|
:format(t1, s1, r1, h1, s1 > 0 and r1 / s1 or 0))
|
||||||
|
print(("[driver] 200X frames=%d cells=%d rolls=%d hits=%d rolls/cell=%.3f")
|
||||||
|
:format(t2, s2, r2, h2, s2 > 0 and r2 / s2 or 0))
|
||||||
|
|
||||||
|
Encounter.triggers = realTriggers
|
||||||
|
Player.update = realUpdate
|
||||||
|
World.step = realWorldStep
|
||||||
|
local rate1 = s1 > 0 and r1 / s1 or 0
|
||||||
|
local rate2 = s2 > 0 and r2 / s2 or 0
|
||||||
|
assert(s1 > 20 and s2 > 20, "not enough walked cells to judge")
|
||||||
|
assert(math.abs(rate1 - rate2) < 0.05,
|
||||||
|
("rolls per cell changed with GAME SPEED: %.3f vs %.3f"):format(rate1, rate2))
|
||||||
|
print("[driver] PASS encounter rolls are one per walked cell at every speed")
|
||||||
|
end
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
-- #1097 (and the absorbed #1116): walking out of Dark Cave and pressing UP
|
||||||
|
-- puts the player inside the mountain on Route 31.
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1097_test.lua love .
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local function wait(n) for _ = 1, n do coroutine.yield() end end
|
||||||
|
local function clearDirs()
|
||||||
|
for _, d in ipairs({ "up", "down", "left", "right" }) do
|
||||||
|
game.input.state[d] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function hold(dir, frames, trace)
|
||||||
|
local last
|
||||||
|
for _ = 1, frames do
|
||||||
|
clearDirs()
|
||||||
|
game.input.state[dir] = true
|
||||||
|
table.insert(game.input.pressQueue, dir)
|
||||||
|
coroutine.yield()
|
||||||
|
local w = game.world
|
||||||
|
local at = ("%s (%d,%d)"):format(w.map.id, w.player.cellX, w.player.cellY)
|
||||||
|
if trace and at ~= last then print(" " .. at); last = at end
|
||||||
|
end
|
||||||
|
clearDirs()
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(30)
|
||||||
|
local world = game.world
|
||||||
|
game.save.party = { Mon.new(game.data, "PIDGEY", 5) }
|
||||||
|
|
||||||
|
-- Route31CheckMomCallCallback (maps/Route31.asm:14) fires on every NEWMAP
|
||||||
|
-- arrival until the errand is over; set its event so the walk is not spent
|
||||||
|
-- behind a phone call. tests/drivers/gold/flag_names.lua:1132.
|
||||||
|
if world.events then world.events:set(64, true) end
|
||||||
|
|
||||||
|
-- maps/DarkCaveVioletEntrance.asm warp 1 is (3,15) -> ROUTE_31 warp 3.
|
||||||
|
world:setMap("DARK_CAVE_VIOLET_ENTRANCE", 3, 14, "down")
|
||||||
|
wait(20)
|
||||||
|
hold("down", 40)
|
||||||
|
-- the ROUTE_31 arrival runs Route31CheckMomCallCallback (maps/Route31.asm:14)
|
||||||
|
for _ = 1, 900 do
|
||||||
|
if not world:busy() then break end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
wait(20)
|
||||||
|
print(("[driver] out of the cave at %s (%d,%d)")
|
||||||
|
:format(world.map.id, world.player.cellX, world.player.cellY))
|
||||||
|
assert(world.map.id == "ROUTE_31", "did not reach ROUTE_31")
|
||||||
|
|
||||||
|
print("[driver] holding UP:")
|
||||||
|
hold("up", 120, true)
|
||||||
|
wait(30)
|
||||||
|
local m, x, y = world.map.id, world.player.cellX, world.player.cellY
|
||||||
|
print(("[driver] ended at %s (%d,%d)"):format(m, x, y))
|
||||||
|
-- Anything above y=5 on Route 31 in that column is the inside of the
|
||||||
|
-- mountain; the honest outcomes are re-entering the cave or bumping.
|
||||||
|
local inside = (m == "ROUTE_31" and y < 5)
|
||||||
|
assert(not inside, ("walked into the mountain at (%d,%d)"):format(x, y))
|
||||||
|
-- The other two Dark Cave mouths, same shape: maps/Route46.asm warp 3 is
|
||||||
|
-- (14,5) and maps/Route45.asm warp 1 is (2,5), both COLL_CAVE tiles with
|
||||||
|
-- ordinary FLOOR above them in the collision data.
|
||||||
|
for _, c in ipairs({ { "ROUTE_46", 14, 5 }, { "ROUTE_45", 2, 5 } }) do
|
||||||
|
local mapId, wx, wy = c[1], c[2], c[3]
|
||||||
|
world:setMap(mapId, wx, wy, "up")
|
||||||
|
wait(20)
|
||||||
|
hold("up", 90)
|
||||||
|
wait(20)
|
||||||
|
if world.map.id == mapId then
|
||||||
|
assert(world.player.cellY >= wy,
|
||||||
|
("%s: walked above the cave mouth to (%d,%d)")
|
||||||
|
:format(mapId, world.player.cellX, world.player.cellY))
|
||||||
|
end
|
||||||
|
print(("[driver] %s mouth ended at %s (%d,%d)"):format(
|
||||||
|
mapId, world.map.id, world.player.cellX, world.player.cellY))
|
||||||
|
end
|
||||||
|
|
||||||
|
print("[driver] PASS the cave mouth cannot be walked through")
|
||||||
|
end
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
-- #1104: the Ruins of Alph ladders warp somewhere else.
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1104_test.lua love .
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
-- maps/RuinsOfAlphInnerChamber.asm:76, maps/RuinsOfAlphOutside.asm:126,
|
||||||
|
-- maps/RuinsOfAlphKabutoChamber.asm and its three siblings.
|
||||||
|
local CASES = {
|
||||||
|
{ "RUINS_OF_ALPH_INNER_CHAMBER", 10, 13, "RUINS_OF_ALPH_OUTSIDE", 10, 13 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 10, 13, "RUINS_OF_ALPH_INNER_CHAMBER", 10, 13 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 2, 17, "RUINS_OF_ALPH_HO_OH_CHAMBER", 3, 9 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 14, 7, "RUINS_OF_ALPH_KABUTO_CHAMBER", 3, 9 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 2, 29, "RUINS_OF_ALPH_OMANYTE_CHAMBER", 3, 9 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 16, 33, "RUINS_OF_ALPH_AERODACTYL_CHAMBER", 3, 9 },
|
||||||
|
{ "RUINS_OF_ALPH_OUTSIDE", 17, 11, "RUINS_OF_ALPH_RESEARCH_CENTER", 2, 7 },
|
||||||
|
{ "RUINS_OF_ALPH_KABUTO_CHAMBER", 3, 9, "RUINS_OF_ALPH_OUTSIDE", 14, 7, "down" },
|
||||||
|
{ "RUINS_OF_ALPH_HO_OH_CHAMBER", 3, 9, "RUINS_OF_ALPH_OUTSIDE", 2, 17, "down" },
|
||||||
|
{ "RUINS_OF_ALPH_OMANYTE_CHAMBER", 3, 9, "RUINS_OF_ALPH_OUTSIDE", 2, 29, "down" },
|
||||||
|
{ "RUINS_OF_ALPH_AERODACTYL_CHAMBER", 3, 9, "RUINS_OF_ALPH_OUTSIDE", 16, 33, "down" },
|
||||||
|
{ "RUINS_OF_ALPH_RESEARCH_CENTER", 2, 7, "RUINS_OF_ALPH_OUTSIDE", 17, 11, "down" },
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local function wait(n) for _ = 1, n do coroutine.yield() end end
|
||||||
|
local function clearDirs()
|
||||||
|
for _, d in ipairs({ "up", "down", "left", "right" }) do
|
||||||
|
game.input.state[d] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Hold until the map changes, then let the load settle and STOP: holding
|
||||||
|
-- past the arrival would walk the player off the landing tile.
|
||||||
|
local function walkUntilWarp(dir, frames, fromMap)
|
||||||
|
for _ = 1, frames do
|
||||||
|
clearDirs()
|
||||||
|
game.input.state[dir] = true
|
||||||
|
table.insert(game.input.pressQueue, dir)
|
||||||
|
coroutine.yield()
|
||||||
|
if game.world.map and game.world.map.id ~= fromMap then
|
||||||
|
clearDirs()
|
||||||
|
-- The ARRIVAL cell is the one the warp table names. Standing on a
|
||||||
|
-- door/cave/staircase tile then forces one step DOWN off it
|
||||||
|
-- (engine/overworld/player_movement.asm:201-213), so the settled cell
|
||||||
|
-- is legitimately one row lower on an outdoor mouth.
|
||||||
|
local ax, ay = game.world.player.cellX, game.world.player.cellY
|
||||||
|
for _ = 1, 45 do coroutine.yield() end
|
||||||
|
return game.world.map.id, ax, ay,
|
||||||
|
game.world.player.cellX, game.world.player.cellY
|
||||||
|
end
|
||||||
|
end
|
||||||
|
clearDirs()
|
||||||
|
return game.world.map and game.world.map.id, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(30)
|
||||||
|
local world = game.world
|
||||||
|
game.save.party = { Mon.new(game.data, "PIDGEY", 5) }
|
||||||
|
|
||||||
|
local bad = 0
|
||||||
|
for _, c in ipairs(CASES) do
|
||||||
|
local src, wx, wy, wantMap, wantX, wantY = c[1], c[2], c[3], c[4], c[5], c[6]
|
||||||
|
-- default approach is from below, the way the report describes facing the
|
||||||
|
-- ladder from below; a chamber's exit sits on the bottom row instead
|
||||||
|
local dir = c[7] or "up"
|
||||||
|
local sy = (dir == "up") and wy + 1 or wy - 1
|
||||||
|
world:setMap(src, wx, sy, dir)
|
||||||
|
wait(12)
|
||||||
|
local from = ("%s (%d,%d)"):format(src, world.player.cellX, world.player.cellY)
|
||||||
|
local m, ax, ay, sx, sy = walkUntilWarp(dir, 90, src)
|
||||||
|
local ok = (m == wantMap and ax == wantX and ay == wantY)
|
||||||
|
if not ok then bad = bad + 1 end
|
||||||
|
print(("[driver] %-12s %s -> %s (%s,%s) settled (%s,%s) want %s (%d,%d)")
|
||||||
|
:format(ok and "OK" or "MISMATCH", from, tostring(m),
|
||||||
|
tostring(ax), tostring(ay), tostring(sx), tostring(sy),
|
||||||
|
wantMap, wantX, wantY))
|
||||||
|
end
|
||||||
|
print(("[driver] %s ruins warps: %d mismatches")
|
||||||
|
:format(bad == 0 and "PASS" or "FAIL", bad))
|
||||||
|
end
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
-- #1155: "MAGNITUDE always rolls a 4".
|
||||||
|
--
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1155_test.lua love .
|
||||||
|
local U = require("tests.drivers.util")
|
||||||
|
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
local ROLLS = 15
|
||||||
|
|
||||||
|
local function battleScreen(game)
|
||||||
|
for _ = 1, 900 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top.battle then return top end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
error("battle screen never came up")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drain(game, screen, frames)
|
||||||
|
for _ = 1, (frames or 400) do
|
||||||
|
if screen.phase == "menu" and #screen.queue == 0 and not screen.anim then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1155"
|
||||||
|
U.wait(45)
|
||||||
|
local world = game.world
|
||||||
|
assert(world and world.map, "gold world did not boot")
|
||||||
|
|
||||||
|
local caster = Mon.new(game.data, "DUNSPARCE", 50)
|
||||||
|
local def = assert(game.data.moves.MAGNITUDE, "MAGNITUDE is not in moves.lua")
|
||||||
|
caster.moves = { { id = "MAGNITUDE", pp = def.pp, maxPp = def.pp } }
|
||||||
|
print("[driver] MAGNITUDE stored effect = " .. tostring(def.effect)
|
||||||
|
.. ", stored power = " .. tostring(def.power))
|
||||||
|
game.save.party = { caster }
|
||||||
|
|
||||||
|
local seen, damages, shown = {}, {}, 0
|
||||||
|
local screen
|
||||||
|
for i = 1, ROLLS do
|
||||||
|
if not (screen and game.stack:top() == screen and not screen.battle.over) then
|
||||||
|
local dummy = Mon.new(game.data, "SNORLAX", 60)
|
||||||
|
dummy.moves = { { id = "SPLASH", pp = 40, maxPp = 40 } }
|
||||||
|
assert(world:startBattle({ wild = dummy }), "startBattle failed")
|
||||||
|
screen = battleScreen(game)
|
||||||
|
drain(game, screen, 300)
|
||||||
|
end
|
||||||
|
local foe = screen.battle.enemy
|
||||||
|
local before = foe.hp
|
||||||
|
caster.moves[1].pp = def.pp
|
||||||
|
screen:submit({ kind = "move", move = "MAGNITUDE" })
|
||||||
|
for _, event in ipairs(screen.queue) do
|
||||||
|
local number = event.text and event.text:match("^Magnitude (%d+)")
|
||||||
|
if number then
|
||||||
|
seen[number] = (seen[number] or 0) + 1
|
||||||
|
if shown < 1 then
|
||||||
|
U.wait(1)
|
||||||
|
U.shot(game, out .. "/01-magnitude-text.png")
|
||||||
|
shown = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
drain(game, screen, 400)
|
||||||
|
local dealt = before - foe.hp
|
||||||
|
if dealt > 0 then damages[dealt] = true end
|
||||||
|
if i % 10 == 0 then U.wait(5) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local numbers = {}
|
||||||
|
for number, count in pairs(seen) do
|
||||||
|
numbers[#numbers + 1] = number .. "x" .. count
|
||||||
|
end
|
||||||
|
table.sort(numbers)
|
||||||
|
print("[driver] magnitudes over " .. ROLLS .. " uses: "
|
||||||
|
.. table.concat(numbers, " "))
|
||||||
|
local distinctPowers = 0
|
||||||
|
for _ in pairs(damages) do distinctPowers = distinctPowers + 1 end
|
||||||
|
|
||||||
|
local failures = {}
|
||||||
|
local function check(ok, what)
|
||||||
|
print((ok and "[ok] " or "[FAIL] ") .. what)
|
||||||
|
if not ok then failures[#failures + 1] = what end
|
||||||
|
end
|
||||||
|
check(#numbers > 1, "MAGNITUDE rolls more than one number")
|
||||||
|
check(seen["4"] == nil or seen["4"] < ROLLS,
|
||||||
|
"and is not stuck on MAGNITUDE 4")
|
||||||
|
check(distinctPowers > 1, "the rolled power reaches the damage (" ..
|
||||||
|
distinctPowers .. " distinct damage figures)")
|
||||||
|
|
||||||
|
if #failures > 0 then
|
||||||
|
error(#failures .. " magnitude checks failed")
|
||||||
|
end
|
||||||
|
print("[driver] #1155 clear; shot in " .. out)
|
||||||
|
end
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
-- #1157: SONIC BOOM has to deal a flat 20, not a rolled 8-10.
|
||||||
|
--
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1157_test.lua love .
|
||||||
|
|
||||||
|
local U = require("tests.drivers.util")
|
||||||
|
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
local function battleScreen(game)
|
||||||
|
for _ = 1, 900 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top.battle then return top end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
error("battle screen never came up")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drain(game, screen, frames)
|
||||||
|
for _ = 1, (frames or 400) do
|
||||||
|
if screen.phase == "menu" and #screen.queue == 0 and not screen.anim then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1157"
|
||||||
|
U.wait(45)
|
||||||
|
local world = game.world
|
||||||
|
assert(world and world.map, "gold world did not boot")
|
||||||
|
local failures = {}
|
||||||
|
local function check(ok, what)
|
||||||
|
print((ok and "[ok] " or "[FAIL] ") .. what)
|
||||||
|
if not ok then failures[#failures + 1] = what end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The reporter's own matchup, three times over.
|
||||||
|
local caster = Mon.new(game.data, "MAGNEMITE", 20)
|
||||||
|
local boom = assert(game.data.moves.SONICBOOM, "SONICBOOM missing")
|
||||||
|
print("[driver] SONICBOOM effect = " .. tostring(boom.effect)
|
||||||
|
.. ", stored power = " .. tostring(boom.power))
|
||||||
|
caster.moves = { { id = "SONICBOOM", pp = boom.pp, maxPp = boom.pp } }
|
||||||
|
game.save.party = { caster }
|
||||||
|
|
||||||
|
-- SONIC BOOM is a 90% move, so a miss (0) is legal and simply does not
|
||||||
|
-- count: what matters is that every use that CONNECTS is 20.
|
||||||
|
local dealt, hits, wrong = {}, 0, 0
|
||||||
|
for i = 1, 6 do
|
||||||
|
local rattata = Mon.new(game.data, "RATTATA", 11)
|
||||||
|
rattata.moves = { { id = "SPLASH", pp = 40, maxPp = 40 } }
|
||||||
|
assert(world:startBattle({ wild = rattata }), "startBattle failed")
|
||||||
|
local screen = battleScreen(game)
|
||||||
|
drain(game, screen, 300)
|
||||||
|
local before = rattata.hp
|
||||||
|
caster.hp = caster.maxHp or caster.hp
|
||||||
|
caster.moves[1].pp = boom.pp
|
||||||
|
screen:submit({ kind = "move", move = "SONICBOOM" })
|
||||||
|
drain(game, screen, 400)
|
||||||
|
dealt[i] = before - rattata.hp
|
||||||
|
if dealt[i] > 0 then
|
||||||
|
hits = hits + 1
|
||||||
|
if dealt[i] ~= 20 then wrong = wrong + 1 end
|
||||||
|
end
|
||||||
|
if i == 1 then U.shot(game, out .. "/01-sonicboom.png") end
|
||||||
|
for _ = 1, 400 do
|
||||||
|
if not game.stack:top() or not game.stack:top().battle then break end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
U.wait(45)
|
||||||
|
end
|
||||||
|
print("[driver] SONIC BOOM dealt " .. table.concat(dealt, ", ")
|
||||||
|
.. " (0 is a miss)")
|
||||||
|
check(hits >= 3, "SONIC BOOM connected at least three times")
|
||||||
|
check(wrong == 0, "and every hit was exactly 20")
|
||||||
|
|
||||||
|
-- The two siblings on the same command.
|
||||||
|
local dragon = Mon.new(game.data, "DRATINI", 25)
|
||||||
|
local rage = assert(game.data.moves.DRAGON_RAGE, "DRAGON_RAGE missing")
|
||||||
|
local toss = assert(game.data.moves.SEISMIC_TOSS, "SEISMIC_TOSS missing")
|
||||||
|
dragon.moves = {
|
||||||
|
{ id = "DRAGON_RAGE", pp = rage.pp, maxPp = rage.pp },
|
||||||
|
{ id = "SEISMIC_TOSS", pp = toss.pp, maxPp = toss.pp },
|
||||||
|
}
|
||||||
|
game.save.party = { dragon }
|
||||||
|
local target = Mon.new(game.data, "SNORLAX", 60)
|
||||||
|
target.moves = { { id = "SPLASH", pp = 40, maxPp = 40 } }
|
||||||
|
assert(world:startBattle({ wild = target }), "startBattle failed")
|
||||||
|
local screen = battleScreen(game)
|
||||||
|
drain(game, screen, 300)
|
||||||
|
local before = target.hp
|
||||||
|
screen:submit({ kind = "move", move = "DRAGON_RAGE" })
|
||||||
|
drain(game, screen, 400)
|
||||||
|
local rageDealt = before - target.hp
|
||||||
|
before = target.hp
|
||||||
|
screen:submit({ kind = "move", move = "SEISMIC_TOSS" })
|
||||||
|
drain(game, screen, 400)
|
||||||
|
local tossDealt = before - target.hp
|
||||||
|
U.shot(game, out .. "/02-siblings.png")
|
||||||
|
print(("[driver] DRAGON RAGE %d, SEISMIC TOSS %d (user is L%d)")
|
||||||
|
:format(rageDealt, tossDealt, dragon.level))
|
||||||
|
check(rageDealt == 40, "DRAGON RAGE deals exactly 40")
|
||||||
|
check(tossDealt == dragon.level, "SEISMIC TOSS deals the user's level")
|
||||||
|
|
||||||
|
if #failures > 0 then
|
||||||
|
error(#failures .. " constant-damage checks failed")
|
||||||
|
end
|
||||||
|
print("[driver] #1157 fixed; shots in " .. out)
|
||||||
|
end
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
-- #1158: you could not see the trainer's incoming mon before the switch offer.
|
||||||
|
--
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1158_test.lua love .
|
||||||
|
|
||||||
|
local U = require("tests.drivers.util")
|
||||||
|
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
|
||||||
|
local function battleScreen(game)
|
||||||
|
for _ = 1, 900 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top.battle then return top end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
error("battle screen never came up")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runToPhase(game, screen, phase, frames)
|
||||||
|
for _ = 1, (frames or 600) do
|
||||||
|
if screen.phase == phase then return true end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1158"
|
||||||
|
U.wait(45)
|
||||||
|
local world = game.world
|
||||||
|
assert(world and world.map, "gold world did not boot")
|
||||||
|
local failures = {}
|
||||||
|
local function check(ok, what)
|
||||||
|
print((ok and "[ok] " or "[FAIL] ") .. what)
|
||||||
|
if not ok then failures[#failures + 1] = what end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- SHIFT is the only style that offers at all (CheckWhetherToAskSwitch).
|
||||||
|
game.options = game.options or {}
|
||||||
|
game.options.battleStyle = "SHIFT"
|
||||||
|
|
||||||
|
local lead = Mon.new(game.data, "TYPHLOSION", 40)
|
||||||
|
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||||
|
local bench = Mon.new(game.data, "FERALIGATR", 40)
|
||||||
|
bench.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||||
|
game.save.party = { lead, bench }
|
||||||
|
|
||||||
|
local foe1 = Mon.new(game.data, "PIDGEY", 5)
|
||||||
|
foe1.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||||
|
local foe2 = Mon.new(game.data, "PIDGEOTTO", 20)
|
||||||
|
foe2.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||||
|
assert(world:startBattle({ trainer = { class = "FALKNER", name = "FALKNER",
|
||||||
|
party = { foe1, foe2 } } }), "trainer startBattle failed")
|
||||||
|
local screen = battleScreen(game)
|
||||||
|
runToPhase(game, screen, "menu", 400)
|
||||||
|
|
||||||
|
foe1.hp = 1
|
||||||
|
screen:submit({ kind = "move", move = "TACKLE" })
|
||||||
|
|
||||||
|
-- Page by page, collecting what the box actually showed.
|
||||||
|
local pages, sawBoxWithName = {}, false
|
||||||
|
local incoming = foe2.nickname or foe2.species or "PIDGEOTTO"
|
||||||
|
for _ = 1, 900 do
|
||||||
|
local message = screen.message
|
||||||
|
if message and pages[#pages] ~= message
|
||||||
|
and (screen.phase == "shift-intro" or screen.phase == "ask-shift") then
|
||||||
|
pages[#pages + 1] = message
|
||||||
|
if #pages == 1 then U.shot(game, out .. "/01-first-page.png") end
|
||||||
|
if message:find(incoming, 1, true) then
|
||||||
|
U.shot(game, out .. "/02-names-the-mon.png")
|
||||||
|
sawBoxWithName = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if screen.phase == "ask-shift" and (screen.messageTimer or 0) <= 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(4)
|
||||||
|
end
|
||||||
|
U.shot(game, out .. "/03-yes-no.png")
|
||||||
|
|
||||||
|
for i, page in ipairs(pages) do
|
||||||
|
print(("[driver] page %d: %s"):format(i, (page:gsub("\n", " / "))))
|
||||||
|
end
|
||||||
|
check(sawBoxWithName,
|
||||||
|
"the incoming " .. incoming .. " is named in the box before the yes/no")
|
||||||
|
check(screen.phase == "ask-shift",
|
||||||
|
"and the prompt still ends on the YES/NO question")
|
||||||
|
check(#pages > 1, "the offer runs as pages, not one over-long line")
|
||||||
|
|
||||||
|
if #failures > 0 then
|
||||||
|
error(#failures .. " send-out order checks failed")
|
||||||
|
end
|
||||||
|
print("[driver] #1158 fixed; shots in " .. out)
|
||||||
|
end
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- #1159 (Fly / Dig leave the user on the field) and #1160 (64x64 front pics).
|
||||||
|
--
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1159_test.lua love .
|
||||||
|
|
||||||
|
local U = require("tests.drivers.util")
|
||||||
|
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
local BattleState = require("src.ui.gen2.BattleState")
|
||||||
|
|
||||||
|
local function battleScreen(game)
|
||||||
|
for _ = 1, 900 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top.battle then return top end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
error("battle screen never came up")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drain(game, screen, frames)
|
||||||
|
for _ = 1, (frames or 400) do
|
||||||
|
if screen.phase == "menu" and #screen.queue == 0 and not screen.anim then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One drawPic call with love.graphics.draw intercepted: how many blits it made
|
||||||
|
-- and where the first one landed.
|
||||||
|
local function probePic(screen, mon, back)
|
||||||
|
local G = love.graphics
|
||||||
|
local real = G.draw
|
||||||
|
local count, x, y = 0, nil, nil
|
||||||
|
G.draw = function(image, a, b, ...)
|
||||||
|
count = count + 1
|
||||||
|
if count == 1 then
|
||||||
|
-- draw(image, x, y, ...) or draw(image, quad, x, y, ...)
|
||||||
|
if type(a) == "number" then x, y = a, b else x, y = b, (...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local ok, err = pcall(screen.drawPic, screen, mon, back)
|
||||||
|
G.draw = real
|
||||||
|
if not ok then error(err) end
|
||||||
|
return count, x, y
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1159"
|
||||||
|
U.wait(45)
|
||||||
|
local world = game.world
|
||||||
|
assert(world and world.map, "gold world did not boot")
|
||||||
|
local failures = {}
|
||||||
|
local function check(ok, what)
|
||||||
|
print((ok and "[ok] " or "[FAIL] ") .. what)
|
||||||
|
if not ok then failures[#failures + 1] = what end
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------------ #1159
|
||||||
|
local digger = Mon.new(game.data, "SANDSHREW", 30)
|
||||||
|
local dig = assert(game.data.moves.DIG, "DIG missing from the Gold cache")
|
||||||
|
digger.moves = { { id = "DIG", pp = dig.pp, maxPp = dig.pp } }
|
||||||
|
game.save.party = { digger }
|
||||||
|
local target = Mon.new(game.data, "SNORLAX", 40)
|
||||||
|
target.moves = { { id = "SPLASH", pp = 40, maxPp = 40 } }
|
||||||
|
assert(world:startBattle({ wild = target }), "startBattle failed")
|
||||||
|
local screen = battleScreen(game)
|
||||||
|
drain(game, screen, 300)
|
||||||
|
|
||||||
|
local before = probePic(screen, screen.battle.player, true)
|
||||||
|
check(before > 0, "the back pic is on the field before DIG")
|
||||||
|
U.shot(game, out .. "/01-before-dig.png")
|
||||||
|
|
||||||
|
screen:submit({ kind = "move", move = "DIG" })
|
||||||
|
drain(game, screen, 600)
|
||||||
|
|
||||||
|
check(BattleState.isVanished(screen.battle.player) == true,
|
||||||
|
"SUBSTATUS_UNDERGROUND is set after the charge turn")
|
||||||
|
local underground = probePic(screen, screen.battle.player, true)
|
||||||
|
print("[driver] back-pic draws while underground: " .. underground)
|
||||||
|
check(underground == 0, "the back pic is GONE for the charge turn")
|
||||||
|
U.shot(game, out .. "/02-underground.png")
|
||||||
|
|
||||||
|
-- The enemy front pic is untouched by the player's own dig.
|
||||||
|
local foe = probePic(screen, screen.battle.enemy, false)
|
||||||
|
check(foe > 0, "the enemy front pic is unaffected")
|
||||||
|
|
||||||
|
-- Turn two: CheckCharge clears the bit and AppearUserRaiseSub redraws it.
|
||||||
|
screen:submit({ kind = "move", move = "DIG" })
|
||||||
|
drain(game, screen, 600)
|
||||||
|
check(BattleState.isVanished(screen.battle.player) == false,
|
||||||
|
"the substatus is cleared when the stored attack lands")
|
||||||
|
local after = probePic(screen, screen.battle.player, true)
|
||||||
|
print("[driver] back-pic draws after the attack: " .. after)
|
||||||
|
check(after > 0, "the back pic is back on the field")
|
||||||
|
U.shot(game, out .. "/03-resurfaced.png")
|
||||||
|
|
||||||
|
------------------------------------------------------------------ #1160
|
||||||
|
-- A stubbed pic of each size through the same placement code. trueColor is
|
||||||
|
-- set so the probe never enters the GBC palette shader.
|
||||||
|
local realPic = screen.pic
|
||||||
|
local function place(size, back)
|
||||||
|
local data = love.image.newImageData(size, size)
|
||||||
|
local image = love.graphics.newImage(data)
|
||||||
|
screen.pic = function() return image, true, "mods/front" .. size .. ".png" end
|
||||||
|
local n, x, y = probePic(screen, screen.battle.enemy, back)
|
||||||
|
screen.pic = realPic
|
||||||
|
return n, x, y
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, x56, y56 = place(56, false)
|
||||||
|
print(("[driver] 56x56 front at (%s, %s)"):format(tostring(x56), tostring(y56)))
|
||||||
|
check(x56 == 96 and y56 == 0, "vanilla 56x56 front pic is unmoved at (96, 0)")
|
||||||
|
|
||||||
|
local _, x40, y40 = place(40, false)
|
||||||
|
print(("[driver] 40x40 front at (%s, %s)"):format(tostring(x40), tostring(y40)))
|
||||||
|
check(x40 == 104 and y40 == 16,
|
||||||
|
"a small 40x40 front pic still stands on the box's ground line")
|
||||||
|
|
||||||
|
local _, x64, y64 = place(64, false)
|
||||||
|
print(("[driver] 64x64 front at (%s, %s)"):format(tostring(x64), tostring(y64)))
|
||||||
|
check(x64 == 96 and y64 == 0,
|
||||||
|
"a 64x64 front pic pins to the box corner and overlaps the HUD")
|
||||||
|
|
||||||
|
if #failures > 0 then
|
||||||
|
error(#failures .. " checks failed")
|
||||||
|
end
|
||||||
|
print("[driver] #1159 / #1160 fixed; shots in " .. out)
|
||||||
|
end
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
-- #1167: the Ilex Forest hidden Ether and hidden Super Potion.
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1167_test.lua love .
|
||||||
|
local Mon = require("src.battle.gen2.Mon")
|
||||||
|
local HiddenItems = require("src.world.gen2.HiddenItems")
|
||||||
|
|
||||||
|
local CASES = {
|
||||||
|
{ name = "ETHER", x = 27, y = 1, event = 136 },
|
||||||
|
{ name = "SUPER_POTION", x = 17, y = 7, event = 137 },
|
||||||
|
{ name = "FULL_HEAL", x = 9, y = 17, event = 138 },
|
||||||
|
}
|
||||||
|
|
||||||
|
local FACE = {
|
||||||
|
up = { 0, 1 }, down = { 0, -1 }, left = { 1, 0 }, right = { -1, 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
local function wait(n) for _ = 1, n do coroutine.yield() end end
|
||||||
|
local function tap(btn)
|
||||||
|
table.insert(game.input.pressQueue, btn)
|
||||||
|
game.input.state[btn] = true
|
||||||
|
coroutine.yield()
|
||||||
|
game.input.state[btn] = false
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(30)
|
||||||
|
local world = game.world
|
||||||
|
game.save.party = { Mon.new(game.data, "PIDGEY", 5) }
|
||||||
|
world:setMap("ILEX_FOREST", 9, 18, "up")
|
||||||
|
wait(30)
|
||||||
|
assert(world.map.id == "ILEX_FOREST", "not in ILEX_FOREST")
|
||||||
|
|
||||||
|
-- what the engine itself thinks it has for this map
|
||||||
|
local listed = HiddenItems.unfound(world.map.def, world.events)
|
||||||
|
print("[driver] engine hidden items on ILEX_FOREST:")
|
||||||
|
for _, row in ipairs(listed) do
|
||||||
|
print((" (%2d,%2d) item=%s flag=%d")
|
||||||
|
:format(row.x, row.y, tostring(row.item), row.event))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bagCount(name)
|
||||||
|
local n = 0
|
||||||
|
for _, row in ipairs((game.save.bag and game.save.bag.items) or {}) do
|
||||||
|
if row.item == name or row.id == name then n = n + (row.count or 1) end
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
local bad = 0
|
||||||
|
for _, c in ipairs(CASES) do
|
||||||
|
local got = false
|
||||||
|
for dir, d in pairs(FACE) do
|
||||||
|
local sx, sy = c.x + d[1], c.y + d[2]
|
||||||
|
if world.map:isWalkable(sx, sy) then
|
||||||
|
world:setMap("ILEX_FOREST", sx, sy, dir)
|
||||||
|
wait(20)
|
||||||
|
world.player.facing = dir
|
||||||
|
local before = bagCount(c.name)
|
||||||
|
tap("a")
|
||||||
|
wait(60)
|
||||||
|
for _ = 1, 240 do
|
||||||
|
if not world:busy() then break end
|
||||||
|
tap("a")
|
||||||
|
end
|
||||||
|
wait(20)
|
||||||
|
if world.events:get(c.event) or bagCount(c.name) > before then
|
||||||
|
got = true
|
||||||
|
print(("[driver] OK %-13s at (%2d,%2d) picked up facing %s")
|
||||||
|
:format(c.name, c.x, c.y, dir))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not got then
|
||||||
|
bad = bad + 1
|
||||||
|
print(("[driver] MISSING %-13s at (%2d,%2d)"):format(c.name, c.x, c.y))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print(("[driver] %s ilex hidden items: %d missing")
|
||||||
|
:format(bad == 0 and "PASS" or "FAIL", bad))
|
||||||
|
end
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- #1208: the launcher's save-slot line read a Gold save with Gen 1 eyes
|
||||||
|
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/gold_bug1208_test.lua love .
|
||||||
|
|
||||||
|
local U = require("tests.drivers.util")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local GoldSave = require("src.core.gen2.Save")
|
||||||
|
|
||||||
|
return function(game)
|
||||||
|
U.wait(45)
|
||||||
|
local save = game.save
|
||||||
|
assert(save and save.player, "gold save did not boot")
|
||||||
|
|
||||||
|
save.player.badges = { ZEPHYR = true, HIVE = true, PLAIN = true, FOG = true,
|
||||||
|
STORM = true, MINERAL = true, GLACIER = true, RISING = true }
|
||||||
|
save.player.kantoBadges = { BOULDER = true, CASCADE = true }
|
||||||
|
save.pokedex = save.pokedex or {}
|
||||||
|
save.pokedex.caught = { [155] = true, [158] = true, [152] = true }
|
||||||
|
|
||||||
|
local ok, err = game:writeSave()
|
||||||
|
if not ok then
|
||||||
|
U.log("FAIL gold writeSave:", tostring(err))
|
||||||
|
else
|
||||||
|
local want = GoldSave.summary(save)
|
||||||
|
local active = SaveData.activeSlot("gold")
|
||||||
|
local row
|
||||||
|
for _, slot in ipairs(SaveData.listSlots("gold")) do
|
||||||
|
if slot.id == active then row = slot end
|
||||||
|
end
|
||||||
|
if not (row and row.meta) then
|
||||||
|
U.log("FAIL launcher has no slot row for", tostring(active))
|
||||||
|
else
|
||||||
|
U.log(("launcher: %d badges - %s - %d caught"):format(
|
||||||
|
row.meta.badges, row.meta.timeText, row.meta.dexCount))
|
||||||
|
U.log(("continue: %d badges - %d:%02d - %d caught"):format(
|
||||||
|
want.badges, want.hours, want.minutes, want.caught))
|
||||||
|
if row.meta.badges == want.badges and row.meta.dexCount == want.caught then
|
||||||
|
U.log("PASS launcher summary matches CONTINUE for slot", tostring(active))
|
||||||
|
else
|
||||||
|
U.log("FAIL launcher summary disagrees with CONTINUE")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
-- #1100 / #1135: the launcher gear offered TOUCH PAD, VIBRATION and the
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check, eq = T.check, T.eq
|
||||||
|
love = love or require("tests.love_stub")
|
||||||
|
|
||||||
|
-- The rows are mobile-gated the same way OptionsMenu's are, so the suite has
|
||||||
|
-- to look like a phone; POKEPORT_TOUCH=1 is the other way in.
|
||||||
|
local realGetOS = love.system.getOS
|
||||||
|
love.system.getOS = function() return "Android" end
|
||||||
|
|
||||||
|
local LauncherSettings = require("src.import.LauncherSettings")
|
||||||
|
local TouchControls = require("src.core.TouchControls")
|
||||||
|
|
||||||
|
local function labels(model)
|
||||||
|
local out = {}
|
||||||
|
for _, section in ipairs(model.sections) do
|
||||||
|
for _, row in ipairs(section.rows) do out[#out + 1] = row.label end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findRow(model, label)
|
||||||
|
for _, section in ipairs(model.sections) do
|
||||||
|
for _, row in ipairs(section.rows) do
|
||||||
|
if row.label == label then return row end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has(model, label)
|
||||||
|
for _, l in ipairs(labels(model)) do
|
||||||
|
if l == label then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local edited = 0
|
||||||
|
local hooks = { editTouchControls = function() edited = edited + 1 end }
|
||||||
|
|
||||||
|
local gold = LauncherSettings.open(hooks, "gold")
|
||||||
|
check(has(gold, "TOUCH PAD"), "Gold's gear offers TOUCH PAD")
|
||||||
|
check(has(gold, "VIBRATION"), "and VIBRATION")
|
||||||
|
check(has(gold, "TOUCH CONTROLS"), "and the layout editor")
|
||||||
|
|
||||||
|
-- Every write has to land in the gold block: the flat keys beside it are
|
||||||
|
-- Red's, and Gold's boot never reads them (src/core/gen2/Save.lua:299).
|
||||||
|
-- loadOptions seeds the flat Gen 1 defaults, so the check is that the Gold
|
||||||
|
-- rows leave them exactly as they found them.
|
||||||
|
local flatPad = gold.opts.touchControls
|
||||||
|
local flatBuzz = gold.opts.haptics
|
||||||
|
|
||||||
|
local pad = findRow(gold, "TOUCH PAD")
|
||||||
|
local before = pad.value()
|
||||||
|
pad.step(1)
|
||||||
|
check(pad.value() ~= before, "stepping TOUCH PAD flips it")
|
||||||
|
check(type(gold.opts.gold) == "table", "into the gold block")
|
||||||
|
eq(gold.opts.gold.touchControls.enabled, false, "which now carries enabled")
|
||||||
|
eq(gold.opts.touchControls, flatPad, "leaving the flat Gen 1 key alone")
|
||||||
|
|
||||||
|
local buzz = findRow(gold, "VIBRATION")
|
||||||
|
local buzzBefore = buzz.value()
|
||||||
|
buzz.step(1)
|
||||||
|
check(buzz.value() ~= buzzBefore, "stepping VIBRATION moves the level")
|
||||||
|
eq(gold.opts.gold.haptics, TouchControls.normalizeHaptics(gold.opts.gold.haptics),
|
||||||
|
"VIBRATION stores a level the shared module knows")
|
||||||
|
eq(gold.opts.haptics, flatBuzz, "also without touching Red's")
|
||||||
|
|
||||||
|
findRow(gold, "TOUCH CONTROLS").action()
|
||||||
|
eq(edited, 1, "the editor row reaches the host hook")
|
||||||
|
|
||||||
|
-- The Gen 1 gear is untouched by the extraction: same three rows, still on
|
||||||
|
-- the flat table.
|
||||||
|
local red = LauncherSettings.open(hooks, "red")
|
||||||
|
check(has(red, "TOUCH PAD"), "Red still offers TOUCH PAD")
|
||||||
|
check(has(red, "VIBRATION"), "and VIBRATION")
|
||||||
|
check(has(red, "TOUCH CONTROLS"), "and the layout editor")
|
||||||
|
findRow(red, "TOUCH PAD").step(1)
|
||||||
|
eq(red.opts.touchControls.enabled, false, "writing to the flat key")
|
||||||
|
eq(red.opts.gold, nil, "with no gold block invented")
|
||||||
|
eq(findRow(red, "TOUCH PAD").value() ~= nil, true, "and reading it back")
|
||||||
|
|
||||||
|
-- With no hook (the standalone save editor's shape) the editor row is gone
|
||||||
|
-- rather than dead, on both sides.
|
||||||
|
eq(has(LauncherSettings.open(nil, "gold"), "TOUCH CONTROLS"), false,
|
||||||
|
"no hook, no editor row on Gold")
|
||||||
|
eq(has(LauncherSettings.open(nil, "red"), "TOUCH CONTROLS"), false,
|
||||||
|
"nor on Red")
|
||||||
|
-- The Edit row hands the screen to the host, and the host has to know WHICH
|
||||||
|
-- game's block to write the dragged layout into: TouchControlsEditor persists
|
||||||
|
-- to opts.gold only when it was loaded with version = "gold". Source-shape
|
||||||
|
-- checks, the same way tests/engine/touch_controls_pad_cursor_test.lua pins
|
||||||
|
-- the handoff either side of this one.
|
||||||
|
local function read(path)
|
||||||
|
local f = assert(io.open(path, "r"))
|
||||||
|
local src = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
return src
|
||||||
|
end
|
||||||
|
|
||||||
|
local importerSrc = read("src/import/RomImporter.lua")
|
||||||
|
check(importerSrc:find("self.onEditTouchControls(version)", 1, true) ~= nil,
|
||||||
|
"the gear hands the launcher tab to the host")
|
||||||
|
|
||||||
|
local mainSrc = read("main.lua")
|
||||||
|
local body = mainSrc:match("local function openTouchControlsEditor%((.-)%)")
|
||||||
|
eq(body, "version", "main.lua's opener takes that tab")
|
||||||
|
check(mainSrc:match("TouchEditor%.load%({%s*version = version") ~= nil,
|
||||||
|
"and loads the editor with it, so Gold's layout lands in the gold block")
|
||||||
|
|
||||||
|
love.system.getOS = realGetOS
|
||||||
|
|
||||||
|
T.finish("launcher gold touch rows")
|
||||||
@@ -51,7 +51,9 @@ check(mainSrc:find("prepareOverlayHandoff", 1, true) ~= nil
|
|||||||
and mainSrc:find("openTouchControlsEditor", 1, true) ~= nil,
|
and mainSrc:find("openTouchControlsEditor", 1, true) ~= nil,
|
||||||
"main.lua mentions prepareOverlayHandoff + touch editor")
|
"main.lua mentions prepareOverlayHandoff + touch editor")
|
||||||
-- prepare must run inside openTouchControlsEditor, not only openEditor
|
-- prepare must run inside openTouchControlsEditor, not only openEditor
|
||||||
local touchOpen = mainSrc:match("local function openTouchControlsEditor%(%)(.-)\nend")
|
-- The signature takes the launcher tab since #1100, so match any parameter
|
||||||
|
-- list rather than pinning the arity.
|
||||||
|
local touchOpen = mainSrc:match("local function openTouchControlsEditor%(.-%)(.-)\nend")
|
||||||
check(touchOpen ~= nil, "openTouchControlsEditor body found")
|
check(touchOpen ~= nil, "openTouchControlsEditor body found")
|
||||||
check(touchOpen:find("prepareOverlayHandoff", 1, true) ~= nil,
|
check(touchOpen:find("prepareOverlayHandoff", 1, true) ~= nil,
|
||||||
"openTouchControlsEditor prepares overlay handoff like the save editor")
|
"openTouchControlsEditor prepares overlay handoff like the save editor")
|
||||||
|
|||||||
Reference in New Issue
Block a user