Port the start menu, the party menu and the pack outside battle
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
Start opens the original start menu. POKéMON shows the party with the Pokémon submenu (stats, switching, move order, held items); PACK shows the four pockets with the field item effects for healing, PP and the Bicycle, plus giving and tossing. The menu runs on a script draft, and its cursors persist until a battle clears them. Select keeps the Key Items shortcut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60e06ae678
commit
1af2beacdc
18 files changed
+1918
-49
No files matched your search
+111
-19
@@ -27,6 +27,7 @@ import {
|
||||
type ScriptState,
|
||||
} from "../scripts/interpreter";
|
||||
import { IntroRun } from "../scripts/intro";
|
||||
import { StartMenuRun, type MenuCursors } from "../scripts/start-menu";
|
||||
import type { MovementPose, MovementStep } from "../scripts/movement";
|
||||
import { emptyBag, validBag, type Bag, type ItemSlot } from "../party/bag";
|
||||
import {
|
||||
@@ -213,6 +214,13 @@ export type Choice =
|
||||
entries: MartEntry[];
|
||||
selected: number;
|
||||
}
|
||||
/** The pack's pockets; `selected` is an entry of pocket `pocket`. */
|
||||
| {
|
||||
kind: "pack";
|
||||
pockets: { name: string; entries: MartEntry[] }[];
|
||||
pocket: number;
|
||||
selected: number;
|
||||
}
|
||||
/** The mart's 1-99 quantity stepper; `selected` is the current amount. */
|
||||
| {
|
||||
kind: "martquantity";
|
||||
@@ -250,7 +258,7 @@ export interface BattleView {
|
||||
const BATTLE_MESSAGE_TICKS = 30;
|
||||
|
||||
interface ActiveScript {
|
||||
run: ScriptRun | IntroRun;
|
||||
run: ScriptRun | IntroRun | StartMenuRun;
|
||||
effect?: ScriptEffect;
|
||||
/** Committed save data for the whole interaction, used for mid-script saves. */
|
||||
committed: NativeSave;
|
||||
@@ -506,6 +514,8 @@ export class GameWorld {
|
||||
* ChangeDirectionScript holds input for three ticks. */
|
||||
private turnTo?: Direction;
|
||||
private turnWait = 0;
|
||||
/** Start menu, party menu and pack cursors (work RAM, not saved). */
|
||||
private menuCursors: MenuCursors = { start: 0, party: 0, pocket: 0 };
|
||||
/** OBJECT_ACTION_BUMP this tick: walking in place against an obstacle. */
|
||||
private bumping = false;
|
||||
/** A screen shake object (earthquake, step_shake) in progress. */
|
||||
@@ -1599,6 +1609,41 @@ export class GameWorld {
|
||||
this.pump();
|
||||
}
|
||||
|
||||
/** Start in PlayerEvents (StartMenuScript): the start menu opens while
|
||||
* the player stands still. Its changes commit when it closes. */
|
||||
openStartMenu(): boolean {
|
||||
if (this.busy || this.introPending) return false;
|
||||
this.clearNotice();
|
||||
this.script = {
|
||||
run: new StartMenuRun(
|
||||
this.state,
|
||||
{
|
||||
reader: this.assets.reader,
|
||||
data: this.assets.pokemon,
|
||||
text: () => this.host(PLAYER).text(),
|
||||
bicycleScript: (state) => this.bicycleScript(state),
|
||||
},
|
||||
this.menuCursors,
|
||||
),
|
||||
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();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** NewGame's PlayerProfileSetup and OakSpeech, before the player
|
||||
* appears in the bedroom. Saves made meanwhile start it again. */
|
||||
beginIntro() {
|
||||
@@ -1823,6 +1868,7 @@ export class GameWorld {
|
||||
),
|
||||
tutorial: effect.tutorial,
|
||||
landmark: this.assets.map(this.mapId).landmark,
|
||||
partyMenuCursor: this.menuCursors.party,
|
||||
});
|
||||
this.battle = { engine, selected: 1 };
|
||||
this.battleRun = engine.run();
|
||||
@@ -1909,6 +1955,18 @@ export class GameWorld {
|
||||
},
|
||||
};
|
||||
return true;
|
||||
case "pack":
|
||||
this.dialogue = {
|
||||
pages: [""],
|
||||
index: 0,
|
||||
choice: {
|
||||
kind: "pack",
|
||||
pockets: effect.pockets,
|
||||
pocket: effect.pocket,
|
||||
selected: effect.selected,
|
||||
},
|
||||
};
|
||||
return true;
|
||||
case "martquantity":
|
||||
this.dialogue = {
|
||||
pages: [""],
|
||||
@@ -2265,8 +2323,14 @@ export class GameWorld {
|
||||
}
|
||||
if (!script.scene || script.run.deferred !== undefined)
|
||||
this.sceneCheck = true;
|
||||
if (script.run instanceof StartMenuRun) this.menuCursors = script.run.cursors;
|
||||
if (script.run.deferred !== undefined)
|
||||
this.startScript(script.run.deferred, PLAYER);
|
||||
this.startScript(script.run.deferred, PLAYER, {
|
||||
usedItem:
|
||||
script.run instanceof StartMenuRun
|
||||
? script.run.deferredItem
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
private abortScript(error: unknown) {
|
||||
const script = this.script!;
|
||||
@@ -2316,6 +2380,8 @@ export class GameWorld {
|
||||
);
|
||||
this.battle = undefined;
|
||||
this.battleRun = undefined;
|
||||
// CleanUpBattleRAM
|
||||
this.menuCursors = { start: 0, party: 0, pocket: 0 };
|
||||
this.pump(next.value.result);
|
||||
return;
|
||||
}
|
||||
@@ -2837,7 +2903,8 @@ export class GameWorld {
|
||||
const choice = this.dialogue.choice;
|
||||
if (choice && this.dialogue.index === this.dialogue.pages.length - 1) {
|
||||
// The naming screen is answered with text through choose().
|
||||
if (choice.kind !== "name") this.choose(choice.selected);
|
||||
if (choice.kind === "pack") this.choosePackEntry();
|
||||
else if (choice.kind !== "name") this.choose(choice.selected);
|
||||
} else this.advanceDialogue();
|
||||
return;
|
||||
}
|
||||
@@ -3065,8 +3132,15 @@ export class GameWorld {
|
||||
!this.state.bag.keyItems.some((slot) => slot === OW.BICYCLE)
|
||||
)
|
||||
return false;
|
||||
const map = this.map,
|
||||
state = this.state;
|
||||
const script = this.bicycleScript(this.state);
|
||||
if (script === undefined) return false;
|
||||
this.startScript(script, PLAYER, { usedItem: OW.BICYCLE });
|
||||
return true;
|
||||
}
|
||||
/** BikeFunction.TryBike: the script QueueScript runs, or undefined when
|
||||
* .CheckEnvironment or the player's state refuses the Bicycle. */
|
||||
private bicycleScript(state: ScriptState): number | undefined {
|
||||
const map = this.map;
|
||||
const outdoors =
|
||||
map.environment === OW.TOWN || map.environment === OW.ROUTE;
|
||||
const allowed =
|
||||
@@ -3075,20 +3149,14 @@ export class GameWorld {
|
||||
map.environment === OW.GATE) &&
|
||||
this.assets.permission(this.assets.collision(map, this.x, this.y)) ===
|
||||
OW.LAND_TILE;
|
||||
if (!allowed) return false;
|
||||
const usedItem = OW.BICYCLE;
|
||||
if (!allowed) return undefined;
|
||||
if (state.playerState === OW.PLAYER_NORMAL)
|
||||
this.startScript(OVERWORLD.Script_GetOnBike, PLAYER, { usedItem });
|
||||
else if (state.playerState === OW.PLAYER_BIKE)
|
||||
this.startScript(
|
||||
state.flags.has(OW.ENGINE_ALWAYS_ON_BIKE)
|
||||
? OVERWORLD.Script_CantGetOffBike
|
||||
: OVERWORLD.Script_GetOffBike,
|
||||
PLAYER,
|
||||
{ usedItem },
|
||||
);
|
||||
else return false;
|
||||
return true;
|
||||
return OVERWORLD.Script_GetOnBike;
|
||||
if (state.playerState === OW.PLAYER_BIKE)
|
||||
return state.flags.has(OW.ENGINE_ALWAYS_ON_BIKE)
|
||||
? OVERWORLD.Script_CantGetOffBike
|
||||
: OVERWORLD.Script_GetOffBike;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** GetPlayerSprite: Chris's or Kris's sprite for wPlayerState. */
|
||||
@@ -3147,7 +3215,8 @@ export class GameWorld {
|
||||
else if (
|
||||
choice.kind === "martlist" ||
|
||||
choice.kind === "martquantity" ||
|
||||
choice.kind === "menu"
|
||||
choice.kind === "menu" ||
|
||||
choice.kind === "pack"
|
||||
)
|
||||
this.choose(-1);
|
||||
return;
|
||||
@@ -3191,6 +3260,20 @@ export class GameWorld {
|
||||
if (direction === "up") choice.selected = (choice.selected + 1) % length;
|
||||
if (direction === "down")
|
||||
choice.selected = (choice.selected + length - 1) % length;
|
||||
} else if (choice.kind === "pack") {
|
||||
// Pack_InterpretJoypad: left and right change pockets.
|
||||
const pockets = choice.pockets.length;
|
||||
if (direction === "left" || direction === "right") {
|
||||
choice.pocket =
|
||||
(choice.pocket + (direction === "left" ? pockets - 1 : 1)) % pockets;
|
||||
choice.selected = 0;
|
||||
return;
|
||||
}
|
||||
const length = choice.pockets[choice.pocket].entries.length;
|
||||
if (!length) return;
|
||||
if (direction === "up") choice.selected = Math.max(0, choice.selected - 1);
|
||||
if (direction === "down")
|
||||
choice.selected = Math.min(length - 1, choice.selected + 1);
|
||||
} else if (choice.kind === "martlist") {
|
||||
const length = choice.entries.length;
|
||||
if (!length) return;
|
||||
@@ -3214,6 +3297,15 @@ export class GameWorld {
|
||||
} else if (direction === "up") choice.selected = (choice.selected + 1) % 7;
|
||||
else if (direction === "down") choice.selected = (choice.selected + 6) % 7;
|
||||
}
|
||||
/** Choose the highlighted pack entry, if its pocket has one. */
|
||||
choosePackEntry(pocket?: number, index?: number) {
|
||||
const choice = this.dialogue?.choice;
|
||||
if (choice?.kind !== "pack") return;
|
||||
pocket ??= choice.pocket;
|
||||
index ??= choice.selected;
|
||||
if (!choice.pockets[pocket]?.entries[index]) return;
|
||||
this.choose(pocket * 256 + index);
|
||||
}
|
||||
/** Answer the current choice: 1/0 for yes/no, a weekday index, or a
|
||||
* name ("" keeps the current one). */
|
||||
choose(answer: number | string) {
|
||||
|
||||
Reference in new issue
Block a user