mirror of
https://github.com/AverageConsumer/kanto-gear.git
synced 2026-08-28 18:24:42 +02:00
450 lines
16 KiB
Lua
450 lines
16 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local path = os.getenv("KANTO_GEAR_MOD_PATH") or "mods/kanto_gear"
|
|
local newCanvas = T.love.graphics.newCanvas
|
|
local rectangle = T.love.graphics.rectangle
|
|
local getTime = T.love.timer.getTime
|
|
local now = 0
|
|
local rectangles = {}
|
|
T.love.timer.getTime = function() return now end
|
|
T.love.graphics.rectangle = function(...)
|
|
rectangles[#rectangles + 1] = { ... }
|
|
return rectangle(...)
|
|
end
|
|
T.love.graphics.newCanvas = function(...)
|
|
local canvas = newCanvas(...)
|
|
function canvas:requestImageData() return true end
|
|
function canvas:pollImageData() return {} end
|
|
return canvas
|
|
end
|
|
local run = T.sdk.loadMod(path, {
|
|
generation = 2,
|
|
data = T.fixtures.load(),
|
|
})
|
|
T.love.graphics.newCanvas = newCanvas
|
|
T.love.system.getPowerInfo = function() return "battery", 80 end
|
|
|
|
T.eq(run.mod and run.mod.state, "loaded",
|
|
"Kanto Gear manifest and entry load for Gen 2")
|
|
T.eq(#run.errors, 0, "Kanto Gear has no Gen 2 boot errors")
|
|
|
|
run.data.gen2Landmarks = { landmarks = {
|
|
LANDMARK_FIX_ROUTE = { index = 2, name = "FIX ROUTE", x = 40, y = 56 },
|
|
} }
|
|
run.data.gen2Maps = { FIX_ROUTE = {
|
|
landmark = 2,
|
|
objects = {
|
|
{ eventFlag = 12, itemball = { item = 1 } },
|
|
{ trainer = { class = 3, member = 1, event = 11 } },
|
|
},
|
|
bgEvents = { { x = 3, y = 4,
|
|
hiddenItem = { item = 1, event = 13 } } },
|
|
} }
|
|
run.data.gen2Trainers = { classes = { YOUNGSTER = {
|
|
index = 3, name = "YOUNGSTER",
|
|
trainers = { { name = "JOEY" } },
|
|
} } }
|
|
run.data.gen2Encounters = { grass = { FIX_ROUTE = { slots = {
|
|
MORN = { { species = "FIXMON_A", level = 2 } },
|
|
DAY = { { species = "FIXMON_A", level = 3 } },
|
|
NITE = { { species = "FIXMON_B", level = 4 } },
|
|
} } }, water = {} }
|
|
local johtoMap = {}
|
|
for i = 1, 20 * 18 do johtoMap[i] = 0 end
|
|
local mapColors = {
|
|
{ 255, 255, 255 }, { 104, 208, 88 }, { 32, 120, 64 }, { 0, 0, 0 },
|
|
}
|
|
run.data.gen2MenuGfx = { pokegear = {
|
|
tiles = "gold-map.png", tilesWide = 1,
|
|
maps = { johto = johtoMap, kanto = johtoMap },
|
|
palettes = { mapColors }, palMap = { [1] = 1 },
|
|
} }
|
|
run.data.gen2Sprites = { SPRITE_CHRIS = {
|
|
image = "gold-player.png", frames = 1, trueColor = true, paletteId = 0,
|
|
} }
|
|
local playerColors = {
|
|
{ 255, 255, 255 }, { 248, 80, 56 }, { 200, 32, 24 }, { 0, 0, 0 },
|
|
}
|
|
run.data.gen2Palettes = { objects = { DAY = { playerColors } } }
|
|
|
|
local game = {
|
|
data = run.data,
|
|
save = {
|
|
generation = 2,
|
|
version = "silver",
|
|
player = {
|
|
name = "SILVER", id = 25, money = 1234, map = "FIX_ROUTE",
|
|
badges = { ZEPHYR = true }, kantoBadges = {},
|
|
},
|
|
party = { {
|
|
species = "FIXMON_A", nickname = "CYNDA", level = 5,
|
|
hp = 20, stats = { hp = 20 }, exp = 0,
|
|
} },
|
|
pokedex = { seen = { FIXMON_A = true }, caught = { FIXMON_A = true } },
|
|
playTime = { hours = 1, minutes = 2, seconds = 3 },
|
|
inventory = {}, boxes = {}, currentBox = 1,
|
|
},
|
|
world = {
|
|
map = { id = "FIX_ROUTE" },
|
|
player = { cellX = 2, cellY = 3, facing = "down" },
|
|
daytime = "DAY",
|
|
},
|
|
stack = { states = {}, top = function(self)
|
|
return self.states[#self.states]
|
|
end },
|
|
}
|
|
run.loader.events:emit("game.ready", { game = game })
|
|
run.loader.modOptions.kanto_gear = { trigger_tabs = true }
|
|
run.loader.events:emit("mod.options_changed",
|
|
{ mod = "kanto_gear", key = "trigger_tabs" })
|
|
|
|
local trigger = 0
|
|
T.love.joystick = { getJoysticks = function()
|
|
return { {
|
|
isGamepadDown = function() return false end,
|
|
getGamepadAxis = function(_, axis)
|
|
return axis == "triggerright" and trigger or 0
|
|
end,
|
|
} }
|
|
end }
|
|
local touchEvents = {}
|
|
local companion = {
|
|
detected = function() return true end,
|
|
pollTouch = function() return table.remove(touchEvents, 1) end,
|
|
push = function() return true end,
|
|
}
|
|
local SpriteRenderer = require("src.render.SpriteRenderer")
|
|
local GbcPalette = require("src.render.GbcPalette")
|
|
local rendererNew = SpriteRenderer.new
|
|
local paletteWith = GbcPalette.with
|
|
local paletteAvailable = GbcPalette.available
|
|
local markerPalette
|
|
local mapPaletteUsed = false
|
|
SpriteRenderer.new = function(def, seed)
|
|
local renderer = rendererNew(def, seed)
|
|
if seed == "kanto-gear-map" then
|
|
local setObjPalette = renderer.setObjPalette
|
|
renderer.setObjPalette = function(self, colors, group)
|
|
markerPalette = { colors = colors, group = group }
|
|
return setObjPalette(self, colors, group)
|
|
end
|
|
end
|
|
return renderer
|
|
end
|
|
GbcPalette.with = function(colors, body)
|
|
if colors == mapColors then mapPaletteUsed = true end
|
|
return body()
|
|
end
|
|
GbcPalette.available = function() return true end
|
|
local mapDraws = T.record.draw()
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
SpriteRenderer.new = rendererNew
|
|
GbcPalette.with, GbcPalette.available = paletteWith, paletteAvailable
|
|
T.check(#mapDraws:fromPath("gold-map.png") > 0,
|
|
"Gold map renders the extracted Pokegear town-map tiles")
|
|
T.check(mapPaletteUsed,
|
|
"Gold map applies its extracted per-tile town-map palettes")
|
|
local playerMarker = mapDraws:fromPath("gold-player.png")[1]
|
|
T.check(playerMarker and playerMarker.args[2] == 44
|
|
and playerMarker.args[3] == 56 and playerMarker.args[5] == 0.75,
|
|
"Gold map uses the native player sprite at the scaled landmark")
|
|
T.check(markerPalette and markerPalette.colors == playerColors
|
|
and markerPalette.group == "gen2:DAY:0",
|
|
"Gold map applies the native daytime player palette")
|
|
for _ = 1, 10 do
|
|
trigger = 0.8
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
trigger = 0
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
end
|
|
mapDraws:stop()
|
|
T.check(true, "Silver-shaped save and world render every companion tab")
|
|
local firstBadge, lastBadge = false, false
|
|
for _, call in ipairs(rectangles) do
|
|
local _, x, y, w, h = unpack(call)
|
|
if y == 57 and w == 12 and h == 12 then
|
|
firstBadge = firstBadge or x == 11
|
|
lastBadge = lastBadge or x == 137
|
|
end
|
|
end
|
|
T.check(firstBadge and lastBadge,
|
|
"Gold badges use smaller icons with equal outer margins")
|
|
|
|
local enemy = { species = "FIXMON_B", level = 4, hp = 11, maxHp = 12,
|
|
moves = {} }
|
|
run.data.gen2Pokedex = { entries = { FIXMON_B = {
|
|
dex = 2, kind = "FLAME", height = 204, weight = 190,
|
|
text = "A small flame stays warm.", text2 = "It glows at night.",
|
|
} } }
|
|
local battle = { player = game.save.party[1], enemy = enemy,
|
|
party = game.save.party, wild = true, turn = 0 }
|
|
function battle:moveDisabled() return false end
|
|
local screen = { screenId = "Gen2BattleState", battle = battle,
|
|
phase = "menu", menuIndex = 1, moveIndex = 1,
|
|
shownHp = { player = 19, enemy = 10 } }
|
|
game.stack.states = { screen }
|
|
run.loader.game = game
|
|
now = now + 1
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.check(true, "Gold battle state and direct mon HP shape render safely")
|
|
|
|
run.loader.modOptions.kanto_gear.battle_view = "gear"
|
|
run.loader.events:emit("mod.options_changed",
|
|
{ mod = "kanto_gear", key = "battle_view" })
|
|
T.eq(run.loader.hooks:call("battle.bottom_ui_visible",
|
|
function() return true end, screen), false,
|
|
"GEAR gives Gold battle text and menus to the companion screen")
|
|
T.eq(run.loader.hooks:call("battle.status_hud_visible",
|
|
function() return true end, screen), true,
|
|
"GEAR leaves Gold's native status HUD visible")
|
|
run.loader.modOptions.kanto_gear.battle_view = "full"
|
|
run.loader.events:emit("mod.options_changed",
|
|
{ mod = "kanto_gear", key = "battle_view" })
|
|
T.eq(run.loader.hooks:call("battle.status_hud_visible",
|
|
function() return true end, screen), false,
|
|
"FULL GEAR gives Gold's status HUD to the companion screen too")
|
|
run.loader.modOptions.kanto_gear.battle_view = "info"
|
|
run.loader.events:emit("mod.options_changed",
|
|
{ mod = "kanto_gear", key = "battle_view" })
|
|
T.eq(run.loader.hooks:call("battle.bottom_ui_visible",
|
|
function() return true end, screen), true,
|
|
"INFO leaves Gold's native battle menu visible")
|
|
T.eq(run.loader.hooks:call("battle.status_hud_visible",
|
|
function() return true end, screen), true,
|
|
"INFO leaves Gold's native status HUD visible")
|
|
local infoDraws = T.record.draw()
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
infoDraws:stop()
|
|
T.check(#infoDraws:fromPath(
|
|
"tests/fixture_data/assets/fixmon_b_front.png") > 0,
|
|
"Gold INFO draws the active enemy species on the companion screen")
|
|
touchEvents[1] = "tap,40,40"
|
|
local profileDraws = T.record.draw()
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
profileDraws:stop()
|
|
T.eq(#profileDraws:fromPath(
|
|
"tests/fixture_data/assets/fixmon_b_front.png"), 0,
|
|
"tapping INFO identity opens the Gold Pokedex detail page")
|
|
game.input = { pressQueue = { "b" } }
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
T.eq(#game.input.pressQueue, 0,
|
|
"Pokedex detail consumes B without backing out of the battle")
|
|
local profileBackDraws = T.record.draw()
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
profileBackDraws:stop()
|
|
T.check(#profileBackDraws:fromPath(
|
|
"tests/fixture_data/assets/fixmon_b_front.png") > 0,
|
|
"B returns from Pokedex detail to the compact INFO card")
|
|
touchEvents[1] = "tap,20,110"
|
|
local matchupDraws = T.record.draw()
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
matchupDraws:stop()
|
|
T.eq(#matchupDraws:fromPath(
|
|
"tests/fixture_data/assets/fixmon_b_front.png"), 0,
|
|
"tapping a matchup column opens its complete focused table")
|
|
run.loader.modOptions.kanto_gear.battle_view = "full"
|
|
run.loader.events:emit("mod.options_changed",
|
|
{ mod = "kanto_gear", key = "battle_view" })
|
|
|
|
-- Gold resolves the turn before its native HP bar finishes chasing the new
|
|
-- value. The companion must follow screen.shownHp, not jump straight to the
|
|
-- already-final battle mon HP.
|
|
local main = assert(io.open(path .. "/main.lua", "rb"))
|
|
local source = main:read("*a")
|
|
main:close()
|
|
T.check(source:find("raw%.battle and raw%.battle%[side%]", 1) ~= nil,
|
|
"Gold FULL GEAR reads chased HP from the nested battle state")
|
|
|
|
run.loader.game = game
|
|
screen.phase = "anim"
|
|
screen.message = "FIXMON_A used TACKLE!"
|
|
screen.hpAnim = { side = "enemy", to = 7 }
|
|
screen.shownHp.enemy = 9
|
|
screen.battle.enemy.hp = 7
|
|
rectangles = {}
|
|
now = now + 1
|
|
run.loader.events:emit("battle.damage_dealt", {})
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
local sawEnemyPanel, sawChasedHp = false, false
|
|
for _, call in ipairs(rectangles) do
|
|
local _, x, y, w, h = unpack(call)
|
|
if x == 4 and y == 3 and w == 152 and h == 40 then
|
|
sawEnemyPanel = true
|
|
elseif x == 29 and y == 28 and w == 49 and h == 5 then
|
|
sawChasedHp = true
|
|
end
|
|
end
|
|
T.check(sawEnemyPanel,
|
|
"Gold HP animation keeps FULL GEAR status panels visible")
|
|
T.check(sawChasedHp,
|
|
"Gold HP animation draws the chased intermediate enemy HP")
|
|
screen.phase, screen.message, screen.hpAnim = "menu", nil, nil
|
|
|
|
screen.statsBoxMon = game.save.party[1]
|
|
screen.statsBoxMon.stats = { hp = 22, attack = 12, defense = 11,
|
|
specialAttack = 13, specialDefense = 14, speed = 15 }
|
|
screen.phase = "stats-box"
|
|
rectangles = {}
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
local sawLevelContinue = false
|
|
for _, call in ipairs(rectangles) do
|
|
local _, x, y, w, h = unpack(call)
|
|
if x == 24 and y == 111 and w == 112 and h == 27 then
|
|
sawLevelContinue = true
|
|
end
|
|
end
|
|
T.check(sawLevelContinue,
|
|
"Gold level-up stats render on the companion screen")
|
|
T.eq(run.loader.hooks:call("screen.render_visible",
|
|
function() return true end, screen), true,
|
|
"Gold level-up keeps the native battle scene visible")
|
|
|
|
screen.phase, screen.statsBoxMon = "resolving", nil
|
|
screen.message = "FIXMON_A gained 12 EXP. Points!"
|
|
now = now + 1
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
game.stack.states = {}
|
|
run.loader.events:emit("screen.popped", { state = screen })
|
|
rectangles = {}
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
local staleBattlePanel = false
|
|
for _, call in ipairs(rectangles) do
|
|
local _, x, y, w, h = unpack(call)
|
|
if x == 4 and y == 3 and w == 152 and h == 40 then
|
|
staleBattlePanel = true
|
|
end
|
|
end
|
|
T.check(not staleBattlePanel,
|
|
"popping Gold's battle screen clears the cached EXP view immediately")
|
|
screen.phase, screen.message = "menu", nil
|
|
|
|
local pack = {
|
|
screenId = "Gen2PackMenu", battle = true, pocketIndex = 1, index = 1,
|
|
rows = { { id = "POTION", name = "POTION", count = 2,
|
|
showCount = true } },
|
|
pocket = function() return { id = "ITEM", label = "ITEMS" } end,
|
|
}
|
|
game.stack.states = { screen, pack }
|
|
now = now + 1
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.check(true, "Gold battle PACK rows render on the companion screen")
|
|
|
|
local party = {
|
|
screenId = "Gen2PartyMenu", index = #game.save.party + 1,
|
|
isCancel = function(self) return self.index > #game.save.party end,
|
|
}
|
|
game.stack.states = { screen, party }
|
|
now = now + 1
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.check(true, "Gold party CANCEL renders as navigation instead of a Pokemon")
|
|
|
|
local summary = {
|
|
screenId = "Gen2SummaryMenu", page = 3, mon = game.save.party[1],
|
|
itemName = function() return "BERRY" end,
|
|
expToNext = function() return 20 end,
|
|
otName = function() return "GOLD" end,
|
|
otId = function() return 25 end,
|
|
}
|
|
summary.mon.experience = 12
|
|
summary.mon.maxHp = 20
|
|
summary.mon.stats.attack = 9
|
|
summary.mon.stats.defense = 10
|
|
summary.mon.stats.specialAttack = 11
|
|
summary.mon.stats.specialDefense = 12
|
|
summary.mon.stats.speed = 13
|
|
game.stack.states = { screen, party, summary }
|
|
now = now + 1
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.eq(run.loader.hooks:call("screen.render_visible",
|
|
function() return true end, summary), false,
|
|
"Gold battle summaries render only on the companion screen")
|
|
game.stack.states = { summary }
|
|
T.eq(run.loader.hooks:call("screen.render_visible",
|
|
function() return true end, summary), true,
|
|
"Gold field summaries keep their native top-screen rendering")
|
|
game.stack.states = { screen, party, summary }
|
|
summary.moveDetail = true
|
|
T.eq(run.loader.hooks:call("screen.render_visible",
|
|
function() return true end, summary), true,
|
|
"unsupported Gold summary subviews safely keep native rendering")
|
|
summary.moveDetail = false
|
|
|
|
local scriptChoice = {
|
|
screenId = "Gen2ScriptMenu", items = { "ONE", "TWO", "THREE" },
|
|
row = 2, col = 1, cols = 1,
|
|
}
|
|
game.stack.states = { scriptChoice }
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.eq(scriptChoice.row, 2,
|
|
"Gold script choices share the companion choice renderer")
|
|
|
|
local nestedChoice = { screenId = "Gen2PackMenu",
|
|
confirm = { choice = 2 } }
|
|
game.stack.states = { nestedChoice }
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.eq(nestedChoice.confirm.choice, 2,
|
|
"Gold nested confirmations share the companion choice renderer")
|
|
|
|
local nameChoice = { screenId = "Gen2NamePick",
|
|
items = { "NEW NAME", "GOLD" }, cursor = 2 }
|
|
game.stack.states = { nameChoice }
|
|
run.loader.hooks:call("input.step", function() end, game, 1 / 60)
|
|
run.loader.hooks:call("render.compose", function() return false end, {}, {
|
|
secondScreen = companion,
|
|
})
|
|
T.eq(nameChoice.cursor, 2,
|
|
"Gold name choices share the companion choice renderer")
|
|
|
|
run.release()
|
|
T.love.graphics.rectangle = rectangle
|
|
T.love.timer.getTime = getTime
|
|
T.finish("Kanto Gear Gen 2 compatibility")
|