mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
Merge pull request #1607 from bryanthaboi/dev
sometimes adrian be reading these PR titles
This commit is contained in:
@@ -327,6 +327,7 @@ local function returnToLauncher()
|
|||||||
if love.audio and love.audio.stop then
|
if love.audio and love.audio.stop then
|
||||||
pcall(love.audio.stop)
|
pcall(love.audio.stop)
|
||||||
end
|
end
|
||||||
|
pcall(function() require("src.render.SecondScreen").setEnabled(false) end)
|
||||||
|
|
||||||
local GameVersion = require("src.core.GameVersion")
|
local GameVersion = require("src.core.GameVersion")
|
||||||
local currentVersion = GameVersion.get()
|
local currentVersion = GameVersion.get()
|
||||||
|
|||||||
@@ -398,11 +398,15 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
secondaryHostResumed = false;
|
||||||
if (vibrator != null) {
|
if (vibrator != null) {
|
||||||
Log.d("GameActivity", "Cancelling vibration");
|
Log.d("GameActivity", "Cancelling vibration");
|
||||||
vibrator.cancel();
|
vibrator.cancel();
|
||||||
}
|
}
|
||||||
unregisterSecondaryDisplayListener();
|
unregisterSecondaryDisplayListener();
|
||||||
|
teardownSecondaryDisplay();
|
||||||
|
secondaryEnabled = false;
|
||||||
|
synchronized (secondaryFrameLock) { secondaryFrame = null; }
|
||||||
unregisterAudioDeviceCallback();
|
unregisterAudioDeviceCallback();
|
||||||
abandonAudioFocus();
|
abandonAudioFocus();
|
||||||
onHostDestroy();
|
onHostDestroy();
|
||||||
@@ -411,6 +415,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
|
secondaryHostResumed = false;
|
||||||
if (vibrator != null) {
|
if (vibrator != null) {
|
||||||
Log.d("GameActivity", "Cancelling vibration");
|
Log.d("GameActivity", "Cancelling vibration");
|
||||||
vibrator.cancel();
|
vibrator.cancel();
|
||||||
@@ -426,6 +431,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
secondaryHostResumed = true;
|
||||||
onHostResume();
|
onHostResume();
|
||||||
requestGameAudioFocus();
|
requestGameAudioFocus();
|
||||||
registerAudioDeviceCallback();
|
registerAudioDeviceCallback();
|
||||||
@@ -1933,6 +1939,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
private static volatile int secondaryActivityTarget = Display.INVALID_DISPLAY;
|
private static volatile int secondaryActivityTarget = Display.INVALID_DISPLAY;
|
||||||
private static volatile long secondaryRetryAfter;
|
private static volatile long secondaryRetryAfter;
|
||||||
private static volatile boolean secondaryEnabled = false;
|
private static volatile boolean secondaryEnabled = false;
|
||||||
|
private static volatile boolean secondaryHostResumed = false;
|
||||||
private static volatile int secondaryTarget = SECONDARY_TARGET_AUTO;
|
private static volatile int secondaryTarget = SECONDARY_TARGET_AUTO;
|
||||||
private static volatile int dualScreenDisplayMode = -1;
|
private static volatile int dualScreenDisplayMode = -1;
|
||||||
private static volatile byte[] secondaryFrame;
|
private static volatile byte[] secondaryFrame;
|
||||||
@@ -1963,7 +1970,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
if (self == null) return;
|
if (self == null) return;
|
||||||
self.runOnUiThread(new Runnable() {
|
self.runOnUiThread(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
if (on) {
|
if (on && secondaryHostResumed) {
|
||||||
self.refreshDualScreenDisplayMode();
|
self.refreshDualScreenDisplayMode();
|
||||||
self.registerSecondaryDisplayListener();
|
self.registerSecondaryDisplayListener();
|
||||||
rebindSecondaryDisplay();
|
rebindSecondaryDisplay();
|
||||||
@@ -2035,9 +2042,11 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
private static void rebindSecondaryDisplay() {
|
private static void rebindSecondaryDisplay() {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
if (self == null || !secondaryEnabled || secondaryOutputIsPreferred(self)) return;
|
if (self == null || !secondaryHostResumed || !secondaryEnabled
|
||||||
|
|| secondaryOutputIsPreferred(self)) return;
|
||||||
self.runOnUiThread(() -> {
|
self.runOnUiThread(() -> {
|
||||||
if (!secondaryEnabled || secondaryOutputIsPreferred(self)) return;
|
if (!secondaryHostResumed || !secondaryEnabled
|
||||||
|
|| secondaryOutputIsPreferred(self)) return;
|
||||||
teardownSecondaryDisplay();
|
teardownSecondaryDisplay();
|
||||||
setupSecondaryDisplay();
|
setupSecondaryDisplay();
|
||||||
});
|
});
|
||||||
@@ -2045,7 +2054,8 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
private static void setupSecondaryDisplay() {
|
private static void setupSecondaryDisplay() {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
if (self == null || !secondaryEnabled || secondaryPresentation != null
|
if (self == null || !secondaryHostResumed || !secondaryEnabled
|
||||||
|
|| secondaryPresentation != null
|
||||||
|| secondaryActivity != null || secondaryActivityPending
|
|| secondaryActivity != null || secondaryActivityPending
|
||||||
|| android.os.SystemClock.elapsedRealtime() < secondaryRetryAfter) return;
|
|| android.os.SystemClock.elapsedRealtime() < secondaryRetryAfter) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+48
-15
@@ -1718,9 +1718,14 @@ function BattleState:enter()
|
|||||||
-- _PlayerBlackedOutText2 (data/text/text_2.asm:896): the two paragraphs
|
-- _PlayerBlackedOutText2 (data/text/text_2.asm:896): the two paragraphs
|
||||||
-- playerMonFainted queues on the battle screen; there is no battle
|
-- playerMonFainted queues on the battle screen; there is no battle
|
||||||
-- screen to queue them on here, so they print over the map.
|
-- screen to queue them on here, so they print over the map.
|
||||||
|
-- _PlayerBlackedOutText (no "2") extracts to the identical wording from
|
||||||
|
-- a different ROM address and is unused anywhere in this engine -- not
|
||||||
|
-- a fallback for this one, just pokered printing the same paragraph
|
||||||
|
-- from a second call site elsewhere.
|
||||||
self.game.stack:push(require("src.render.TextBox").new(self.game,
|
self.game.stack:push(require("src.render.TextBox").new(self.game,
|
||||||
Strings("%s is out of\nuseable POKéMON!", name) .. "\f"
|
self:romText("_PlayerBlackedOutText2",
|
||||||
.. Strings("%s blacked\nout!", name), blackedOut))
|
"%s is out of\nuseable POKéMON!\f%s blacked\nout!", name, name),
|
||||||
|
blackedOut))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.musicKind = self:computeMusicKind()
|
self.musicKind = self:computeMusicKind()
|
||||||
@@ -1857,7 +1862,8 @@ function BattleState:enter()
|
|||||||
self:slidePic("foe")
|
self:slidePic("foe")
|
||||||
end)
|
end)
|
||||||
-- _TrainerSentOutText ends `done`, not `prompt` (data/text/text_2.asm:923)
|
-- _TrainerSentOutText ends `done`, not `prompt` (data/text/text_2.asm:923)
|
||||||
self:sayAuto(Strings("%s sent\nout %s!", foeName, self.enemy.name))
|
self:sayAuto(self:romText("_TrainerSentOutText", "%s sent\nout %s!",
|
||||||
|
foeName, self.enemy.name))
|
||||||
self:act(function()
|
self:act(function()
|
||||||
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
|
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
|
||||||
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
|
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
|
||||||
@@ -3638,12 +3644,13 @@ function BattleState:executeAction(user, target, action)
|
|||||||
})
|
})
|
||||||
self.aiUses = self:aiUsesFor()
|
self.aiUses = self:aiUsesFor()
|
||||||
markSeen(self.game, self.enemy.mon.species)
|
markSeen(self.game, self.enemy.mon.species)
|
||||||
-- _AIBattleWithdrawText: "X with-/drew Y!"
|
self:sayNext(self:romText("_AIBattleWithdrawText", "%s with-\ndrew %s!",
|
||||||
self:sayNext(Strings("%s with-\ndrew %s!", self.trainer.name, oldName))
|
self.trainer.name, oldName))
|
||||||
-- EnemySendOut falls into EnemySendOutFirstMon: TrainerSentOutText,
|
-- EnemySendOut falls into EnemySendOutFirstMon: TrainerSentOutText,
|
||||||
-- then AnimateSendingOutMon and PlayCry (core.asm:1276-1434)
|
-- then AnimateSendingOutMon and PlayCry (core.asm:1276-1434)
|
||||||
self.enemySendingOut = true
|
self.enemySendingOut = true
|
||||||
self:sayNextAuto(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
|
self:sayNextAuto(self:romText("_TrainerSentOutText", "%s sent\nout %s!",
|
||||||
|
self.trainer.name, self.enemy.name))
|
||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
self.enemySendingOut = false
|
self.enemySendingOut = false
|
||||||
self:startGrowIn(self.enemy)
|
self:startGrowIn(self.enemy)
|
||||||
@@ -4142,8 +4149,12 @@ function BattleState:onFaint(battler)
|
|||||||
-- acknowledged core.asm:797-798 bug.)
|
-- acknowledged core.asm:797-798 bug.)
|
||||||
self:actNext(function() self:playVictoryMusic() end)
|
self:actNext(function() self:playVictoryMusic() end)
|
||||||
end
|
end
|
||||||
-- _EnemyMonFaintedText "Enemy X fainted!" / _PlayerMonFaintedText
|
-- _EnemyMonFaintedText already carries its own "Enemy" wording, so this
|
||||||
self:sayNext(Strings("%s\nfainted!", displayName(battler)))
|
-- passes the raw name -- displayName's separate Strings("Enemy %s", ...)
|
||||||
|
-- would double it up
|
||||||
|
self:sayNext(battler.isPlayer
|
||||||
|
and self:romText("_PlayerMonFaintedText", "%s\nfainted!", battler.name)
|
||||||
|
or self:romText("_EnemyMonFaintedText", "Enemy %s\nfainted!", battler.name))
|
||||||
if battler.isPlayer then
|
if battler.isPlayer then
|
||||||
self:act(function() self:playerMonFainted() end)
|
self:act(function() self:playerMonFainted() end)
|
||||||
else
|
else
|
||||||
@@ -4319,6 +4330,13 @@ function BattleState:enemyMonFainted()
|
|||||||
-- "X is" off so "about to use" stays above the name, instead of the
|
-- "X is" off so "about to use" stays above the name, instead of the
|
||||||
-- page ending on a bare nick (#565). Then para "Will PLAYER" /
|
-- page ending on a bare nick (#565). Then para "Will PLAYER" /
|
||||||
-- "change POKéMON?" with YES/NO.
|
-- "change POKéMON?" with YES/NO.
|
||||||
|
--
|
||||||
|
-- _TrainerAboutToUseText combines both \f-paged, but unlike
|
||||||
|
-- _ItemUseBallText00's say()+say() merge above, this is say()+
|
||||||
|
-- sayChoice(): tried merging into one romText/sayChoice call and
|
||||||
|
-- confirmed via tests/engine/trainer_shift_prompt_bug565.lua that
|
||||||
|
-- the battle queue's own \f handling (not TextBox.lua's) does not
|
||||||
|
-- page a sayChoice string the same way -- left as two calls.
|
||||||
self:say(Strings("%s is\nabout to use\v%s!", self.trainer.name, nextName))
|
self:say(Strings("%s is\nabout to use\v%s!", self.trainer.name, nextName))
|
||||||
self:sayChoice(
|
self:sayChoice(
|
||||||
Strings("Will %s\nchange POKéMON?", self.game.save.player.name),
|
Strings("Will %s\nchange POKéMON?", self.game.save.player.name),
|
||||||
@@ -4362,7 +4380,8 @@ function BattleState:enemyMonFainted()
|
|||||||
-- (AnimateSendingOutMon) with the cry; no POOF -- that animation
|
-- (AnimateSendingOutMon) with the cry; no POOF -- that animation
|
||||||
-- belongs to the player-side SendOutMon (core.asm:1757-1762)
|
-- belongs to the player-side SendOutMon (core.asm:1757-1762)
|
||||||
self.enemySendingOut = true
|
self.enemySendingOut = true
|
||||||
self:sayNextAuto(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
|
self:sayNextAuto(self:romText("_TrainerSentOutText", "%s sent\nout %s!",
|
||||||
|
self.trainer.name, self.enemy.name))
|
||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
self.enemySendingOut = false
|
self.enemySendingOut = false
|
||||||
self:startGrowIn(self.enemy)
|
self:startGrowIn(self.enemy)
|
||||||
@@ -4878,7 +4897,8 @@ function BattleState:storeCaughtMon()
|
|||||||
-- text_promptbutton (item_effects.asm:624-629), so the fanfare follows
|
-- text_promptbutton (item_effects.asm:624-629), so the fanfare follows
|
||||||
-- the box rather than firing when the dex bit is set
|
-- the box rather than firing when the dex bit is set
|
||||||
self:sayNextWaitSfx(
|
self:sayNextWaitSfx(
|
||||||
Strings("New POKéDEX data\nwill be added for\n%s!", self.enemy.name),
|
self:romText("_ItemUseBallText06",
|
||||||
|
"New POKéDEX data\nwill be added for\n%s!", self.enemy.name),
|
||||||
function() return require("src.core.Sound").play(self.data, "Dex_Page_Added") end)
|
function() return require("src.core.Sound").play(self.data, "Dex_Page_Added") end)
|
||||||
self:uiNext(function()
|
self:uiNext(function()
|
||||||
return self:buildScreen("DexEntryMenu", species)
|
return self:buildScreen("DexEntryMenu", species)
|
||||||
@@ -4899,9 +4919,12 @@ function BattleState:storeCaughtMon()
|
|||||||
if boxNum then
|
if boxNum then
|
||||||
askCaughtNickname()
|
askCaughtNickname()
|
||||||
-- _ItemUseBallText07/08 keyed on EVENT_MET_BILL
|
-- _ItemUseBallText07/08 keyed on EVENT_MET_BILL
|
||||||
local pc = (game.save.flags and game.save.flags.EVENT_MET_BILL)
|
local metBill = game.save.flags and game.save.flags.EVENT_MET_BILL
|
||||||
and "BILL's PC" or Strings("someone's PC")
|
self:sayNext(self:romText(
|
||||||
self:sayNext(Strings("%s was\ntransferred to\n%s!", self.enemy.name, pc))
|
metBill and "_ItemUseBallText07" or "_ItemUseBallText08",
|
||||||
|
metBill and "%s was\ntransferred to\nBILL's PC!"
|
||||||
|
or "%s was\ntransferred to\nsomeone's PC!",
|
||||||
|
self.enemy.name))
|
||||||
else
|
else
|
||||||
self:sayNext(Strings("But every BOX\nis full!"))
|
self:sayNext(Strings("But every BOX\nis full!"))
|
||||||
end
|
end
|
||||||
@@ -5007,8 +5030,18 @@ function BattleState:throwBall(ball)
|
|||||||
-- RESTLESS SOUL dodges balls even once the scope has revealed it,
|
-- RESTLESS SOUL dodges balls even once the scope has revealed it,
|
||||||
-- so it is not a ghost battle any more (#444)
|
-- so it is not a ghost battle any more (#444)
|
||||||
self:animNext(self:tossAnimFor(ball), true, nil, ball)
|
self:animNext(self:tossAnimFor(ball), true, nil, ball)
|
||||||
self:sayNext(Strings("It dodged the\nthrown BALL!"))
|
-- _ItemUseBallText00 is one label for both lines, \f-paged. Unlike
|
||||||
self:sayNext(Strings("This POKéMON\ncan't be caught!"))
|
-- TextBox.new() (which splits \f itself), the battle queue's own
|
||||||
|
-- startMessage() only splits on \n/\v -- confirmed live: the \f
|
||||||
|
-- landed mid-line and the second sentence overflowed off the box
|
||||||
|
-- instead of starting a fresh page. Resolve the label once, then
|
||||||
|
-- split it the same way TextBox.lua does and queue one sayNext per
|
||||||
|
-- page, so the two ROM sentences still render as two pages.
|
||||||
|
local dodgeText = self:romText("_ItemUseBallText00",
|
||||||
|
"It dodged the\nthrown BALL!\fThis POKéMON\ncan't be caught!")
|
||||||
|
for page in (dodgeText .. "\f"):gmatch("(.-)\f") do
|
||||||
|
self:sayNext(page)
|
||||||
|
end
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||||
end)
|
end)
|
||||||
|
|||||||
+25
-11
@@ -982,7 +982,11 @@ end
|
|||||||
-- parked the player until every direction was re-pressed (#799).
|
-- parked the player until every direction was re-pressed (#799).
|
||||||
function Game:focus(f)
|
function Game:focus(f)
|
||||||
Input:reset()
|
Input:reset()
|
||||||
if f then Input:reconcile() end
|
if f then
|
||||||
|
Input:reconcile()
|
||||||
|
local eng = self:syncEngine()
|
||||||
|
if eng then pcall(eng.noteResumed, eng) end
|
||||||
|
end
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
self:cancelPointers()
|
self:cancelPointers()
|
||||||
end
|
end
|
||||||
@@ -1002,6 +1006,8 @@ function Game:onResume()
|
|||||||
Input:reconcile()
|
Input:reconcile()
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
self:cancelPointers()
|
self:cancelPointers()
|
||||||
|
local eng = self:syncEngine()
|
||||||
|
if eng then pcall(eng.noteResumed, eng) end
|
||||||
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
|
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
|
||||||
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
||||||
-- Desktop/mobile window-visible flips must not kill overworld music.
|
-- Desktop/mobile window-visible flips must not kill overworld music.
|
||||||
@@ -1209,18 +1215,26 @@ end
|
|||||||
|
|
||||||
function Game:syncEngine()
|
function Game:syncEngine()
|
||||||
if self._syncOff then return nil end
|
if self._syncOff then return nil end
|
||||||
if self._syncEngineRef then return self._syncEngineRef end
|
local eng = self._syncEngineRef
|
||||||
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
|
||||||
if not ok or type(SyncEngine) ~= "table" then
|
|
||||||
self._syncOff = true
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local eng = SyncEngine.shared()
|
|
||||||
if not eng then
|
if not eng then
|
||||||
self._syncOff = true
|
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
||||||
return nil
|
if not ok or type(SyncEngine) ~= "table" then
|
||||||
|
self._syncOff = true
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
eng = SyncEngine.shared()
|
||||||
|
if not eng then
|
||||||
|
self._syncOff = true
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
self._syncEngineRef = eng
|
||||||
|
end
|
||||||
|
if type(eng.protectPlaythrough) == "function" then
|
||||||
|
local meta = self.save and self.save.meta
|
||||||
|
eng:protectPlaythrough(
|
||||||
|
(self.save and self.save.version) or require("src.core.GameVersion").get(),
|
||||||
|
type(meta) == "table" and meta.playthroughId or nil)
|
||||||
end
|
end
|
||||||
self._syncEngineRef = eng
|
|
||||||
return eng
|
return eng
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+86
-13
@@ -124,12 +124,48 @@ function LauncherView.detach(imp)
|
|||||||
Kit.clearCaches()
|
Kit.clearCaches()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function markNoDrag(imp, x, y, w, h)
|
||||||
|
if Kit.blockClicks then return end
|
||||||
|
local t = imp._noDragRects
|
||||||
|
if not t then t = {}; imp._noDragRects = t end
|
||||||
|
local n = (imp._noDragN or 0) + 1
|
||||||
|
imp._noDragN = n
|
||||||
|
local r = t[n]
|
||||||
|
if not r then r = {}; t[n] = r end
|
||||||
|
r.x, r.y, r.w, r.h = x, y, w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
local function noDragAt(imp, x, y)
|
||||||
|
local rects = imp._noDragRects
|
||||||
|
for i = 1, imp._noDragN or 0 do
|
||||||
|
if inRect(rects[i], x, y) then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function armMouse(imp, x, y)
|
||||||
|
if noDragAt(imp, x, y) then
|
||||||
|
imp._clickPt = { x = x, y = y }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local shielded = imp._modalUpNow
|
||||||
|
imp._mouseAt = {
|
||||||
|
x = x, y = y,
|
||||||
|
region = not shielded and tabScrollMax(imp) > 0
|
||||||
|
and inRect(imp._tabRegionRect, x, y) or false,
|
||||||
|
page = not shielded and (imp._pageScrollMax or 0) > 0 or false,
|
||||||
|
}
|
||||||
|
Kit.dragBegin(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------------- input
|
-- ---------------------------------------------------------------- input
|
||||||
-- The kit is polled, not evented: update() samples the mouse and turns a
|
-- The kit is polled, not evented: update() samples the mouse. A press arms
|
||||||
-- rising edge into a click point that the next draw consumes. Host-forwarded
|
-- a drag that scrolls like a finger, and the click dispatches on RELEASE so
|
||||||
-- mousepressed stays unused, exactly as before, so Android's synthesized
|
-- the drag can disqualify it, exactly like the touch path below; only the
|
||||||
-- mouse path cannot double-fire a tap (#553) -- the dedup window below is the
|
-- cartridge (which owns its own spin-drag) keeps the press-down click.
|
||||||
-- other half of that guarantee.
|
-- Host-forwarded mousepressed stays unused, exactly as before, so Android's
|
||||||
|
-- synthesized mouse path cannot double-fire a tap (#553) -- the dedup window
|
||||||
|
-- below is the other half of that guarantee.
|
||||||
function LauncherView.update(imp, dt)
|
function LauncherView.update(imp, dt)
|
||||||
if not imp._flex then return end
|
if not imp._flex then return end
|
||||||
if imp._launchFade then return end
|
if imp._launchFade then return end
|
||||||
@@ -154,7 +190,39 @@ function LauncherView.update(imp, dt)
|
|||||||
if not touching and now >= (imp._suppressMouseUntil or 0)
|
if not touching and now >= (imp._suppressMouseUntil or 0)
|
||||||
and now >= (imp._suppressClickUntil or 0) then
|
and now >= (imp._suppressClickUntil or 0) then
|
||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
imp._clickPt = { x = mx, y = my }
|
armMouse(imp, mx, my)
|
||||||
|
end
|
||||||
|
elseif down and imp._mouseAt then
|
||||||
|
local start = imp._mouseAt
|
||||||
|
local mx, my = love.mouse.getPosition()
|
||||||
|
local ddx, ddy = mx - start.x, my - start.y
|
||||||
|
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
|
||||||
|
start.dragged = true
|
||||||
|
end
|
||||||
|
if start.dragged then
|
||||||
|
local last = start.lastY or start.y
|
||||||
|
local move = -(my - last)
|
||||||
|
if move ~= 0 and start.region then
|
||||||
|
local at, leftover = Kit.scrollHandoff(tabScrollAt(imp),
|
||||||
|
tabScrollMax(imp), move)
|
||||||
|
setTabScroll(imp, at)
|
||||||
|
move = leftover
|
||||||
|
end
|
||||||
|
if move ~= 0 and start.page and (imp._pageScrollMax or 0) > 0 then
|
||||||
|
local at, leftover = Kit.scrollHandoff(imp._pageScroll or 0,
|
||||||
|
imp._pageScrollMax, move)
|
||||||
|
imp._pageScroll = at
|
||||||
|
move = leftover
|
||||||
|
end
|
||||||
|
if move ~= 0 then Kit.dragAdd(move) end
|
||||||
|
end
|
||||||
|
start.lastY = my
|
||||||
|
elseif not down and imp._mouseAt then
|
||||||
|
local start = imp._mouseAt
|
||||||
|
imp._mouseAt = nil
|
||||||
|
Kit.dragEnd()
|
||||||
|
if not start.dragged then
|
||||||
|
imp._clickPt = { x = start.x, y = start.y }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
imp._prevMouseDown = down
|
imp._prevMouseDown = down
|
||||||
@@ -233,10 +301,12 @@ function LauncherView.clickAt(imp, x, y)
|
|||||||
imp._clickPt = { x = x, y = y }
|
imp._clickPt = { x = x, y = y }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Event-driven click: a macOS trackpad tap delivers press+release inside one
|
-- Event-driven press: a macOS trackpad tap delivers press+release inside one
|
||||||
-- frame, so update()'s love.mouse.isDown poll never sees it. Mint the click
|
-- frame, so update()'s love.mouse.isDown poll never sees it. Arm the drag
|
||||||
-- from the press event under the poll's own suppression rules, and mark the
|
-- from the press event under the poll's own suppression rules -- the poll's
|
||||||
-- press seen so the poll cannot mint a second one when isDown does catch it.
|
-- release branch then mints the tap, still within the same frame for a
|
||||||
|
-- one-frame tap -- and mark the press seen so the poll cannot arm a second
|
||||||
|
-- one when isDown does catch it.
|
||||||
function LauncherView.mousepressed(imp, x, y)
|
function LauncherView.mousepressed(imp, x, y)
|
||||||
if not imp._flex then return end
|
if not imp._flex then return end
|
||||||
local now = love.timer.getTime()
|
local now = love.timer.getTime()
|
||||||
@@ -245,7 +315,7 @@ function LauncherView.mousepressed(imp, x, y)
|
|||||||
or now < (imp._suppressClickUntil or 0) then
|
or now < (imp._suppressClickUntil or 0) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
imp._clickPt = { x = x, y = y }
|
if not imp._mouseAt then armMouse(imp, x, y) end
|
||||||
imp._prevMouseDown = true
|
imp._prevMouseDown = true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -495,6 +565,7 @@ end
|
|||||||
|
|
||||||
local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action)
|
local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action)
|
||||||
local state = cartridgeState(imp, version)
|
local state = cartridgeState(imp, version)
|
||||||
|
markNoDrag(imp, x, y, w, h)
|
||||||
local focused = Kit.focusable(key, x, y, w, h)
|
local focused = Kit.focusable(key, x, y, w, h)
|
||||||
local hot = Kit.hover(x, y, w, h)
|
local hot = Kit.hover(x, y, w, h)
|
||||||
local active = state.active
|
local active = state.active
|
||||||
@@ -4483,7 +4554,7 @@ local function buildSyncHome(imp, m, eng)
|
|||||||
local linked = eng:linked()
|
local linked = eng:linked()
|
||||||
local codes = eng.codes
|
local codes = eng.codes
|
||||||
local body = linked
|
local body = linked
|
||||||
and Strings("This device is linked. Saves and the mod list sync when the launcher opens and a few seconds after each save.")
|
and Strings("This device is linked. Saves sync when the launcher opens, a few seconds after each save, and every few minutes while the app is running.")
|
||||||
or Strings(SYNC_HINT)
|
or Strings(SYNC_HINT)
|
||||||
local innerW = w - 2 * pad
|
local innerW = w - 2 * pad
|
||||||
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
||||||
@@ -4868,13 +4939,15 @@ function LauncherView.draw(imp)
|
|||||||
Kit.beginFrame(mx, my, click ~= nil, imp._wheelY or 0)
|
Kit.beginFrame(mx, my, click ~= nil, imp._wheelY or 0)
|
||||||
imp._clickPt = nil
|
imp._clickPt = nil
|
||||||
imp._wheelY = 0
|
imp._wheelY = 0
|
||||||
|
imp._noDragN = 0
|
||||||
|
|
||||||
Theme.field()
|
Theme.field()
|
||||||
|
|
||||||
-- Everything from here to buildModals sits UNDER any open modal, so the
|
-- Everything from here to buildModals sits UNDER any open modal, so the
|
||||||
-- whole stage draws shielded (no clicks, no hover, no focus ring) while
|
-- whole stage draws shielded (no clicks, no hover, no focus ring) while
|
||||||
-- one is up; buildModals lowers the shield for the modal's own controls.
|
-- one is up; buildModals lowers the shield for the modal's own controls.
|
||||||
Kit.blockClicks = modalUp(imp)
|
imp._modalUpNow = modalUp(imp)
|
||||||
|
Kit.blockClicks = imp._modalUpNow
|
||||||
|
|
||||||
local step = Kit.scrollStep(m.s)
|
local step = Kit.scrollStep(m.s)
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -1537,6 +1537,9 @@ function RomImporter:focus(f)
|
|||||||
self._modPress = nil
|
self._modPress = nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if type(self._sync) == "table" then
|
||||||
|
pcall(self._sync.noteResumed, self._sync)
|
||||||
|
end
|
||||||
if not (f and self.android and self.workState ~= "working") then return end
|
if not (f and self.android and self.workState ~= "working") then return end
|
||||||
-- SAF create-document finished: GameActivity wrote export_done.flag.
|
-- SAF create-document finished: GameActivity wrote export_done.flag.
|
||||||
if love.filesystem.getInfo("export_done.flag", "file") then
|
if love.filesystem.getInfo("export_done.flag", "file") then
|
||||||
|
|||||||
@@ -262,7 +262,8 @@ function LinkBattle.new(game, net, opts)
|
|||||||
self.opponentName = theirName
|
self.opponentName = theirName
|
||||||
-- _TrainerWantsToFightText (data/text/text_2.asm:1257): wIsInBattle == 2
|
-- _TrainerWantsToFightText (data/text/text_2.asm:1257): wIsInBattle == 2
|
||||||
-- takes PrintBeginningBattleText's .trainerBattle arm, link included
|
-- takes PrintBeginningBattleText's .trainerBattle arm, link included
|
||||||
self.introText = Strings("%s wants\nto fight!", theirName)
|
self.introText = self:romText("_TrainerWantsToFightText",
|
||||||
|
"%s wants\nto fight!", theirName)
|
||||||
self.remoteHashes = {}
|
self.remoteHashes = {}
|
||||||
self.localHashes = {}
|
self.localHashes = {}
|
||||||
self.remoteParts = {}
|
self.remoteParts = {}
|
||||||
|
|||||||
+25
-2
@@ -7,6 +7,7 @@ SyncEngine.__index = SyncEngine
|
|||||||
|
|
||||||
SyncEngine.UPLOAD_DEBOUNCE = 5
|
SyncEngine.UPLOAD_DEBOUNCE = 5
|
||||||
SyncEngine.AUTO_INTERVAL = 300
|
SyncEngine.AUTO_INTERVAL = 300
|
||||||
|
SyncEngine.RESUME_MIN_GAP = 60
|
||||||
SyncEngine.MAX_STEPS_PER_UPDATE = 8
|
SyncEngine.MAX_STEPS_PER_UPDATE = 8
|
||||||
|
|
||||||
local IDLE_STATUS = "Ready"
|
local IDLE_STATUS = "Ready"
|
||||||
@@ -133,6 +134,7 @@ function SyncEngine.new(opts)
|
|||||||
eng.modPlan = nil
|
eng.modPlan = nil
|
||||||
eng.shareCode = nil
|
eng.shareCode = nil
|
||||||
eng.clock = 0
|
eng.clock = 0
|
||||||
|
eng.autoAt = SyncEngine.AUTO_INTERVAL
|
||||||
eng.queue = {}
|
eng.queue = {}
|
||||||
eng.pending = nil
|
eng.pending = nil
|
||||||
eng.uploadAt = nil
|
eng.uploadAt = nil
|
||||||
@@ -258,6 +260,11 @@ function SyncEngine:update(dt)
|
|||||||
self.uploadAt = nil
|
self.uploadAt = nil
|
||||||
if self.state.enabled and self:linked() then self:syncNow() end
|
if self.state.enabled and self:linked() then self:syncNow() end
|
||||||
end
|
end
|
||||||
|
if self.clock >= self.autoAt and not self:busy()
|
||||||
|
and (self.phase == "idle" or self.phase == "error")
|
||||||
|
and self.state.enabled and self:linked() then
|
||||||
|
self:syncNow()
|
||||||
|
end
|
||||||
local steps = 0
|
local steps = 0
|
||||||
while not self.pending and #self.queue > 0
|
while not self.pending and #self.queue > 0
|
||||||
and steps < SyncEngine.MAX_STEPS_PER_UPDATE do
|
and steps < SyncEngine.MAX_STEPS_PER_UPDATE do
|
||||||
@@ -296,6 +303,7 @@ function SyncEngine:createAccount(label)
|
|||||||
eng.phase = "idle"
|
eng.phase = "idle"
|
||||||
eng.status = "Sync account created"
|
eng.status = "Sync account created"
|
||||||
eng:_persist()
|
eng:_persist()
|
||||||
|
eng:syncNow()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -385,9 +393,24 @@ function SyncEngine:setEnabled(enabled)
|
|||||||
return self.state.enabled
|
return self.state.enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SyncEngine:protectPlaythrough(version, playthroughId)
|
||||||
|
self.protectedKey = SyncState.key(version, playthroughId)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SyncEngine:noteResumed()
|
||||||
|
if not (self.state.enabled and self:linked()) then return end
|
||||||
|
if self:busy() or self.phase == "conflict" then return end
|
||||||
|
if self.now() - (tonumber(self.state.lastSyncAt) or 0)
|
||||||
|
< SyncEngine.RESUME_MIN_GAP then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:syncNow()
|
||||||
|
end
|
||||||
|
|
||||||
function SyncEngine:syncNow()
|
function SyncEngine:syncNow()
|
||||||
if not self:linked() then return false, "this device is not linked" end
|
if not self:linked() then return false, "this device is not linked" end
|
||||||
if self.pending then return false, "sync is busy" end
|
if self.pending then return false, "sync is busy" end
|
||||||
|
self.autoAt = self.clock + SyncEngine.AUTO_INTERVAL
|
||||||
self.queue = {}
|
self.queue = {}
|
||||||
self.conflicts = {}
|
self.conflicts = {}
|
||||||
self.state.pendingConflicts = {}
|
self.state.pendingConflicts = {}
|
||||||
@@ -437,13 +460,13 @@ function SyncEngine:_planFrom(remoteState)
|
|||||||
self:_addConflict(entry, key, row)
|
self:_addConflict(entry, key, row)
|
||||||
elseif localChanged then
|
elseif localChanged then
|
||||||
self:_queueUpload(entry, key, false)
|
self:_queueUpload(entry, key, false)
|
||||||
elseif remoteChanged then
|
elseif remoteChanged and key ~= self.protectedKey then
|
||||||
self:_queueDownload(key, entry.version, entry.playthroughId, "replace")
|
self:_queueDownload(key, entry.version, entry.playthroughId, "replace")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for key, row in pairs(remote) do
|
for key, row in pairs(remote) do
|
||||||
if not seen[key] then
|
if not seen[key] and key ~= self.protectedKey then
|
||||||
local version, id = SyncState.splitKey(key)
|
local version, id = SyncState.splitKey(key)
|
||||||
if version and id then
|
if version and id then
|
||||||
self:_queueDownload(key, version, id, "replace", tonumber(row.rev))
|
self:_queueDownload(key, version, id, "replace", tonumber(row.rev))
|
||||||
|
|||||||
+4
-2
@@ -185,14 +185,16 @@ local function release(game)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
Strings("Once released,\n%s is\ngone forever. OK?", name), nil, {
|
(t._OnceReleasedText or Strings("Once released,\n%s is\ngone forever. OK?", name))
|
||||||
|
:gsub("{RAM:wStringBuffer}", name), nil, {
|
||||||
defaultNo = true, noSound = true,
|
defaultNo = true, noSound = true,
|
||||||
choice = function(yes)
|
choice = function(yes)
|
||||||
if not yes then return end
|
if not yes then return end
|
||||||
table.remove(box, list.index)
|
table.remove(box, list.index)
|
||||||
require("src.core.Sound").playCry(game.data, mon.species)
|
require("src.core.Sound").playCry(game.data, mon.species)
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
Strings("%s was\nreleased outside.\fBye %s!", name, name)))
|
((t._MonWasReleasedText or Strings("%s was\nreleased outside.\fBye %s!", name, name))
|
||||||
|
:gsub("{RAM:wStringBuffer}", name))))
|
||||||
list:removeCurrent()
|
list:removeCurrent()
|
||||||
end,
|
end,
|
||||||
}))
|
}))
|
||||||
|
|||||||
+5
-2
@@ -13,6 +13,7 @@ local ListMenu = require("src.ui.ListMenu")
|
|||||||
local Menu = require("src.ui.Menu")
|
local Menu = require("src.ui.Menu")
|
||||||
local QuantityBox = require("src.ui.QuantityBox")
|
local QuantityBox = require("src.ui.QuantityBox")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
|
local romText = require("src.core.RomText")
|
||||||
|
|
||||||
local ShopMenu = {}
|
local ShopMenu = {}
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ local function buy(game, stock)
|
|||||||
end
|
end
|
||||||
local cost = qty * def.price
|
local cost = qty * def.price
|
||||||
-- _PokemartTellBuyPriceText + yes/no confirm
|
-- _PokemartTellBuyPriceText + yes/no confirm
|
||||||
list.footer = Strings("%s?\nThat will be\n¥%d. OK?", def.name, cost)
|
list.footer = romText(game.data, "_PokemartTellBuyPriceText",
|
||||||
|
"%s?\nThat will be\n¥%d. OK?", def.name, cost)
|
||||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||||
if not yes then
|
if not yes then
|
||||||
list.footer = greet
|
list.footer = greet
|
||||||
@@ -147,7 +149,8 @@ local function sell(game)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- _PokemartTellSellPriceText + yes/no confirm
|
-- _PokemartTellSellPriceText + yes/no confirm
|
||||||
list.footer = Strings("I can pay you\n¥%d for that.", unit * qty)
|
list.footer = romText(game.data, "_PokemartTellSellPriceText",
|
||||||
|
"I can pay you\n¥%d for that.", unit * qty)
|
||||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||||
if not yes then
|
if not yes then
|
||||||
list.footer = greet
|
list.footer = greet
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
local Sound = require("src.core.Sound")
|
local Sound = require("src.core.Sound")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
|
local romText = require("src.core.RomText")
|
||||||
|
|
||||||
local SlotMachine = {}
|
local SlotMachine = {}
|
||||||
SlotMachine.__index = SlotMachine
|
SlotMachine.__index = SlotMachine
|
||||||
@@ -357,7 +358,8 @@ function SlotMachine:resolveWin(win)
|
|||||||
-- SlotReward300Func prints "Yeah!" (text_pause) before the flash; the port
|
-- SlotReward300Func prints "Yeah!" (text_pause) before the flash; the port
|
||||||
-- shows it in the box while the screen flashes. LinedUpText follows.
|
-- shows it in the box while the screen flashes. LinedUpText follows.
|
||||||
self.yeah = (sym == "7")
|
self.yeah = (sym == "7")
|
||||||
self.message = Strings("%s lined up!\nScored %d coins!", sym, pay)
|
self.message = sym .. romText(self.game.data, "_LinedUpText",
|
||||||
|
" lined up!\nScored %d coins!", pay)
|
||||||
-- .flashScreenLoop: flip rBGP, wait 5 frames, b times. The coins are not
|
-- .flashScreenLoop: flip rBGP, wait 5 frames, b times. The coins are not
|
||||||
-- credited until the player dismisses the "lined up" text (see startPayout).
|
-- credited until the player dismisses the "lined up" text (see startPayout).
|
||||||
self.stage = "flash"
|
self.stage = "flash"
|
||||||
|
|||||||
+47
-5
@@ -918,16 +918,58 @@ function Kit.rowsThatFit(h, rowH, gap, minRows, maxRows)
|
|||||||
return math.max(minRows or 1, math.min(maxRows or 99, per))
|
return math.max(minRows or 1, math.min(maxRows or 99, per))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Kit.dragX = nil
|
||||||
|
Kit.dragY = nil
|
||||||
|
Kit.dragAccum = 0
|
||||||
|
|
||||||
|
function Kit.dragBegin(x, y)
|
||||||
|
Kit.dragX, Kit.dragY, Kit.dragAccum = x, y, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function Kit.dragAdd(dy)
|
||||||
|
if Kit.dragX then Kit.dragAccum = Kit.dragAccum + (dy or 0) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Kit.dragEnd()
|
||||||
|
Kit.dragX, Kit.dragY, Kit.dragAccum = nil, nil, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dragOriginIn(x, y, w, h)
|
||||||
|
if not Kit.dragX then return false end
|
||||||
|
local x1, y1, x2, y2 = x, y, x + w, y + h
|
||||||
|
local c = Kit._clipRect
|
||||||
|
if c then
|
||||||
|
x1, y1 = math.max(x1, c.x), math.max(y1, c.y)
|
||||||
|
x2, y2 = math.min(x2, c.x + c.w), math.min(y2, c.y + c.h)
|
||||||
|
end
|
||||||
|
return Kit.dragX >= x1 and Kit.dragX <= x2
|
||||||
|
and Kit.dragY >= y1 and Kit.dragY <= y2
|
||||||
|
end
|
||||||
|
|
||||||
-- Mouse wheel over a paginated list turns PAGES. The wheel still has to do
|
-- Mouse wheel over a paginated list turns PAGES. The wheel still has to do
|
||||||
-- something (users expect it), but it moves a bounded page index rather than
|
-- something (users expect it), but it moves a bounded page index rather than
|
||||||
-- driving a pixel offset, so there is no scroll state and no interpolation.
|
-- driving a pixel offset, so there is no scroll state and no interpolation.
|
||||||
function Kit.wheelPage(x, y, w, h, page, total, perPage)
|
function Kit.wheelPage(x, y, w, h, page, total, perPage)
|
||||||
if Kit.blockClicks or (Kit.wheelY or 0) == 0 then return page end
|
if Kit.blockClicks then return page end
|
||||||
if not Kit.hit(x, y, w, h) then return page end
|
|
||||||
local pages = math.max(1, math.ceil(total / math.max(1, perPage)))
|
local pages = math.max(1, math.ceil(total / math.max(1, perPage)))
|
||||||
local moved = Theme.clamp((page or 1) + (Kit.wheelY > 0 and -1 or 1), 1, pages)
|
local out = page
|
||||||
Kit.wheelY = 0
|
if (Kit.wheelY or 0) ~= 0 and Kit.hit(x, y, w, h) then
|
||||||
return math.floor(moved)
|
out = math.floor(Theme.clamp((out or 1) + (Kit.wheelY > 0 and -1 or 1),
|
||||||
|
1, pages))
|
||||||
|
Kit.wheelY = 0
|
||||||
|
end
|
||||||
|
local acc = Kit.dragAccum or 0
|
||||||
|
if acc ~= 0 and dragOriginIn(x, y, w, h) then
|
||||||
|
local stepPx = math.max(1, math.floor((h or 0) / 2))
|
||||||
|
local flips = acc >= 0 and math.floor(acc / stepPx)
|
||||||
|
or -math.floor(-acc / stepPx)
|
||||||
|
if flips ~= 0 then
|
||||||
|
local want = (out or 1) + flips
|
||||||
|
out = math.floor(Theme.clamp(want, 1, pages))
|
||||||
|
Kit.dragAccum = out ~= want and 0 or acc - flips * stepPx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
function Kit.scrollExtent(contentH, viewH)
|
function Kit.scrollExtent(contentH, viewH)
|
||||||
|
|||||||
@@ -846,15 +846,20 @@ function OverworldState:useSoftboiledFieldMove(user, target)
|
|||||||
if not user or not user.stats or not target or not target.stats
|
if not user or not user.stats or not target or not target.stats
|
||||||
or target == user or target.hp <= 0
|
or target == user or target.hp <= 0
|
||||||
or target.hp >= target.stats.hp or user.hp <= heal then
|
or target.hp >= target.stats.hp or user.hp <= heal then
|
||||||
Game.stack:push(TextBox.new(Game, Strings("It won't have\nany effect.")))
|
Game.stack:push(TextBox.new(Game,
|
||||||
|
romText(Game.data, "_ItemUseNoEffectText", "It won't have\nany effect.")))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
local before = target.hp
|
||||||
user.hp = user.hp - heal
|
user.hp = user.hp - heal
|
||||||
target.hp = math.min(target.stats.hp, target.hp + heal)
|
target.hp = math.min(target.stats.hp, target.hp + heal)
|
||||||
require("src.core.Sound").play(Game.data, "Heal_HP")
|
require("src.core.Sound").play(Game.data, "Heal_HP")
|
||||||
local def = Game.data.pokemon[target.species]
|
local def = Game.data.pokemon[target.species]
|
||||||
|
-- _PotionText's second slot is the recovered amount, same as
|
||||||
|
-- ItemEffects.lua's potion message -- the engine fallback never shows it
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s's HP\nwas restored!", target.nickname or def.name)))
|
romText(Game.data, "_PotionText", "%s's HP\nwas restored!",
|
||||||
|
target.nickname or def.name, target.hp - before)))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2126,7 +2131,8 @@ function OverworldState:tryHiddenObject(fx, fy)
|
|||||||
-- leaves the spot unfound; _CantCarryMoreText is the Toss line (#872)
|
-- leaves the spot unfound; _CantCarryMoreText is the Toss line (#872)
|
||||||
local name = Game.data.items[h.item] and Game.data.items[h.item].name or h.item
|
local name = Game.data.items[h.item] and Game.data.items[h.item].name or h.item
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%s!", save.player.name, name) .. "\f"
|
romText(Game.data, "_FoundHiddenItemText", "%s found\n%s!",
|
||||||
|
save.player.name, name) .. "\f"
|
||||||
.. romText(Game.data, "_HiddenItemBagFullText",
|
.. romText(Game.data, "_HiddenItemBagFullText",
|
||||||
"But, {PLAYER} has\nno more room for\vother items!")))
|
"But, {PLAYER} has\nno more room for\vother items!")))
|
||||||
return true
|
return true
|
||||||
@@ -2137,7 +2143,8 @@ function OverworldState:tryHiddenObject(fx, fy)
|
|||||||
-- text_asm tail runs it as PlaySoundWaitForCurrent +
|
-- text_asm tail runs it as PlaySoundWaitForCurrent +
|
||||||
-- WaitForSoundToFinish once the box has printed (hidden_items.asm)
|
-- WaitForSoundToFinish once the box has printed (hidden_items.asm)
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%s!", save.player.name, name),
|
romText(Game.data, "_FoundHiddenItemText", "%s found\n%s!",
|
||||||
|
save.player.name, name),
|
||||||
nil, TextBox.soundOpts(Game, "Get_Item2")))
|
nil, TextBox.soundOpts(Game, "Get_Item2")))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -2795,8 +2802,8 @@ function OverworldState:talkTo(npc)
|
|||||||
"No more room for\nitems!")
|
"No more room for\nitems!")
|
||||||
if GameVersion.isYellow() then
|
if GameVersion.isYellow() then
|
||||||
local name = Game.data.items[d.item] and Game.data.items[d.item].name or d.item
|
local name = Game.data.items[d.item] and Game.data.items[d.item].name or d.item
|
||||||
noRoom = Strings("%s found\n%s!", Game.save.player.name, name)
|
noRoom = romText(Game.data, "_FoundItemText", "%s found\n%s!",
|
||||||
.. "\f" .. noRoom
|
Game.save.player.name, name) .. "\f" .. noRoom
|
||||||
end
|
end
|
||||||
Game.stack:push(TextBox.new(Game, noRoom))
|
Game.stack:push(TextBox.new(Game, noRoom))
|
||||||
return
|
return
|
||||||
@@ -2813,7 +2820,8 @@ function OverworldState:talkTo(npc)
|
|||||||
local ddef = Game.data.items[d.item]
|
local ddef = Game.data.items[d.item]
|
||||||
-- FoundItemText: text_far, sound_get_item_1, text_end (pick_up_item.asm)
|
-- FoundItemText: text_far, sound_get_item_1, text_end (pick_up_item.asm)
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%s!", Game.save.player.name, name), nil,
|
romText(Game.data, "_FoundItemText", "%s found\n%s!",
|
||||||
|
Game.save.player.name, name), nil,
|
||||||
TextBox.soundOpts(Game,
|
TextBox.soundOpts(Game,
|
||||||
(ddef and ddef.keyItem) and "Get_Key_Item" or "Get_Item1")))
|
(ddef and ddef.keyItem) and "Get_Key_Item" or "Get_Item1")))
|
||||||
return
|
return
|
||||||
@@ -3745,7 +3753,7 @@ function OverworldState:applyFieldPoison()
|
|||||||
local queue = {}
|
local queue = {}
|
||||||
for _, mon in ipairs(fainted) do
|
for _, mon in ipairs(fainted) do
|
||||||
local name = mon.nickname or Game.data.pokemon[mon.species].name
|
local name = mon.nickname or Game.data.pokemon[mon.species].name
|
||||||
table.insert(queue, Strings("%s\nfainted!", name))
|
table.insert(queue, romText(Game.data, "_PokemonFaintedText", "%s\nfainted!", name))
|
||||||
end
|
end
|
||||||
local alive = false
|
local alive = false
|
||||||
for _, mon in ipairs(save.party) do
|
for _, mon in ipairs(save.party) do
|
||||||
|
|||||||
@@ -58,6 +58,38 @@ check(position("if (secondaryEnabled) registerSecondaryDisplayListener();") <
|
|||||||
"secondary display monitoring starts before initial discovery")
|
"secondary display monitoring starts before initial discovery")
|
||||||
check(source:find("!monitor.hasDisplay(display.getDisplayId())", 1, true),
|
check(source:find("!monitor.hasDisplay(display.getDisplayId())", 1, true),
|
||||||
"a disconnected active display is rebound without replacing a live one")
|
"a disconnected active display is rebound without replacing a live one")
|
||||||
|
check(source:find("private static volatile boolean secondaryHostResumed = false;",
|
||||||
|
1, true), "secondary output tracks the primary activity lifecycle")
|
||||||
|
check(source:find("if (on && secondaryHostResumed)", 1, true)
|
||||||
|
and source:find("self == null || !secondaryHostResumed || !secondaryEnabled",
|
||||||
|
1, true),
|
||||||
|
"paused hosts cannot reopen secondary output from a late mod frame")
|
||||||
|
|
||||||
|
local pause = position("protected void onPause()")
|
||||||
|
local paused = assert(source:find("secondaryHostResumed = false;", pause, true))
|
||||||
|
local teardown = assert(source:find("teardownSecondaryDisplay();", pause, true))
|
||||||
|
local pauseSuper = assert(source:find("super.onPause();", pause, true))
|
||||||
|
check(pause < paused and paused < teardown and teardown < pauseSuper,
|
||||||
|
"pause blocks secondary setup before dismissing its output")
|
||||||
|
|
||||||
|
local resume = position("public void onResume()")
|
||||||
|
local resumeSuper = assert(source:find("super.onResume();", resume, true))
|
||||||
|
local resumed = assert(source:find("secondaryHostResumed = true;", resume, true))
|
||||||
|
local resumeSetup = assert(source:find("setupSecondaryDisplay();", resume, true))
|
||||||
|
check(resume < resumeSuper and resumeSuper < resumed and resumed < resumeSetup,
|
||||||
|
"resume permits secondary setup only after the primary activity resumes")
|
||||||
|
|
||||||
|
local destroy = position("protected void onDestroy()")
|
||||||
|
local destroyTeardown = assert(source:find("teardownSecondaryDisplay();", destroy, true))
|
||||||
|
local destroySuper = assert(source:find("super.onDestroy();", destroy, true))
|
||||||
|
check(destroy < destroyTeardown and destroyTeardown < destroySuper,
|
||||||
|
"destroy always dismisses secondary output before SDL destruction")
|
||||||
|
|
||||||
|
local mainFile = assert(io.open("main.lua", "rb"))
|
||||||
|
local main = mainFile:read("*a")
|
||||||
|
mainFile:close()
|
||||||
|
check(main:find('require("src.render.SecondScreen").setEnabled(false)', 1, true),
|
||||||
|
"returning from a game disables mod-owned secondary output")
|
||||||
|
|
||||||
check(not source:lower():find("openxr", 1, true),
|
check(not source:lower():find("openxr", 1, true),
|
||||||
"generic Android activity must not require OpenXR")
|
"generic Android activity must not require OpenXR")
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- BattleState:throwBall()'s can't-be-caught path used to queue two
|
||||||
|
-- separate Strings() literals ("It dodged the\nthrown BALL!" then "This
|
||||||
|
-- POKéMON\ncan't be caught!"). The real ROM label _ItemUseBallText00
|
||||||
|
-- combines both as one \f-paged string. TextBox.new() would split \f
|
||||||
|
-- itself, but throwBall() queues through self:sayNext(), which goes
|
||||||
|
-- through the battle queue's own BattleState:startMessage() -- and that
|
||||||
|
-- one only splits on \n/\v, not \f (confirmed live: the \f landed
|
||||||
|
-- mid-line and the second sentence overflowed off the box instead of
|
||||||
|
-- starting a fresh page). The fix resolves the label once, then splits
|
||||||
|
-- it the same way TextBox.lua does and queues one sayNext per page. This
|
||||||
|
-- test fakes the label and checks the two pages reach the queue as two
|
||||||
|
-- separate messages, in order, not merged into one with a raw \f still
|
||||||
|
-- inside it.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
|
local function mkbattle()
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.party = { Pokemon.new(Data, "FIXMON_A", 10) }
|
||||||
|
local game = { data = Data, save = save,
|
||||||
|
stack = { top = function() return nil end, push = function() end } }
|
||||||
|
local battle = BattleState.newWild(game, "FIXMON_C", 8)
|
||||||
|
battle.ghost = true -- forces the can't-be-caught path
|
||||||
|
return battle
|
||||||
|
end
|
||||||
|
|
||||||
|
-- throwBall defers its message-queuing work into self.queue via one
|
||||||
|
-- top-level self:act(fn); run only that one function to reach the
|
||||||
|
-- say() calls the fix touches. It's the last entry throwBall itself
|
||||||
|
-- appends (after the immediate sayAuto), and it further queues its own
|
||||||
|
-- self:act(function() self:executeAction(...) end) for the actual enemy
|
||||||
|
-- turn -- deliberately NOT run here (out of scope, and running the queue
|
||||||
|
-- generically after mutation risks looping into a real turn simulation)
|
||||||
|
local function runThrowBallAct(battle)
|
||||||
|
for i = #battle.queue, 1, -1 do
|
||||||
|
if battle.queue[i].fn then
|
||||||
|
battle.queue[i].fn()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function textEntries(battle)
|
||||||
|
local out = {}
|
||||||
|
for _, entry in ipairs(battle.queue) do
|
||||||
|
if entry.text then out[#out + 1] = entry.text end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- translated: the faked label's two \f-separated pages reach the queue
|
||||||
|
-- as two separate messages, in order, and neither one still contains a
|
||||||
|
-- raw \f (which would mean the battle queue's own renderer has to deal
|
||||||
|
-- with it, and it can't)
|
||||||
|
do
|
||||||
|
local battle = mkbattle()
|
||||||
|
Data.text._ItemUseBallText00 = "FAKE-DODGE!\fFAKE-CANTCATCH!"
|
||||||
|
battle:throwBall("FIX_BALL")
|
||||||
|
runThrowBallAct(battle)
|
||||||
|
local texts = textEntries(battle)
|
||||||
|
T.eq(texts[1], "FAKE-DODGE!", "page 1 reaches the queue on its own")
|
||||||
|
T.eq(texts[2], "FAKE-CANTCATCH!", "page 2 follows right after, still in order")
|
||||||
|
for _, t in ipairs(texts) do
|
||||||
|
T.check(not t:find("\f", 1, true), "no queued message still carries a raw \\f")
|
||||||
|
end
|
||||||
|
Data.text._ItemUseBallText00 = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: no catalog entry still falls back to the two English pages,
|
||||||
|
-- split the same way
|
||||||
|
do
|
||||||
|
local battle = mkbattle()
|
||||||
|
battle:throwBall("FIX_BALL")
|
||||||
|
runThrowBallAct(battle)
|
||||||
|
local texts = textEntries(battle)
|
||||||
|
T.eq(texts[1], "It dodged the\nthrown BALL!", "vanilla page 1")
|
||||||
|
T.eq(texts[2], "This POKéMON\ncan't be caught!", "vanilla page 2")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("battle_ball_dodge_romtext")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
-- BattleState:storeCaughtMon() queues up to two plain-Lua-literal
|
||||||
|
-- messages: the new-Pokedex-data line (_ItemUseBallText06) and, when the
|
||||||
|
-- party is full, the box-transfer line (_ItemUseBallText07/08, keyed on
|
||||||
|
-- EVENT_MET_BILL -- two full, independently-translated ROM strings, not
|
||||||
|
-- one template with a substituted PC name). This test fakes all three
|
||||||
|
-- labels and checks the queued messages use them, not the English
|
||||||
|
-- literals.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
|
local function findText(battle, needle)
|
||||||
|
for _, entry in ipairs(battle.queue) do
|
||||||
|
if entry.text and entry.text:find(needle, 1, true) then return entry.text end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkbattle(partySize, metBill)
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.party = {}
|
||||||
|
for i = 1, partySize do
|
||||||
|
save.party[i] = Pokemon.new(Data, "FIXMON_A", 5)
|
||||||
|
end
|
||||||
|
save.flags = save.flags or {}
|
||||||
|
save.flags.EVENT_MET_BILL = metBill
|
||||||
|
local game = { data = Data, save = save,
|
||||||
|
stack = { top = function() return nil end, push = function() end } }
|
||||||
|
return BattleState.newWild(game, "FIXMON_C", 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- empty party: Party.add succeeds, only the new-Pokedex-data message fires
|
||||||
|
do
|
||||||
|
local battle = mkbattle(0, false)
|
||||||
|
Data.text._ItemUseBallText06 = "FAKE-DEX {RAM:wEnemyMonNick} FAKE!"
|
||||||
|
battle:storeCaughtMon()
|
||||||
|
T.eq(findText(battle, "FAKE-DEX"), "FAKE-DEX " .. battle.enemy.name .. " FAKE!",
|
||||||
|
"a translated _ItemUseBallText06 reaches the new-Pokedex-data message")
|
||||||
|
Data.text._ItemUseBallText06 = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- full party, EVENT_MET_BILL true: box transfer via _ItemUseBallText07
|
||||||
|
do
|
||||||
|
local battle = mkbattle(6, true)
|
||||||
|
Data.text._ItemUseBallText07 = "FAKE-BILL {RAM:wBoxMonNicks} FAKE!"
|
||||||
|
battle:storeCaughtMon()
|
||||||
|
T.eq(findText(battle, "FAKE-BILL"), "FAKE-BILL " .. battle.enemy.name .. " FAKE!",
|
||||||
|
"EVENT_MET_BILL true routes through the translated _ItemUseBallText07")
|
||||||
|
Data.text._ItemUseBallText07 = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- full party, EVENT_MET_BILL false: box transfer via _ItemUseBallText08
|
||||||
|
do
|
||||||
|
local battle = mkbattle(6, false)
|
||||||
|
Data.text._ItemUseBallText08 = "FAKE-SOMEONE {RAM:wBoxMonNicks} FAKE!"
|
||||||
|
battle:storeCaughtMon()
|
||||||
|
T.eq(findText(battle, "FAKE-SOMEONE"), "FAKE-SOMEONE " .. battle.enemy.name .. " FAKE!",
|
||||||
|
"EVENT_MET_BILL false routes through the translated _ItemUseBallText08")
|
||||||
|
Data.text._ItemUseBallText08 = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla, full party, EVENT_MET_BILL true: English literal, BILL's PC
|
||||||
|
do
|
||||||
|
local battle = mkbattle(6, true)
|
||||||
|
battle:storeCaughtMon()
|
||||||
|
T.eq(findText(battle, "transferred"),
|
||||||
|
battle.enemy.name .. " was\ntransferred to\nBILL's PC!",
|
||||||
|
"no catalog entry falls back to the English BILL's-PC literal")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("battle_catch_messages_romtext")
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
-- BattleState:onFaint's "%s\nfainted!" collapsed two distinct ROM
|
||||||
|
-- strings (_EnemyMonFaintedText already carries its own "Enemy" wording;
|
||||||
|
-- _PlayerMonFaintedText does not) into one literal, substituted with
|
||||||
|
-- displayName(battler) -- which itself runs the enemy name through a
|
||||||
|
-- SEPARATE Strings("Enemy %s", ...) call. The fix passes the raw
|
||||||
|
-- battler.name and lets each label supply its own wording. This test
|
||||||
|
-- fakes both labels and checks the raw name reaches the right one, with
|
||||||
|
-- no "Enemy" ever duplicated.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
|
local function mkbattle()
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.party = { Pokemon.new(Data, "FIXMON_A", 10) }
|
||||||
|
local game = { data = Data, save = save,
|
||||||
|
stack = { top = function() return nil end, push = function() end } }
|
||||||
|
return BattleState.newWild(game, "FIXMON_C", 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- finds the queued say-message text (onFaint queues several entries: a
|
||||||
|
-- wait, then the say)
|
||||||
|
local function findText(battle)
|
||||||
|
for _, entry in ipairs(battle.queue) do
|
||||||
|
if entry.text then return entry.text end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- enemy: translated _EnemyMonFaintedText reaches onFaint, raw name only
|
||||||
|
do
|
||||||
|
local battle = mkbattle()
|
||||||
|
Data.text._EnemyMonFaintedText = "FAKE-ENEMY {RAM:wEnemyMonNick} FAKE!"
|
||||||
|
battle:onFaint(battle.enemy)
|
||||||
|
T.eq(findText(battle), "FAKE-ENEMY " .. battle.enemy.name .. " FAKE!",
|
||||||
|
"a translated _EnemyMonFaintedText reaches the enemy faint message")
|
||||||
|
Data.text._EnemyMonFaintedText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- enemy vanilla: the English literal already carries "Enemy " itself
|
||||||
|
do
|
||||||
|
local battle = mkbattle()
|
||||||
|
battle:onFaint(battle.enemy)
|
||||||
|
T.eq(findText(battle), "Enemy " .. battle.enemy.name .. "\nfainted!",
|
||||||
|
"no catalog entry falls back to the English literal, Enemy included")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- player: translated _PlayerMonFaintedText reaches onFaint
|
||||||
|
do
|
||||||
|
local battle = mkbattle()
|
||||||
|
Data.text._PlayerMonFaintedText = "FAKE-PLAYER {RAM:wBattleMonNick} FAKE!"
|
||||||
|
battle:onFaint(battle.player)
|
||||||
|
T.eq(findText(battle), "FAKE-PLAYER " .. battle.player.name .. " FAKE!",
|
||||||
|
"a translated _PlayerMonFaintedText reaches the player faint message")
|
||||||
|
Data.text._PlayerMonFaintedText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("battle_fainted_message_romtext")
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
-- BoxMenu's RELEASE confirmation ("Once released,\n%s is\ngone forever.
|
||||||
|
-- OK?") used to be a bare Lua literal. tests/engine/pc_release.lua only
|
||||||
|
-- ever runs with an empty Data.text, so it can't tell a properly-wired
|
||||||
|
-- t._OnceReleasedText or "..." fallback apart from a literal that never
|
||||||
|
-- looked at t at all -- every assertion there passes either way. This
|
||||||
|
-- test drives the same interactive release flow with a faked
|
||||||
|
-- Data.text._OnceReleasedText and checks the pushed TextBox's raw text
|
||||||
|
-- (captured via a TextBox.new spy, so real pagination/choice behavior is
|
||||||
|
-- untouched) uses the translated value, not the English literal.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
local ids = T.fixtures.ids
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local Boxes = require("src.pokemon.Boxes")
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
local BoxMenu = require("src.ui.BoxMenu")
|
||||||
|
local ListMenu = require("src.ui.ListMenu")
|
||||||
|
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Sound = require("src.core.Sound")
|
||||||
|
|
||||||
|
local realCry, realPlay = Sound.playCry, Sound.play
|
||||||
|
Sound.playCry = function() end
|
||||||
|
Sound.play = function() end
|
||||||
|
|
||||||
|
-- spy: records every raw text TextBox.new receives, without disturbing
|
||||||
|
-- pagination/choice re-push, so the interactive flow behaves exactly like
|
||||||
|
-- pc_release.lua's
|
||||||
|
local captured
|
||||||
|
local realNew = TextBox.new
|
||||||
|
TextBox.new = function(game, text, onDone, opts)
|
||||||
|
captured[#captured + 1] = text
|
||||||
|
return realNew(game, text, onDone, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
local stack = { states = {} }
|
||||||
|
function stack:push(s) self.states[#self.states + 1] = s end
|
||||||
|
function stack:pop()
|
||||||
|
local t = self.states[#self.states]
|
||||||
|
self.states[#self.states] = nil
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
function stack:top() return self.states[#self.states] end
|
||||||
|
function stack:update(dt)
|
||||||
|
local t = self:top()
|
||||||
|
if t and t.update then t:update(dt) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local pressed = {}
|
||||||
|
local function press(btn)
|
||||||
|
pressed = { [btn] = true }
|
||||||
|
stack:update(1 / 60)
|
||||||
|
pressed = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function topMt() return getmetatable(stack:top()) end
|
||||||
|
local function mash(btn, cond, n)
|
||||||
|
for _ = 1, (n or 400) do
|
||||||
|
if cond() then return true end
|
||||||
|
press(btn)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkGame()
|
||||||
|
stack.states = {}
|
||||||
|
captured = {}
|
||||||
|
local game = {
|
||||||
|
data = Data,
|
||||||
|
save = SaveData.newGame(),
|
||||||
|
stack = stack,
|
||||||
|
input = {
|
||||||
|
wasPressed = function(_, key) return pressed[key] or false end,
|
||||||
|
isDown = function() return false end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
game.save.options = game.save.options or {}
|
||||||
|
game.save.options.textSpeed = 1
|
||||||
|
local box = Boxes.active(game.save)
|
||||||
|
box[1] = Pokemon.new(Data, ids.species[1], 5)
|
||||||
|
return game, box
|
||||||
|
end
|
||||||
|
|
||||||
|
local function releaseFirstMon(game)
|
||||||
|
stack:push(BoxMenu.new(game))
|
||||||
|
press("down"); press("down"); press("a") -- open RELEASE list
|
||||||
|
T.check(topMt() == ListMenu, "RELEASE opens the box list")
|
||||||
|
press("a") -- choose the first (only) mon
|
||||||
|
T.check(mash("a", function() return topMt() == ChoiceBox end),
|
||||||
|
"confirm choice opens")
|
||||||
|
-- the confirmation TextBox is captured[1] the moment it was pushed,
|
||||||
|
-- before this mash even ran
|
||||||
|
return captured[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- monName (BoxMenu.lua): mon.nickname or def.name
|
||||||
|
local function monName(box)
|
||||||
|
local mon = box[1]
|
||||||
|
local def = Data.pokemon[mon.species]
|
||||||
|
return mon.nickname or def.name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- translated: the fake value must reach the pushed TextBox
|
||||||
|
do
|
||||||
|
local game, box = mkGame()
|
||||||
|
local name = monName(box)
|
||||||
|
Data.text._OnceReleasedText = "FAKE {RAM:wStringBuffer} released!"
|
||||||
|
local text = releaseFirstMon(game)
|
||||||
|
T.eq(text, "FAKE " .. name .. " released!",
|
||||||
|
"a translated _OnceReleasedText reaches the release confirmation")
|
||||||
|
Data.text._OnceReleasedText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: with no catalog entry, the English literal still substitutes
|
||||||
|
do
|
||||||
|
local game, box = mkGame()
|
||||||
|
local name = monName(box)
|
||||||
|
local text = releaseFirstMon(game)
|
||||||
|
T.eq(text, "Once released,\n" .. name .. " is\ngone forever. OK?",
|
||||||
|
"no catalog entry still falls back to the English literal")
|
||||||
|
end
|
||||||
|
|
||||||
|
TextBox.new = realNew
|
||||||
|
Sound.playCry, Sound.play = realCry, realPlay
|
||||||
|
T.finish("box_release_confirmation_romtext")
|
||||||
@@ -300,6 +300,88 @@ LauncherView.draw(edgeImp)
|
|||||||
check((edgeImp._tabScroll.skins or 0) > 0,
|
check((edgeImp._tabScroll.skins or 0) > 0,
|
||||||
"which reaches the tab region even though the cursor is below it")
|
"which reaches the tab region even though the cursor is below it")
|
||||||
|
|
||||||
|
Kit.blockClicks = false
|
||||||
|
Kit._clipRect = nil
|
||||||
|
Kit.wheelY = 0
|
||||||
|
Kit.mouseX, Kit.mouseY = 400, 400
|
||||||
|
Kit.dragBegin(50, 50)
|
||||||
|
Kit.dragAdd(120)
|
||||||
|
local pg = Kit.wheelPage(0, 0, 100, 100, 1, 100, 10)
|
||||||
|
eq(pg, 3, "a drag that crossed two half-heights turns two pages")
|
||||||
|
eq(Kit.dragAccum, 20, "and keeps the remainder for the next flip")
|
||||||
|
Kit.dragAdd(-140)
|
||||||
|
pg = Kit.wheelPage(0, 0, 100, 100, pg, 100, 10)
|
||||||
|
eq(pg, 1, "dragging back down returns those pages")
|
||||||
|
eq(Kit.dragAccum, -20, "with the remainder's sign preserved")
|
||||||
|
Kit.dragAdd(-80)
|
||||||
|
pg = Kit.wheelPage(0, 0, 100, 100, pg, 100, 10)
|
||||||
|
eq(pg, 1, "a drag past the first page clamps to it")
|
||||||
|
eq(Kit.dragAccum, 0, "and drops the pile-up so reversing is instant")
|
||||||
|
Kit.dragAdd(200)
|
||||||
|
pg = Kit.wheelPage(200, 200, 100, 100, pg, 100, 10)
|
||||||
|
eq(pg, 1, "a drag that began outside the list is not the list's")
|
||||||
|
eq(Kit.dragAccum, 200, "and its travel stays queued")
|
||||||
|
Kit.dragEnd()
|
||||||
|
eq(Kit.dragAccum, 0, "releasing the button retires the gesture")
|
||||||
|
|
||||||
|
window(360, 780)
|
||||||
|
local mouseImp = skinLauncher(12)
|
||||||
|
LauncherView.draw(mouseImp)
|
||||||
|
LauncherView.draw(mouseImp)
|
||||||
|
local mreg = mouseImp._tabRegionRect
|
||||||
|
local mmax = mouseImp._tabScrollMax.skins
|
||||||
|
check(mmax > 0, "the mouse-dragged panel has travel")
|
||||||
|
local mdown = false
|
||||||
|
love.mouse.isDown = function() return mdown end
|
||||||
|
|
||||||
|
pointer(mreg.x + 20, mreg.y + 40)
|
||||||
|
mdown = true
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
check(mouseImp._clickPt == nil,
|
||||||
|
"a press over a scrollable region mints no click")
|
||||||
|
check(mouseImp._mouseAt ~= nil, "it arms a drag instead")
|
||||||
|
pointer(mreg.x + 20, mreg.y + 40 - 200)
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
eq(mouseImp._tabScroll.skins, math.min(200, mmax),
|
||||||
|
"dragging the held mouse scrolls the panel by its travel")
|
||||||
|
eq(mouseImp._pageScroll or 0, 0,
|
||||||
|
"while the panel still has travel, the page waits")
|
||||||
|
pointer(mreg.x + 20, mreg.y + 40 - 200 - mmax * 2)
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
eq(mouseImp._tabScroll.skins, mmax,
|
||||||
|
"a longer mouse drag reaches the panel's bottom")
|
||||||
|
check((mouseImp._pageScroll or 0) > 0, "and spills into the page from there")
|
||||||
|
mdown = false
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
check(mouseImp._clickPt == nil, "a released drag is not a click")
|
||||||
|
check(mouseImp._mouseAt == nil, "and the gesture is retired")
|
||||||
|
|
||||||
|
pointer(mreg.x + 20, mreg.y + 40)
|
||||||
|
mdown = true
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
check(mouseImp._clickPt == nil, "a fresh press still holds its click back")
|
||||||
|
mdown = false
|
||||||
|
LauncherView.update(mouseImp, 0.016)
|
||||||
|
check(mouseImp._clickPt ~= nil,
|
||||||
|
"press and release without travel is still a tap, on release")
|
||||||
|
mouseImp._clickPt = nil
|
||||||
|
|
||||||
|
LauncherView.draw(gameImp)
|
||||||
|
LauncherView.draw(gameImp)
|
||||||
|
check((gameImp._noDragN or 0) > 0, "the game tab publishes its cartridge rect")
|
||||||
|
local cart = gameImp._noDragRects[1]
|
||||||
|
pointer(cart.x + cart.w / 2, cart.y + cart.h / 2)
|
||||||
|
mdown = true
|
||||||
|
LauncherView.update(gameImp, 0.016)
|
||||||
|
check(gameImp._clickPt ~= nil,
|
||||||
|
"a press on the cartridge clicks at once so its own spin-drag still owns "
|
||||||
|
.. "the gesture")
|
||||||
|
check(gameImp._mouseAt == nil, "and never arms the scroll drag")
|
||||||
|
gameImp._clickPt = nil
|
||||||
|
mdown = false
|
||||||
|
LauncherView.update(gameImp, 0.016)
|
||||||
|
love.mouse.isDown = nil
|
||||||
|
|
||||||
local function read(path)
|
local function read(path)
|
||||||
local f = assert(io.open(path, "r"))
|
local f = assert(io.open(path, "r"))
|
||||||
local src = f:read("*a")
|
local src = f:read("*a")
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Two OverworldController.lua messages used to be bare Lua literals:
|
||||||
|
-- applyFieldPoison()'s "%s\nfainted!" (the third of three collapsed
|
||||||
|
-- fainted-message ROM strings, _PokemonFaintedText) and
|
||||||
|
-- useSoftboiledFieldMove()'s "It won't have\nany effect."/"%s's HP\nwas
|
||||||
|
-- restored!" (the same _ItemUseNoEffectText/_PotionText labels
|
||||||
|
-- ItemEffects.lua's real potion message already uses -- _PotionText's
|
||||||
|
-- second slot is the actual amount healed, which the old literal never
|
||||||
|
-- showed at all).
|
||||||
|
--
|
||||||
|
-- Uses the debug.setupvalue technique already established in
|
||||||
|
-- oaks_pc_flow.lua to fake the module-level Game/TextBox upvalues
|
||||||
|
-- ROM-free, without going through the heavy OverworldState:enter().
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local OW = require("src.world.OverworldController")
|
||||||
|
|
||||||
|
local function setUpvalue(fn, name, val)
|
||||||
|
local i = 1
|
||||||
|
while true do
|
||||||
|
local n = debug.getupvalue(fn, i)
|
||||||
|
if not n then return false end
|
||||||
|
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local pushed = {}
|
||||||
|
local textBoxStub = {
|
||||||
|
new = function(_, text, onDone, opts)
|
||||||
|
return { text = text, onDone = onDone, opts = opts }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local realSound = package.loaded["src.core.Sound"]
|
||||||
|
package.loaded["src.core.Sound"] = { play = function() end, playCry = function() end }
|
||||||
|
|
||||||
|
local function mkGame()
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
|
||||||
|
pushed = {}
|
||||||
|
return {
|
||||||
|
data = Data, save = save,
|
||||||
|
stack = { push = function(_, item) pushed[#pushed + 1] = item end },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, name in ipairs({ "applyFieldPoison", "useSoftboiledFieldMove" }) do
|
||||||
|
T.check(setUpvalue(OW[name], "Game", mkGame()), ("Game upvalue on %s"):format(name))
|
||||||
|
T.check(setUpvalue(OW[name], "TextBox", textBoxStub), ("TextBox upvalue on %s"):format(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
local fakeSelf = setmetatable({}, { __index = OW })
|
||||||
|
|
||||||
|
-- ---- applyFieldPoison: _PokemonFaintedText ----
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.applyFieldPoison, "Game", game)
|
||||||
|
local mon = game.save.party[1]
|
||||||
|
mon.status = "PSN"
|
||||||
|
mon.hp = 1 -- one poison tick (1 dmg by default) faints it
|
||||||
|
game.save.poisonSteps = 3 -- (3+1) % 4 == 0: this step ticks poison
|
||||||
|
|
||||||
|
Data.text._PokemonFaintedText = "FAKE {RAM:wNameBuffer} FAKE!"
|
||||||
|
fakeSelf:applyFieldPoison()
|
||||||
|
T.eq(pushed[1] and pushed[1].text, "FAKE " .. (mon.nickname or "FIXMON A") .. " FAKE!",
|
||||||
|
"a translated _PokemonFaintedText reaches the field-poison faint message")
|
||||||
|
Data.text._PokemonFaintedText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: no catalog entry, English literal
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.applyFieldPoison, "Game", game)
|
||||||
|
local mon = game.save.party[1]
|
||||||
|
mon.status = "PSN"
|
||||||
|
mon.hp = 1
|
||||||
|
game.save.poisonSteps = 3
|
||||||
|
|
||||||
|
fakeSelf:applyFieldPoison()
|
||||||
|
T.eq(pushed[1] and pushed[1].text,
|
||||||
|
(mon.nickname or "FIXMON A") .. "\nfainted!",
|
||||||
|
"no catalog entry falls back to the English fainted literal")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- useSoftboiledFieldMove: _ItemUseNoEffectText / _PotionText ----
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||||
|
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||||
|
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||||
|
target.hp = target.stats.hp -- already full: no effect
|
||||||
|
|
||||||
|
Data.text._ItemUseNoEffectText = "FAKE-NOEFFECT!"
|
||||||
|
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||||
|
T.check(ok == false, "a full-HP target reports no effect")
|
||||||
|
T.eq(pushed[1] and pushed[1].text, "FAKE-NOEFFECT!",
|
||||||
|
"a translated _ItemUseNoEffectText reaches the no-effect message")
|
||||||
|
Data.text._ItemUseNoEffectText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||||
|
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||||
|
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||||
|
target.hp = target.stats.hp - 10 -- missing exactly 10 HP
|
||||||
|
|
||||||
|
Data.text._PotionText = "FAKE {RAM:wNameBuffer} healed {NUM:wHPBarHPDifference, 2, 3}!"
|
||||||
|
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||||
|
T.check(ok == true, "a damaged target heals successfully")
|
||||||
|
T.eq(pushed[1] and pushed[1].text,
|
||||||
|
"FAKE " .. (target.nickname or "FIXMON B") .. " healed 10!",
|
||||||
|
"a translated _PotionText reaches the heal message, amount included")
|
||||||
|
Data.text._PotionText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: no catalog entry, the fallback's single %s slot still fills
|
||||||
|
-- correctly (the amount is silently dropped by design, same as
|
||||||
|
-- ItemEffects.lua's own _PotionText fallback -- not a regression, this
|
||||||
|
-- matches the pre-fix literal's behavior exactly)
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||||
|
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||||
|
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||||
|
target.hp = target.stats.hp - 10
|
||||||
|
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||||
|
T.check(ok == true, "a damaged target heals successfully (vanilla)")
|
||||||
|
T.eq(pushed[1] and pushed[1].text,
|
||||||
|
(target.nickname or "FIXMON B") .. "'s HP\nwas restored!",
|
||||||
|
"no catalog entry falls back to the English literal (no amount shown)")
|
||||||
|
end
|
||||||
|
|
||||||
|
package.loaded["src.core.Sound"] = realSound
|
||||||
|
T.finish("overworld_field_faint_heal_romtext")
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
-- OverworldState:tryHiddenObject()'s "%s found\n%s!" message used to
|
||||||
|
-- substitute both the player name and the item name into one bare Lua
|
||||||
|
-- literal. The real _FoundHiddenItemText label leads with a {PLAYER}
|
||||||
|
-- named token, which romText auto-fills from a 2-arg call in the same
|
||||||
|
-- order the literal already used -- this test checks both slots land
|
||||||
|
-- correctly (an accidental argument swap is the easy mistake this shape
|
||||||
|
-- invites).
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local OW = require("src.world.OverworldController")
|
||||||
|
|
||||||
|
local function setUpvalue(fn, name, val)
|
||||||
|
local i = 1
|
||||||
|
while true do
|
||||||
|
local n = debug.getupvalue(fn, i)
|
||||||
|
if not n then return false end
|
||||||
|
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local pushed = {}
|
||||||
|
local textBoxStub = {
|
||||||
|
new = function(_, text, onDone, opts)
|
||||||
|
return { text = text, onDone = onDone, opts = opts }
|
||||||
|
end,
|
||||||
|
soundOpts = function() return {} end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local MAP_ID = "FIX_TOWN"
|
||||||
|
Data.field.hiddenItems[MAP_ID] = { { x = 3, y = 3, item = "FIX_BALL" } }
|
||||||
|
|
||||||
|
local function mkGame()
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.player.name = "FAKEPLAYER"
|
||||||
|
pushed = {}
|
||||||
|
return {
|
||||||
|
data = Data, save = save,
|
||||||
|
stack = { push = function(_, item) pushed[#pushed + 1] = item end },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
T.check(setUpvalue(OW.tryHiddenObject, "Game", mkGame()), "Game upvalue on tryHiddenObject")
|
||||||
|
T.check(setUpvalue(OW.tryHiddenObject, "TextBox", textBoxStub), "TextBox upvalue on tryHiddenObject")
|
||||||
|
|
||||||
|
local fakeSelf = setmetatable({ map = { id = MAP_ID } }, { __index = OW })
|
||||||
|
|
||||||
|
-- translated: player name and item name both land in the right slots
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.tryHiddenObject, "Game", game)
|
||||||
|
Data.text._FoundHiddenItemText = "FAKE {PLAYER} found FAKE {RAM:wNameBuffer} FAKE!"
|
||||||
|
local found = fakeSelf:tryHiddenObject(3, 3)
|
||||||
|
T.check(found == true, "the hidden item at (3,3) is found")
|
||||||
|
T.eq(pushed[1] and pushed[1].text,
|
||||||
|
"FAKE FAKEPLAYER found FAKE FIX BALL FAKE!",
|
||||||
|
"a translated _FoundHiddenItemText fills both {PLAYER} and the item name")
|
||||||
|
Data.text._FoundHiddenItemText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: no catalog entry, so romText falls back to plain
|
||||||
|
-- Strings(fallback, ...) -- the fallback literal is "%s found\n%s!" (both
|
||||||
|
-- slots plain %s, matching the pre-fix literal's own shape), not the
|
||||||
|
-- {PLAYER} token the real label uses, since Strings() never does
|
||||||
|
-- {TOKEN} substitution on its own. (A {PLAYER}-token fallback would
|
||||||
|
-- still render correctly too, since the real TextBox.new always runs
|
||||||
|
-- TextBox.substitute over whatever text it's given -- but this test
|
||||||
|
-- stubs TextBox without that call, and the fallback shouldn't lean on a
|
||||||
|
-- substitution pass happening downstream regardless.)
|
||||||
|
do
|
||||||
|
local game = mkGame()
|
||||||
|
setUpvalue(OW.tryHiddenObject, "Game", game)
|
||||||
|
game.save.hiddenTaken = {} -- fresh spot
|
||||||
|
local found = fakeSelf:tryHiddenObject(3, 3)
|
||||||
|
T.check(found == true, "the hidden item is found again in a fresh game")
|
||||||
|
T.eq(pushed[1] and pushed[1].text, "FAKEPLAYER found\nFIX BALL!",
|
||||||
|
"with no catalog entry, the fallback still fills both the player "
|
||||||
|
.. "and item name via plain %s substitution")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("overworld_hidden_item_romtext")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- SlotMachine:resolveWin's "%s lined up!\nScored %d coins!" message used
|
||||||
|
-- to interpolate the symbol id AND the payout into one bare Lua literal.
|
||||||
|
-- The real _LinedUpText label has no slot for the symbol at all (the
|
||||||
|
-- original ROM drew it separately) -- only for the coin count. This test
|
||||||
|
-- fakes _LinedUpText and checks the symbol is concatenated in front of the
|
||||||
|
-- translated suffix, with the payout correctly substituted into it.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
|
||||||
|
local SlotMachine = require("src.ui.SlotMachine")
|
||||||
|
|
||||||
|
local function mkSelf()
|
||||||
|
local game = { data = Data, save = { coins = 0 } }
|
||||||
|
return setmetatable({ game = game, allowMatchesCounter = 0 }, SlotMachine)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- translated: the fake suffix reaches self.message, with the symbol
|
||||||
|
-- concatenated in front and the payout substituted into the fake text
|
||||||
|
do
|
||||||
|
local self = mkSelf()
|
||||||
|
Data.text._LinedUpText = " FAKE-SUFFIX {RAM:wStringBuffer}!"
|
||||||
|
self:resolveWin({ symbol = "CHERRY", payout = 8 })
|
||||||
|
T.eq(self.message, "CHERRY FAKE-SUFFIX 8!",
|
||||||
|
"a translated _LinedUpText reaches the lined-up message")
|
||||||
|
Data.text._LinedUpText = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla: with no catalog entry, the English literal still substitutes,
|
||||||
|
-- with its own leading space (the original had no slot for the symbol)
|
||||||
|
do
|
||||||
|
local self = mkSelf()
|
||||||
|
self:resolveWin({ symbol = "CHERRY", payout = 8 })
|
||||||
|
T.eq(self.message, "CHERRY lined up!\nScored 8 coins!",
|
||||||
|
"no catalog entry still falls back to the English literal")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("slot_machine_lined_up_romtext")
|
||||||
@@ -94,20 +94,26 @@ do
|
|||||||
local eng, transport = engine({
|
local eng, transport = engine({
|
||||||
["POST /sync/create"] = { code = 200, body =
|
["POST /sync/create"] = { code = 200, body =
|
||||||
'{"account":"aa11","code1":"11112222","code2":"33334444","deviceToken":"tok"}' },
|
'{"account":"aa11","code1":"11112222","code2":"33334444","deviceToken":"tok"}' },
|
||||||
}, {}, SyncState.defaults())
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
|
["PUT /sync/save"] = { code = 200, body = '{"ok":true,"rev":1}' },
|
||||||
|
}, { saveEntry("red", "abc", 500, 400) }, SyncState.defaults())
|
||||||
|
|
||||||
T.eq(eng:linked(), false, "a fresh engine is not linked")
|
T.eq(eng:linked(), false, "a fresh engine is not linked")
|
||||||
T.eq(eng.status, "Not set up", "and says so")
|
T.eq(eng.status, "Not set up", "and says so")
|
||||||
|
|
||||||
eng:createAccount("laptop")
|
eng:createAccount("laptop")
|
||||||
pump(eng, 3)
|
pump(eng)
|
||||||
T.eq(eng:linked(), true, "creating an account links this device")
|
T.eq(eng:linked(), true, "creating an account links this device")
|
||||||
T.eq(eng.state.account, "aa11", "and stores the account id")
|
T.eq(eng.state.account, "aa11", "and stores the account id")
|
||||||
T.eq(eng.codes.code1, "1111-2222", "the first code is shown grouped")
|
T.eq(eng.codes.code1, "1111-2222", "the first code is shown grouped")
|
||||||
T.eq(eng.codes.code2, "3333-4444", "and so is the second")
|
T.eq(eng.codes.code2, "3333-4444", "and so is the second")
|
||||||
T.eq(eng.state.code1, nil, "codes never enter the persisted state")
|
T.eq(eng.state.code1, nil, "codes never enter the persisted state")
|
||||||
|
T.eq(transport.sent[2].url, "http://sync.test/sync/state",
|
||||||
|
"creating the account starts a sync straight away")
|
||||||
|
T.eq(transport.sent[3].method, "PUT",
|
||||||
|
"so the saves that existed before setup are uploaded")
|
||||||
|
T.eq(SyncState.rev(eng.state, "red/abc"), 1, "and the served rev is remembered")
|
||||||
T.eq(eng.phase, "idle", "and the engine settles")
|
T.eq(eng.phase, "idle", "and the engine settles")
|
||||||
T.eq(#transport.sent, 1, "one request was made")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
@@ -293,14 +299,105 @@ do
|
|||||||
T.eq(#transport.sent, 0, "with sync off an in-game save uploads nothing")
|
T.eq(#transport.sent, 0, "with sync off an in-game save uploads nothing")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local eng, transport = engine({
|
||||||
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
|
}, {})
|
||||||
|
eng:update(SyncEngine.AUTO_INTERVAL - 1)
|
||||||
|
T.eq(#transport.sent, 0, "an idle linked engine does not poll early")
|
||||||
|
eng:update(1)
|
||||||
|
T.eq(#transport.sent, 1, "after the auto interval it checks the server")
|
||||||
|
pump(eng)
|
||||||
|
T.eq(eng.phase, "idle", "and settles")
|
||||||
|
eng:update(SyncEngine.AUTO_INTERVAL - 10)
|
||||||
|
T.eq(#transport.sent, 1, "the next poll waits a whole interval again")
|
||||||
|
eng:update(10)
|
||||||
|
T.eq(#transport.sent, 2, "then fires")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local eng, transport = engine({
|
||||||
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
|
}, {}, SyncState.defaults())
|
||||||
|
eng:update(SyncEngine.AUTO_INTERVAL * 2)
|
||||||
|
T.eq(#transport.sent, 0, "an unlinked engine never polls on its own")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local calls = 0
|
||||||
|
local eng = engine({
|
||||||
|
["GET /sync/state"] = function()
|
||||||
|
calls = calls + 1
|
||||||
|
if calls == 1 then return { code = 500, body = '{"error":"down"}' } end
|
||||||
|
return { code = 200, body = '{"saves":{}}' }
|
||||||
|
end,
|
||||||
|
}, {})
|
||||||
|
eng:syncNow()
|
||||||
|
pump(eng, 3)
|
||||||
|
T.eq(eng.phase, "error", "the first sync fails")
|
||||||
|
eng:update(SyncEngine.AUTO_INTERVAL)
|
||||||
|
pump(eng, 3)
|
||||||
|
T.eq(eng.phase, "idle", "the auto interval retries and recovers")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local eng, transport = conflictEngine()
|
||||||
|
eng:syncNow()
|
||||||
|
pump(eng)
|
||||||
|
local sent = #transport.sent
|
||||||
|
eng:update(SyncEngine.AUTO_INTERVAL * 2)
|
||||||
|
T.eq(#transport.sent, sent, "a waiting conflict is never auto-synced over")
|
||||||
|
T.eq(eng.phase, "conflict", "the player still decides")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local eng, transport = engine({
|
||||||
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
|
}, {})
|
||||||
|
eng:noteResumed()
|
||||||
|
T.eq(#transport.sent, 1, "regaining the app checks the server")
|
||||||
|
pump(eng)
|
||||||
|
eng:noteResumed()
|
||||||
|
T.eq(#transport.sent, 1, "but not twice in quick succession")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local eng, transport = engine({}, {}, SyncState.defaults())
|
||||||
|
eng:noteResumed()
|
||||||
|
T.eq(#transport.sent, 0, "an unlinked engine ignores a resume")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local state = linkedState()
|
||||||
|
SyncState.setRev(state, "red/abc", 2, 500)
|
||||||
|
local eng, transport, saves = engine({
|
||||||
|
["GET /sync/state"] = { code = 200,
|
||||||
|
body = '{"saves":{"red/abc":{"rev":4,"meta":{"savedAt":900}},' ..
|
||||||
|
'"gold/xyz":{"rev":1,"meta":{"savedAt":900}}}}' },
|
||||||
|
["GET /sync/save"] = { code = 200,
|
||||||
|
body = '{"rev":1,"meta":{"savedAt":900},"blob":"return { player = {} }"}' },
|
||||||
|
}, { saveEntry("red", "abc", 500, 400) }, state)
|
||||||
|
eng:protectPlaythrough("red", "abc")
|
||||||
|
eng:syncNow()
|
||||||
|
pump(eng)
|
||||||
|
T.eq(#saves.writes, 1, "only the save that is not being played downloads")
|
||||||
|
T.eq(saves.writes[1].version, "gold", "the other playthrough still arrives")
|
||||||
|
T.eq(SyncState.rev(eng.state, "red/abc"), 2,
|
||||||
|
"the live playthrough keeps its rev so the launcher can fetch it later")
|
||||||
|
T.eq(eng.phase, "idle", "and the sync settles")
|
||||||
|
eng:protectPlaythrough("red", nil)
|
||||||
|
T.eq(eng.protectedKey, nil, "no live playthrough means no protection")
|
||||||
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local eng = engine({
|
local eng = engine({
|
||||||
["POST /sync/create"] = { code = 200, body =
|
["POST /sync/create"] = { code = 200, body =
|
||||||
'{"account":"aa11","code1":"11112222","code2":"33334444",' ..
|
'{"account":"aa11","code1":"11112222","code2":"33334444",' ..
|
||||||
'"deviceToken":"tok","device":"0a1b2c3d"}' },
|
'"deviceToken":"tok","device":"0a1b2c3d"}' },
|
||||||
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
}, {}, SyncState.defaults())
|
}, {}, SyncState.defaults())
|
||||||
eng:createAccount("laptop")
|
eng:createAccount("laptop")
|
||||||
pump(eng, 3)
|
pump(eng)
|
||||||
T.eq(eng.state.deviceId, "0a1b2c3d",
|
T.eq(eng.state.deviceId, "0a1b2c3d",
|
||||||
"creating an account records the id the server gave this device")
|
"creating an account records the id the server gave this device")
|
||||||
T.eq(SyncState.sanitize(eng.state).deviceId, "0a1b2c3d",
|
T.eq(SyncState.sanitize(eng.state).deviceId, "0a1b2c3d",
|
||||||
|
|||||||
Reference in New Issue
Block a user