Skip to content
Closed
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
26 changes: 16 additions & 10 deletions src/app/damagecalc/damageCalc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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") {
Expand All @@ -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") {
Expand Down Expand Up @@ -544,17 +547,20 @@ 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;
}

// 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;

Expand Down
20 changes: 20 additions & 0 deletions src/app/data/items/FragileLocketItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LoadedItem } from "@/preload/loadedDataClasses";
import { Item } from "../tectonic/Item";
import { Stats } from "../tectonic/Pokemon";

export class FragileLocketItem extends Item {
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"];
}
9 changes: 6 additions & 3 deletions src/app/data/tectonic/Move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/app/data/tectonic/TectonicData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -103,6 +104,7 @@ const itemSubclasses = [
CategoryBoostingItem,
EvioliteItem,
FlatDamageBoostItem,
FragileLocketItem,
LumberAxeItem,
StatBoostItem,
StatLockItem,
Expand All @@ -111,6 +113,7 @@ const itemSubclasses = [
TypeBoostingItem,
TypeChangingItem,
WeatherImmuneItem,
FragileLocketItem,
];

const abilitySubclasses = [
Expand Down
44 changes: 23 additions & 21 deletions src/app/data/typeChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -45,58 +45,60 @@ 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") {
defType1Calc = defType1Calc == 0 ? 1.0 : defType1Calc;
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()) {
Expand All @@ -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);
}
}

Expand All @@ -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;
}
48 changes: 43 additions & 5 deletions src/app/data/types/PartyPokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -45,6 +46,26 @@ export class PartyPokemon {
(Object.fromEntries(volatileStatusEffects.map((e) => [e, false])) as Record<VolatileStatusEffect, boolean>);
}

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);
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/teambuilder/components/DefTotalCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/app/teambuilder/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const TeamBuilder: NextPage = () => {
{
type1: c.types.type1,
type2: c.types.type2,
ability: c.ability,
abilities: c.getActiveAbilities(),
}
)}
/>
Expand Down
11 changes: 11 additions & 0 deletions src/components/PokemonCardHorizontal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -189,6 +190,16 @@ export default function PokemonCardHorizontal({
))}
</Dropdown>

{partyMon.items.some((i) => i instanceof FragileLocketItem) &&
partyMon.getSecondaryAbility() && (
<div
className="text-sm text-gray-300 px-2 py-1 bg-gray-700 rounded select-none cursor-default text-center"
title={partyMon.getSecondaryAbility()!.description}
>
+ {partyMon.getSecondaryAbility()!.name}
</div>
)}

{Array.from({ length: partyMon.ability instanceof TwoItemAbility ? 2 : 1 }).map(
(_, i) => (
<div key={i}>
Expand Down
6 changes: 3 additions & 3 deletions src/components/PokemonModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ const PokemonModal: React.FC<PokemonModalProps> = ({ 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 },
),
);
Expand Down