Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Gen 1 Wrap #10936

Merged
merged 10 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 60 additions & 10 deletions data/mods/gen1/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,31 +185,81 @@ export const Conditions: import('../../../sim/dex-conditions').ModdedConditionDa
},
partiallytrapped: {
name: 'partiallytrapped',
// this is the duration of Wrap if it doesn't continue.
// (i.e. if the attacker switches out.)
// the full duration is tracked in partialtrappinglock
duration: 2,
onBeforeMovePriority: 9,
onBeforeMove(pokemon) {
this.add('cant', pokemon, 'partiallytrapped');
// defender still takes PSN damage, etc
// TODO: research exact mechanics
onBeforeMovePriority: 0,
onBeforeMove() {
return false;
},
onRestart() {
this.effectState.duration = 2;
},
onLockMove() {
// exact move doesn't matter, no move is ever actually used
return 'struggle';
},
onDisableMove(target) {
target.maybeLocked = true;
},
},
fakepartiallytrapped: {
name: 'fakepartiallytrapped',
// Wrap ended this turn, but you don't know that
// until you try to use an attack
duration: 2,
onDisableMove(target) {
target.maybeLocked = true;
},
},
partialtrappinglock: {
name: 'partialtrappinglock',
durationCallback() {
const duration = this.sample([2, 2, 2, 3, 3, 3, 4, 5]);
return duration;
return this.sample([2, 2, 2, 3, 3, 3, 4, 5]);
},
onStart(target, source, effect) {
const foe = target.foes()[0];
if (!foe) return false;

this.effectState.move = effect.id;
this.effectState.totalDuration = this.effectState.duration!;
this.effectState.damage = target.lastDamage;
this.effectState.trapTarget = foe;
foe.addVolatile('partiallytrapped', target, effect);
this.add('cant', foe, 'partiallytrapped');
},
onDisableMove(pokemon) {
if (!pokemon.hasMove(this.effectState.move)) {
onOverrideAction(pokemon, target, move) {
return this.effectState.move;
},
// attacker still takes PSN damage, etc
onBeforeMovePriority: 0,
onBeforeMove(pokemon, target, move) {
const foe = pokemon.foes()[0];
if (!foe || foe !== this.effectState.trapTarget) {
pokemon.removeVolatile('partialtrappinglock');
return;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id !== this.effectState.move) {
pokemon.disableMove(moveSlot.id);

this.add('move', pokemon, this.effectState.move, foe, `[from] ${this.effectState.move}`);
this.damage(this.effectState.damage, foe, pokemon, move);
if (this.effectState.duration === 1) {
if (this.effectState.totalDuration !== 5) {
pokemon.addVolatile('fakepartiallytrapped');
foe.addVolatile('fakepartiallytrapped');
}
} else {
foe.addVolatile('partiallytrapped', pokemon, move);
}
return false;
},
onLockMove() {
return this.effectState.move;
},
onDisableMove(pokemon) {
pokemon.maybeLocked = true;
},
},
mustrecharge: {
Expand Down
56 changes: 0 additions & 56 deletions data/mods/gen1/moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
bind: {
inherit: true,
ignoreImmunity: true,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
Expand All @@ -86,19 +85,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
bite: {
inherit: true,
Expand Down Expand Up @@ -137,7 +123,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
inherit: true,
accuracy: 75,
pp: 10,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
Expand All @@ -147,19 +132,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
constrict: {
inherit: true,
Expand Down Expand Up @@ -320,7 +292,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
inherit: true,
accuracy: 70,
basePower: 15,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
Expand All @@ -330,19 +301,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
fly: {
inherit: true,
Expand Down Expand Up @@ -950,7 +908,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
inherit: true,
accuracy: 85,
ignoreImmunity: true,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
Expand All @@ -960,18 +917,5 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
};
7 changes: 2 additions & 5 deletions data/mods/gen9ssb/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,7 @@ export const Scripts: ModdedBattleScriptsData = {

if (this.requestState === 'move') {
if (pokemon.trapped) {
const includeRequest = this.updateRequestForPokemon(pokemon, req => {
return this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, { pokemon, update: req => {
let updated = false;
if (req.maybeTrapped) {
delete req.maybeTrapped;
Expand All @@ -1958,10 +1958,7 @@ export const Scripts: ModdedBattleScriptsData = {
updated = true;
}
return updated;
});
const status = this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, includeRequest);
if (includeRequest) this.emitRequest(this.activeRequest!);
return status;
} });
} else if (pokemon.maybeTrapped) {
this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive();
}
Expand Down
10 changes: 8 additions & 2 deletions sim/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export class Pokemon {
trapped: boolean | "hidden";
maybeTrapped: boolean;
maybeDisabled: boolean;
/** true = locked, */
maybeLocked: boolean | null;

illusion: Pokemon | null;
transformed: boolean;
Expand Down Expand Up @@ -418,6 +420,7 @@ export class Pokemon {
this.trapped = false;
this.maybeTrapped = false;
this.maybeDisabled = false;
this.maybeLocked = false;

this.illusion = null;
this.transformed = false;
Expand Down Expand Up @@ -1048,7 +1051,7 @@ export class Pokemon {
}

getMoveRequestData() {
let lockedMove = this.getLockedMove();
let lockedMove = this.maybeLocked ? null : this.getLockedMove();

// Information should be restricted for the last active Pokémon
const isLastActive = this.isLastActive();
Expand All @@ -1066,7 +1069,10 @@ export class Pokemon {

if (isLastActive) {
if (this.maybeDisabled) {
data.maybeDisabled = true;
data.maybeDisabled = this.maybeDisabled;
}
if (this.maybeLocked) {
data.maybeLocked = this.maybeLocked;
}
if (canSwitchIn) {
if (this.trapped === true) {
Expand Down
49 changes: 33 additions & 16 deletions sim/side.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ export interface PokemonSwitchRequestData {
terastallized?: string;
}
export interface PokemonMoveRequestData {
moves: { move: string, id: ID, target?: string, disabled?: string | boolean }[];
moves: { move: string, id: ID, target?: string, disabled?: string | boolean, disabledSource?: string }[];
maybeDisabled?: boolean;
maybeLocked?: boolean;
trapped?: boolean;
maybeTrapped?: boolean;
canMegaEvo?: boolean;
Expand Down Expand Up @@ -485,10 +486,14 @@ export class Side {
this.activeRequest = update;
}

emitChoiceError(message: string, unavailable?: boolean) {
emitChoiceError(
message: string, update?: { pokemon: Pokemon, update: (req: PokemonMoveRequestData) => boolean | void }
) {
this.choice.error = message;
const type = `[${unavailable ? 'Unavailable' : 'Invalid'} choice]`;
const updated = update ? this.updateRequestForPokemon(update.pokemon, update.update) : null;
const type = `[${updated ? 'Unavailable' : 'Invalid'} choice]`;
this.battle.send('sideupdate', `${this.id}\n|error|${type} ${message}`);
if (updated) this.emitRequest(this.activeRequest!);
if (this.battle.strictChoices) throw new Error(`${type} ${message}`);
return false;
}
Expand Down Expand Up @@ -570,7 +575,11 @@ export class Side {
}
}
if (!targetType) {
return this.emitChoiceError(`Can't move: Your ${pokemon.name} doesn't have a move matching ${moveid}`);
if (moveid === 'testfight') {
targetType = 'normal';
} else {
return this.emitChoiceError(`Can't move: Your ${pokemon.name} doesn't have a move matching ${moveid}`);
}
}
}

Expand Down Expand Up @@ -638,7 +647,17 @@ export class Side {
targetLoc: lockedMoveTargetLoc,
moveid: lockedMoveID,
});
if (moveid === 'testfight') this.choice.cantUndo = true;
return true;
} else if (moveid === 'testfight') {
// test fight button
if (!pokemon.maybeLocked) {
return this.emitChoiceError(`Can't move: ${pokemon.name}'s Fight button is known to be safe`);
}
pokemon.maybeLocked = false;
return this.emitChoiceError(`${pokemon.name} is not locked`, { pokemon, update: req => {
delete req.maybeLocked;
} });
} else if (!moves.length && !zMove) {
// Override action and use Struggle if there are no enabled moves with PP
// Gen 4 and earlier announce a Pokemon has no moves left before the turn begins, and only to that player's side.
Expand All @@ -665,7 +684,7 @@ export class Side {
if (!isEnabled) {
// Request a different choice
if (autoChoose) throw new Error(`autoChoose chose a disabled move`);
const includeRequest = this.updateRequestForPokemon(pokemon, req => {
return this.emitChoiceError(`Can't move: ${pokemon.name}'s ${move.name} is disabled`, { pokemon, update: req => {
let updated = false;
for (const m of req.moves) {
if (m.id === moveid) {
Expand All @@ -681,10 +700,7 @@ export class Side {
}
}
return updated;
});
const status = this.emitChoiceError(`Can't move: ${pokemon.name}'s ${move.name} is disabled`, includeRequest);
if (includeRequest) this.emitRequest(this.activeRequest!);
return status;
} });
}
// The chosen move is valid yay
}
Expand Down Expand Up @@ -769,13 +785,13 @@ export class Side {
return true;
}

updateRequestForPokemon(pokemon: Pokemon, update: (req: AnyObject) => boolean) {
updateRequestForPokemon(pokemon: Pokemon, update: (req: PokemonMoveRequestData) => boolean | void) {
if (!(this.activeRequest as MoveRequest)?.active) {
throw new Error(`Can't update a request without active Pokemon`);
}
const req = (this.activeRequest as MoveRequest).active[pokemon.position];
if (!req) throw new Error(`Pokemon not found in request's active field`);
return update(req);
return update(req) ?? true;
}

chooseSwitch(slotText?: string) {
Expand Down Expand Up @@ -849,7 +865,7 @@ export class Side {

if (this.requestState === 'move') {
if (pokemon.trapped) {
const includeRequest = this.updateRequestForPokemon(pokemon, req => {
return this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, { pokemon, update: req => {
let updated = false;
if (req.maybeTrapped) {
delete req.maybeTrapped;
Expand All @@ -860,10 +876,7 @@ export class Side {
updated = true;
}
return updated;
});
const status = this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, includeRequest);
if (includeRequest) this.emitRequest(this.activeRequest!);
return status;
} });
} else if (pokemon.maybeTrapped) {
this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive();
}
Expand Down Expand Up @@ -1045,6 +1058,10 @@ export class Side {
for (const choiceString of choiceStrings) {
let [choiceType, data] = Utils.splitFirst(choiceString.trim(), ' ');
data = data.trim();
if (choiceType === 'testfight') {
choiceType = 'move';
data = 'testfight';
}

switch (choiceType) {
case 'move':
Expand Down