mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 15:41:29 -04:00
CLOSES #2130, CLOSES #2131, CLOSES #2132, CLOSES #2134, CLOSES #2135, CLOSES #2136, CLOSES #2137, CLOSES #2138
This commit is contained in:
@@ -501,6 +501,11 @@ function Battle:monName(mon)
|
||||
return mon.nickname or mon.name or mon.species or "?"
|
||||
end
|
||||
|
||||
-- ../pokecrystal/data/text/battle.asm:240-246
|
||||
function Battle.sentOutText(trainerName, monName)
|
||||
return Strings("%s\nsent out\v%s!", trainerName, monName)
|
||||
end
|
||||
|
||||
function Battle:moveDef(moveId)
|
||||
return self.data.moves and self.data.moves[moveId] or nil
|
||||
end
|
||||
@@ -3554,8 +3559,7 @@ function Battle:resolveFaints()
|
||||
replacement = true,
|
||||
hp = self.enemy.hp or 0, status = self.enemy.status or false,
|
||||
level = self.enemy.level, experience = self.enemy.experience,
|
||||
text = Strings("%s sent out %s!",
|
||||
self.trainer and self.trainer.name or "Foe",
|
||||
text = Battle.sentOutText(self.trainer and self.trainer.name or "Foe",
|
||||
self:monName(self.enemy)) })
|
||||
Runtime.emit("battle.battler_switched", {
|
||||
battle = self, side = self:sideRecord(self.enemy), battler = self.enemy,
|
||||
@@ -4546,8 +4550,7 @@ function Battle:switchEnemy(index)
|
||||
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
|
||||
hp = self.enemy.hp or 0, status = self.enemy.status or false,
|
||||
level = self.enemy.level, experience = self.enemy.experience,
|
||||
text = Strings("%s sent out %s!", trainerName,
|
||||
self:monName(self.enemy)) })
|
||||
text = Battle.sentOutText(trainerName, self:monName(self.enemy)) })
|
||||
Runtime.emit("battle.battler_switched", {
|
||||
battle = self, side = self:sideRecord(self.enemy), battler = self.enemy,
|
||||
previous = outgoing,
|
||||
@@ -5313,8 +5316,8 @@ function Battle:forcedReplacement(side, index)
|
||||
self:emit({ kind = "send", side = "enemy", mon = mon, replacement = true,
|
||||
hp = mon.hp or 0, status = mon.status or false,
|
||||
level = mon.level, experience = mon.experience,
|
||||
text = Strings("%s sent out %s!",
|
||||
(self.trainer and self.trainer.name) or "Foe", self:monName(mon)) })
|
||||
text = Battle.sentOutText((self.trainer and self.trainer.name) or "Foe",
|
||||
self:monName(mon)) })
|
||||
Runtime.emit("battle.battler_switched", {
|
||||
battle = self, side = self:sideRecord(mon), battler = mon,
|
||||
previous = previous,
|
||||
|
||||
+10
-6
@@ -640,10 +640,19 @@ function TextBox:draw()
|
||||
and (self.game.save.generation == 2 or self.game.save.version == "gold")
|
||||
local Chrome = gold and Chrome2 or nil
|
||||
local drawGlyph, finishGlyph = Font.drawCode, nil
|
||||
-- pokegold home/joypad.asm:430
|
||||
local arrowOn = self.blink % 60 < 30
|
||||
if gold then arrowOn = self.blink % 32 < 16 end
|
||||
local arrowX = (self.boxTx + self.boxTw - 2) * 8
|
||||
local arrowY = (self.boxTy + self.boxTh - 1) * 8 - (gold and 0 or 4)
|
||||
if Chrome then
|
||||
local base = paper and { paper, paper, paper, { 0, 0, 0 } }
|
||||
or Chrome.DEFAULT_BOX_PALETTE
|
||||
Chrome.paletteBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, base)
|
||||
if arrowOn and self:arrowVisible() then
|
||||
-- ../pokecrystal/home/text.asm:630
|
||||
Chrome.paletteFill(arrowX, arrowY, 8, 8, base)
|
||||
end
|
||||
local _, dg, fg = Chrome.paletteGlyphs(base)
|
||||
drawGlyph, finishGlyph = dg, fg
|
||||
else
|
||||
@@ -697,16 +706,11 @@ function TextBox:draw()
|
||||
pen = pen + Font.advanceOf(code)
|
||||
end
|
||||
end
|
||||
-- pokegold home/joypad.asm:430
|
||||
local arrowOn = self.blink % 60 < 30
|
||||
if gold then arrowOn = self.blink % 32 < 16 end
|
||||
if self:arrowVisible() and arrowOn then
|
||||
-- page-advance cursor: glyph $EE by default, the blinking down arrow
|
||||
-- the original prints via `ld a, "▼"` (home/text.asm)
|
||||
-- pokegold home/text.asm:549
|
||||
drawGlyph(Theme.moreArrow or 0xEE,
|
||||
(self.boxTx + self.boxTw - 2) * 8,
|
||||
(self.boxTy + self.boxTh - 1) * 8 - (gold and 0 or 4))
|
||||
drawGlyph(Theme.moreArrow or 0xEE, arrowX, arrowY)
|
||||
end
|
||||
if finishGlyph then finishGlyph() end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
@@ -179,6 +179,22 @@ end
|
||||
Movement.TELEPORT_RISE_HEIGHT = 0x10
|
||||
Movement.TELEPORT_FALL_HEIGHT = 0
|
||||
|
||||
-- engine/overworld/map_objects.asm:1796-1817
|
||||
Movement.JUMP_Y = {
|
||||
-4, -6, -8, -10, -11, -12, -12, -12,
|
||||
-11, -10, -9, -8, -6, -4, 0, 0,
|
||||
}
|
||||
|
||||
function Movement.jumpYOffset(progress, frames)
|
||||
local curve = Movement.JUMP_Y
|
||||
local n = #curve
|
||||
local t = ((progress or 0) - 1) * (n - 1) / math.max((frames or n) - 1, 1) + 1
|
||||
local idx = math.floor(t)
|
||||
if idx < 1 then idx = 1 end
|
||||
if idx >= n then return curve[n] end
|
||||
return math.floor(curve[idx] + (curve[idx + 1] - curve[idx]) * (t - idx) + 0.5)
|
||||
end
|
||||
|
||||
local DIR_BYTE = { down = 0, up = 1, left = 2, right = 3 }
|
||||
|
||||
-- The `step <dir>` family ($0c-$0f), which is what InitMovementBuffer fills
|
||||
|
||||
@@ -1151,6 +1151,8 @@ local function runCmd(self, cmd, op)
|
||||
return "end"
|
||||
end
|
||||
if self.reloadMapFn then self.reloadMapFn(op ~= "refreshmap") end
|
||||
-- engine/overworld/scripting.asm:1209
|
||||
if op ~= "refreshmap" then self:waitFrames(1) end
|
||||
elseif op == "catchtutorial" then
|
||||
-- `catchtutorial battle_type` runs the DUDE's catch demo
|
||||
-- (engine/events/catch_tutorial.asm): the player's name is parked in
|
||||
@@ -1182,6 +1184,8 @@ local function runCmd(self, cmd, op)
|
||||
-- `jp Script_reloadmap`, so the setup script (and its ForceMapMusic row)
|
||||
-- really does run here.
|
||||
if self.reloadMapFn then self.reloadMapFn(true) end
|
||||
-- engine/overworld/scripting.asm:1209
|
||||
self:waitFrames(1)
|
||||
elseif op == "winlosstext" then
|
||||
-- Overrides the struct's win/loss text for this battle only; a 0
|
||||
-- argument zeroes that pointer (engine/overworld/scripting.asm:651)
|
||||
|
||||
@@ -228,10 +228,11 @@ local PAGE, SCROLL, LINE = "\f", "\v", "\n"
|
||||
local SEPARATORS = "([^" .. LINE .. PAGE .. SCROLL .. "]*)([" ..
|
||||
LINE .. PAGE .. SCROLL .. "])"
|
||||
local function paginate(text)
|
||||
local pages, rows = {}, {}
|
||||
local pages, rows, carried = {}, {}, {}
|
||||
local function flush(scroll)
|
||||
if #rows > 0 then pages[#pages + 1] = table.concat(rows, LINE) end
|
||||
rows = scroll and { rows[#rows] or "" } or {}
|
||||
if scroll then carried[#pages + 1] = true end
|
||||
end
|
||||
for chunk, sep in (tostring(text or "") .. PAGE):gmatch(SEPARATORS) do
|
||||
rows[#rows + 1] = chunk
|
||||
@@ -239,7 +240,23 @@ local function paginate(text)
|
||||
elseif sep == SCROLL then flush(true) end
|
||||
end
|
||||
if #pages == 0 then pages[1] = tostring(text or "") end
|
||||
return pages
|
||||
return pages, carried
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/text.asm:517-526
|
||||
local function foldPages(pages, carried)
|
||||
local out, outCarried = {}, {}
|
||||
for i, page in ipairs(pages) do
|
||||
local lines = Chrome.wrap(page, TEXT_WIDTH)
|
||||
out[#out + 1] = #lines > TEXT_ROWS
|
||||
and table.concat(lines, LINE, 1, TEXT_ROWS) or page
|
||||
outCarried[#out] = carried[i]
|
||||
for j = TEXT_ROWS + 1, #lines do
|
||||
out[#out + 1] = table.concat(lines, LINE, j - TEXT_ROWS + 1, j)
|
||||
outCarried[#out] = true
|
||||
end
|
||||
end
|
||||
return out, outCarried
|
||||
end
|
||||
|
||||
function BattleState.fillScale(winW, winH)
|
||||
@@ -496,7 +513,7 @@ function BattleState.new(game, opts)
|
||||
-- (core.asm:2978-2980, 3354): this is where the mon's frontpic first
|
||||
-- appears, where ANIM_SEND_OUT_MON plays and where the HUD comes up.
|
||||
self:push({ kind = "send", side = "enemy", mon = enemy,
|
||||
text = Strings("%s sent out %s!", trainerName, self:name(enemy)) })
|
||||
text = Battle.sentOutText(trainerName, self:name(enemy)) })
|
||||
end
|
||||
end
|
||||
local player = self.battle and self.battle.player
|
||||
@@ -1671,6 +1688,12 @@ end
|
||||
|
||||
function BattleState:advanceQueue()
|
||||
local event = table.remove(self.queue, 1)
|
||||
self.messagePages, self.messageCarry = nil, nil
|
||||
local pendingSend = self.pendingSendOut
|
||||
if pendingSend then
|
||||
self.pendingSendOut = nil
|
||||
self:startSendOut(pendingSend.side, pendingSend.mon)
|
||||
end
|
||||
-- StartBattle runs `call z, UpdateEnemyHUD` AFTER BattleStartMessage returns,
|
||||
-- and only for a wild battle (engine/battle/core.asm:7808-7817): the appeared
|
||||
-- line is read against an empty HUD area and the bar comes up on the step
|
||||
@@ -1995,7 +2018,7 @@ function BattleState:advanceQueue()
|
||||
end
|
||||
end
|
||||
if event.text and not event.lineShown then
|
||||
self.message = event.text
|
||||
self:showPages(event.text)
|
||||
self.typedText = nil
|
||||
-- engine/battle/core.asm:8733
|
||||
if self.startHuds then
|
||||
@@ -2111,7 +2134,12 @@ function BattleState:advanceQueue()
|
||||
-- player's voluntary switch through SendOutPlayerMon (:3796). Without it
|
||||
-- the replacement simply appeared, which with two of a species back to back
|
||||
-- reads as one mon growing a second health bar.
|
||||
self:startSendOut(event.side, event.mon)
|
||||
-- ../pokecrystal/engine/battle/core.asm:3146-3147
|
||||
if self.messagePages then
|
||||
self.pendingSendOut = { side = event.side, mon = event.mon }
|
||||
else
|
||||
self:startSendOut(event.side, event.mon)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3470,10 +3498,12 @@ end
|
||||
-- One prompt page in the box, with the rest held for the presses `para` and
|
||||
-- `cont` wait on (home/text.asm:403-448).
|
||||
function BattleState:showPages(text)
|
||||
local pages = paginate(text)
|
||||
local pages, carried = foldPages(paginate(text))
|
||||
self.messagePages = #pages > 1 and pages or nil
|
||||
self.messageCarried = carried
|
||||
self.messagePage = 1
|
||||
self.message = pages[1]
|
||||
self.messageCarry = nil
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
end
|
||||
|
||||
@@ -3483,8 +3513,18 @@ function BattleState:nextPage()
|
||||
local i = self.messagePage + 1
|
||||
self.messagePage = i
|
||||
self.message = pages[i]
|
||||
self.messageCarry = self.messageCarried and self.messageCarried[i]
|
||||
and pages[i] or nil
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
if i >= #pages then self.messagePages = nil end
|
||||
if i >= #pages then
|
||||
self.messagePages = nil
|
||||
-- ../pokecrystal/engine/battle/core.asm:3146-3147
|
||||
local pending = self.pendingSendOut
|
||||
if pending then
|
||||
self.pendingSendOut = nil
|
||||
self:startSendOut(pending.side, pending.mon)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -4209,7 +4249,14 @@ function BattleState:syncTyper()
|
||||
for i = #lines, TEXT_ROWS + 1, -1 do lines[i] = nil end
|
||||
self.typer = Typer.new(self.game)
|
||||
self.typer:start(lines)
|
||||
-- ../pokecrystal/home/text.asm:521-523
|
||||
if self.messageCarry == text and lines[1] then
|
||||
local carried = Typer.new(self.game)
|
||||
carried:start({ lines[1] })
|
||||
self.typer.shown = math.max(self.typer.shown, carried.total)
|
||||
end
|
||||
end
|
||||
self.messageCarry = nil
|
||||
end
|
||||
return self.typer ~= nil and not self.typer:done()
|
||||
end
|
||||
|
||||
@@ -341,10 +341,12 @@ function Chrome.wrap(text, width)
|
||||
end
|
||||
|
||||
-- Print wrapped text from (tx, ty) downward, at most `rows` lines.
|
||||
function Chrome.printWrapped(text, tx, ty, width, rows)
|
||||
-- ../pokecrystal/home/text.asm:473
|
||||
function Chrome.printWrapped(text, tx, ty, width, rows, step)
|
||||
step = step or 1
|
||||
local lines = Chrome.wrap(text, width)
|
||||
for i = 1, math.min(#lines, rows or #lines) do
|
||||
Chrome.print(lines[i], tx, ty + i - 1)
|
||||
Chrome.print(lines[i], tx, ty + (i - 1) * step)
|
||||
end
|
||||
return #lines
|
||||
end
|
||||
|
||||
@@ -111,7 +111,8 @@ function GenderSelect:drawPanel()
|
||||
G.rectangle("fill", 0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
Chrome.textbox(SAY_X, SAY_Y, SAY_W, SAY_H)
|
||||
Chrome.printWrapped(self.text, SAY_TEXT_X, SAY_TEXT_Y, SAY_W, SAY_H - 1)
|
||||
-- ../pokecrystal/home/text.asm:473
|
||||
Chrome.printWrapped(self.text, SAY_TEXT_X, SAY_TEXT_Y, SAY_W, 2, 2)
|
||||
Chrome.box(BOX_X, BOX_Y, BOX_W, BOX_H)
|
||||
for i, option in ipairs(GenderSelect.OPTIONS) do
|
||||
local row = TEXT_Y + (i - 1) * ROW_STEP
|
||||
|
||||
+29
-10
@@ -67,8 +67,9 @@ local MORN_HOUR, DAY_HOUR, NITE_HOUR = 4, 10, 18
|
||||
local DAYS = Clock.DAY_NAMES
|
||||
InitClock.DAYS = DAYS
|
||||
|
||||
function InitClock:wantsFillScale() return true end
|
||||
function InitClock:drawsWidescreen() return true end
|
||||
-- ../pokecrystal/engine/rtc/timeset.asm:385
|
||||
function InitClock:wantsFillScale() return self.mode ~= "day" end
|
||||
function InitClock:drawsWidescreen() return self.mode ~= "day" end
|
||||
|
||||
-- PrintHour (engine/rtc/timeset.asm:672) is GetTimeOfDayString + PlaceString,
|
||||
-- then AdjustHourForAMorPM as a left-aligned two-digit number. So the cart
|
||||
@@ -123,6 +124,8 @@ function InitClock.new(game, opts)
|
||||
local self = setmetatable({}, InitClock)
|
||||
self.game = game
|
||||
self.mode = opts.mode == "day" and "day" or "clock"
|
||||
-- ../pokecrystal/engine/rtc/timeset.asm:385
|
||||
self.isOpaque = self.mode ~= "day"
|
||||
self.save = opts.save or (game and game.save)
|
||||
self.onDone = opts.onDone
|
||||
self.autoConfirm = opts.autoConfirm or false
|
||||
@@ -144,11 +147,24 @@ end
|
||||
|
||||
-- The current question, split into its pages.
|
||||
function InitClock:pages()
|
||||
local question = self:question()
|
||||
if self.pagesText == question and self.pagesCache then
|
||||
return self.pagesCache
|
||||
end
|
||||
local out = {}
|
||||
for page in (self:question() .. "\f"):gmatch("(.-)\f") do
|
||||
if page ~= "" then out[#out + 1] = page end
|
||||
for page in (question .. "\f"):gmatch("(.-)\f") do
|
||||
if page ~= "" then
|
||||
local lines = {}
|
||||
for line in (page .. "\n"):gmatch("(.-)\n") do lines[#lines + 1] = line end
|
||||
-- ../pokecrystal/home/text.asm:502
|
||||
out[#out + 1] = table.concat({ lines[1], lines[2] }, "\n")
|
||||
for i = 3, #lines do
|
||||
out[#out + 1] = table.concat({ lines[i - 1], lines[i] }, "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
if #out == 0 then out[1] = "" end
|
||||
self.pagesText, self.pagesCache = question, out
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -329,7 +345,8 @@ local function arrow(tx, ty, up)
|
||||
end
|
||||
|
||||
function InitClock:drawPanel()
|
||||
Chrome.clear()
|
||||
-- ../pokecrystal/engine/rtc/timeset.asm:22
|
||||
if self.mode ~= "day" then Chrome.clear() end
|
||||
local value = self:display()
|
||||
if value then
|
||||
local bx, by, bw, bh, arrowX, tx, ty = self:pickerBox()
|
||||
@@ -343,12 +360,14 @@ function InitClock:drawPanel()
|
||||
-- The question (and the confirmations) share the bottom textbox every other
|
||||
-- Gold prompt uses.
|
||||
Chrome.textbox(0, 12, 18, 4)
|
||||
Chrome.printWrapped(self:pageText(), 1, 14, 18, 3)
|
||||
-- ../pokecrystal/home/text.asm:473
|
||||
Chrome.printWrapped(self:pageText(), 1, 14, 18, 2, 2)
|
||||
if self:confirming() then
|
||||
Chrome.box(14, 6, 6, 5)
|
||||
Chrome.print(Strings("YES"), 16, 7)
|
||||
Chrome.print(Strings("NO"), 16, 9)
|
||||
Chrome.cursor(15, self.yesNo == 1 and 7 or 9)
|
||||
-- ../pokecrystal/home/menu.asm:418
|
||||
Chrome.box(14, 7, 6, 5)
|
||||
Chrome.print(Strings("YES"), 16, 8)
|
||||
Chrome.print(Strings("NO"), 16, 10)
|
||||
Chrome.cursor(15, self.yesNo == 1 and 8 or 10)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+54
-15
@@ -29,6 +29,7 @@ local SaveMenu = require("src.ui.gen2.SaveMenu")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Sound = require("src.core.Sound")
|
||||
local Strings = require("src.core.Strings")
|
||||
local Typer = require("src.ui.gen2.Typer")
|
||||
|
||||
-- _PCMonHoldingMailText (data/text/common_2.asm), the refusal
|
||||
-- BillsPC_MovePKMNMenu prints instead of opening the list. Two pages, because
|
||||
@@ -202,9 +203,8 @@ end
|
||||
-- engine/menus/save.asm:40 ChangeBoxSaveGame
|
||||
function PcMenu:beginChangeBox(index)
|
||||
self.changeBox = index
|
||||
self.savePhase = "confirm"
|
||||
self:setSavePhase("confirm")
|
||||
self.saveChoice = 1
|
||||
self.saveTimer = 0
|
||||
self.saved = nil
|
||||
local existed = self.saveExists
|
||||
if existed == nil then existed = Save.exists() end
|
||||
@@ -217,15 +217,20 @@ function PcMenu:refuseChangeBox()
|
||||
self.saveTimer = 0
|
||||
end
|
||||
|
||||
function PcMenu:setSavePhase(phase)
|
||||
self.savePhase = phase
|
||||
self.savePage = 1
|
||||
self.saveTimer = 0
|
||||
end
|
||||
|
||||
function PcMenu:acceptChangeBox()
|
||||
if self.saveChoice == 2 then return self:refuseChangeBox() end
|
||||
if self.savePhase == "confirm" and self.existed then
|
||||
self.savePhase = "overwrite"
|
||||
self:setSavePhase("overwrite")
|
||||
self.saveChoice = 1
|
||||
return
|
||||
end
|
||||
self.savePhase = "saving"
|
||||
self.saveTimer = 0
|
||||
self:setSavePhase("saving")
|
||||
end
|
||||
|
||||
-- `pop de / ld a, e / ld [wCurBox], a` sits between SaveBox and
|
||||
@@ -241,32 +246,54 @@ function PcMenu:writeChangeBox()
|
||||
end
|
||||
end
|
||||
|
||||
function PcMenu:savePrompt()
|
||||
function PcMenu:savePromptText()
|
||||
if self.savePhase == "overwrite" then
|
||||
return SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))
|
||||
return Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE)
|
||||
end
|
||||
if self.savePhase == "saving" then
|
||||
return SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))
|
||||
return Strings(SaveMenu.SAVING_PROMPT_SOURCE)
|
||||
end
|
||||
if self.savePhase == "done" then
|
||||
if self.saved then
|
||||
local name = (self.save.player and self.save.player.name) or "GOLD"
|
||||
return SaveMenu.twoLines(Strings("%s saved\nthe game.", name))
|
||||
return Strings("%s saved\nthe game.", name)
|
||||
end
|
||||
return SaveMenu.twoLines(Strings("Could not save."))
|
||||
return Strings("Could not save.")
|
||||
end
|
||||
return SaveMenu.twoLines(Strings(CHANGE_BOX_SAVE_SOURCE))
|
||||
return Strings(CHANGE_BOX_SAVE_SOURCE)
|
||||
end
|
||||
|
||||
function PcMenu:savePages()
|
||||
local text = self:savePromptText()
|
||||
if self.savePagesText ~= text or not self.savePagesCache then
|
||||
self.savePagesText = text
|
||||
self.savePagesCache = SaveMenu.pagesOf(text)
|
||||
end
|
||||
return self.savePagesCache
|
||||
end
|
||||
|
||||
function PcMenu:savePrompt()
|
||||
local pages = self:savePages()
|
||||
return pages[self.savePage or 1] or pages[1]
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/menus/save.asm:209 SaveTheGame_yesorno
|
||||
function PcMenu:saveYesNoVisible(pages)
|
||||
if self.savePhase ~= "confirm" and self.savePhase ~= "overwrite" then
|
||||
return false
|
||||
end
|
||||
return (self.savePage or 1) >= #(pages or self:savePages())
|
||||
end
|
||||
|
||||
function PcMenu:updateChangeBox()
|
||||
Typer.step(self)
|
||||
-- SavingDontTurnOffThePower is DelayFrames, not a prompt: no button does
|
||||
-- anything until the sequence runs out (engine/menus/save.asm:55).
|
||||
if self.savePhase == "saving" then
|
||||
self.saveTimer = self.saveTimer + 1
|
||||
if self.saveTimer >= SaveMenu.SAVING_FRAMES then
|
||||
self:writeChangeBox()
|
||||
self.savePhase = "done"
|
||||
self.saveTimer = 0
|
||||
self:setSavePhase("done")
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -281,6 +308,14 @@ function PcMenu:updateChangeBox()
|
||||
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
-- ../pokecrystal/home/text.asm:502 _ContText
|
||||
if not self:saveYesNoVisible() then
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
self.savePage = (self.savePage or 1) + 1
|
||||
end
|
||||
return
|
||||
end
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.saveChoice = self.saveChoice == 1 and 2 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
@@ -442,10 +477,14 @@ function PcMenu:drawPanel()
|
||||
if self.savePhase then
|
||||
-- ChangeBoxSaveGame's MenuTextbox, then YesNoBox over the box list.
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
local lines = self:savePrompt()
|
||||
local pages = self:savePages()
|
||||
local lines = pages[self.savePage or 1] or pages[1]
|
||||
Chrome.print(lines[1] or "", 1, 14)
|
||||
Chrome.print(lines[2] or "", 1, 16)
|
||||
if self.savePhase == "confirm" or self.savePhase == "overwrite" then
|
||||
if (self.savePage or 1) < #pages and Typer.arrowOn(self) then
|
||||
Chrome.print(SaveMenu.DOWN_ARROW, SaveMenu.ARROW_X, SaveMenu.ARROW_Y)
|
||||
end
|
||||
if self:saveYesNoVisible(pages) then
|
||||
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
||||
Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1)
|
||||
Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3)
|
||||
|
||||
+80
-29
@@ -24,7 +24,6 @@
|
||||
-- one-line call.
|
||||
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Save = require("src.core.gen2.Save")
|
||||
local Sound = require("src.core.Sound")
|
||||
local Strings = require("src.core.Strings")
|
||||
@@ -64,7 +63,7 @@ local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
||||
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
|
||||
-- SavingDontTurnOffThePower's own line -- one \n-joined translatable key
|
||||
-- each, used both by this screen's own prompt() below and, through the
|
||||
-- SOURCE/twoLines() exports at the bottom of this file, by the PC's CHANGE
|
||||
-- SOURCE/pagesOf() exports at the bottom of this file, by the PC's CHANGE
|
||||
-- BOX save (src/ui/gen2/PcMenu.lua:savePrompt()), which shares these exact
|
||||
-- same two cart messages. One key per prompt lets a translation write one
|
||||
-- whole, freely reordered sentence instead of two fragments translated in
|
||||
@@ -77,27 +76,46 @@ local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
||||
-- string harvester only recognizes a literal inside Strings.source(...),
|
||||
-- not a computed expression, so a concat call here would quietly never
|
||||
-- reach a translator.
|
||||
local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it")
|
||||
-- ../pokecrystal/data/text/common_3.asm:202 _AlreadyASaveFileText
|
||||
local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it\vOK to overwrite?")
|
||||
local SAVING_PROMPT_SOURCE = Strings.source("SAVING… DON'T TURN\nOFF THE POWER.")
|
||||
|
||||
-- Splits a translated "line one\nline two" string back into the two-slot
|
||||
-- table drawPanel's fixed Chrome.print calls expect. No "\n" at all (a
|
||||
-- single-line message, or German's one-line SAVING prompt) lands whole on
|
||||
-- the first slot, matching the untranslated code's own { text, "" } shape.
|
||||
--
|
||||
-- Only the first "\n" splits, since this box has room for exactly two
|
||||
-- lines. A third line would otherwise draw as a raw newline byte -- garbage
|
||||
-- glyph data -- with no other sign anything went wrong, so this warns once
|
||||
-- per string instead.
|
||||
local warnedTooManyLines = {}
|
||||
local function twoLines(text)
|
||||
local first, second = text:match("^(.-)\n(.*)$")
|
||||
if second and second:find("\n", 1, true) and not warnedTooManyLines[text] then
|
||||
warnedTooManyLines[text] = true
|
||||
Logger.warn("SaveMenu: translation of %q has more than two lines; " ..
|
||||
"only the first two fit this box", text)
|
||||
-- ../pokecrystal/home/text.asm:630 LoadBlinkingCursor
|
||||
local DOWN_ARROW = "\xe2\x96\xbc"
|
||||
local ARROW_X, ARROW_Y = 18, 17
|
||||
|
||||
-- ../pokecrystal/home/text.asm:479 Paragraph
|
||||
-- ../pokecrystal/home/text.asm:520 _ContTextNoPause
|
||||
local function pagesOf(body)
|
||||
local pages = {}
|
||||
for chunk in (tostring(body) .. "\f"):gmatch("(.-)\f") do
|
||||
local flat, pos, scrolled = {}, 1, false
|
||||
while true do
|
||||
local brk = chunk:find("[\n\v]", pos)
|
||||
local line = brk and chunk:sub(pos, brk - 1) or chunk:sub(pos)
|
||||
if line ~= "" then flat[#flat + 1] = { line, scrolled } end
|
||||
if not brk then break end
|
||||
scrolled = chunk:sub(brk, brk) == "\v"
|
||||
pos = brk + 1
|
||||
end
|
||||
local page
|
||||
for _, entry in ipairs(flat) do
|
||||
if not page then
|
||||
page = { entry[1] }
|
||||
pages[#pages + 1] = page
|
||||
elseif entry[2] then
|
||||
page = { page[#page], entry[1] }
|
||||
pages[#pages + 1] = page
|
||||
elseif #page >= 2 then
|
||||
page = { entry[1] }
|
||||
pages[#pages + 1] = page
|
||||
else
|
||||
page[#page + 1] = entry[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
return { first or text, second or "" }
|
||||
if #pages == 0 then pages[1] = { "" } end
|
||||
return pages
|
||||
end
|
||||
|
||||
function SaveMenu:wantsFillScale() return true end
|
||||
@@ -132,6 +150,13 @@ function SaveMenu:playSfx(id)
|
||||
SaveMenu.playSaveSfx(self.game, id)
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/joypad.asm:392
|
||||
function SaveMenu:playSfxNamed(name)
|
||||
local data = self.game and self.game.data
|
||||
local sfx = data and data.audio and data.audio.sfx
|
||||
if sfx and sfx[Sound.resolve(data, name)] then Sound.play(data, name) end
|
||||
end
|
||||
|
||||
function SaveMenu:finish(saved)
|
||||
if self.onDone then self.onDone(saved) end
|
||||
end
|
||||
@@ -152,9 +177,11 @@ function SaveMenu:enterPhase(phase)
|
||||
self.phase = phase
|
||||
self.timer = 0
|
||||
self.typedPhase = phase
|
||||
self.pages = pagesOf(self:promptText())
|
||||
self.page = 1
|
||||
local pinned = (phase == "saving" or phase == "done") and "MID" or nil
|
||||
self.typer = Typer.new(self.game, { speed = pinned })
|
||||
self.typer:start(self:prompt())
|
||||
self.typer:start(self.pages[1])
|
||||
end
|
||||
|
||||
function SaveMenu:accept()
|
||||
@@ -215,6 +242,15 @@ function SaveMenu:update(_dt)
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
if Typer.typing(self) then return end
|
||||
-- ../pokecrystal/home/text.asm:502 _ContText
|
||||
if self.page < #self.pages then
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.page = self.page + 1
|
||||
self.typer:start(self.pages[self.page])
|
||||
self:playSfxNamed("Sfx_ReadText2")
|
||||
end
|
||||
return
|
||||
end
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.choice = self.choice == 1 and 2 or 1
|
||||
return
|
||||
@@ -230,28 +266,36 @@ end
|
||||
-- The two lines the speech box holds, in the cart's own wording. `line` puts
|
||||
-- the second one on the box's lower line; `cont` scrolls, which the overwrite
|
||||
-- prompt uses for its third line and which this shows as a second page.
|
||||
function SaveMenu:prompt()
|
||||
function SaveMenu:promptText()
|
||||
if self.phase == "overwrite" then
|
||||
-- AlreadyASaveFileText when the file is this player's; AnotherSaveFileText
|
||||
-- when the ID differs. Only the first can happen here.
|
||||
return twoLines(Strings(OVERWRITE_PROMPT_SOURCE))
|
||||
return Strings(OVERWRITE_PROMPT_SOURCE)
|
||||
end
|
||||
if self.phase == "saving" then
|
||||
return twoLines(Strings(SAVING_PROMPT_SOURCE))
|
||||
return Strings(SAVING_PROMPT_SOURCE)
|
||||
end
|
||||
if self.phase == "done" then
|
||||
if self.saved then
|
||||
return twoLines(Strings("%s saved\nthe game.", self:playerName()))
|
||||
return Strings("%s saved\nthe game.", self:playerName())
|
||||
end
|
||||
return twoLines(Strings("Could not save."))
|
||||
return Strings("Could not save.")
|
||||
end
|
||||
return twoLines(Strings("Would you like to\nsave the game?"))
|
||||
return Strings("Would you like to\nsave the game?")
|
||||
end
|
||||
|
||||
function SaveMenu:prompt()
|
||||
if self.typedPhase == self.phase and self.pages then
|
||||
return self.pages[self.page] or self.pages[1]
|
||||
end
|
||||
return pagesOf(self:promptText())[1]
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/menus/save.asm:209 SaveTheGame_yesorno
|
||||
function SaveMenu:yesNoVisible()
|
||||
if self.phase ~= "confirm" and self.phase ~= "overwrite" then return false end
|
||||
return self.typedPhase == self.phase and not Typer.typing(self)
|
||||
if self.typedPhase ~= self.phase or Typer.typing(self) then return false end
|
||||
return self.page >= #self.pages
|
||||
end
|
||||
|
||||
function SaveMenu:drawPanel()
|
||||
@@ -278,6 +322,11 @@ function SaveMenu:drawPanel()
|
||||
Chrome.print(lines[1] or "", 1, 14)
|
||||
Chrome.print(lines[2] or "", 1, 16)
|
||||
|
||||
if self.typedPhase == self.phase and not Typer.typing(self)
|
||||
and self.page < #self.pages and Typer.arrowOn(self) then
|
||||
Chrome.print(DOWN_ARROW, ARROW_X, ARROW_Y)
|
||||
end
|
||||
|
||||
if self:yesNoVisible() then
|
||||
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
||||
Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1)
|
||||
@@ -307,6 +356,8 @@ SaveMenu.SAVED_TAIL_FRAMES = SAVED_TAIL_FRAMES
|
||||
SaveMenu.SAVED_FRAMES = SAVED_FRAMES
|
||||
SaveMenu.OVERWRITE_PROMPT_SOURCE = OVERWRITE_PROMPT_SOURCE
|
||||
SaveMenu.SAVING_PROMPT_SOURCE = SAVING_PROMPT_SOURCE
|
||||
SaveMenu.twoLines = twoLines
|
||||
SaveMenu.pagesOf = pagesOf
|
||||
SaveMenu.DOWN_ARROW = DOWN_ARROW
|
||||
SaveMenu.ARROW_X, SaveMenu.ARROW_Y = ARROW_X, ARROW_Y
|
||||
|
||||
return SaveMenu
|
||||
|
||||
+30
-1
@@ -359,6 +359,26 @@ function NPC:scriptStep(dir)
|
||||
return true
|
||||
end
|
||||
|
||||
-- engine/overworld/movement.asm:741
|
||||
function NPC:scriptJump(dir)
|
||||
if self.moving then return false end
|
||||
self.stepDir = dir or self.facing
|
||||
if not self.fixedFacing and not self.sliding then
|
||||
self.facing = self.stepDir
|
||||
end
|
||||
local d = Map.DELTA[self.stepDir]
|
||||
if not d then
|
||||
self.stepDir = nil
|
||||
return false
|
||||
end
|
||||
self.targetX, self.targetY = self.cellX + d[1] * 2, self.cellY + d[2] * 2
|
||||
self.moving, self.jumping = true, true
|
||||
self.inGrass, self.grassShake = false, nil
|
||||
self.progress = 0
|
||||
self.frozen = true
|
||||
return true
|
||||
end
|
||||
|
||||
-- StepFunction_TeleportFrom / _TeleportTo (engine/overworld/map_objects.asm).
|
||||
-- `from` is sixteen frames of OBJECT_ACTION_SPIN on the spot and then sixteen
|
||||
-- more spinning while OBJECT_JUMP_HEIGHT walks OBJECT_SPRITE_Y_OFFSET up a
|
||||
@@ -575,7 +595,7 @@ function NPC:update(map, entities)
|
||||
if self.moving then
|
||||
-- NormalStep's begin-of-step grass work (engine/overworld/movement.asm:657-674);
|
||||
-- UpdateTallGrassFlags only RE-tests while IN_GRASS is set (map_objects.asm:226).
|
||||
if self.progress == 0 and map then
|
||||
if self.progress == 0 and map and not self.jumping then
|
||||
local grass = NPC.grassAt(map, self.targetX, self.targetY)
|
||||
if self.inGrass then self.inGrass = grass end
|
||||
self.grassShake = grass or nil
|
||||
@@ -585,17 +605,26 @@ function NPC:update(map, entities)
|
||||
-- a two-cell move over one step (src/world/gen2/Player.lua:150 does the
|
||||
-- same), and `stepFrames` is what lets it keep pace with a bike.
|
||||
local frames = self.stepFrames or STEP_FRAMES
|
||||
-- engine/overworld/map_objects.asm:1129
|
||||
if self.jumping then frames = STEP_FRAMES * 2 end
|
||||
local moved = math.floor(self.progress * 16 / frames)
|
||||
local dx = (self.targetX or self.cellX) - self.cellX
|
||||
local dy = (self.targetY or self.cellY) - self.cellY
|
||||
self.px = self.cellX * 16 + dx * moved
|
||||
self.py = self.cellY * 16 + dy * moved
|
||||
if self.jumping then
|
||||
self.spriteYOffset = Movement.jumpYOffset(self.progress, frames)
|
||||
end
|
||||
if self.progress >= frames then
|
||||
self.cellX, self.cellY = self.targetX, self.targetY
|
||||
self.targetX, self.targetY = nil, nil
|
||||
self.px, self.py = self.cellX * 16, self.cellY * 16
|
||||
self.moving = false
|
||||
self.stepDir = nil
|
||||
if self.jumping then
|
||||
self.jumping = nil
|
||||
self.spriteYOffset = 0
|
||||
end
|
||||
self.stepFlip = not self.stepFlip
|
||||
-- CopyCoordsTileToLastCoordsTile -> SetTallGrassFlags at the step's end
|
||||
-- (map_objects.asm:196-208, :247).
|
||||
|
||||
+20
-18
@@ -2,6 +2,7 @@
|
||||
-- Draws via shared SpriteRenderer (same 16x96 facing layout as Gen 1).
|
||||
|
||||
local Map = require("src.world.gen2.Map")
|
||||
local Movement = require("src.script.gen2.Movement")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
|
||||
@@ -17,12 +18,6 @@ local TURN_FRAMES = 4
|
||||
-- at the walking rate, which is what stops a bike step flickering the legs.
|
||||
Player.STEP_FRAMES = STEP_FRAMES
|
||||
|
||||
-- engine/overworld/map_objects.asm:1815
|
||||
local JUMP_Y = {
|
||||
-4, -6, -8, -10, -11, -12, -12, -12,
|
||||
-11, -10, -9, -8, -6, -4, 0, 0,
|
||||
}
|
||||
|
||||
-- FacingFish*'s loose rod OAM, offset from the sprite's top-left, and which
|
||||
-- 8x8 of the sheet's rod row it draws (data/sprites/facings.asm:122-152).
|
||||
local ROD_OAM = {
|
||||
@@ -149,8 +144,9 @@ function Player:scriptStep(dir)
|
||||
-- A scripted step names its own STEP_* on the cart (SurfStartStep is a slow
|
||||
-- step), so it never inherits the bike's shorter one.
|
||||
self.stepFrames = STEP_FRAMES
|
||||
self.facing = dir or self.facing
|
||||
local d = Map.DELTA[self.facing]
|
||||
-- engine/overworld/map_objects.asm:284-294
|
||||
if not self.fixedFacing then self.facing = dir or self.facing end
|
||||
local d = Map.DELTA[dir or self.facing]
|
||||
if not d then return false end
|
||||
self.targetX, self.targetY = self.cellX + d[1], self.cellY + d[2]
|
||||
self.moving = true
|
||||
@@ -159,6 +155,21 @@ function Player:scriptStep(dir)
|
||||
return true
|
||||
end
|
||||
|
||||
-- engine/overworld/movement.asm:741
|
||||
function Player:scriptJump(dir)
|
||||
if self.moving then return false end
|
||||
if not self.fixedFacing then self.facing = dir or self.facing end
|
||||
local d = Map.DELTA[dir or self.facing]
|
||||
if not d then return false end
|
||||
self.targetX, self.targetY = self.cellX + d[1] * 2, self.cellY + d[2] * 2
|
||||
self.moving, self.jumping = true, true
|
||||
self.bumpFrames = nil
|
||||
self.inGrass, self.grassShake = false, nil
|
||||
self.progress = 0
|
||||
self.stepFrames = STEP_FRAMES * 2
|
||||
return true
|
||||
end
|
||||
|
||||
-- CounterclockwiseSpinAction's .facings, seeded from the current direction by
|
||||
-- Movement_step_dig -- map_object_action.asm:96-152, movement.asm:113-116
|
||||
local SPIN_FACINGS = { "down", "right", "up", "left" }
|
||||
@@ -235,16 +246,7 @@ function Player:update()
|
||||
if self.jumping then
|
||||
-- engine/overworld/map_objects.asm:1796 -- one table entry per cart frame,
|
||||
-- tweened across our doubled step (#1713)
|
||||
local t = (self.progress - 1) * (#JUMP_Y - 1)
|
||||
/ math.max(frames - 1, 1) + 1
|
||||
local idx = math.floor(t)
|
||||
if idx < 1 then idx = 1 end
|
||||
if idx >= #JUMP_Y then
|
||||
self.spriteYOffset = JUMP_Y[#JUMP_Y]
|
||||
else
|
||||
self.spriteYOffset = math.floor(
|
||||
JUMP_Y[idx] + (JUMP_Y[idx + 1] - JUMP_Y[idx]) * (t - idx) + 0.5)
|
||||
end
|
||||
self.spriteYOffset = Movement.jumpYOffset(self.progress, frames)
|
||||
end
|
||||
if self.progress >= frames then
|
||||
self.cellX, self.cellY = self.targetX, self.targetY
|
||||
|
||||
@@ -1941,6 +1941,11 @@ end
|
||||
-- command -- unlike a plain `setevent`, which the cart only reads back on the
|
||||
-- next map load.
|
||||
function World:appearObject(objectId)
|
||||
-- home/map_objects.asm:309
|
||||
if objectId == 0 then
|
||||
self.playerMasked = nil
|
||||
return
|
||||
end
|
||||
local index = (objectId or 0) - 1
|
||||
local def = self.map and self.map.def
|
||||
local obj = def and def.objects and def.objects[index]
|
||||
@@ -3522,6 +3527,8 @@ function World:runMapSetup(method, load, fly)
|
||||
self:roamMonsBeforeLoad(method)
|
||||
local wrapped = function()
|
||||
local ok = load()
|
||||
-- engine/overworld/map_objects_2.asm:1
|
||||
self.playerMasked = nil
|
||||
self:roamMonsAfterLoad(method)
|
||||
return ok
|
||||
end
|
||||
@@ -3893,6 +3900,11 @@ function World:turnObject(objectId, facing)
|
||||
end
|
||||
|
||||
function World:disappearObject(objectId)
|
||||
-- home/map_objects.asm:317
|
||||
if objectId == 0 then
|
||||
self.playerMasked = true
|
||||
return
|
||||
end
|
||||
local index = (objectId or 0) - 1
|
||||
local def = self.map and self.map.def
|
||||
local obj = def and def.objects and def.objects[index]
|
||||
@@ -4021,9 +4033,7 @@ function World:updateMovement()
|
||||
return
|
||||
end
|
||||
if ent.moving then return end
|
||||
-- StepFunction_NPCJump's `.Land` beat (engine/overworld/map_objects.asm:1150):
|
||||
-- `.Jump` already walked one cell and ran GetNextTile again, so the second
|
||||
-- cell belongs to the jump and not to the next movement byte.
|
||||
-- engine/overworld/map_objects.asm:1150
|
||||
if st.pendingStep then
|
||||
local dir = st.pendingStep
|
||||
st.pendingStep = nil
|
||||
@@ -4083,7 +4093,9 @@ function World:updateMovement()
|
||||
-- GetNextTile only record the tile for the grass flag and never block
|
||||
-- scripted movement.
|
||||
local fromX, fromY = ent.cellX, ent.cellY
|
||||
if ent:scriptStep(act.dir) then
|
||||
if ent.scriptJump and ent:scriptJump(act.dir) then
|
||||
self:followStep(ent, fromX, fromY)
|
||||
elseif not ent.scriptJump and ent:scriptStep(act.dir) then
|
||||
st.pendingStep = act.dir
|
||||
self:followStep(ent, fromX, fromY)
|
||||
end
|
||||
@@ -9721,6 +9733,7 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
-- A follow pairing points at two live objects, and a map load rebuilds them
|
||||
-- (RefreshMapSprites); nothing on the cart survives that either.
|
||||
self.followState = nil
|
||||
self.playerMasked = nil
|
||||
-- EnterMap's SetUpFiveStepWildEncounterCooldown (engine/overworld/events.asm:
|
||||
-- 110, :367-370): four encounter-free steps after every map entry.
|
||||
self.wildCooldown = 5
|
||||
@@ -11102,7 +11115,7 @@ function World:drawPeople(s, billboard)
|
||||
local filter = self.spriteFilter
|
||||
local drawList = {}
|
||||
if not hideAll then
|
||||
if not hidePlayer then
|
||||
if not hidePlayer and not self.playerMasked then
|
||||
drawList[1] = { kind = "player", py = p.py, ox = 0, oy = 0 }
|
||||
end
|
||||
for _, npc in ipairs(self.npcs) do
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
-- ../pokecrystal/engine/rtc/timeset.asm:385
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function say(line) print("[2138] " .. line) end
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
say((cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
local function tap(button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 4)
|
||||
end
|
||||
|
||||
local function dayWheel()
|
||||
local top = game.stack and game.stack:top()
|
||||
if top and top.mode == "day" and top.pickerBox then return top end
|
||||
return nil
|
||||
end
|
||||
|
||||
U.wait(60)
|
||||
local world = game.world
|
||||
if not (world and world.map) then
|
||||
say("FAIL the gen 2 world did not boot")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
-- ../pokecrystal/maps/PlayersHouse1F.asm:395
|
||||
world:warpToMapId("PLAYERS_HOUSE_1F", 9, 1, "down")
|
||||
U.wait(30)
|
||||
ok(world.map and world.map.id == "PLAYERS_HOUSE_1F", "standing at the top of the stairs")
|
||||
for _ = 1, 3 do tap("down", 10) end
|
||||
U.wait(30)
|
||||
U.shot(game, SHOT_DIR .. "/2138_scene_start.png")
|
||||
|
||||
local wheel
|
||||
for _ = 1, 120 do
|
||||
wheel = dayWheel()
|
||||
if wheel then break end
|
||||
tap("a", 6)
|
||||
end
|
||||
if not wheel then
|
||||
say("the Mom scene did not reach SetDayOfWeek; pushing the wheel directly")
|
||||
world:setDayOfWeek(function() end)
|
||||
U.wait(10)
|
||||
wheel = dayWheel()
|
||||
end
|
||||
ok(wheel ~= nil, "the day-of-week wheel is up")
|
||||
if wheel then
|
||||
ok(wheel.isOpaque == false, "and it is an overlay, not an opaque page")
|
||||
ok(not wheel:drawsWidescreen(), "with no widescreen surround of its own")
|
||||
end
|
||||
U.wait(10)
|
||||
U.shot(game, SHOT_DIR .. "/2138_day_wheel.png")
|
||||
say("eyeball 2138_day_wheel.png: the living room, Mom and the player stay "
|
||||
.. "visible around the SUNDAY picker and the question box")
|
||||
|
||||
tap("a", 20)
|
||||
U.shot(game, SHOT_DIR .. "/2138_confirm.png")
|
||||
say("eyeball 2138_confirm.png: YES/NO box at (14,7), YES on row 8, NO on row 10")
|
||||
tap("a", 30)
|
||||
ok(dayWheel() == nil, "YES pops the wheel")
|
||||
U.shot(game, SHOT_DIR .. "/2138_after.png")
|
||||
|
||||
say(fails == 0 and "all claims passed" or (fails .. " claims failed"))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
local EVENT_LANCES_ROOM_ENTRANCE_CLOSED = 785
|
||||
local EVENT_LANCES_ROOM_OAK_AND_MARY = 1887
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function say(line) print("[2135] " .. line) end
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
say((cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
U.wait(60)
|
||||
local world = game.world
|
||||
if not (world and world.map and world.vm and game.save) then
|
||||
say("FAIL the gen 2 world did not boot")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
game.save.party = { Mon.new(game.data, "TYPHLOSION", 100) }
|
||||
world.events:set(EVENT_LANCES_ROOM_ENTRANCE_CLOSED, true)
|
||||
world.events:set(EVENT_LANCES_ROOM_OAK_AND_MARY, true)
|
||||
world.mapScenes = world.mapScenes or {}
|
||||
world.mapScenes.LANCES_ROOM = 1
|
||||
world:warpToMapId("LANCES_ROOM", 4, 6, "up")
|
||||
for _ = 1, 120 do if not world:busy() then break end U.wait(1) end
|
||||
U.wait(10)
|
||||
ok(world.map and world.map.id == "LANCES_ROOM", "standing in Lance's room")
|
||||
|
||||
U.hold(game, "up", 8)
|
||||
for _ = 1, 120 do
|
||||
if world.vm:running() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(world.vm:running(), "the (4,5) coord event started the approach script")
|
||||
|
||||
local screen
|
||||
for _ = 1, 1200 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then screen = top break end
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
end
|
||||
ok(screen ~= nil, "the Lance battle opened")
|
||||
if not screen then
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
for _ = 1, 6000 do
|
||||
if game.stack:top() ~= screen then break end
|
||||
for _, m in ipairs(screen.battle.enemyParty or {}) do m.hp = 0 end
|
||||
if screen.battle.enemy then screen.battle.enemy.hp = 0 end
|
||||
U.tap(game, "a"); U.wait(2)
|
||||
end
|
||||
ok(game.stack:top() ~= screen, "Lance went down")
|
||||
|
||||
local masked, maskedShot, unmaskedInHof, shots = false, false, false, 0
|
||||
local frames = 0
|
||||
for _ = 1, 6000 do
|
||||
frames = frames + 1
|
||||
if world.map and world.map.id == "LANCES_ROOM" and world.playerMasked then
|
||||
masked = true
|
||||
if not maskedShot and not world.mapSetup then
|
||||
maskedShot = true
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2135_01_player_gone.png")
|
||||
say("the shot must show an EMPTY doorway while Mary runs up")
|
||||
end
|
||||
end
|
||||
if world.map and world.map.id == "HALL_OF_FAME" then
|
||||
if not world.mapSetup then
|
||||
unmaskedInHof = not world.playerMasked
|
||||
U.wait(30)
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2135_02_hof.png")
|
||||
break
|
||||
end
|
||||
elseif masked and shots < 3 and frames % 30 == 0 then
|
||||
shots = shots + 1
|
||||
U.shot(game, string.format("%s/crystal_bug2135_mary_%d.png", SHOT_DIR, shots))
|
||||
end
|
||||
if frames % 6 == 0 then U.tap(game, "a") else U.wait(1) end
|
||||
end
|
||||
ok(masked, "disappear PLAYER masked the player before the warp")
|
||||
ok(world.map and world.map.id == "HALL_OF_FAME", "warpfacing reached HALL_OF_FAME")
|
||||
ok(unmaskedInHof, "and the load respawned the player there")
|
||||
|
||||
say(fails == 0 and "all claims passed" or (fails .. " claims failed"))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function say(line) print("[2130] " .. line) end
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
say((cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
U.wait(60)
|
||||
local world = game.world
|
||||
if not (world and world.map and world.vm and game.save) then
|
||||
say("FAIL the crystal world did not boot")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
game.save.party = { Mon.new(game.data, "TYPHLOSION", 90) }
|
||||
world.mapScenes = world.mapScenes or {}
|
||||
world.mapScenes.WILLS_ROOM = 1
|
||||
world:warpToMapId("WILLS_ROOM", 5, 8, "up")
|
||||
for _ = 1, 120 do if not world:busy() then break end U.wait(1) end
|
||||
U.wait(10)
|
||||
ok(world.map and world.map.id == "WILLS_ROOM", "standing in Will's room")
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2130_00_room.png")
|
||||
|
||||
local started = world.vm:start({
|
||||
{ op = "loadtrainer", class = 11, member = 1 },
|
||||
{ op = "startbattle" },
|
||||
{ op = "reloadmapafterbattle" },
|
||||
{ op = "opentext" },
|
||||
{ op = "writetext", text = "60:4644" },
|
||||
{ op = "waitbutton" },
|
||||
{ op = "closetext" },
|
||||
{ op = "end" },
|
||||
})
|
||||
ok(started, "the battle-then-text script started")
|
||||
|
||||
local screen
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then screen = top break end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(screen ~= nil, "the trainer battle opened")
|
||||
if not screen then
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
for _ = 1, 3000 do
|
||||
if game.stack:top() ~= screen then break end
|
||||
for _, m in ipairs(screen.battle.enemyParty or {}) do m.hp = 0 end
|
||||
if screen.battle.enemy then screen.battle.enemy.hp = 0 end
|
||||
U.tap(game, "a"); U.wait(2)
|
||||
end
|
||||
ok(game.stack:top() ~= screen, "the battle screen popped")
|
||||
say("battle popped at frame " .. U.frame() .. " fade=" .. tostring(world.fade)
|
||||
.. " mapSetup=" .. tostring(world.mapSetup and world.mapSetup.phase))
|
||||
|
||||
local sawFade, boxFrame, boxFade, boxSetup = false, nil, nil, nil
|
||||
for _ = 1, 240 do
|
||||
if world.mapSetup then sawFade = true end
|
||||
local top = game.stack:top()
|
||||
if world.textbox or (top and top.text and not top.battle) then
|
||||
boxFrame = U.frame()
|
||||
boxFade, boxSetup = world.fade, world.mapSetup and world.mapSetup.phase
|
||||
break
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(sawFade, "FadeInFromWhite ran after the battle")
|
||||
ok(boxFrame ~= nil, "the after-battle text box opened")
|
||||
ok(boxFade == nil, "no white sheet under the text box (fade="
|
||||
.. tostring(boxFade) .. ")")
|
||||
ok(boxSetup == nil, "the map setup chain finished before the box (phase="
|
||||
.. tostring(boxSetup) .. ")")
|
||||
U.wait(4)
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2130_01_text.png")
|
||||
say("the shot must show Will's room and both sprites behind the text box")
|
||||
U.wait(20)
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2130_02_text.png")
|
||||
|
||||
for _ = 1, 300 do
|
||||
if not world.vm:running() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(4)
|
||||
end
|
||||
ok(not world.vm:running(), "the script ran to completion")
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2130_03_end.png")
|
||||
|
||||
say(fails == 0 and "all claims passed" or (fails .. " claims failed"))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
local EVENT_RIVAL_NEW_BARK_TOWN = 1725
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function say(line) print("[2136] " .. line) end
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
say((cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
U.wait(60)
|
||||
local world = game.world
|
||||
if not (world and world.map and world.vm and game.save) then
|
||||
say("FAIL the gen 2 world did not boot")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
world.events:set(EVENT_RIVAL_NEW_BARK_TOWN, false)
|
||||
world:warpToMapId("NEW_BARK_TOWN", 3, 3, "up")
|
||||
for _ = 1, 120 do if not world:busy() then break end U.wait(1) end
|
||||
U.wait(10)
|
||||
ok(world.map and world.map.id == "NEW_BARK_TOWN", "standing under the rival")
|
||||
local rival
|
||||
for _, npc in ipairs(world.npcs or {}) do
|
||||
if npc.cellX == 3 and npc.cellY == 2 then rival = npc end
|
||||
end
|
||||
ok(rival ~= nil, "the rival is at (3,2)")
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2136_00_before.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 60 do
|
||||
if world.vm:running() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(world.vm:running(), "talking to the rival started NewBarkTownRivalScript")
|
||||
|
||||
local p = world.player
|
||||
local lowest, jumpFrames, shot, pushedTo = 0, 0, false, nil
|
||||
local facingDuringJump
|
||||
for _ = 1, 1800 do
|
||||
if not world.vm:running() then break end
|
||||
if p.jumping then
|
||||
jumpFrames = jumpFrames + 1
|
||||
lowest = math.min(lowest, p.spriteYOffset or 0)
|
||||
facingDuringJump = facingDuringJump or p.facing
|
||||
if not shot and (p.spriteYOffset or 0) <= -11 then
|
||||
shot = true
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2136_01_airborne.png")
|
||||
say("the shot must show the player lifted off the ground with a shadow under them")
|
||||
end
|
||||
elseif p.moving and not pushedTo then
|
||||
pushedTo = p.targetY
|
||||
end
|
||||
local top = game.stack:top()
|
||||
if (world.textbox or (top and top.text)) and jumpFrames == 0 then
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
ok(not world.vm:running(), "the rival script ran to completion")
|
||||
ok(pushedTo == 4, "the push walks the player one cell down (target="
|
||||
.. tostring(pushedTo) .. ")")
|
||||
ok(jumpFrames > 0, "jump_step DOWN set the player jumping")
|
||||
ok(jumpFrames >= 28 and jumpFrames <= 34,
|
||||
"for one doubled step (" .. jumpFrames .. " frames)")
|
||||
ok(lowest == -12, "the arc peaks twelve pixels up (lowest=" .. lowest .. ")")
|
||||
ok(facingDuringJump == "up", "under fix_facing the player keeps facing up (facing="
|
||||
.. tostring(facingDuringJump) .. ")")
|
||||
ok(p.cellX == 3 and p.cellY == 6, "and lands at (3,6) (at "
|
||||
.. p.cellX .. "," .. p.cellY .. ")")
|
||||
ok(p.spriteYOffset == 0 and not p.jumping, "on the ground again")
|
||||
U.shot(game, SHOT_DIR .. "/crystal_bug2136_02_landed.png")
|
||||
|
||||
say(fails == 0 and "all claims passed" or (fails .. " claims failed"))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
-- POKEPORT_IDENTITY=crystal-2134 POKEPORT_VERSION=crystal \
|
||||
-- POKEPORT_DRIVER=tests/drivers/crystal_sendout_name_bug2134.lua \
|
||||
-- POKEPORT_SHOT_DIR=/tmp/c2134 love .
|
||||
-- ../pokecrystal/data/text/battle.asm:240-246
|
||||
-- ../pokecrystal/engine/battle/core.asm:3146-3147
|
||||
-- ../pokecrystal/home/text.asm:502-526
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/c2134"
|
||||
local fails = 0
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
print("[sendout-2134] " .. (cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
local function lines(screen)
|
||||
return table.concat(screen:messageLines(), "|")
|
||||
end
|
||||
local function sendStarted(screen)
|
||||
return screen.afterSendOut ~= nil or screen.showEnemyHud == true
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "the crystal world did not boot")
|
||||
|
||||
local lead = Mon.new(game.data, "DRAGONITE", 47)
|
||||
assert(lead and #lead.moves > 0, "could not build a DRAGONITE")
|
||||
for i, move in ipairs(lead.moves) do
|
||||
local def = game.data.moves[move.id]
|
||||
if def and (def.power or 0) > 0 then
|
||||
table.remove(lead.moves, i)
|
||||
table.insert(lead.moves, 1, move)
|
||||
break
|
||||
end
|
||||
end
|
||||
game.save.party = { lead }
|
||||
game.save.inventory = { POKE_BALL = 5 }
|
||||
local trainer = {
|
||||
class = 16, classId = "CHAMPION", memberId = "LANCE",
|
||||
name = "CHAMPION LANCE", trainerName = "LANCE", className = "CHAMPION",
|
||||
party = { Mon.new(game.data, "GYARADOS", 44),
|
||||
Mon.new(game.data, "DRAGONITE", 61) },
|
||||
baseMoney = 25,
|
||||
}
|
||||
assert(world:startBattle({ trainer = trainer }), "startBattle failed")
|
||||
local screen
|
||||
for _ = 1, 600 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then screen = top break end
|
||||
U.wait(1)
|
||||
end
|
||||
assert(screen and screen.battle, "battle screen never came up")
|
||||
|
||||
local function settle()
|
||||
for _ = 1, 600 do
|
||||
if screen.typer and screen.typer:done() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function awaitSendOut(name, tag)
|
||||
for _ = 1, 2400 do
|
||||
local m = screen.message
|
||||
if m and m:find("sent out", 1, true) and screen.messagePages then
|
||||
settle()
|
||||
local first = lines(screen)
|
||||
ok(first == "CHAMPION LANCE|sent out",
|
||||
tag .. " page 1 reads CHAMPION LANCE / sent out: " .. first)
|
||||
ok(not sendStarted(screen) and screen.pendingSendOut ~= nil,
|
||||
tag .. " the ball is still shut on page 1")
|
||||
U.shot(game, out .. "/" .. tag .. "-page1.png")
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
local early = screen:messageLines()
|
||||
ok(early[1] == "sent out",
|
||||
tag .. " 'sent out' is already on row 1 of page 2")
|
||||
settle()
|
||||
local second = lines(screen)
|
||||
ok(second == "sent out|" .. name .. "!",
|
||||
tag .. " page 2 reads sent out / " .. name .. "!: " .. second)
|
||||
ok(sendStarted(screen), tag .. " ANIM_SEND_OUT_MON runs with page 2")
|
||||
U.shot(game, out .. "/" .. tag .. "-page2.png")
|
||||
return true
|
||||
end
|
||||
if screen.phase == "menu" then
|
||||
if screen.battle.enemyIndex == 1
|
||||
and (screen.battle.enemy.hp or 0) > 1 then
|
||||
screen.battle.enemy.hp = 1
|
||||
screen.battle.enemy.moves = {}
|
||||
screen.battle.enemy.status = false
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
elseif (screen.messageTimer or 0) > 0 and screen.typer
|
||||
and screen.typer:done() then
|
||||
U.tap(game, "a")
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(false, tag .. " send-out line never came up")
|
||||
return false
|
||||
end
|
||||
|
||||
awaitSendOut("GYARADOS", "intro")
|
||||
awaitSendOut("DRAGONITE", "replacement")
|
||||
|
||||
print("[sendout-2134] " .. (fails == 0 and "PASS all claims" or
|
||||
(fails .. " claims failed")) .. " -- shots in " .. out)
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
-- ../pokecrystal/home/text.asm:630
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function say(line) print("[2132] " .. line) end
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
say((cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
U.wait(60)
|
||||
local world = game.world
|
||||
if not (world and world.map) then
|
||||
say("FAIL the gen 2 world did not boot")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
-- ../pokecrystal/maps/NewBarkTown.asm:296
|
||||
world:warpToMapId("NEW_BARK_TOWN", 8, 9, "up")
|
||||
U.wait(30)
|
||||
ok(world.map and world.map.id == "NEW_BARK_TOWN", "standing under the town sign")
|
||||
|
||||
U.tap(game, "a")
|
||||
local box
|
||||
for _ = 1, 600 do
|
||||
local top = game.stack and game.stack:top()
|
||||
if top and top.isTextBox and top:arrowVisible() then box = top; break end
|
||||
U.wait(1)
|
||||
end
|
||||
ok(box ~= nil, "the sign's first page is up and waiting on the arrow")
|
||||
if not box then
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
|
||||
box.blink = 0
|
||||
U.shot(game, SHOT_DIR .. "/2132_arrow.png")
|
||||
|
||||
local canvas = love.graphics.newCanvas(160, 144)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setCanvas(canvas)
|
||||
love.graphics.clear(1, 0, 1, 1)
|
||||
box.blink = 0
|
||||
box:draw()
|
||||
love.graphics.setCanvas()
|
||||
love.graphics.pop()
|
||||
local id = canvas:newImageData()
|
||||
|
||||
local function px(x, y)
|
||||
local r, g, b = id:getPixel(x, y)
|
||||
return r, g, b
|
||||
end
|
||||
local function near(x, y, r, g, b)
|
||||
local pr, pg, pb = px(x, y)
|
||||
return math.abs(pr - r) < 0.05 and math.abs(pg - g) < 0.05
|
||||
and math.abs(pb - b) < 0.05
|
||||
end
|
||||
local pr, pg, pb = px(12, 108)
|
||||
say(("paper sampled at (12,108): %.2f %.2f %.2f"):format(pr, pg, pb))
|
||||
ok(not near(12, 108, 1, 0, 1), "the box interior was painted")
|
||||
|
||||
local function paperInRow(y)
|
||||
local n = 0
|
||||
for x = 144, 151 do
|
||||
if near(x, y, pr, pg, pb) then n = n + 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
local function inkInCell()
|
||||
local n = 0
|
||||
for y = 136, 143 do
|
||||
for x = 144, 151 do
|
||||
local r, g, b = px(x, y)
|
||||
if r < 0.3 and g < 0.3 and b < 0.3 then n = n + 1 end
|
||||
end
|
||||
end
|
||||
return n
|
||||
end
|
||||
local ink = inkInCell()
|
||||
say(("ink pixels in tile (18,17): %d"):format(ink))
|
||||
ok(ink > 0, "the arrow is drawn in the corner cell")
|
||||
local function inkInRow(cx, y)
|
||||
local n = 0
|
||||
for x = cx * 8, cx * 8 + 7 do
|
||||
local r, g, b = px(x, y)
|
||||
if r < 0.3 and g < 0.3 and b < 0.3 then n = n + 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
local lineRows = {}
|
||||
for y = 136, 143 do
|
||||
if inkInRow(17, y) >= 6 then lineRows[#lineRows + 1] = y end
|
||||
end
|
||||
ok(#lineRows > 0, "tile (17,17) carries the frame line rows")
|
||||
for _, y in ipairs(lineRows) do
|
||||
ok(paperInRow(y) > 0, ("frame row %d of tile (18,17) shows paper under the arrow"):format(y - 136))
|
||||
end
|
||||
|
||||
box.blink = 16
|
||||
love.graphics.push("all")
|
||||
love.graphics.setCanvas(canvas)
|
||||
love.graphics.clear(1, 0, 1, 1)
|
||||
box:draw()
|
||||
love.graphics.setCanvas()
|
||||
love.graphics.pop()
|
||||
id = canvas:newImageData()
|
||||
local same = true
|
||||
for y = 136, 143 do
|
||||
for x = 0, 7 do
|
||||
local r1, g1, b1 = px(136 + x, y)
|
||||
local r2, g2, b2 = px(144 + x, y)
|
||||
if math.abs(r1 - r2) > 0.05 or math.abs(g1 - g2) > 0.05 or math.abs(b1 - b2) > 0.05 then
|
||||
same = false
|
||||
end
|
||||
end
|
||||
end
|
||||
ok(same, "on the off phase tile (18,17) matches the plain frame tile (17,17)")
|
||||
|
||||
say("eyeball 2132_arrow.png: the down arrow in the bottom-right corner sits "
|
||||
.. "in a paper cell with no frame lines through it (compare shots/2132_2.png)")
|
||||
|
||||
say(fails == 0 and "all claims passed" or (fails .. " claims failed"))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
-- ../pokecrystal/engine/menus/save.asm:209 SaveTheGame_yesorno
|
||||
-- ../pokecrystal/data/text/common_3.asm:202 _AlreadyASaveFileText
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local SaveMenu = require("src.ui.gen2.SaveMenu")
|
||||
local PcMenu = require("src.ui.gen2.PcMenu")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gen2-save-overwrite-2131"
|
||||
local failed = 0
|
||||
|
||||
local function pass(ok, line)
|
||||
if not ok then failed = failed + 1 end
|
||||
U.log((ok and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
|
||||
local function settle(menu)
|
||||
for _ = 1, 600 do
|
||||
if menu.typer and menu.typedPhase == menu.phase and menu.typer:done() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(2)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gen2 world did not boot")
|
||||
|
||||
local closed = false
|
||||
local menu = SaveMenu.new(game, {
|
||||
existed = true,
|
||||
writer = function() return true end,
|
||||
onDone = function()
|
||||
closed = true
|
||||
game.stack:pop()
|
||||
end,
|
||||
})
|
||||
game.stack:push(menu)
|
||||
settle(menu)
|
||||
pass(menu:yesNoVisible(), "confirm YES/NO is up after the question")
|
||||
U.shot(game, out .. "/01-confirm.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
pass(menu.phase == "overwrite", "YES on an existing file opens the overwrite prompt")
|
||||
settle(menu)
|
||||
pass(menu.page == 1 and #menu.pages == 2,
|
||||
("overwrite prompt is on page 1 of 2 (page %s of %s)"):format(
|
||||
tostring(menu.page), tostring(menu.pages and #menu.pages)))
|
||||
pass(not menu:yesNoVisible(), "no YES/NO while the cont page waits")
|
||||
U.log("02-overwrite-page1: 'There is already a / save file. Is it' with the")
|
||||
U.log("blinking arrow at the bottom right and NO yes/no box yet.")
|
||||
U.shot(game, out .. "/02-overwrite-page1.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
pass(menu.page == 2, "A turns to the cont page")
|
||||
pass(menu.phase == "overwrite", "without answering the question")
|
||||
settle(menu)
|
||||
pass(menu:yesNoVisible(), "YES/NO goes up once 'OK to overwrite?' has printed")
|
||||
U.log("03-overwrite-page2: 'save file. Is it / OK to overwrite?' with the")
|
||||
U.log("YES/NO box at (0,7) over the summary panel, as on the cart.")
|
||||
U.shot(game, out .. "/03-overwrite-page2.png")
|
||||
|
||||
U.tap(game, "b")
|
||||
U.wait(5)
|
||||
pass(closed, "B on the overwrite YES/NO closes without saving")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
if not (game.save.party and #game.save.party > 0) then
|
||||
game.save.party = { Mon.new(game.data, "CYNDAQUIL", 10) }
|
||||
end
|
||||
local pc = PcMenu.new(game, {
|
||||
save = game.save,
|
||||
saveExists = true,
|
||||
bills = true,
|
||||
writer = function() return true end,
|
||||
onClose = function() game.stack:pop() end,
|
||||
})
|
||||
game.stack:push(pc)
|
||||
U.wait(3)
|
||||
pc.picking = true
|
||||
pc.pickIndex = ((game.save.currentBox or 1) % 14) + 1
|
||||
pc:beginChangeBox(pc.pickIndex)
|
||||
U.wait(3)
|
||||
pass(pc.savePhase == "confirm" and pc:saveYesNoVisible(),
|
||||
"CHANGE BOX asks '#MON BOX, data will be saved. OK?' with YES/NO up")
|
||||
U.shot(game, out .. "/04-changebox-confirm.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
pass(pc.savePhase == "overwrite" and pc.savePage == 1,
|
||||
"YES on an existing file opens the overwrite prompt on page 1")
|
||||
pass(not pc:saveYesNoVisible(), "no YES/NO while the cont page waits")
|
||||
U.log("05-changebox-page1: 'There is already a / save file. Is it', arrow,")
|
||||
U.log("no yes/no box.")
|
||||
U.shot(game, out .. "/05-changebox-page1.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
pass(pc.savePhase == "overwrite" and pc.savePage == 2,
|
||||
"A turns to the cont page without answering")
|
||||
pass(pc:saveYesNoVisible(), "YES/NO is up on 'OK to overwrite?'")
|
||||
U.log("06-changebox-page2: 'save file. Is it / OK to overwrite?' with the")
|
||||
U.log("YES/NO box at (14,7).")
|
||||
U.shot(game, out .. "/06-changebox-page2.png")
|
||||
|
||||
U.tap(game, "b")
|
||||
U.wait(3)
|
||||
pass(pc.savePhase == nil and pc.picking,
|
||||
"B on the overwrite YES/NO drops the change and keeps the box picker")
|
||||
U.tap(game, "b")
|
||||
U.wait(3)
|
||||
U.tap(game, "b")
|
||||
U.wait(5)
|
||||
|
||||
U.log(("%d check(s) failed"):format(failed))
|
||||
U.log((failed == 0 and "PASS" or "FAIL") .. " gen2 save overwrite cont page (#2131) shots in " .. out)
|
||||
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -92,6 +92,60 @@ do
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Could not save.", "the failed-save message")
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/menus/save.asm:39 ChangeBoxSaveGame
|
||||
local ARROW_X, ARROW_Y = 18 * 8, 17 * 8
|
||||
do
|
||||
local input = { pressed = {} }
|
||||
function input:press(button) self.pressed[button] = true end
|
||||
function input:wasPressed(button)
|
||||
if self.pressed[button] then
|
||||
self.pressed[button] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
local menu = PcMenu.new({ input = input }, {
|
||||
save = SAVE,
|
||||
saveExists = true,
|
||||
writer = function() return true end,
|
||||
})
|
||||
menu.picking = true
|
||||
menu.pickIndex = 2
|
||||
menu:beginChangeBox(2)
|
||||
T.eq(menu.savePhase, "confirm", "CHANGE BOX asks to save first")
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(YES_X, YES_Y), "YES", "with its YES/NO up at once")
|
||||
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
T.eq(menu.savePhase, "overwrite", "YES on an existing file asks to overwrite")
|
||||
T.eq(menu.savePage, 1, "on the prompt's first page")
|
||||
menu.arrowBlink = 0
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "There is already a", "page one, first line")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save file. Is it", "page one, second line")
|
||||
T.eq(drawnAt(YES_X, YES_Y), nil, "no YES on page one")
|
||||
T.eq(drawnAt(ARROW_X, ARROW_Y), "\xe2\x96\xbc", "the cont arrow blinks at (18,17)")
|
||||
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
T.eq(menu.savePhase, "overwrite", "A turns the page rather than answering")
|
||||
T.eq(menu.savePage, 2, "to the cont page")
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "save file. Is it", "page two scrolls the second line up")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "OK to overwrite?", "and prints the cont line")
|
||||
T.eq(drawnAt(YES_X, YES_Y), "YES", "YES is up on the last page")
|
||||
T.eq(drawnAt(ARROW_X, ARROW_Y), nil, "and the arrow is gone")
|
||||
|
||||
input:press("b")
|
||||
menu:update(0)
|
||||
T.eq(menu.savePhase, nil, "B on the YES/NO is NO and drops the change")
|
||||
T.eq(menu.picking, true, "with the box picker still up")
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- a translation mod's turn
|
||||
--
|
||||
-- Same catalog values as gen2_save_menu_translation_test.lua's own
|
||||
@@ -105,7 +159,7 @@ do
|
||||
["YES"] = "OUI",
|
||||
["NO"] = "NON",
|
||||
["#MON BOX, data\nwill be saved. OK?"] = "Les donnees de la\nBOITE seront sauv.",
|
||||
["There is already a\nsave file. Is it"] = "Un fichier existe\ndeja. Est-ce",
|
||||
["There is already a\nsave file. Is it\vOK to overwrite?"] = "Un fichier existe\ndeja. Est-ce\vOK pour ecraser?",
|
||||
["SAVING… DON'T TURN\nOFF THE POWER."] = "SAUVEGARDE...\nN'ETEIGNEZ PAS.",
|
||||
["%s saved\nthe game."] = "%s a sauvegarde\nla partie.",
|
||||
["Could not save."] = "Echec de sauvegarde.",
|
||||
|
||||
@@ -103,7 +103,7 @@ do
|
||||
["YES"] = "OUI",
|
||||
["NO"] = "NON",
|
||||
["Would you like to\nsave the game?"] = "Voulez-vous\nsauvegarder ?",
|
||||
["There is already a\nsave file. Is it"] = "Un fichier existe\ndeja. Est-ce",
|
||||
["There is already a\nsave file. Is it\vOK to overwrite?"] = "Un fichier existe\ndeja. Est-ce\vOK pour ecraser?",
|
||||
["SAVING… DON'T TURN\nOFF THE POWER."] = "SAUVEGARDE...\nN'ETEIGNEZ PAS.",
|
||||
["%s saved\nthe game."] = "%s a sauvegarde\nla partie.",
|
||||
["Could not save."] = "Echec de sauvegarde.",
|
||||
@@ -156,32 +156,101 @@ do
|
||||
T.check(not Strings.active(), "the catalog is unloaded for the checks after this one")
|
||||
end
|
||||
|
||||
-- src/ui/gen2/PcMenu.lua:savePrompt() shares SaveMenu's overwrite/saving
|
||||
-- src/ui/gen2/PcMenu.lua:savePages() shares SaveMenu's overwrite/saving
|
||||
-- prompts through SaveMenu.OVERWRITE_PROMPT_SOURCE/SAVING_PROMPT_SOURCE and
|
||||
-- SaveMenu.twoLines(), rather than duplicating them -- both exported below,
|
||||
-- SaveMenu.pagesOf(), rather than duplicating them -- both exported below,
|
||||
-- both used by PcMenu's own translation test
|
||||
-- (tests/engine/gen2_pcmenu_changebox_save_translation_test.lua). Checked
|
||||
-- here that they stay callable the shape twoLines() expects: a table in,
|
||||
-- one \n-joined string out with the split back on load.
|
||||
-- here that they stay callable the shape pagesOf() expects: one
|
||||
-- \n/\v-joined string in, a list of two-line pages out.
|
||||
-- ../pokecrystal/data/text/common_3.asm:202 _AlreadyASaveFileText
|
||||
do
|
||||
T.eq(SaveMenu.OVERWRITE_PROMPT_SOURCE, "There is already a\nsave file. Is it",
|
||||
"OVERWRITE_PROMPT_SOURCE stays the cart's own \\n-joined text")
|
||||
T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[1], "There is already a",
|
||||
"and twoLines() splits its untranslated fallback back to the first line")
|
||||
T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[2], "save file. Is it",
|
||||
"and its second line")
|
||||
T.eq(SaveMenu.OVERWRITE_PROMPT_SOURCE,
|
||||
"There is already a\nsave file. Is it\vOK to overwrite?",
|
||||
"OVERWRITE_PROMPT_SOURCE is the cart's whole text, cont line included")
|
||||
local pages = SaveMenu.pagesOf(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))
|
||||
T.eq(#pages, 2, "and pagesOf() turns the cont into a second page")
|
||||
T.eq(pages[1][1], "There is already a", "page one, first line")
|
||||
T.eq(pages[1][2], "save file. Is it", "page one, second line")
|
||||
T.eq(pages[2][1], "save file. Is it", "page two scrolls the old second line up")
|
||||
T.eq(pages[2][2], "OK to overwrite?", "and prints the cont line under it")
|
||||
T.eq(SaveMenu.SAVING_PROMPT_SOURCE, "SAVING… DON'T TURN\nOFF THE POWER.",
|
||||
"SAVING_PROMPT_SOURCE stays the cart's own \\n-joined text")
|
||||
T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[1], "SAVING… DON'T TURN",
|
||||
"and twoLines() splits its untranslated fallback back to the first line")
|
||||
T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[2], "OFF THE POWER.",
|
||||
"and its second line")
|
||||
pages = SaveMenu.pagesOf(Strings(SaveMenu.SAVING_PROMPT_SOURCE))
|
||||
T.eq(#pages, 1, "a \\n-only message is a single page")
|
||||
T.eq(pages[1][1], "SAVING… DON'T TURN", "with its first line")
|
||||
T.eq(pages[1][2], "OFF THE POWER.", "and its second line")
|
||||
T.eq(SaveMenu.pagesOf("Could not save.")[1][2], nil,
|
||||
"a one-line message leaves the second slot empty")
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/menus/save.asm:209 SaveTheGame_yesorno
|
||||
local ARROW_X, ARROW_Y = 18 * 8, 17 * 8
|
||||
local function newInput()
|
||||
local input = { pressed = {} }
|
||||
function input:press(button) self.pressed[button] = true end
|
||||
function input:wasPressed(button)
|
||||
if self.pressed[button] then
|
||||
self.pressed[button] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
function input:isDown() return false end
|
||||
return input
|
||||
end
|
||||
|
||||
do
|
||||
local input = newInput()
|
||||
local menu = SaveMenu.new({ input = input }, { save = SAVE, existed = true })
|
||||
typeOut(menu)
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
T.eq(menu.phase, "overwrite", "YES on an existing file asks to overwrite")
|
||||
typeOut(menu)
|
||||
menu.arrowBlink = 0
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "There is already a", "page one, first line")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save file. Is it", "page one, second line")
|
||||
T.eq(drawnAt(YES_X, YES_Y), nil, "no YES on page one")
|
||||
T.eq(drawnAt(ARROW_X, ARROW_Y), "\xe2\x96\xbc", "the cont arrow blinks at (18,17)")
|
||||
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
T.eq(menu.page, 2, "A turns to the cont page")
|
||||
typeOut(menu)
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "save file. Is it", "page two scrolls the second line up")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "OK to overwrite?", "and prints the cont line")
|
||||
T.eq(drawnAt(YES_X, YES_Y), "YES", "YES is up on the last page")
|
||||
T.eq(drawnAt(NO_X, NO_Y), "NO", "and NO")
|
||||
T.eq(drawnAt(ARROW_X, ARROW_Y), nil, "and the arrow is gone")
|
||||
end
|
||||
|
||||
do
|
||||
Strings.load({
|
||||
strings = {
|
||||
["There is already a\nsave file. Is it\vOK to overwrite?"] = "Ecraser la\nsauvegarde?",
|
||||
},
|
||||
})
|
||||
local input = newInput()
|
||||
local menu = SaveMenu.new({ input = input }, { save = SAVE, existed = true })
|
||||
typeOut(menu)
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
typeOut(menu)
|
||||
T.eq(#menu.pages, 1, "a translation without \\v is one page")
|
||||
T.check(menu:yesNoVisible(), "and its YES/NO goes straight up")
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Ecraser la", "the translated first line")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "sauvegarde?", "and second")
|
||||
T.eq(drawnAt(YES_X, YES_Y), "YES", "with YES drawn")
|
||||
Strings.load({})
|
||||
end
|
||||
|
||||
-- A translation with a THIRD line (a second embedded "\n") has nowhere on
|
||||
-- screen to go -- drawPanel's box has room for exactly two Chrome.print
|
||||
-- calls -- so it must not silently draw the literal newline byte as glyph
|
||||
-- garbage on the second line, and should warn so a translator notices.
|
||||
do
|
||||
Strings.load({
|
||||
strings = {
|
||||
@@ -189,24 +258,24 @@ do
|
||||
},
|
||||
})
|
||||
|
||||
local warned = {}
|
||||
require("src.core.Logger").warn = function(fmt, ...)
|
||||
warned[#warned + 1] = select("#", ...) > 0 and fmt:format(...) or fmt
|
||||
end
|
||||
|
||||
local menu = SaveMenu.new({}, { save = SAVE, existed = false })
|
||||
local input = newInput()
|
||||
local menu = SaveMenu.new({ input = input }, { save = SAVE, existed = false })
|
||||
typeOut(menu)
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Ligne un", "only the first line reaches the box")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "Ligne deux\nLigne trois",
|
||||
"the rest lands in the second slot rather than vanishing")
|
||||
T.check(#warned == 1, "and a single warning is logged")
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Ligne un", "the first line reaches the box")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "Ligne deux", "and the second, without the third")
|
||||
T.eq(drawnAt(YES_X, YES_Y), nil, "no YES while a page is still to come")
|
||||
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
typeOut(menu)
|
||||
drawn = {}
|
||||
menu:drawPanel()
|
||||
T.check(#warned == 1, "the warning does not repeat for the same text")
|
||||
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Ligne trois", "the third line gets its own page")
|
||||
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y) or "", "", "with an empty second slot")
|
||||
T.eq(drawnAt(YES_X, YES_Y), "YES", "and YES on the last page")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
Strings.load({})
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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")
|
||||
|
||||
require("src.core.Logger").warn = function() end
|
||||
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local ARROW = Theme.moreArrow or 0xEE
|
||||
local CREAM = { 231, 227, 231 }
|
||||
|
||||
local function newGame(gen, paper)
|
||||
return {
|
||||
save = { player = {}, options = { textSpeed = "FAST" }, generation = gen },
|
||||
data = { text = {} },
|
||||
input = {
|
||||
wasPressed = function() return false end,
|
||||
isDown = function() return false end,
|
||||
},
|
||||
textboxPaper = paper and function() return paper end or nil,
|
||||
}
|
||||
end
|
||||
|
||||
local events
|
||||
local realDraw, realFill = Font.drawCode, Chrome.paletteFill
|
||||
Font.drawCode = function(code, x, y)
|
||||
events[#events + 1] = { kind = "glyph", code = code, x = x, y = y }
|
||||
end
|
||||
Chrome.paletteFill = function(x, y, w, h, palette)
|
||||
events[#events + 1] = { kind = "fill", x = x, y = y, w = w, h = h,
|
||||
palette = palette }
|
||||
end
|
||||
|
||||
local function frame(box, blink)
|
||||
box.blink = blink
|
||||
events = {}
|
||||
box:draw()
|
||||
local fill, arrow
|
||||
for i, e in ipairs(events) do
|
||||
if e.kind == "fill" and e.x == 144 and e.y == 136 then fill = i end
|
||||
if e.kind == "glyph" and e.code == ARROW then arrow = i end
|
||||
end
|
||||
return fill, arrow
|
||||
end
|
||||
|
||||
local function box(gen, paper)
|
||||
local b = TextBox.new(newGame(gen, paper), "A")
|
||||
b.waiting = true
|
||||
b.shown = {}
|
||||
return b
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/text.asm:630
|
||||
do
|
||||
local gold = box(2)
|
||||
local fill, arrow = frame(gold, 0)
|
||||
check(arrow, "gold arrow draws on the on phase")
|
||||
check(fill, "and the (18,17) border cell is painted paper first")
|
||||
check(fill and arrow and fill < arrow, "paper goes under the arrow, not over it")
|
||||
local e = fill and events[fill]
|
||||
check(e and e.w == 8 and e.h == 8, "one whole 8x8 tile")
|
||||
check(e and e.palette == Chrome.DEFAULT_BOX_PALETTE,
|
||||
"through the box's own palette")
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/joypad.asm:458
|
||||
do
|
||||
local gold = box(2)
|
||||
local fill, arrow = frame(gold, 16)
|
||||
eq(arrow, nil, "gold arrow is off from frame 16")
|
||||
eq(fill, nil, "and the border tile is left alone on the off phase")
|
||||
end
|
||||
|
||||
do
|
||||
local gear = box(2, CREAM)
|
||||
local fill = frame(gear, 0)
|
||||
local e = fill and events[fill]
|
||||
check(e and e.palette and e.palette[1] == CREAM,
|
||||
"a pushed textbox on the Pokegear's cream paper fills the cell cream")
|
||||
end
|
||||
|
||||
do
|
||||
local red = box(1)
|
||||
local fill, arrow = frame(red, 0)
|
||||
check(arrow, "gen1 arrow still draws")
|
||||
eq(fill, nil, "and gen1 never paints the cell (its arrow sits on the interior)")
|
||||
end
|
||||
|
||||
Font.drawCode, Chrome.paletteFill = realDraw, realFill
|
||||
T.finish()
|
||||
@@ -64,6 +64,24 @@ while Typer.typing(menu) and frames < 600 do
|
||||
frames = frames + 1
|
||||
end
|
||||
check(frames > 0 and frames < 600, "the overwrite question finishes typing")
|
||||
-- ../pokecrystal/data/text/common_3.asm:205
|
||||
eq(menu.page, 1, "the overwrite prompt's first page is up")
|
||||
eq(#menu.pages, 2, "and it has a cont page after it")
|
||||
check(not menu:yesNoVisible(), "no YES/NO while the cont page waits for a button")
|
||||
|
||||
input:press("a")
|
||||
menu:update(0)
|
||||
eq(menu.page, 2, "A turns to the cont page")
|
||||
eq(menu.phase, "overwrite", "without answering the question")
|
||||
check(Typer.typing(menu), "the cont page types letter by letter")
|
||||
check(not menu:yesNoVisible(), "and the YES/NO waits for it")
|
||||
|
||||
frames = 0
|
||||
while Typer.typing(menu) and frames < 600 do
|
||||
menu:update(0)
|
||||
frames = frames + 1
|
||||
end
|
||||
check(frames > 0 and frames < 600, "the cont page finishes typing")
|
||||
check(menu:yesNoVisible(), "then the overwrite YES/NO goes up")
|
||||
|
||||
input:press("a")
|
||||
|
||||
@@ -850,7 +850,8 @@ do
|
||||
if event.kind == "send" then sentOut = event.text end
|
||||
end
|
||||
check("EnemyWithdrewText comes first", withdrew, "JOEY withdrew GEODUDE!")
|
||||
check("then the send-out line", sentOut, "JOEY sent out PIDGEY!")
|
||||
-- ../pokecrystal/data/text/battle.asm:240-246
|
||||
check("then the send-out line", sentOut, "JOEY\nsent out\vPIDGEY!")
|
||||
|
||||
-- A refused RUN costs nothing. .cant_run_from_trainer leaves
|
||||
-- wBattlePlayerAction at BATTLEPLAYERACTION_USEMOVE, so BattleMenu_Run's
|
||||
|
||||
@@ -1754,7 +1754,7 @@ end
|
||||
do
|
||||
local screen, lead = learnScreen()
|
||||
screen:push({ kind = "send", side = "enemy", mon = { hp = 1 }, hp = 1,
|
||||
text = "JOE sent out PIDGEY!" })
|
||||
text = Battle.sentOutText("JOE", "PIDGEY") })
|
||||
check(runToPhase(screen, "ask-forget"), "the pages reach the question")
|
||||
local tap = tapper(screen)
|
||||
tap("a") -- read the question
|
||||
@@ -1779,6 +1779,72 @@ do
|
||||
"and its line prints ahead of the send-out that was already queued")
|
||||
end
|
||||
|
||||
-- ../pokecrystal/data/text/battle.asm:240-246
|
||||
-- ../pokecrystal/engine/battle/core.asm:3146-3147
|
||||
-- ../pokecrystal/home/text.asm:502-526
|
||||
do
|
||||
local function typed(screen)
|
||||
for _ = 1, 400 do
|
||||
if not screen:syncTyper() then break end
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
end
|
||||
return table.concat(screen:messageLines(), "|")
|
||||
end
|
||||
local function sendOutScreen(trainer, monName)
|
||||
local screen, battle = newScreen()
|
||||
check(runToMenu(screen), "reached the menu")
|
||||
battle:takeEvents()
|
||||
local mon = Mon.new(DATA, "PIDGEY", 5, { dvs = perfect })
|
||||
mon.nickname = monName
|
||||
screen.phase = "resolving"
|
||||
screen.showEnemyHud = false
|
||||
screen:push({ kind = "send", side = "enemy", mon = mon, hp = mon.hp,
|
||||
text = Battle.sentOutText(trainer, monName) })
|
||||
screen:advanceQueue()
|
||||
return screen
|
||||
end
|
||||
local function sendStarted(screen)
|
||||
return screen.afterSendOut ~= nil or screen.showEnemyHud == true
|
||||
end
|
||||
|
||||
local screen = sendOutScreen("CHAMPION LANCE", "DRAGONITE")
|
||||
eq(typed(screen), "CHAMPION LANCE|sent out",
|
||||
"the trainer and 'sent out' are the first page, no name on it")
|
||||
check(not sendStarted(screen) and screen.pendingSendOut ~= nil,
|
||||
"the ball stays shut while the first page waits for A")
|
||||
local tap = tapper(screen)
|
||||
tap("a")
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
local lines = screen:messageLines()
|
||||
eq(lines[1], "sent out", "cont scrolls 'sent out' up already printed")
|
||||
check(lines[2] ~= "DRAGONITE!", "and only the name types")
|
||||
eq(typed(screen), "sent out|DRAGONITE!",
|
||||
"a nine-letter name lands whole on the scrolled row")
|
||||
check(sendStarted(screen) and screen.pendingSendOut == nil,
|
||||
"and ANIM_SEND_OUT_MON starts with the name page")
|
||||
|
||||
local short = sendOutScreen("CHAMPION LANCE", "PIDGEY")
|
||||
eq(typed(short), "CHAMPION LANCE|sent out",
|
||||
"a short name still gets the cart's unconditional third row")
|
||||
tapper(short)("a")
|
||||
eq(typed(short), "sent out|PIDGEY!", "and lands on the scrolled page")
|
||||
|
||||
local long = newScreen()
|
||||
check(runToMenu(long), "reached the menu")
|
||||
long.battle:takeEvents()
|
||||
long.phase = "resolving"
|
||||
long:push({ kind = "message",
|
||||
text = "SOMELONGNAME gained 12345 EXP. Points!" })
|
||||
long:advanceQueue()
|
||||
eq(typed(long), "SOMELONGNAME|gained 12345 EXP.",
|
||||
"an overlong line shows its first two rows")
|
||||
tapper(long)("a")
|
||||
eq(typed(long), "gained 12345 EXP.|Points!",
|
||||
"and the third row scrolls in instead of being cut")
|
||||
end
|
||||
|
||||
-- ---- MoveSelectionScreen's two boxes (#1478) ------------------------------
|
||||
-- engine/battle/core.asm:5074-5094, MoveInfoBox :5403-5478
|
||||
do
|
||||
|
||||
@@ -138,10 +138,15 @@ do
|
||||
end,
|
||||
})
|
||||
vm:start("s:t")
|
||||
eq(table.concat(order, ","),
|
||||
"start:CATCH_TUTORIAL,battle,stop,reload",
|
||||
"the battle runs inside the lockout, and the script parks on the "
|
||||
.. "map reload")
|
||||
check(vm:running(), "catchtutorial is not a terminator")
|
||||
vm:update()
|
||||
eq(table.concat(order, ","),
|
||||
"start:CATCH_TUTORIAL,battle,stop,reload,text",
|
||||
"the battle runs inside the lockout, and the script continues after the "
|
||||
.. "map reload -- catchtutorial is not a terminator")
|
||||
"and the script continues once the map setup has run")
|
||||
eq(got and got.battleType, 3, "wBattleType is the command's own byte")
|
||||
eq(got and got.wild and got.wild.species, 19,
|
||||
"and the wild mon is the one loadwildmon left behind")
|
||||
|
||||
@@ -255,6 +255,51 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/text.asm:473
|
||||
do
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local priorPrint = Chrome.print
|
||||
local rows = {}
|
||||
Chrome.print = function(text, _tx, ty) rows[text] = ty end
|
||||
local screen = InitClock.new({}, { save = {} })
|
||||
screen:drawPanel()
|
||||
Chrome.print = priorPrint
|
||||
eq(rows["Zzz... Hm? Wha...?"], 14, "Oak's first line prints on row 14")
|
||||
eq(rows["You woke me up!"], 16, "and `line` puts the second on row 16")
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/rtc/timeset.asm:385
|
||||
do
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local day = InitClock.new({}, { mode = "day", save = {} })
|
||||
eq(day.isOpaque, false, "SetDayOfWeek draws over the map")
|
||||
eq(day:drawsWidescreen(), false, "and paints no surround of its own")
|
||||
eq(day:wantsFillScale(), false, "nor asks for the fill scale")
|
||||
local clock = InitClock.new({}, { mode = "clock", save = {} })
|
||||
eq(clock.isOpaque, true, "InitClock still runs on a cleared page")
|
||||
eq(clock:drawsWidescreen(), true, "with its own widescreen surround")
|
||||
local priorClear, cleared = Chrome.clear, 0
|
||||
Chrome.clear = function() cleared = cleared + 1 end
|
||||
day:drawPanel()
|
||||
eq(cleared, 0, "the day wheel never clears the tilemap")
|
||||
clock:drawPanel()
|
||||
eq(cleared, 1, "the clock does")
|
||||
Chrome.clear = priorClear
|
||||
end
|
||||
|
||||
-- ../pokecrystal/home/text.asm:502
|
||||
do
|
||||
local screen = InitClock.new({}, { save = {}, hour = 12, minute = 0 })
|
||||
screen.phase = "response"
|
||||
eq(InitClock.responseKey(12), "yikes", "noon is the three-line response")
|
||||
local pages = screen:pages()
|
||||
eq(#pages, 2, "`cont` scrolls the third line in as a second page")
|
||||
check(pages[1]:find("Yikes! I over-", 1, true) ~= nil
|
||||
and not pages[1]:find("slept!", 1, true), "page one holds lines 1-2")
|
||||
check(pages[2]:find("^Yikes! I over%-\nslept!$") ~= nil,
|
||||
"page two keeps line 2 on top and scrolls line 3 under it")
|
||||
end
|
||||
|
||||
-- Clock.DAY_NAMES / Clock.weekdayName / Clock.daytimeLabel: the single home
|
||||
-- InitClock, MainMenu and the Pokegear clock card all share, so a weekday
|
||||
-- cannot be named one way on one screen and another way on the next.
|
||||
|
||||
@@ -193,10 +193,12 @@ end
|
||||
-- home/text.asm:566
|
||||
do
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local priorPrint, priorWrapped = Chrome.print, Chrome.printWrapped
|
||||
local printed = {}
|
||||
Chrome.print = function(text) printed[#printed + 1] = text end
|
||||
Chrome.printWrapped = function(text) printed[#printed + 1] = text end
|
||||
local priorPrint = Chrome.print
|
||||
local printed, rows = {}, {}
|
||||
Chrome.print = function(text, _tx, ty)
|
||||
printed[#printed + 1] = text
|
||||
rows[text] = ty
|
||||
end
|
||||
local done = GenderSelect.new({ data = { text = {
|
||||
_AreYouABoyOrAreYouAGirlText = "Are you a boy?\nOr are you a girl?{DONE}",
|
||||
} } })
|
||||
@@ -205,11 +207,14 @@ do
|
||||
done:drawPanel()
|
||||
check(not table.concat(printed, "|"):find("DONE", 1, true),
|
||||
"and never reaches the tile grid")
|
||||
-- ../pokecrystal/home/text.asm:473
|
||||
eq(rows["Are you a boy?"], 14, "the prompt's first line sits on row 14")
|
||||
eq(rows["Or are you a girl?"], 16, "and `line` drops the second to row 16")
|
||||
local prompt = GenderSelect.new({ data = { text = {
|
||||
_AreYouABoyOrAreYouAGirlText = "Are you a boy?{PROMPT}",
|
||||
} } })
|
||||
eq(prompt.text, "Are you a boy?", "nor is {PROMPT}")
|
||||
Chrome.print, Chrome.printWrapped = priorPrint, priorWrapped
|
||||
Chrome.print = priorPrint
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- the beat in Oak's speech
|
||||
|
||||
@@ -240,6 +240,65 @@ previous, alpha = BorderFill.crossfade(bare, trees, nil)
|
||||
check("a caller with no map key never fades", previous, nil)
|
||||
check("and draws opaque", alpha, 1)
|
||||
|
||||
-- JumpStep engine/overworld/movement.asm:747-776
|
||||
-- map_objects.asm:1828-1849
|
||||
check("the curve starts four pixels up", Movement.jumpYOffset(1, 16), -4)
|
||||
check("peaks at twelve", Movement.jumpYOffset(6, 16), -12)
|
||||
check("and lands on the tile", Movement.jumpYOffset(16, 16), 0)
|
||||
check("a doubled step reads the same peak", Movement.jumpYOffset(12, 32), -12)
|
||||
|
||||
local Player = require("src.world.gen2.Player")
|
||||
local hopper = Player.new(3, 4, "down")
|
||||
hopper.fixedFacing = true
|
||||
hopper:scriptFace("up")
|
||||
check("a scripted jump starts", hopper:scriptJump("down"), true)
|
||||
check("it targets two cells", hopper.targetY, 6)
|
||||
check("keeps a fixed facing", hopper.facing, "up")
|
||||
check("and is a jump", hopper.jumping, true)
|
||||
check("over one doubled step", hopper.stepFrames, 32)
|
||||
local lowest, shadowFrames = 0, 0
|
||||
for _ = 1, 32 do
|
||||
hopper:update()
|
||||
lowest = math.min(lowest, hopper.spriteYOffset or 0)
|
||||
if hopper.jumping then shadowFrames = shadowFrames + 1 end
|
||||
end
|
||||
check("the player arc peaks at -12", lowest, -12)
|
||||
check("the shadow flag holds for the whole hop", shadowFrames, 31)
|
||||
check("it lands two cells down", hopper.cellY, 6)
|
||||
check("with the offset cleared", hopper.spriteYOffset, 0)
|
||||
check("and the jump over", hopper.jumping, nil)
|
||||
check("a fixed-facing scripted step also keeps its facing",
|
||||
hopper:scriptStep("left") and hopper.facing, "up")
|
||||
check("while still moving left", hopper.targetX, 2)
|
||||
hopper.fixedFacing = nil
|
||||
|
||||
local jumper = NPC.new("NEW_BARK_TOWN", { index = 2, x = 4, y = 5,
|
||||
movement = 1 }, SPRITE)
|
||||
jumper.fixedFacing = nil
|
||||
check("an NPC jump starts", jumper:scriptJump("right"), true)
|
||||
check("it targets two cells", jumper.targetX, 6)
|
||||
check("faces the way it jumps", jumper.facing, "right")
|
||||
check("and is a jump", jumper.jumping, true)
|
||||
local npcLowest, npcFrames = 0, 0
|
||||
while jumper.moving and npcFrames < 64 do
|
||||
jumper:update(nil, {})
|
||||
npcFrames = npcFrames + 1
|
||||
npcLowest = math.min(npcLowest, jumper.spriteYOffset or 0)
|
||||
end
|
||||
check("the NPC arc peaks at -12", npcLowest, -12)
|
||||
check("over the doubled step", npcFrames, 32)
|
||||
check("it lands two cells right", jumper.cellX, 6)
|
||||
check("on the tile", jumper.spriteYOffset, 0)
|
||||
check("with the jump over", jumper.jumping, nil)
|
||||
check("a following plain step is back to one cell",
|
||||
jumper:scriptStep("down") and jumper.targetY, 6)
|
||||
local plainFrames = 0
|
||||
while jumper.moving and plainFrames < 64 do
|
||||
jumper:update(nil, {})
|
||||
plainFrames = plainFrames + 1
|
||||
end
|
||||
check("at the walking pace", plainFrames, 16)
|
||||
|
||||
print(("gen2 field anim: %d checks, %d failures"):format(checks, failures))
|
||||
if failures > 0 then
|
||||
error(("%d assertion(s) failed"):format(failures), 0)
|
||||
|
||||
@@ -172,10 +172,11 @@ do
|
||||
for _ = 1, 30 do world:updatePeople() end
|
||||
eq(npc.facing, before, "his spin cannot roll a new facing mid-script")
|
||||
-- Drain the engagement: the emote's 30-frame hold, the seen text, the
|
||||
-- battle, the after script. updatePeople keeps running under it, which is
|
||||
-- exactly when an unfrozen spinner would drift.
|
||||
-- battle, the reload's FadeInFromWhite, the after script. updatePeople
|
||||
-- keeps running under it, which is exactly when an unfrozen spinner would
|
||||
-- drift.
|
||||
local drifted = false
|
||||
for _ = 1, 60 do
|
||||
for _ = 1, 90 do
|
||||
world:step()
|
||||
world:updatePeople()
|
||||
if world.vm:running() and npc.facing ~= before then drifted = true end
|
||||
|
||||
@@ -221,8 +221,11 @@ local function runScript(opts)
|
||||
reloadMap = function() log.reloaded = (log.reloaded or 0) + 1 end,
|
||||
})
|
||||
local started = vm:start(map.coordEvents[1].scriptKey)
|
||||
local parkedAfterReload = log.reloaded and vm.busy and #moves == 1
|
||||
for _ = 1, 4 do vm:update() end
|
||||
return {
|
||||
started = started, busy = vm.busy, log = log, moves = moves,
|
||||
parkedAfterReload = parkedAfterReload,
|
||||
battles = battles, save = save,
|
||||
}
|
||||
end
|
||||
@@ -237,7 +240,9 @@ end
|
||||
do -- the first visit of the day, accepted
|
||||
local run = runScript({ accept = true })
|
||||
check(run.started, "the script starts")
|
||||
check(not run.busy, "and runs to completion without parking on a command")
|
||||
check(run.parkedAfterReload,
|
||||
"reloadmapafterbattle parks the script for the map setup")
|
||||
check(not run.busy, "then it runs to completion")
|
||||
eq(run.log.turned and run.log.turned.facing, "up",
|
||||
"turnobject PLAYER, UP squares the player up to the desk")
|
||||
check(saw(run.log, "TRAINING HALL"), "the welcome")
|
||||
|
||||
@@ -161,6 +161,9 @@ local vm = Vm.new(scripts, texts, events, {
|
||||
vm.trainerObject = record
|
||||
check(vm:start(SEEN_SCRIPT), "an inline command list runs")
|
||||
for _ = 1, 60 do vm:update() end
|
||||
check(battled and vm:running() and #shown == 1,
|
||||
"reloadmapafterbattle parks the script for the map setup")
|
||||
vm:update()
|
||||
check(not vm:running(), "the trainer script finished")
|
||||
eq(emoted, -2, "the bubble goes over LAST_TALKED")
|
||||
check(approached, "the trainer walked up")
|
||||
@@ -170,6 +173,40 @@ eq(shown[1], "Wait! Let's battle!", "TRAINERTEXT_SEEN is the struct's seen text"
|
||||
eq(shown[2], "Train harder.", "the after-battle script ran")
|
||||
check(events:get(BEAT_FLAG), "SET_FLAG marked the trainer beaten")
|
||||
|
||||
-- engine/overworld/scripting.asm:1209
|
||||
do
|
||||
local reloaded, opened = {}, {}
|
||||
local rvm = Vm.new(scripts, texts, events, {
|
||||
showText = function(body, onDone) opened[#opened + 1] = body onDone() end,
|
||||
reloadMap = function(setup) reloaded[#reloaded + 1] = setup end,
|
||||
startBattle = function(_trainer, _wild, onDone) onDone("win") end,
|
||||
})
|
||||
check(rvm:start({
|
||||
{ op = "startbattle" },
|
||||
{ op = "reloadmapafterbattle" },
|
||||
{ op = "opentext" },
|
||||
{ op = "writetext", text = "t:after" },
|
||||
{ op = "closetext" },
|
||||
{ op = "end" },
|
||||
}), "battle-then-text script runs")
|
||||
eq(#reloaded, 1, "reloadmapafterbattle ran the reload hook")
|
||||
eq(reloaded[1], true, "as a map setup")
|
||||
eq(#opened, 0, "and parked the script before the text")
|
||||
check(rvm:running(), "the script is still live")
|
||||
rvm:update()
|
||||
eq(#opened, 1, "the text opens on the next VM tick")
|
||||
check(not rvm:running(), "and the script runs out")
|
||||
|
||||
reloaded, opened = {}, {}
|
||||
rvm:start({
|
||||
{ op = "refreshmap" },
|
||||
{ op = "writetext", text = "t:after" },
|
||||
{ op = "end" },
|
||||
})
|
||||
eq(reloaded[1], false, "refreshmap runs no setup script")
|
||||
eq(#opened, 1, "and does not park")
|
||||
end
|
||||
|
||||
-- A beaten trainer takes the CHECK_FLAG branch straight to its after script.
|
||||
local TALK_SCRIPT = {
|
||||
{ op = "faceplayer" },
|
||||
|
||||
@@ -2019,6 +2019,44 @@ objWorld:appearObject(3)
|
||||
check(objWorld.objectMasks["TEST_MAP:2"] == false,
|
||||
"and appear unmasks it, so the pair is not one-way")
|
||||
|
||||
-- maps/LancesRoom.asm:118
|
||||
do
|
||||
local rebuildsBefore = objWorld.rebuilds or 0
|
||||
objWorld:disappearObject(0)
|
||||
check(objWorld.playerMasked == true,
|
||||
"disappear PLAYER masks the player sprite")
|
||||
check(objWorld.objectMasks["TEST_MAP:-1"] == nil, "and touches no map object")
|
||||
eq(objWorld.rebuilds or 0, rebuildsBefore, "and rebuilds nobody")
|
||||
objWorld:appearObject(0)
|
||||
check(not objWorld.playerMasked, "appear PLAYER unmasks it")
|
||||
end
|
||||
|
||||
-- NewBarkTown_RivalShovesYouOutMovement maps/NewBarkTown.asm:178-183
|
||||
do
|
||||
local Player = require("src.world.gen2.Player")
|
||||
local jw = hookWorld()
|
||||
jw.player = Player.new(3, 4, "down")
|
||||
local done = false
|
||||
jw:beginMovement(0, { 0x01, 0x3b, 0x30, 0x3a, 0x47 }, function() done = true end)
|
||||
jw:updateMovement()
|
||||
eq(jw.player.facing, "up", "turn_head UP turns the player")
|
||||
jw:updateMovement()
|
||||
check(jw.player.moving and jw.player.jumping, "jump_step DOWN is a jump")
|
||||
eq(jw.player.targetY, 6, "two cells long")
|
||||
eq(jw.player.facing, "up", "under fix_facing the player keeps facing up")
|
||||
eq(jw.moveState and jw.moveState.pendingStep, nil, "with no second walk queued")
|
||||
local lowest = 0
|
||||
for _ = 1, 32 do
|
||||
jw.player:update()
|
||||
lowest = math.min(lowest, jw.player.spriteYOffset or 0)
|
||||
jw:updateMovement()
|
||||
end
|
||||
eq(lowest, -12, "the sprite arcs twelve pixels up on the way")
|
||||
eq(jw.player.cellY, 6, "and lands two cells down")
|
||||
check(done, "then the stream ends")
|
||||
check(not jw.player.fixedFacing, "remove_fixed_facing released the facing")
|
||||
end
|
||||
|
||||
-- Three object_events SHARING one MAPOBJECT_EVENT_FLAG is ordinary: the
|
||||
-- animated Burned Tower beasts all carry EVENT_BURNED_TOWER_B1F_BEASTS_1
|
||||
-- (maps/BurnedTowerB1F.asm:152) and ReleaseTheBeasts appears and jumps them
|
||||
@@ -2351,8 +2389,11 @@ setupWorld.setMap = function() setupLoads = setupLoads + 1 return true end
|
||||
setupWorld:runMapSetup(0xf7, function() return setupWorld:setMap() end)
|
||||
eq(setupLoads, 1, "MAPSETUP_CONNECTION loads on the spot")
|
||||
check(setupWorld.mapSetup == nil, "with no chain behind it")
|
||||
setupWorld.playerMasked = true
|
||||
setupWorld:runMapSetup(0xf1, function() return setupWorld:setMap() end)
|
||||
eq(setupLoads, 2, "MAPSETUP_WARP opens on DisableLCD, so it loads at once too")
|
||||
check(setupWorld.playerMasked == nil,
|
||||
"and the load respawns a `disappear`ed player")
|
||||
eq(setupWorld.mapSetup.phase, "in", "and only fades back in")
|
||||
setupWorld.mapSetup = nil
|
||||
setupWorld:runMapSetup(0xf6, function() return setupWorld:setMap() end)
|
||||
|
||||
Reference in New Issue
Block a user