Port the trainer card and the play time
The player's name in the start menu opens TrainerCard: page 1 with the name, ID, money, Pokédex count and play time, page 2 with the Johto leaders and the badges owned, turned and closed with the original buttons. The card picture, leader faces and animated badges come from the ROM with _CGB_TrainerCard's palettes. GameTimer now counts the play time from the end of the introduction, capped at 999:59:59, and saves keep it. Closes #23 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
38b9562bd3
commit
47698565ab
24 files changed
+954
-30
No files matched your search
+69
-3
@@ -21,6 +21,17 @@ import {
|
||||
type PokegearView,
|
||||
} from "../scripts/pokegear";
|
||||
import { MOM_BANK_BUTTONS, type MomBankView } from "../scripts/mom";
|
||||
import {
|
||||
TRAINER_CARD_BUTTONS,
|
||||
refreshTrainerCard,
|
||||
type TrainerCardView,
|
||||
} from "../scripts/trainer-card";
|
||||
import {
|
||||
advanceGameTime,
|
||||
emptyGameTime,
|
||||
validGameTime,
|
||||
type GameTime,
|
||||
} from "./game-time";
|
||||
import {
|
||||
PLAYER,
|
||||
ScriptRun,
|
||||
@@ -84,6 +95,8 @@ export interface NativeSave {
|
||||
name: string;
|
||||
avatar: "chris" | "kris";
|
||||
tilesWalked: number;
|
||||
/** wGameTime: the play time the trainer card shows. */
|
||||
gameTime: GameTime;
|
||||
events: number[];
|
||||
flags: number[];
|
||||
variables: Record<number, number>;
|
||||
@@ -244,6 +257,8 @@ export type Choice =
|
||||
| { kind: "pokegear"; view: PokegearView; selected: 0 }
|
||||
/** BankOfMom's amount box, answered with the button pressed. */
|
||||
| { kind: "mombank"; view: MomBankView; selected: 0 }
|
||||
/** A trainer card page, answered with the button pressed. */
|
||||
| { kind: "trainercard"; view: TrainerCardView; selected: 0 }
|
||||
/** The mart's 1-99 quantity stepper; `selected` is the current amount. */
|
||||
| {
|
||||
kind: "martquantity";
|
||||
@@ -258,7 +273,9 @@ export type Choice =
|
||||
/** Choices that read the buttons themselves and are answered with the
|
||||
* button's name. */
|
||||
const buttonChoice = (choice: Choice) =>
|
||||
choice.kind === "pokegear" || choice.kind === "mombank";
|
||||
choice.kind === "pokegear" ||
|
||||
choice.kind === "mombank" ||
|
||||
choice.kind === "trainercard";
|
||||
export interface Dialogue {
|
||||
pages: string[];
|
||||
index: number;
|
||||
@@ -525,6 +542,9 @@ export class GameWorld {
|
||||
name = "CHRIS";
|
||||
avatar: "chris" | "kris" = "chris";
|
||||
tilesWalked = 0;
|
||||
/** wGameTime. It counts from the end of the introduction, through
|
||||
* scenes, menus and battles, as GameTimer does in every VBlank. */
|
||||
gameTime: GameTime = emptyGameTime();
|
||||
state: ScriptState = emptyScriptState();
|
||||
dialogue: Dialogue | undefined;
|
||||
emote: { object: number; emote: number } | undefined;
|
||||
@@ -1888,6 +1908,7 @@ export class GameWorld {
|
||||
this.menuFieldMove(state, mon, move),
|
||||
scriptHost: () => this.host(PLAYER),
|
||||
landmarkOf: (mapId) => this.assets.map(mapId).landmark,
|
||||
gameTime: () => this.gameTime,
|
||||
},
|
||||
this.menuCursors,
|
||||
select,
|
||||
@@ -1916,6 +1937,8 @@ export class GameWorld {
|
||||
beginIntro() {
|
||||
if (this.script) throw new Error("The introduction needs an idle world.");
|
||||
this.introPending = true;
|
||||
// A new game's ResetGameTime; the timer starts when it finishes.
|
||||
this.gameTime = emptyGameTime();
|
||||
this.script = {
|
||||
run: new IntroRun(this.state, {
|
||||
reader: this.assets.reader,
|
||||
@@ -2199,6 +2222,21 @@ export class GameWorld {
|
||||
choice: { kind: "pokegear", view: effect.view, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "trainercard":
|
||||
if (!effect.view) {
|
||||
this.trainerCard = undefined;
|
||||
return false;
|
||||
}
|
||||
// Each page starts its colon and badge timing when it loads.
|
||||
if (this.trainerCard?.page !== effect.view.page) this.cardFrames = 0;
|
||||
this.trainerCard = effect.view;
|
||||
refreshTrainerCard(effect.view, this.cardFrames, this.gameTime);
|
||||
this.dialogue = {
|
||||
pages: [""],
|
||||
index: 0,
|
||||
choice: { kind: "trainercard", view: effect.view, selected: 0 },
|
||||
};
|
||||
return true;
|
||||
case "mombank":
|
||||
this.dialogue = {
|
||||
pages: effect.pages.length ? effect.pages : [""],
|
||||
@@ -2625,6 +2663,7 @@ export class GameWorld {
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -2665,6 +2704,7 @@ export class GameWorld {
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -3107,10 +3147,18 @@ export class GameWorld {
|
||||
/** The Pokégear card on screen while the Pokégear is open, including
|
||||
* during the phone calls it makes. */
|
||||
pokegear: PokegearView | undefined;
|
||||
/** The trainer card page on screen, and the frames since it loaded. */
|
||||
trainerCard: TrainerCardView | undefined;
|
||||
private cardFrames = 0;
|
||||
ticks = 0;
|
||||
|
||||
tick(held?: Direction) {
|
||||
this.ticks++;
|
||||
// GameTimer: two frames per overworld tick, once the game has started.
|
||||
if (!this.introPending) {
|
||||
advanceGameTime(this.gameTime);
|
||||
advanceGameTime(this.gameTime);
|
||||
}
|
||||
// StepFunction_ScreenShake counts down once per tick of the map loop.
|
||||
if (this.shake && !this.battle && --this.shake.ticks <= 0)
|
||||
this.shake = undefined;
|
||||
@@ -3138,6 +3186,11 @@ export class GameWorld {
|
||||
minutes,
|
||||
);
|
||||
}
|
||||
const card = this.trainerCard;
|
||||
if (card) {
|
||||
this.cardFrames += 2;
|
||||
refreshTrainerCard(card, this.cardFrames, this.gameTime);
|
||||
}
|
||||
const evolution = this.evolution;
|
||||
if (evolution) {
|
||||
const frame = evolution.schedule[++evolution.step];
|
||||
@@ -3699,7 +3752,9 @@ export class GameWorld {
|
||||
(dialogue.choice.kind === "pokegear" &&
|
||||
!POKEGEAR_BUTTONS.includes(answer as never)) ||
|
||||
(dialogue.choice.kind === "mombank" &&
|
||||
!MOM_BANK_BUTTONS.includes(answer as never))
|
||||
!MOM_BANK_BUTTONS.includes(answer as never)) ||
|
||||
(dialogue.choice.kind === "trainercard" &&
|
||||
!TRAINER_CARD_BUTTONS.includes(answer as never))
|
||||
)
|
||||
return;
|
||||
this.townMap = false;
|
||||
@@ -3712,7 +3767,12 @@ export class GameWorld {
|
||||
|
||||
save(): NativeSave {
|
||||
// Incomplete interactions are replayed after reload; no partial quest writes.
|
||||
if (this.script) return structuredClone(this.script.committed);
|
||||
// The play time keeps counting through scenes, like GameTimer.
|
||||
if (this.script)
|
||||
return {
|
||||
...structuredClone(this.script.committed),
|
||||
gameTime: { ...this.gameTime },
|
||||
};
|
||||
const state = this.state;
|
||||
return {
|
||||
format: "crystal-native",
|
||||
@@ -3726,6 +3786,7 @@ export class GameWorld {
|
||||
name: this.name,
|
||||
avatar: this.avatar,
|
||||
tilesWalked: this.tilesWalked,
|
||||
gameTime: { ...this.gameTime },
|
||||
events: [...state.events].sort((a, b) => a - b),
|
||||
flags: [...state.flags].sort((a, b) => a - b),
|
||||
variables: { ...state.variables },
|
||||
@@ -3802,6 +3863,7 @@ export class GameWorld {
|
||||
!/^[A-Za-z0-9 -]{1,7}$/.test(save.name) ||
|
||||
!["chris", "kris"].includes(save.avatar) ||
|
||||
!integer(save.tilesWalked, 0, Number.MAX_SAFE_INTEGER) ||
|
||||
!validGameTime(save.gameTime) ||
|
||||
!ids(save.events) ||
|
||||
!ids(save.flags) ||
|
||||
!new DecorationData(this.assets.reader, this.assets.pokemon).valid(
|
||||
@@ -3977,10 +4039,12 @@ export class GameWorld {
|
||||
this.name = save.name;
|
||||
this.avatar = save.avatar;
|
||||
this.tilesWalked = save.tilesWalked;
|
||||
this.gameTime = { ...save.gameTime };
|
||||
this.playerId = save.playerId;
|
||||
this.townMap = false;
|
||||
this.statsScreen = undefined;
|
||||
this.pokegear = undefined;
|
||||
this.trainerCard = undefined;
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
@@ -4040,6 +4104,7 @@ export function migrateSave(
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
}),
|
||||
...(!Object.hasOwn(save, "intro") && { intro: false }),
|
||||
...(!Object.hasOwn(save, "gameTime") && { gameTime: emptyGameTime() }),
|
||||
// Clocks saved before InitClock was ported follow the device time.
|
||||
...(save.clock !== null &&
|
||||
typeof save.clock === "object" &&
|
||||
@@ -4085,5 +4150,6 @@ export function migrateSave(
|
||||
noWildEncounters: false,
|
||||
registeredItem: { which: 0, item: 0 },
|
||||
intro: false,
|
||||
gameTime: emptyGameTime(),
|
||||
} as unknown as NativeSave;
|
||||
}
|
||||
Reference in new issue
Block a user