diff --git a/app/src/libs/combat/process.ts b/app/src/libs/combat/process.ts index 9536680ee..1c306e6cf 100644 --- a/app/src/libs/combat/process.ts +++ b/app/src/libs/combat/process.ts @@ -6,7 +6,7 @@ import { calcApplyRatio } from "./util"; import { calcEffectRoundInfo, isEffectActive } from "./util"; import { nanoid } from "nanoid"; import { clone, move, heal, damageBarrier, damageUser, calcDmgModifier } from "./tags"; -import { absorb, reflect, recoil, lifesteal, drain, shield, poison } from "./tags"; +import { absorb, reflect, recoil, lifesteal, drain, shield, poison, copy, mirror } from "./tags"; import { increaseStats, decreaseStats } from "./tags"; import { increaseDamageGiven, decreaseDamageGiven } from "./tags"; import { increaseDamageTaken, decreaseDamageTaken } from "./tags"; @@ -100,12 +100,17 @@ export const realizeTag = (props: { barrierAbsorb?: number; }): T => { const { tag, user, target, level, round, barrierAbsorb } = props; + + // Ensure rounds exist when necessary if ("rounds" in tag) { tag.timeTracker = {}; + tag.rounds = tag.rounds ?? 1; // Default to 1 if rounds are undefined } + if ("power" in tag) { tag.power = tag.power; } + tag.id = nanoid(); tag.createdRound = round || 0; tag.creatorId = user.userId; @@ -119,11 +124,28 @@ export const realizeTag = (props: { tag.highestGenerals = user.highestGenerals; tag.barrierAbsorb = barrierAbsorb || 0; tag.actionId = props.actionId; + if (target) { tag.targetHighestOffence = target.highestOffence; tag.targetHighestDefence = target.highestDefence; tag.targetHighestGenerals = target.highestGenerals; } + + // Ensure targetId is present in tag (if applicable) + if ("targetId" in tag) { + // Handle Copy Effect (Copies positive effects from target to self) + if (tag.type === "copy" && target) { + tag.targetId = user.userId; // Copy effects to self + tag.creatorId = user.userId; + } + + // Handle Mirror Effect (Transfers negative effects from self to target) + if (tag.type === "mirror" && target) { + tag.targetId = target.userId; // Apply to opponent + tag.creatorId = user.userId; + } + } + return structuredClone(tag); }; @@ -344,6 +366,20 @@ export const applyEffects = ( info = stun(e, newUsersEffects, curTarget); } else if (e.type === "drain") { info = drain(e, usersEffects, consequences, curTarget); + } else if (e.type === "copy") { + info = copy(e, usersEffects, curUser, curTarget); + if (info && usersEffects) { + usersEffects.push(...usersEffects.filter( + (eff) => eff.targetId === curUser.userId && eff.isNew + )); + } + } else if (e.type === "mirror") { + info = mirror(e, usersEffects, curUser, curTarget); + if (info && usersEffects) { + usersEffects.push(...usersEffects.filter( + (eff) => eff.targetId === curTarget.userId && eff.isNew + )); + } } } diff --git a/app/src/libs/combat/tags.ts b/app/src/libs/combat/tags.ts index 162da4999..290ef5e3e 100644 --- a/app/src/libs/combat/tags.ts +++ b/app/src/libs/combat/tags.ts @@ -1266,6 +1266,76 @@ export const shield = (effect: UserEffect, target: BattleUserState) => { return info; }; +/** Copy positive effects from opponent to self */ +export const copy = ( + effect: UserEffect, + usersEffects: UserEffect[], + user: BattleUserState, + target: BattleUserState +): ActionEffect | undefined => { + // Find all positive effects on the target + const positiveEffects = usersEffects.filter( + (e) => e.targetId === target.userId && isPositiveUserEffect(e) + ); + + if (positiveEffects.length === 0) { + return { txt: `${user.username} tries to copy but finds no effects to copy.`, color: "blue" }; + } + + positiveEffects.forEach((posEffect) => { + const copiedEffect = structuredClone(posEffect); + copiedEffect.id = nanoid(); // Give it a new unique ID + copiedEffect.targetId = user.userId; + copiedEffect.creatorId = user.userId; + copiedEffect.isNew = true; + copiedEffect.castThisRound = true; + usersEffects.push(copiedEffect); + }); + + return { + txt: `${user.username} copies ${positiveEffects.length} effects from ${target.username}.`, + color: "blue", + }; +}; + +/** Copy negative effects from self to target */ +export const mirror = ( + effect: UserEffect, + usersEffects: UserEffect[], + user: BattleUserState, + target: BattleUserState +): ActionEffect | undefined => { + // Find all negative effects on the user that have rounds between 1-10 + const negativeEffects = usersEffects.filter( + (e) => + e.targetId === user.userId && + isNegativeUserEffect(e) && + e.rounds !== undefined && // Ensure rounds are set + e.rounds > 0 && + e.rounds <= 10 // Only allow effects with 1-10 rounds + ); + + if (negativeEffects.length === 0) { + return { txt: `${user.username} tries to mirror but finds no valid effects to reflect.`, color: "red" }; + } + + negativeEffects.forEach((negEffect) => { + const mirroredEffect = structuredClone(negEffect); + mirroredEffect.id = nanoid(); // Give it a new unique ID + mirroredEffect.targetId = target.userId; + mirroredEffect.creatorId = user.userId; + mirroredEffect.isNew = true; + mirroredEffect.castThisRound = true; + usersEffects.push(mirroredEffect); + }); + + return { + txt: `${user.username} mirrors ${negativeEffects.length} effects onto ${target.username}.`, + color: "red", + }; +}; + + /** * Move user on the battlefield * 1. Remove user from current ground effect diff --git a/app/src/libs/combat/types.ts b/app/src/libs/combat/types.ts index cea6ea31c..d07447100 100644 --- a/app/src/libs/combat/types.ts +++ b/app/src/libs/combat/types.ts @@ -708,6 +708,26 @@ export const WeaknessTag = z.object({ }); export type WeaknessTagType = z.infer; +export const CopyTag = z.object({ + ...BaseAttributes, + ...PowerAttributes, + type: z.literal("copy").default("copy"), + description: msg("Copies all positive effects from the target to the user"), + calculation: z.enum(["percentage"]).default("percentage"), + rounds: z.coerce.number().int().min(1).max(10).default(3), +}); +export type CopyTagType = z.infer; + +export const MirrorTag = z.object({ + ...BaseAttributes, + ...PowerAttributes, + type: z.literal("mirror").default("mirror"), + description: msg("Mirrors all negative effects from the user to the target"), + calculation: z.enum(["percentage"]).default("percentage"), + rounds: z.coerce.number().int().min(1).max(10).default(3), +}); +export type MirrorTagType = z.infer; + export const UnknownTag = z.object({ ...BaseAttributes, type: z.literal("unknown").default("unknown"), @@ -734,6 +754,7 @@ export const AllTags = z.union([ ClearPreventTag.default({}), ClearTag.default({}), CloneTag.default({}), + CopyTag.default({}), DamageTag.default({}), DebuffPreventTag.default({}), DecreaseDamageGivenTag.default({}), @@ -741,6 +762,7 @@ export const AllTags = z.union([ DecreaseHealGivenTag.default({}), DecreasePoolCostTag.default({}), DecreaseStatTag.default({}), + DrainTag.default({}), FleePreventTag.default({}), FleeTag.default({}), HealTag.default({}), @@ -751,7 +773,7 @@ export const AllTags = z.union([ IncreasePoolCostTag.default({}), IncreaseStatTag.default({}), LifeStealTag.default({}), - DrainTag.default({}), + MirrorTag.default({}), MoveTag.default({}), MovePreventTag.default({}), OneHitKillPreventTag.default({}), diff --git a/app/src/libs/combat/util.ts b/app/src/libs/combat/util.ts index a306cdda0..468f28a50 100644 --- a/app/src/libs/combat/util.ts +++ b/app/src/libs/combat/util.ts @@ -315,6 +315,8 @@ export const sortEffects = ( "reflect", "decreaseheal", "increaseheal", + "copy", + "mirror", // End-modifiers "move", "visual",