From 10d63ba630477f70b1e006ae834a79e81d3ee98b Mon Sep 17 00:00:00 2001 From: AlphaKretin Date: Sat, 7 Feb 2026 09:10:37 +1030 Subject: [PATCH 1/2] Add functional support for Fragile Locket --- src/app/damagecalc/damageCalc.ts | 26 ++++++---- src/app/data/items/FragileLocketItem.ts | 10 ++++ src/app/data/tectonic/Move.ts | 9 ++-- src/app/data/tectonic/TectonicData.ts | 2 + src/app/data/typeChart.ts | 44 +++++++++-------- src/app/data/types/PartyPokemon.ts | 48 +++++++++++++++++-- .../teambuilder/components/DefTotalCell.tsx | 2 +- src/app/teambuilder/page.tsx | 2 +- src/components/PokemonCardHorizontal.tsx | 11 +++++ src/components/PokemonModal.tsx | 6 +-- 10 files changed, 116 insertions(+), 44 deletions(-) create mode 100644 src/app/data/items/FragileLocketItem.ts diff --git a/src/app/damagecalc/damageCalc.ts b/src/app/damagecalc/damageCalc.ts index 57e7709a..f273366a 100644 --- a/src/app/damagecalc/damageCalc.ts +++ b/src/app/damagecalc/damageCalc.ts @@ -185,9 +185,11 @@ function pbCalcAbilityDamageMultipliers( // multipliers.base_damage_multiplier *= 1.4; // } - // User or user ally ability effects that alter damage - multipliers.base_damage_multiplier *= user.ability.movePowerMultiplier(move, user, target); - multipliers.attack_multiplier *= user.ability.attackMultiplier(move, user, battleState); + // User or user ally ability effects that alter damage (apply all active abilities for Fragile Locket) + for (const ability of user.getActiveAbilities()) { + multipliers.base_damage_multiplier *= ability.movePowerMultiplier(move, user, target); + multipliers.attack_multiplier *= ability.attackMultiplier(move, user, battleState); + } // user.eachAlly((b: any) => { // b.eachAbilityShouldApply(aiCheck, (ability: any) => { // BattleHandlers.triggerDamageCalcUserAllyAbility( @@ -239,8 +241,8 @@ function applySunDebuff(move: MoveData, user: PartyPokemon, battleState: BattleS if (user.items.some((i) => i instanceof WeatherImmuneItem)) { return false; } - // i'm not 100% sure we're actually passing ability flags yet, i'll check when we get to implementing abilitites - if (user.ability.flags.includes("SunshineSynergy") || user.ability.flags.includes("AllWeatherSynergy")) { + // Check all active abilities for weather synergy (supports Fragile Locket) + if (user.getActiveAbilities().some((a) => a.flags.includes("SunshineSynergy") || a.flags.includes("AllWeatherSynergy"))) { return false; } if (user.types.type1.id === "FIRE" || user.types.type2?.id === "FIRE" || user.types.type1.id === "GRASS" || user.types.type2?.id === "GRASS") { @@ -256,7 +258,8 @@ function applyRainDebuff(move: MoveData, user: PartyPokemon, battleState: Battle if (user.items.some((i) => i instanceof WeatherImmuneItem)) { return false; } - if (user.ability.flags.includes("RainstormSynergy") || user.ability.flags.includes("AllWeatherSynergy")) { + // Check all active abilities for weather synergy (supports Fragile Locket) + if (user.getActiveAbilities().some((a) => a.flags.includes("RainstormSynergy") || a.flags.includes("AllWeatherSynergy"))) { return false; } if (user.types.type1.id === "WATER" || user.types.type2?.id === "WATER" || user.types.type1.id === "ELECTRIC" || user.types.type2?.id === "ELECTRIC") { @@ -544,8 +547,11 @@ function pbCalcTypeBasedDamageMultipliers( // STAB if (stabActive) { let stab = 1.5; - if (user.ability instanceof STABBoostAbility) { - stab *= user.ability.boost; + // Check all active abilities for STAB boost (supports Fragile Locket) + for (const ability of user.getActiveAbilities()) { + if (ability instanceof STABBoostAbility) { + stab *= ability.boost; + } } multipliers.final_damage_multiplier *= stab; } @@ -553,8 +559,8 @@ function pbCalcTypeBasedDamageMultipliers( // Type effectiveness // variable type moves are handled here in Tectonic, but on the data level here const effectiveness = calcTypeMatchup( - { type: type, move: move.move, ability: user.ability }, - { type1: target.types.type1, type2: target.types.type2, ability: target.ability } + { type: type, move: move.move, abilities: user.getActiveAbilities() }, + { type1: target.types.type1, type2: target.types.type2, abilities: target.getActiveAbilities() } ); multipliers.final_damage_multiplier *= effectiveness; diff --git a/src/app/data/items/FragileLocketItem.ts b/src/app/data/items/FragileLocketItem.ts new file mode 100644 index 00000000..342427a3 --- /dev/null +++ b/src/app/data/items/FragileLocketItem.ts @@ -0,0 +1,10 @@ +import { LoadedItem } from "@/preload/loadedDataClasses"; +import { Item } from "../tectonic/Item"; + +export class FragileLocketItem extends Item { + constructor(loaded: LoadedItem) { + super(loaded); + } + + static itemIds = ["FRAGILELOCKET"]; +} diff --git a/src/app/data/tectonic/Move.ts b/src/app/data/tectonic/Move.ts index 5c1a3805..a31d1c6b 100644 --- a/src/app/data/tectonic/Move.ts +++ b/src/app/data/tectonic/Move.ts @@ -156,7 +156,7 @@ export class Move { return this.type && (mon.types.type1 === this.type || mon.types.type2 === this.type || - (mon.ability instanceof ExtraTypeAbility && mon.ability.extraType.id === this.type.id)); + mon.getActiveAbilities().some((a) => a instanceof ExtraTypeAbility && a.extraType.id === this.type.id)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -168,8 +168,11 @@ export class Move { // to be extended by subclasses // eslint-disable-next-line @typescript-eslint/no-unused-vars public getType(user: PartyPokemon, battleState: BattleState): PokemonType { - if (user.ability instanceof MoveTypeChangeAbility && user.ability.shouldChangeType(this)) { - return user.ability.moveType; + // Check all active abilities for type-changing effects (supports Fragile Locket) + for (const ability of user.getActiveAbilities()) { + if (ability instanceof MoveTypeChangeAbility && ability.shouldChangeType(this)) { + return ability.moveType; + } } return this.type; } diff --git a/src/app/data/tectonic/TectonicData.ts b/src/app/data/tectonic/TectonicData.ts index 012e9865..a5193d1a 100644 --- a/src/app/data/tectonic/TectonicData.ts +++ b/src/app/data/tectonic/TectonicData.ts @@ -17,6 +17,7 @@ import { TypeWeaknessAbility } from "../abilities/TypeWeaknessAbility"; import { CategoryBoostingItem } from "../items/CategoryBoostingItem"; import { EvioliteItem } from "../items/EvioliteItem"; import { FlatDamageBoostItem } from "../items/FlatDamageBoostItem"; +import { FragileLocketItem } from "../items/FragileLocketItem"; import { LumberAxeItem } from "../items/LumberAxeItem"; import { StatBoostItem } from "../items/StatBoostItem"; import { StatLockItem } from "../items/StatLockItem"; @@ -103,6 +104,7 @@ const itemSubclasses = [ CategoryBoostingItem, EvioliteItem, FlatDamageBoostItem, + FragileLocketItem, LumberAxeItem, StatBoostItem, StatLockItem, diff --git a/src/app/data/typeChart.ts b/src/app/data/typeChart.ts index 71985703..b56d231a 100644 --- a/src/app/data/typeChart.ts +++ b/src/app/data/typeChart.ts @@ -12,20 +12,20 @@ import { isNull } from "./util"; interface AttackerData { type: PokemonType; move?: Move; - ability?: Ability; + abilities?: Ability[]; } interface DefenderData { type1: PokemonType; type2?: PokemonType; - ability?: Ability; + abilities?: Ability[]; } // Calculates the best mult value given all attacker moves in that case export function calcTypeMatchup(atk: AttackerData, def: DefenderData) { const atkType = atk.type; const defType1 = def.type1; - let thirdType = null; + let thirdType: PokemonType | null = null; let defType1Calc = TectonicData.typeChart[atkType.index][defType1.index]; @@ -45,43 +45,45 @@ export function calcTypeMatchup(atk: AttackerData, def: DefenderData) { defType2Calc = Math.max(defType2Calc, 1); } } - const defAbility = def.ability; - if (defAbility !== undefined) { - // if check removed - abilities that don't modify matchups will just return 1 + + // Process all defender abilities (supports Fragile Locket dual abilities) + for (const defAbility of def.abilities ?? []) { + // Apply modifiedMatchup from each ability defAbilityCalc *= defAbility.modifiedMatchup(atk.type); // certain moves pierce ground immunity if (defAbilityCalc === 0 && atk.move instanceof HitsFliersMove && atk.type.id === "GROUND") { defAbilityCalc = 1; } - if ( - defAbility instanceof ExtraTypeAbility - ) { + if (defAbility instanceof ExtraTypeAbility) { const defType3 = defAbility.extraType; if ( defType3.id !== defType1.id && defType3.id !== def.type2?.id ) { - defAbilityCalc = TectonicData.typeChart[atkType.index][defType3.index]; + defAbilityCalc *= TectonicData.typeChart[atkType.index][defType3.index]; thirdType = defType3; } } + // Special ability effects based on type matchup if (defAbility.id == "WONDERGUARD" && defType1Calc * defType2Calc <= 1) { defAbilityCalc = 0; } else if (defAbility.id == "UNFAZED" && defType1Calc * defType2Calc == 1) { - defAbilityCalc = 0.8; + defAbilityCalc *= 0.8; } else if (defAbility.id == "WELLSUITED" && defType1Calc * defType2Calc < 1) { - defAbilityCalc = 0.5; + defAbilityCalc *= 0.5; } else if (defAbility.id == "FILTER" && defType1Calc * defType2Calc > 1) { - defAbilityCalc = 0.75; + defAbilityCalc *= 0.75; } } let atkAbilityCalc = 1.0; let atkMoveCalc = 1; - const atkAbility = atk.ability; - if (atkAbility !== undefined) { + + // Process all attacker abilities (supports Fragile Locket dual abilities) + const atkAbilities = atk.abilities ?? []; + for (const atkAbility of atkAbilities) { if (atkAbility.flags.includes("MoldBreaking")) { defAbilityCalc = 1.0; } else if (atkAbility.id == "BREAKTHROUGH") { @@ -89,14 +91,14 @@ export function calcTypeMatchup(atk: AttackerData, def: DefenderData) { defType2Calc = defType2Calc == 0 ? 1.0 : defType2Calc; defAbilityCalc = defAbilityCalc == 0 && thirdType != null ? 1.0 : defAbilityCalc; } else if (atkAbility.id == "TINTEDLENS") { - atkAbilityCalc = defType1Calc * defType2Calc * defAbilityCalc < 1 ? 2 : 1; + atkAbilityCalc *= defType1Calc * defType2Calc * defAbilityCalc < 1 ? 2 : 1; } else if (atkAbility.id == "EXPERTISE" && defType1Calc * defType2Calc > 1) { // only boost super effective hits - atkAbilityCalc = 1.3; + atkAbilityCalc *= 1.3; } else if (atkAbility.id == "DRAGONSLAYER" && (defType1.id === "DRAGON" || def.type2?.id === "DRAGON")) { - atkAbilityCalc = 2; + atkAbilityCalc *= 2; } - } + } const atkMove = atk.move; if (atkMove && atkMove.isAttackingMove()) { @@ -113,7 +115,7 @@ export function calcTypeMatchup(atk: AttackerData, def: DefenderData) { if (atkMove instanceof ExtraTypeMove) { // should not recur by a depth of more than 1, since move is no longer defined - atkMoveCalc *= calcTypeMatchup({ type: atkMove.extraType, ability: atk.ability }, def); + atkMoveCalc *= calcTypeMatchup({ type: atkMove.extraType, abilities: atkAbilities }, def); } } @@ -123,6 +125,6 @@ export function calcTypeMatchup(atk: AttackerData, def: DefenderData) { export function calcBestMoveMatchup(mon: PartyPokemon, def: DefenderData): number { const calcs = mon.moves .filter((m) => m != undefined && !isNull(m) && m.isAttackingMove()) - .map((m) => calcTypeMatchup({ type: m.type, move: m, ability: mon.ability }, def)); + .map((m) => calcTypeMatchup({ type: m.type, move: m, abilities: mon.getActiveAbilities() }, def)); return calcs.length > 0 ? Math.max(...calcs) : 1; } diff --git a/src/app/data/types/PartyPokemon.ts b/src/app/data/types/PartyPokemon.ts index 3699c8f8..33eccc81 100644 --- a/src/app/data/types/PartyPokemon.ts +++ b/src/app/data/types/PartyPokemon.ts @@ -2,6 +2,7 @@ import { MoveData } from "@/app/damagecalc/components/MoveCard"; import { Side } from "@/app/damagecalc/damageCalc"; import { TwoItemAbility } from "../abilities/TwoItemAbility"; import { StatusEffect, VolatileStatusEffect, volatileStatusEffects } from "../conditions"; +import { FragileLocketItem } from "../items/FragileLocketItem"; import { TypeChangingItem } from "../items/TypeChangingItem"; import { IgnoreStatMove } from "../moves/IgnoreStatMove"; import { calculateHP, calculateStat } from "../stats"; @@ -45,6 +46,26 @@ export class PartyPokemon { (Object.fromEntries(volatileStatusEffects.map((e) => [e, false])) as Record); } + public hasFragileLocket(): boolean { + return this.items.some((i) => i instanceof FragileLocketItem); + } + + public getSecondaryAbility(): Ability | null { + const abilities = this.species?.getAbilities?.(this.form) || []; + const other = abilities.find((a) => a && a != Ability.NULL && a.id !== this.ability.id); + return other || null; + } + + public getActiveAbilities(): Ability[] { + if (this.hasFragileLocket()) { + const secondary = this.getSecondaryAbility(); + if (secondary) { + return [this.ability, secondary]; + } + } + return [this.ability]; + } + // modify base stats separately so they can be shown on the UI public getBaseStats(): Stats { let stats = this.species.getStats(this.form); @@ -55,8 +76,9 @@ export class PartyPokemon { } public getStats(move?: MoveData, side?: Side): Stats { - const stylish = this.ability.id === "STYLISH"; - const accumulation = this.ability.id === "ACCUMULATION"; + const activeAbilities = this.getActiveAbilities(); + const stylish = activeAbilities.some((a) => a.id === "STYLISH"); + const accumulation = activeAbilities.some((a) => a.id === "ACCUMULATION"); const steps = { ...this.statSteps }; if (move?.criticalHit) { // crits ignore player attack drops @@ -86,8 +108,22 @@ export class PartyPokemon { let calculatedStats: Stats = { hp: calculateHP(stats.hp, this.level, this.stylePoints.hp, stylish, accumulation), - attack: calculateStat(stats.attack, this.level, this.stylePoints.attacks, steps.attack, stylish, accumulation), - defense: calculateStat(stats.defense, this.level, this.stylePoints.defense, steps.defense, stylish, accumulation), + attack: calculateStat( + stats.attack, + this.level, + this.stylePoints.attacks, + steps.attack, + stylish, + accumulation, + ), + defense: calculateStat( + stats.defense, + this.level, + this.stylePoints.defense, + steps.defense, + stylish, + accumulation, + ), spatk: calculateStat(stats.spatk, this.level, this.stylePoints.attacks, steps.spatk, stylish, accumulation), spdef: calculateStat(stats.spdef, this.level, this.stylePoints.spdef, steps.spdef, stylish, accumulation), speed, @@ -97,7 +133,9 @@ export class PartyPokemon { calculatedStats = item.modifyStats(calculatedStats); } - calculatedStats = this.ability.modifyStats(calculatedStats); + for (const ability of this.getActiveAbilities()) { + calculatedStats = ability.modifyStats(calculatedStats); + } return calculatedStats; } diff --git a/src/app/teambuilder/components/DefTotalCell.tsx b/src/app/teambuilder/components/DefTotalCell.tsx index aedb6ec1..e52f8403 100644 --- a/src/app/teambuilder/components/DefTotalCell.tsx +++ b/src/app/teambuilder/components/DefTotalCell.tsx @@ -30,7 +30,7 @@ export default function DefTotalCell({ }): React.ReactNode { const num = cards.filter((c) => calcCompare( - calcTypeMatchup({ type: type }, { type1: c.types.type1, type2: c.types.type2, ability: c.ability }), + calcTypeMatchup({ type: type }, { type1: c.types.type1, type2: c.types.type2, abilities: c.getActiveAbilities() }), compare ) ).length; diff --git a/src/app/teambuilder/page.tsx b/src/app/teambuilder/page.tsx index e1276170..c85a0e16 100644 --- a/src/app/teambuilder/page.tsx +++ b/src/app/teambuilder/page.tsx @@ -98,7 +98,7 @@ const TeamBuilder: NextPage = () => { { type1: c.types.type1, type2: c.types.type2, - ability: c.ability, + abilities: c.getActiveAbilities(), } )} /> diff --git a/src/components/PokemonCardHorizontal.tsx b/src/components/PokemonCardHorizontal.tsx index 036fd03b..c0af3b3e 100644 --- a/src/components/PokemonCardHorizontal.tsx +++ b/src/components/PokemonCardHorizontal.tsx @@ -3,6 +3,7 @@ import { TwoItemAbility } from "@/app/data/abilities/TwoItemAbility"; import { nullBattleState } from "@/app/data/battleState"; import { StatusEffect, statusEffects, volatileStatusEffects } from "@/app/data/conditions"; +import { FragileLocketItem } from "@/app/data/items/FragileLocketItem"; import { TypeChangingItem } from "@/app/data/items/TypeChangingItem"; import { MAX_LEVEL, @@ -189,6 +190,16 @@ export default function PokemonCardHorizontal({ ))} + {partyMon.items.some((i) => i instanceof FragileLocketItem) && + partyMon.getSecondaryAbility() && ( +
+ + {partyMon.getSecondaryAbility()!.name} +
+ )} + {Array.from({ length: partyMon.ability instanceof TwoItemAbility ? 2 : 1 }).map( (_, i) => (
diff --git a/src/components/PokemonModal.tsx b/src/components/PokemonModal.tsx index 6f61755c..20f10745 100644 --- a/src/components/PokemonModal.tsx +++ b/src/components/PokemonModal.tsx @@ -91,14 +91,14 @@ const PokemonModal: React.FC = ({ pokemon: mon, moveSelector, { type1: currentPokemon.getType1(currentForm), type2: currentPokemon.getType2(currentForm), - ability: a, + abilities: [a], }, ); stabMatchupCalcs[a.id][t.id] = Math.max( - calcTypeMatchup({ type: currentPokemon.getType1(currentForm), ability: a }, { type1: t }), + calcTypeMatchup({ type: currentPokemon.getType1(currentForm), abilities: [a] }, { type1: t }), calcTypeMatchup( - { type: currentPokemon.getType2(currentForm) || currentPokemon.getType1(currentForm), ability: a }, + { type: currentPokemon.getType2(currentForm) || currentPokemon.getType1(currentForm), abilities: [a] }, { type1: t }, ), ); From 265c6f6f3466d848f460729164af0a40b0bd37b9 Mon Sep 17 00:00:00 2001 From: eseria Date: Thu, 30 Oct 2025 06:23:31 -0400 Subject: [PATCH 2/2] partial implementation of fragile locket --- src/app/data/items/FragileLocketItem.ts | 14 ++++++++++++-- src/app/data/tectonic/TectonicData.ts | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/data/items/FragileLocketItem.ts b/src/app/data/items/FragileLocketItem.ts index 342427a3..e8f730fc 100644 --- a/src/app/data/items/FragileLocketItem.ts +++ b/src/app/data/items/FragileLocketItem.ts @@ -1,9 +1,19 @@ import { LoadedItem } from "@/preload/loadedDataClasses"; import { Item } from "../tectonic/Item"; +import { Stats } from "../tectonic/Pokemon"; export class FragileLocketItem extends Item { - constructor(loaded: LoadedItem) { - super(loaded); + constructor(item: LoadedItem) { + super(item); + } + + public modifyStats(stats: Stats): Stats { + const newStats = { ...stats }; + const statKeys: (keyof Stats)[] = ["hp", "attack", "defense", "speed", "spatk", "spdef"]; + for (const stat of statKeys) { + newStats[stat] = Math.floor(newStats[stat] * 0.9); + } + return newStats; } static itemIds = ["FRAGILELOCKET"]; diff --git a/src/app/data/tectonic/TectonicData.ts b/src/app/data/tectonic/TectonicData.ts index a5193d1a..63cfaf70 100644 --- a/src/app/data/tectonic/TectonicData.ts +++ b/src/app/data/tectonic/TectonicData.ts @@ -113,6 +113,7 @@ const itemSubclasses = [ TypeBoostingItem, TypeChangingItem, WeatherImmuneItem, + FragileLocketItem, ]; const abilitySubclasses = [