Open Mr. Pokémon's house, the first rival battle and the return to Elm
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
Mr. Pokémon's ROM scene gives the Mystery Egg and Pokédex, heals the party, sets the Cherrygrove spawn with blackoutmod and queues Elm's call. Cherrygrove's rival scene runs both triggers and all three RIVAL1_1 parties as a CANLOSE battle; reloadmap returns to the script. Smart AI is accepted for effects with no AI_Smart_EffectHandlers entry (Normal Hit, Attack Down, Defense Down) and still refuses others before any random draw, including for reserve Pokémon. Special calls read SpecialPhoneCallList: the entry's condition decides, and the ROM's CheckSpecialPhoneCall.script and Script_ReceivePhoneCall run its caller script through memcall, ending with HangUp's timed "Click!". Ringing and the caller box are not presented yet. In Elm's lab, NameRival shows the ROM's "RIVAL'S NAME?" title (the prompt previously had no page and soft-locked) and applies InitName's default to blank names. Handing over the Egg and the aide's Poké Balls run from the ROM. Saves gain rivalName and specialPhoneCall. The quest fixtures use a seeded RNG (a constant byte never ends the damage-variation loop) and no longer carry the house's moved objects into Cherrygrove. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7c1c0cc45b
commit
dc10343520
18 files changed
+812
-34
No files matched your search
+27
-10
@@ -31,19 +31,32 @@ export interface TrainerAIInput {
|
||||
|
||||
// AIScoringPointers: Basic, Setup, Types, Offensive, Smart, Opportunist,
|
||||
// Aggressive, Cautious, Status, Risky, then six actual AI_None entries.
|
||||
// Smart (bit 4) is the only translated pointer still refused. Bits 10-15
|
||||
// point to AI_None in the original and are valid no-ops.
|
||||
const SUPPORTED = 0xffef;
|
||||
export function validateTrainerAI(weights: number): void {
|
||||
if (
|
||||
!Number.isInteger(weights) ||
|
||||
weights < 0 ||
|
||||
weights > 0xffff ||
|
||||
weights & ~SUPPORTED
|
||||
)
|
||||
// Bits 10-15 point to AI_None in the original and are valid no-ops.
|
||||
// AI_Smart_EffectHandlers has no entries for these effects: Tackle/Scratch,
|
||||
// Growl and Leer therefore leave scores and the random stream unchanged.
|
||||
// Other Smart effects remain refused until their dispatch is translated.
|
||||
const SMART_NOOP_EFFECTS: ReadonlySet<number> = new Set([
|
||||
C.EFFECT_NORMAL_HIT,
|
||||
C.EFFECT_ATTACK_DOWN,
|
||||
C.EFFECT_DEFENSE_DOWN,
|
||||
]);
|
||||
export function validateTrainerAI(
|
||||
weights: number,
|
||||
moveEffects: readonly number[] = [],
|
||||
): void {
|
||||
if (!Number.isInteger(weights) || weights < 0 || weights > 0xffff)
|
||||
throw new UnsupportedBattle(
|
||||
`Trainer AI layers 0x${weights.toString(16)} are not translated.`,
|
||||
);
|
||||
if (weights & 16) {
|
||||
const unsupported = moveEffects.find(
|
||||
(effect) => !SMART_NOOP_EFFECTS.has(effect),
|
||||
);
|
||||
if (unsupported !== undefined)
|
||||
throw new UnsupportedBattle(
|
||||
`Trainer Smart AI for move effect ${unsupported} is not translated.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const has = (side: TrainerAISide, byte: number, bit: number) =>
|
||||
Boolean(side.sub[byte] & (1 << bit));
|
||||
@@ -154,6 +167,10 @@ export function chooseTrainerMove(input: TrainerAIInput): {
|
||||
"Trainer move AI requires at least one nonzero move.",
|
||||
);
|
||||
const attributes = moves.map((move) => data.move(move));
|
||||
validateTrainerAI(
|
||||
weights,
|
||||
attributes.map((move) => move.effect),
|
||||
);
|
||||
const scores: number[] = Array.from({ length: 4 }, (_, i) =>
|
||||
moves[i] === enemy.disabledMove || !(enemy.mon.pp[i] & 0x3f) ? 80 : 20,
|
||||
);
|
||||
|
||||
@@ -531,7 +531,9 @@ export class Battle {
|
||||
*run(): Gen<BattleOutcome> {
|
||||
const trainer = this.options.trainer;
|
||||
if (trainer) {
|
||||
validateTrainerAI(trainer.aiFlags);
|
||||
validateTrainerAI(trainer.aiFlags, trainer.party.flatMap((mon) =>
|
||||
mon.moves.filter(Boolean).map((move) => this.options.data.move(move).effect),
|
||||
));
|
||||
if (trainer.party.length < 1 || trainer.party.length > 6)
|
||||
throw new UnsupportedBattle("Invalid trainer party.");
|
||||
if (
|
||||
|
||||
@@ -9,6 +9,7 @@ export const C = {
|
||||
EFFECT_DREAM_EATER: 8,
|
||||
EFFECT_ALWAYS_HIT: 17,
|
||||
EFFECT_ATTACK_DOWN: 18,
|
||||
EFFECT_DEFENSE_DOWN: 19,
|
||||
EFFECT_EVASION_DOWN: 24,
|
||||
EFFECT_BIDE: 26,
|
||||
EFFECT_RAMPAGE: 27,
|
||||
|
||||
@@ -27,6 +27,12 @@ export const SYMBOLS = {
|
||||
standardScripts: 0xbc000,
|
||||
mapScenes: 0x4d01e,
|
||||
specials: 0xc029,
|
||||
/** CheckSpecialPhoneCall.script: pause, then Script_ReceivePhoneCall. */
|
||||
receiveSpecialPhoneCall: 0x90173,
|
||||
specialPhoneCallList: 0x90627,
|
||||
specialCallOnlyWhenOutside: 0x90188,
|
||||
specialCallWhereverYouAre: 0x90197,
|
||||
phoneClickText: 0x9032a,
|
||||
emotes: 0x1444d,
|
||||
landmarks: 0x1ca8c3,
|
||||
weekdayNames: 0x909f2,
|
||||
@@ -52,6 +58,8 @@ export const SYMBOLS = {
|
||||
findItemInBallScript: 0x122ce,
|
||||
caughtAskNicknameText: 0x4db44,
|
||||
nicknameStrings: 0x11780,
|
||||
rivalNameString: 0x117c3,
|
||||
rivalDefaultName: 0xc2b2,
|
||||
hiddenItemScript: 0x13625,
|
||||
talkToTrainerScript: 0xbe66a,
|
||||
seenByTrainerScript: 0xbe675,
|
||||
@@ -97,18 +105,26 @@ export const NATIVE_ROUTINES = {
|
||||
pickedFruitTree: 0x4405f,
|
||||
trainerWalkToPlayer: 0x831e,
|
||||
setMemEvent: 0x1364f,
|
||||
ringTwiceStartCall: 0x9026f,
|
||||
hangUp: 0x902eb,
|
||||
initCallReceiveDelay: 0x113e5,
|
||||
} as const;
|
||||
|
||||
/** Native handlers are selected by the routine a special index points to,
|
||||
* so the ROM's own special table remains the source of truth. */
|
||||
export const SPECIAL_ROUTINES = {
|
||||
nameRival: 0xc29d,
|
||||
restartMapMusic: 0x3d47,
|
||||
fadeOutMusic: 0xc48f,
|
||||
setDayOfWeek: 0x90913,
|
||||
initialSetDst: 0x90a54,
|
||||
initialClearDst: 0x90a88,
|
||||
toggleDecorationsVisibility: 0x27043,
|
||||
toggleMaptileDecorations: 0x26feb,
|
||||
fadeOutToWhite: 0x8c084,
|
||||
fadeOutToBlack: 0x8c0b6,
|
||||
fadeInFromBlack: 0x8c0ab,
|
||||
reloadSpritesNoPalettes: 0xd91,
|
||||
healParty: 0xc658,
|
||||
healMachineAnim: 0x12324,
|
||||
checkPokerus: 0xc419,
|
||||
@@ -138,6 +154,7 @@ export const RAM = {
|
||||
movementBuffer: 0xd007,
|
||||
hiddenItemID: 0xd040,
|
||||
runningTrainerBattleScript: 0xd04d,
|
||||
callerContact: 0xd03f,
|
||||
/** hMoneyTemp: HRAM, not WRAM like the other entries above. */
|
||||
moneyTemp: 0xffc3,
|
||||
} as const;
|
||||
@@ -310,5 +327,6 @@ export const NATIVE_MAPS: Record<string, string> = {
|
||||
"26:7": "Guide Gent’s house",
|
||||
"26:8": "Evolution speech house",
|
||||
"26:9": "Berry house",
|
||||
"26:10": "Mr. Pokémon’s house",
|
||||
};
|
||||
export const INITIAL_POSITION = { map: "24:7", x: 3, y: 3 } as const;
|
||||
@@ -62,9 +62,9 @@ export interface ScriptState {
|
||||
* through a Pokémon Center door (EnterMapWarp's SetSpawn), used by
|
||||
* GetWhiteoutSpawn. Null before any Center has been entered. */
|
||||
lastSpawnMap: string | null;
|
||||
/** wSpecialPhoneCallID's low byte (checkphonecall): nonzero while a call
|
||||
* is pending. No ported content ever sets it, so it is not saved. */
|
||||
/** wSpecialPhoneCallID: nonzero while a saved story call is pending. */
|
||||
specialPhoneCall: number;
|
||||
rivalName: string;
|
||||
}
|
||||
export const emptyScriptState = (): ScriptState => ({
|
||||
events: new Set(),
|
||||
@@ -81,6 +81,7 @@ export const emptyScriptState = (): ScriptState => ({
|
||||
dailyReset: null,
|
||||
lastSpawnMap: null,
|
||||
specialPhoneCall: 0,
|
||||
rivalName: "???",
|
||||
});
|
||||
export const copyScriptState = (state: ScriptState): ScriptState => ({
|
||||
events: new Set(state.events),
|
||||
@@ -100,6 +101,7 @@ export const copyScriptState = (state: ScriptState): ScriptState => ({
|
||||
dailyReset: state.dailyReset,
|
||||
lastSpawnMap: state.lastSpawnMap,
|
||||
specialPhoneCall: state.specialPhoneCall,
|
||||
rivalName: state.rivalName,
|
||||
});
|
||||
|
||||
/** Object operands keep the script's numbering: 0 is the player, 2+ are map
|
||||
@@ -204,6 +206,8 @@ export class UnsupportedScript extends Error {
|
||||
type Input = number | string | undefined;
|
||||
/** ITEM_FROM_MEM: take the item from wScriptVar. */
|
||||
const ITEM_FROM_MEM = 0xff;
|
||||
/** wCallerContact's script-2 pointer (bank, address) within the contact. */
|
||||
const PHONE_CONTACT_SCRIPT2_BANK = 9;
|
||||
|
||||
/** TrainerWalkToPlayer and ComputePathToWalkToPlayer (home/movement.asm):
|
||||
* a one-tick sleep, then normal steps to the player's tile, dropping the
|
||||
@@ -289,6 +293,9 @@ export class ScriptRun {
|
||||
trainer?: { header: number; distance: number };
|
||||
/** wHiddenItemData, copied from a BGEVENT_ITEM's hiddenitem data. */
|
||||
hiddenItem?: { event: number; item: number };
|
||||
/** wCallerContact's second script, copied by CheckSpecialPhoneCall
|
||||
* from the queued SpecialPhoneCallList entry. */
|
||||
callerScript?: number;
|
||||
} = {},
|
||||
) {
|
||||
this.state = copyScriptState(state);
|
||||
@@ -313,6 +320,7 @@ export class ScriptRun {
|
||||
private text(): TextContext {
|
||||
return {
|
||||
...this.host.text(),
|
||||
rival: this.state.rivalName,
|
||||
strings: { ...this.host.text().strings, ...this.strings },
|
||||
numbers: { ...this.host.text().numbers, ...this.numbers },
|
||||
};
|
||||
@@ -406,6 +414,16 @@ export class ScriptRun {
|
||||
p = address;
|
||||
break;
|
||||
}
|
||||
case 0x02: {
|
||||
// memcall: only Script_ReceivePhoneCall's caller script is known.
|
||||
const address = word();
|
||||
const script = this.options.callerScript;
|
||||
if (address !== RAM.callerContact + PHONE_CONTACT_SCRIPT2_BANK || script === undefined)
|
||||
throw new UnsupportedScript(offset, opcode);
|
||||
stack.push(p);
|
||||
p = script;
|
||||
break;
|
||||
}
|
||||
case 0x03:
|
||||
p = near();
|
||||
break;
|
||||
@@ -527,6 +545,18 @@ export class ScriptRun {
|
||||
value = Number(
|
||||
receiveItem(data, this.state.bag, ball.item, ball.quantity),
|
||||
);
|
||||
} else if (routine === NATIVE_ROUTINES.hangUp) {
|
||||
// HangUp_Beep prints "Click!", then seven 20-frame waits for the
|
||||
// hang-up tones pass before closetext; no button is needed.
|
||||
write(SYMBOLS.phoneClickText);
|
||||
yield { kind: "text", pages: take(), holdTicks: 140 };
|
||||
} else if (
|
||||
routine === NATIVE_ROUTINES.ringTwiceStartCall ||
|
||||
routine === NATIVE_ROUTINES.initCallReceiveDelay
|
||||
) {
|
||||
// The ringing tones and caller name box are not presented yet.
|
||||
// InitCallReceiveDelay restarts the random-call timers, and
|
||||
// random calls are not ported.
|
||||
} else if (!Object.values(EMPTY_ROUTINES).includes(routine as never))
|
||||
throw new UnsupportedScript(offset, opcode);
|
||||
break;
|
||||
@@ -649,6 +679,14 @@ export class ScriptRun {
|
||||
if ((battleResult & 0x3f) === 1) p = SYMBOLS.battleWhiteoutScript;
|
||||
canLose = false;
|
||||
break;
|
||||
case 0x7b:
|
||||
// MAPSETUP_RELOADMAP restores graphics/audio after battle without
|
||||
// re-running callbacks or rebuilding objects. Native presentation
|
||||
// already retains the map; clear wBattleScriptFlags equivalents.
|
||||
wild = undefined;
|
||||
trainer = undefined;
|
||||
canLose = false;
|
||||
break;
|
||||
case 0x8a: {
|
||||
byte(); // newloadmap MAPSETUP_WARP
|
||||
const entry = SYMBOLS.spawnPoints + spawn * 4;
|
||||
@@ -663,6 +701,10 @@ export class ScriptRun {
|
||||
case 0x1c:
|
||||
value = this.readVar(byte(), offset);
|
||||
break;
|
||||
case 0x3b:
|
||||
// blackoutmod writes wLastSpawnMapGroup/wLastSpawnMapNumber.
|
||||
this.state.lastSpawnMap = `${byte()}:${byte()}`;
|
||||
break;
|
||||
case 0x9c:
|
||||
this.state.specialPhoneCall = word();
|
||||
break;
|
||||
@@ -1227,6 +1269,8 @@ export class ScriptRun {
|
||||
return this.host.position().x;
|
||||
case 0x13:
|
||||
return this.host.position().y;
|
||||
case 0x14:
|
||||
return this.state.specialPhoneCall;
|
||||
default:
|
||||
throw new UnsupportedScript(offset, 0x1c, ` (variable ${id})`);
|
||||
}
|
||||
@@ -1244,7 +1288,23 @@ export class ScriptRun {
|
||||
): Generator<ScriptEffect, number | undefined, Input> {
|
||||
const r = this.reader;
|
||||
switch (routine) {
|
||||
case SPECIAL_ROUTINES.nameRival: {
|
||||
// The naming screen's own title; any pending text was already shown.
|
||||
const fallback = readString(r, SYMBOLS.rivalDefaultName, this.text());
|
||||
const name = yield {
|
||||
kind: "name",
|
||||
pages: [...take(), readString(r, SYMBOLS.rivalNameString, this.text())],
|
||||
current: fallback,
|
||||
max: 7,
|
||||
};
|
||||
if (typeof name !== "string" || name.length > 7 || (name && !NICKNAME_PATTERN.test(name)))
|
||||
throw new Error("Invalid rival name.");
|
||||
// InitName: a name of zero or more spaces takes the default.
|
||||
this.state.rivalName = /^ *$/.test(name) ? fallback : name;
|
||||
return value;
|
||||
}
|
||||
case SPECIAL_ROUTINES.restartMapMusic:
|
||||
case SPECIAL_ROUTINES.fadeOutMusic:
|
||||
case SPECIAL_ROUTINES.fadeOutToWhite:
|
||||
case SPECIAL_ROUTINES.warpToSpawnPoint:
|
||||
case SPECIAL_ROUTINES.stubbedTrainerRankingsHealings:
|
||||
@@ -1253,6 +1313,9 @@ export class ScriptRun {
|
||||
// Rankings feature (Pokémon News) was cut, leaving a dead stub.
|
||||
return value;
|
||||
case SPECIAL_ROUTINES.healMachineAnim:
|
||||
case SPECIAL_ROUTINES.fadeOutToBlack:
|
||||
case SPECIAL_ROUTINES.fadeInFromBlack:
|
||||
case SPECIAL_ROUTINES.reloadSpritesNoPalettes:
|
||||
// Graphics, palettes and sound only: wPartyCount/wScriptVar select
|
||||
// an animation sequence but nothing persists past it.
|
||||
return value;
|
||||
|
||||
+46
-3
@@ -50,6 +50,9 @@ export interface NativeSave {
|
||||
variables: Record<number, number>;
|
||||
scenes: Record<string, number>;
|
||||
phone: number[];
|
||||
/** Queued special call, persisted until its script completes. */
|
||||
specialPhoneCall: number;
|
||||
rivalName: string;
|
||||
clock: { weekdayOffset: number; dst: boolean };
|
||||
/** Current-map objects moved or turned by scripts, as saved object structs. */
|
||||
objects: { id: number; x: number; y: number; direction: Direction }[];
|
||||
@@ -253,6 +256,10 @@ const WONT_DELETE = 1 << 1,
|
||||
NOCLIP_OBJS = 1 << 6,
|
||||
SWIMMING = 1 << 5;
|
||||
const OBJECTTYPE_TRAINER = 2;
|
||||
/** Map environments (map_data_constants.asm) and the special call count. */
|
||||
const TOWN = 1,
|
||||
ROUTE = 2;
|
||||
const NUM_SPECIALCALLS = 8;
|
||||
/** STEP_SLOW: random walkers take 16 ticks per tile. */
|
||||
const NPC_STEP_TICKS = 16;
|
||||
const isTimed = (effect: ScriptEffect | undefined) =>
|
||||
@@ -594,7 +601,7 @@ export class GameWorld {
|
||||
let sourceState = this.state;
|
||||
if (
|
||||
(target.tileset === 7 || target.tileset === 21) && // TILESET_POKECENTER, TILESET_POKECOM_CENTER
|
||||
(this.map.environment === 1 || this.map.environment === 2) // TOWN, ROUTE
|
||||
(this.map.environment === TOWN || this.map.environment === ROUTE)
|
||||
)
|
||||
sourceState = {
|
||||
...copyScriptState(sourceState),
|
||||
@@ -689,7 +696,28 @@ export class GameWorld {
|
||||
event.y === player.y,
|
||||
);
|
||||
if (event) this.startScript(event.script, PLAYER, { committedPosition: previous });
|
||||
else this.randomEncounter(previous);
|
||||
else if (!this.checkSpecialPhoneCall(previous)) this.randomEncounter(previous);
|
||||
}
|
||||
|
||||
/** CountStep / CheckSpecialPhoneCall: a queued call waits for a step
|
||||
* after trainer, warp and coordinate events, before encounters. Its
|
||||
* SpecialPhoneCallList entry supplies the condition and caller script,
|
||||
* which runs inside the ROM's receive-call script; the caller script
|
||||
* commits the cleared queue and any story flag together. */
|
||||
private checkSpecialPhoneCall(previous: { x: number; y: number }): boolean {
|
||||
const id = this.state.specialPhoneCall;
|
||||
if (id < 1 || id > NUM_SPECIALCALLS) return false;
|
||||
const r = this.assets.reader,
|
||||
entry = SYMBOLS.specialPhoneCallList + (id - 1) * 6;
|
||||
const condition = r.near(entry, r.bank(entry));
|
||||
if (condition === SYMBOLS.specialCallOnlyWhenOutside) {
|
||||
if (this.map.environment !== TOWN && this.map.environment !== ROUTE) return false;
|
||||
} else if (condition !== SYMBOLS.specialCallWhereverYouAre) return false;
|
||||
this.startScript(SYMBOLS.receiveSpecialPhoneCall, PLAYER, {
|
||||
committedPosition: previous,
|
||||
callerScript: r.far(entry + 3),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** RandomEncounter: cooldown, grass, the map's rate, then a slot. */
|
||||
@@ -822,6 +850,7 @@ export class GameWorld {
|
||||
deviceWeekday: () => this.now().getDay(),
|
||||
text: () => ({
|
||||
...defaultTextContext(this.name),
|
||||
rival: this.state.rivalName,
|
||||
weekday: this.weekdayName(this.clock().weekday),
|
||||
}),
|
||||
data: this.assets.pokemon,
|
||||
@@ -862,6 +891,7 @@ export class GameWorld {
|
||||
trainer,
|
||||
hiddenItem,
|
||||
sighted,
|
||||
callerScript,
|
||||
}: {
|
||||
committedPosition?: { x: number; y: number };
|
||||
scene?: boolean;
|
||||
@@ -870,6 +900,7 @@ export class GameWorld {
|
||||
trainer?: { header: number; distance: number };
|
||||
hiddenItem?: { event: number; item: number };
|
||||
sighted?: number;
|
||||
callerScript?: number;
|
||||
} = {},
|
||||
) {
|
||||
const committed = this.save();
|
||||
@@ -890,6 +921,7 @@ export class GameWorld {
|
||||
itemBall,
|
||||
trainer,
|
||||
hiddenItem,
|
||||
callerScript,
|
||||
},
|
||||
),
|
||||
committed,
|
||||
@@ -1828,6 +1860,8 @@ export class GameWorld {
|
||||
variables: { ...state.variables },
|
||||
scenes: { ...state.scenes },
|
||||
phone: [...state.phone],
|
||||
specialPhoneCall: state.specialPhoneCall,
|
||||
rivalName: state.rivalName,
|
||||
clock: { ...state.clock },
|
||||
objects: [...this.actors]
|
||||
.filter(([, actor]) => actor.moved)
|
||||
@@ -1893,6 +1927,10 @@ export class GameWorld {
|
||||
Object.keys(save.scenes).some(
|
||||
(id) => this.assets.sceneOwner(id) !== id,
|
||||
) ||
|
||||
!integer(save.specialPhoneCall, 0, 0xffff) ||
|
||||
typeof save.rivalName !== "string" ||
|
||||
save.rivalName.length > 7 ||
|
||||
!NICKNAME_PATTERN.test(save.rivalName) ||
|
||||
!Array.isArray(save.phone) ||
|
||||
save.phone.length > 10 ||
|
||||
!save.phone.every((id) => integer(id, 0, 255)) ||
|
||||
@@ -1968,7 +2006,8 @@ export class GameWorld {
|
||||
fruitTrees: new Set(save.fruitTrees),
|
||||
dailyReset: save.dailyReset,
|
||||
lastSpawnMap: save.lastSpawnMap,
|
||||
specialPhoneCall: 0,
|
||||
specialPhoneCall: save.specialPhoneCall,
|
||||
rivalName: save.rivalName,
|
||||
};
|
||||
let entry;
|
||||
try {
|
||||
@@ -2034,6 +2073,8 @@ export function migrateSave(
|
||||
...(!Object.hasOwn(save, "fruitTrees") && { fruitTrees: [] }),
|
||||
...(!Object.hasOwn(save, "dailyReset") && { dailyReset: null }),
|
||||
...(!Object.hasOwn(save, "lastSpawnMap") && { lastSpawnMap: null }),
|
||||
...(!Object.hasOwn(save, "specialPhoneCall") && { specialPhoneCall: 0 }),
|
||||
...(!Object.hasOwn(save, "rivalName") && { rivalName: "???" }),
|
||||
} as unknown as NativeSave;
|
||||
}
|
||||
if (save.version !== 1 && save.version !== 2) return undefined;
|
||||
@@ -2055,6 +2096,8 @@ export function migrateSave(
|
||||
bag: emptyBag(),
|
||||
pokedex: { seen: [], caught: [] },
|
||||
lastSpawnMap: null,
|
||||
specialPhoneCall: 0,
|
||||
rivalName: "???",
|
||||
money: C.START_MONEY,
|
||||
fruitTrees: [],
|
||||
dailyReset: null,
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ function showChoice(choice: Choice | undefined) {
|
||||
const input = document.createElement("input");
|
||||
input.maxLength = choice.max;
|
||||
input.placeholder = choice.current;
|
||||
input.setAttribute("aria-label", `Nickname for ${choice.current}`);
|
||||
input.setAttribute("aria-label", `Name (empty keeps ${choice.current})`);
|
||||
input.autocomplete = "off";
|
||||
input.spellcheck = false;
|
||||
const submit = () => {
|
||||
|
||||
Reference in new issue
Block a user