A new game now runs NewGame's InitGender and OakSpeech natively before the bedroom: Boy or Girl, InitClock's hour and minute steppers with their YES/NO confirmations, Oak's speech with Wooper, and NamePlayer's preset names or naming field (a blank name takes CHRIS or KRIS). Texts, menus and time strings come from a new INTRO profile table verified by check:reference; Oak's and the player's pictures decode from TrainerPicPointers, ChrisPic/KrisPic and TrainerPalettes. The chosen time is kept as a minute offset from the device clock, which time of day, the weekday, NPC hours and the daily reset now read. The intro runs only when nothing is saved; a save made during it starts it again. Saves gain intro and clock.minuteOffset, migrated by absence. The shell's name and character fields are gone. Intentional change: the steppers start at the device time rather than 10:00. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a0fe1e313
commit
b13fd20be4
28 files changed
+975
-101
No files matched your search
+97
-8
@@ -23,8 +23,10 @@ import {
|
||||
type MartMode,
|
||||
type ScriptEffect,
|
||||
type ScriptHost,
|
||||
type Portrait,
|
||||
type ScriptState,
|
||||
} from "../scripts/interpreter";
|
||||
import { IntroRun } from "../scripts/intro";
|
||||
import type { MovementPose, MovementStep } from "../scripts/movement";
|
||||
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
|
||||
import {
|
||||
@@ -77,7 +79,9 @@ export interface NativeSave {
|
||||
/** Queued special call, persisted until its script completes. */
|
||||
specialPhoneCall: number;
|
||||
rivalName: string;
|
||||
clock: { weekdayOffset: number; dst: boolean };
|
||||
clock: { minuteOffset: number; weekdayOffset: number; dst: boolean };
|
||||
/** The introduction has not finished: it starts again on load. */
|
||||
intro: boolean;
|
||||
/** Current-map objects moved or turned by scripts, as saved object structs. */
|
||||
objects: { id: number; x: number; y: number; direction: Direction }[];
|
||||
/** wPlayerID, drawn at a new game. */
|
||||
@@ -194,6 +198,8 @@ export interface Pose {
|
||||
export type Choice =
|
||||
| { kind: "yesno"; selected: 0 | 1 }
|
||||
| { kind: "weekday"; names: string[]; selected: number }
|
||||
/** InitClock's hour or minute: Up and Down wrap through `labels`. */
|
||||
| { kind: "clock"; labels: string[]; selected: number }
|
||||
/** The naming screen: an empty answer keeps `current`. */
|
||||
| { kind: "name"; current: string; max: number; selected?: undefined }
|
||||
/** A vertical menu; `selected` answers with that item's index. */
|
||||
@@ -244,7 +250,7 @@ export interface BattleView {
|
||||
const BATTLE_MESSAGE_TICKS = 30;
|
||||
|
||||
interface ActiveScript {
|
||||
run: ScriptRun;
|
||||
run: ScriptRun | IntroRun;
|
||||
effect?: ScriptEffect;
|
||||
/** Committed save data for the whole interaction, used for mid-script saves. */
|
||||
committed: NativeSave;
|
||||
@@ -463,6 +469,10 @@ export class GameWorld {
|
||||
emote: { object: number; emote: number } | undefined;
|
||||
/** Species whose front picture is shown (pokepic), if any. */
|
||||
picture: number | undefined;
|
||||
/** The introduction's trainer or player picture, if any. */
|
||||
portrait: Portrait | undefined;
|
||||
/** True from a new game until the introduction finishes. */
|
||||
private introPending = false;
|
||||
/** The battle in progress, if any. */
|
||||
battle: BattleView | undefined;
|
||||
private battleRun?: Generator<BattleEffect, BattleOutcome, BattleInput>;
|
||||
@@ -593,13 +603,21 @@ export class GameWorld {
|
||||
if (mode === 4) return this.liveState.flash ? 2 : 3;
|
||||
return this.clockTimeOfDay;
|
||||
}
|
||||
/** The introduction is running, or has yet to run after a load. */
|
||||
get intro() {
|
||||
return this.introPending;
|
||||
}
|
||||
/** The game clock: the device time plus the offset InitClock chose. */
|
||||
private gameNow(clock = this.liveState.clock) {
|
||||
return new Date(this.now().getTime() + clock.minuteOffset * 60_000);
|
||||
}
|
||||
/** wTimeOfDay from the game clock: morning 4-10, day 10-18, night. */
|
||||
get clockTimeOfDay(): number {
|
||||
const hour = this.now().getHours();
|
||||
const hour = this.gameNow().getHours();
|
||||
return hour >= 4 && hour < 10 ? 0 : hour >= 10 && hour < 18 ? 1 : 2;
|
||||
}
|
||||
clock() {
|
||||
const now = this.now();
|
||||
const now = this.gameNow();
|
||||
return {
|
||||
weekday: (now.getDay() + this.liveState.clock.weekdayOffset) % 7,
|
||||
hours: now.getHours(),
|
||||
@@ -624,7 +642,7 @@ export class GameWorld {
|
||||
if (object.eventFlag !== 0xffff && state.events.has(object.eventFlag))
|
||||
return false;
|
||||
if (!this.assets.sprite(this.spriteOf(object, state))) return false;
|
||||
const hour = this.now().getHours(),
|
||||
const hour = this.gameNow().getHours(),
|
||||
timeMask = 1 << this.clockTimeOfDay;
|
||||
if (object.hour1 === 0xff)
|
||||
return object.hour2 === 0xff || Boolean(object.hour2 & timeMask);
|
||||
@@ -1470,7 +1488,7 @@ export class GameWorld {
|
||||
sceneOwner: (id) => this.assets.sceneOwner(id),
|
||||
timeOfDay: () => this.clockTimeOfDay,
|
||||
clock: () => this.clock(),
|
||||
deviceWeekday: () => this.now().getDay(),
|
||||
clockDay: () => this.gameNow().getDay(),
|
||||
text: () => ({
|
||||
...defaultTextContext(this.name),
|
||||
rival: this.state.rivalName,
|
||||
@@ -1581,6 +1599,35 @@ export class GameWorld {
|
||||
this.pump();
|
||||
}
|
||||
|
||||
/** NewGame's PlayerProfileSetup and OakSpeech, before the player
|
||||
* appears in the bedroom. Saves made meanwhile start it again. */
|
||||
beginIntro() {
|
||||
if (this.script) throw new Error("The introduction needs an idle world.");
|
||||
this.introPending = true;
|
||||
this.script = {
|
||||
run: new IntroRun(this.state, {
|
||||
reader: this.assets.reader,
|
||||
now: () => this.now(),
|
||||
text: (player) => ({ ...this.host(PLAYER).text(), player }),
|
||||
}),
|
||||
committed: this.save(),
|
||||
rollback: {
|
||||
state: this.state,
|
||||
mapId: this.mapId,
|
||||
player: copyActor(this.player),
|
||||
actors: new Map(
|
||||
[...this.actors].map(([id, actor]) => [id, copyActor(actor)]),
|
||||
),
|
||||
spawned: new Set(this.spawned),
|
||||
blocks: new Uint8Array(this.map.blocks),
|
||||
},
|
||||
waitTicks: 0,
|
||||
moving: new Map(),
|
||||
scene: false,
|
||||
};
|
||||
this.pump();
|
||||
}
|
||||
|
||||
/** Run the active script until it needs presentation time or input. */
|
||||
private pump(input?: number | string) {
|
||||
const script = this.script;
|
||||
@@ -1717,6 +1764,20 @@ export class GameWorld {
|
||||
case "picture":
|
||||
this.picture = effect.species || undefined;
|
||||
return false;
|
||||
case "portrait":
|
||||
this.portrait = effect.portrait;
|
||||
return false;
|
||||
case "clock":
|
||||
this.dialogue = {
|
||||
pages: effect.pages,
|
||||
index: 0,
|
||||
choice: {
|
||||
kind: "clock",
|
||||
labels: effect.labels,
|
||||
selected: effect.selected,
|
||||
},
|
||||
};
|
||||
return true;
|
||||
case "battle": {
|
||||
const draft = script.run.state;
|
||||
const trainers = new TrainerData(this.assets.reader);
|
||||
@@ -2190,6 +2251,12 @@ export class GameWorld {
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
this.portrait = undefined;
|
||||
if (script.run instanceof IntroRun) {
|
||||
this.name = script.run.name;
|
||||
this.avatar = script.run.avatar;
|
||||
this.introPending = false;
|
||||
}
|
||||
if (!script.scene || script.run.deferred !== undefined)
|
||||
this.sceneCheck = true;
|
||||
if (script.run.deferred !== undefined)
|
||||
@@ -2211,6 +2278,7 @@ export class GameWorld {
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
this.portrait = undefined;
|
||||
if (script.sighted !== undefined)
|
||||
this.refusedSighting = {
|
||||
map: this.mapId,
|
||||
@@ -2305,7 +2373,7 @@ export class GameWorld {
|
||||
/** CheckDailyResetTimer, from CheckTimeEvents after scene scripts: the
|
||||
* first idle check on a new game day clears the daily engine flags. */
|
||||
private checkDailyReset() {
|
||||
const now = this.now();
|
||||
const now = this.gameNow(this.state.clock);
|
||||
const day =
|
||||
Math.floor(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / 86_400_000,
|
||||
@@ -3098,6 +3166,11 @@ export class GameWorld {
|
||||
choice.items.length - 1,
|
||||
choice.selected + 1,
|
||||
);
|
||||
} else if (choice.kind === "clock") {
|
||||
const length = choice.labels.length;
|
||||
if (direction === "up") choice.selected = (choice.selected + 1) % length;
|
||||
if (direction === "down")
|
||||
choice.selected = (choice.selected + length - 1) % length;
|
||||
} else if (choice.kind === "martlist") {
|
||||
const length = choice.entries.length;
|
||||
if (!length) return;
|
||||
@@ -3165,6 +3238,7 @@ export class GameWorld {
|
||||
specialPhoneCall: state.specialPhoneCall,
|
||||
rivalName: state.rivalName,
|
||||
clock: { ...state.clock },
|
||||
intro: this.introPending,
|
||||
objects: [...this.actors]
|
||||
.filter(([, actor]) => actor.moved)
|
||||
.map(([id, actor]) => ({
|
||||
@@ -3249,7 +3323,9 @@ export class GameWorld {
|
||||
!save.phone.every((id) => integer(id, 0, 255)) ||
|
||||
new Set(save.phone).size !== save.phone.length ||
|
||||
!save.clock ||
|
||||
!integer(save.clock.minuteOffset, -720, 719) ||
|
||||
!integer(save.clock.weekdayOffset, 0, 6) ||
|
||||
typeof save.intro !== "boolean" ||
|
||||
typeof save.clock.dst !== "boolean" ||
|
||||
!Array.isArray(save.objects) ||
|
||||
save.objects.length > 16 ||
|
||||
@@ -3396,10 +3472,14 @@ export class GameWorld {
|
||||
this.dialogue = undefined;
|
||||
this.emote = undefined;
|
||||
this.picture = undefined;
|
||||
this.portrait = undefined;
|
||||
this.battle = undefined;
|
||||
this.battleRun = undefined;
|
||||
this.memScript = undefined;
|
||||
this.clearNotice();
|
||||
this.introPending = false;
|
||||
// A save made during the introduction starts it again.
|
||||
if (save.intro) this.beginIntro();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3437,6 +3517,13 @@ export function migrateSave(
|
||||
...(!Object.hasOwn(save, "noWildEncounters") && {
|
||||
noWildEncounters: false,
|
||||
}),
|
||||
...(!Object.hasOwn(save, "intro") && { intro: false }),
|
||||
// Clocks saved before InitClock was ported follow the device time.
|
||||
...(save.clock !== null &&
|
||||
typeof save.clock === "object" &&
|
||||
!Object.hasOwn(save.clock, "minuteOffset") && {
|
||||
clock: { minuteOffset: 0, ...(save.clock as object) },
|
||||
}),
|
||||
} as unknown as NativeSave;
|
||||
}
|
||||
if (save.version !== 1 && save.version !== 2) return undefined;
|
||||
@@ -3446,13 +3533,14 @@ export function migrateSave(
|
||||
...save,
|
||||
scenes: {},
|
||||
phone: [],
|
||||
clock: { weekdayOffset: 0, dst: false },
|
||||
clock: { minuteOffset: 0, weekdayOffset: 0, dst: false },
|
||||
objects: [],
|
||||
}
|
||||
: save;
|
||||
return {
|
||||
...previous,
|
||||
version: 3,
|
||||
clock: { minuteOffset: 0, ...(previous.clock as Record<string, unknown>) },
|
||||
decorations: initialDecorations(),
|
||||
playerId: newPlayerId(),
|
||||
party: [],
|
||||
@@ -3470,5 +3558,6 @@ export function migrateSave(
|
||||
backupWarp: null,
|
||||
flash: false,
|
||||
noWildEncounters: false,
|
||||
intro: false,
|
||||
} as unknown as NativeSave;
|
||||
}
|
||||
Reference in new issue
Block a user