113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { Component, type EvalContext } from "../core/world";
|
|
import { evaluateCondition } from "../utils/conditions";
|
|
import { action, component, ComponentTag, tag, variable } from "../utils/decorators";
|
|
import { Stat } from "./stat";
|
|
|
|
abstract class BaseEffect extends Component<{
|
|
targetStat: string; // component key, e.g. 'health'
|
|
targetField: 'value' | 'max' | 'min';
|
|
delta: number;
|
|
duration: number | null; // null = permanent until removed
|
|
remaining: number | null; // countdown in seconds; null for condition-based/permanent
|
|
condition: string | null; // keep effect while true; remove when it becomes false
|
|
stacking: 'stack' | 'unique' | 'replace';
|
|
tag: string | null; // discriminator for stacking; null = no stacking enforcement
|
|
}> {
|
|
constructor(opts: {
|
|
targetStat: string;
|
|
delta: number;
|
|
targetField?: 'value' | 'max' | 'min';
|
|
duration?: number;
|
|
condition?: string;
|
|
stacking?: 'stack' | 'unique' | 'replace';
|
|
tag?: string;
|
|
}) {
|
|
super({
|
|
targetStat: opts.targetStat,
|
|
targetField: opts.targetField ?? 'value',
|
|
delta: opts.delta,
|
|
duration: opts.duration ?? null,
|
|
remaining: opts.duration ?? null,
|
|
condition: opts.condition ?? null,
|
|
stacking: opts.stacking ?? 'stack',
|
|
tag: opts.tag ?? null,
|
|
});
|
|
}
|
|
}
|
|
|
|
@tag(ComponentTag.Equippable)
|
|
@component
|
|
export class Effect extends BaseEffect {
|
|
/** True while the effect's delta is applied to the target stat. */
|
|
@variable('.') active: boolean = false;
|
|
|
|
override onAdd(): void {
|
|
const { stacking, tag } = this.state;
|
|
|
|
if (tag != null && stacking !== 'stack') {
|
|
const siblings = this.entity.getAll(Effect).filter(e => e !== this && e.state.tag === tag);
|
|
|
|
if (stacking === 'unique' && siblings.length > 0) {
|
|
// An effect with this tag is already active — discard the incoming one.
|
|
this.entity.remove(this.key);
|
|
return;
|
|
}
|
|
|
|
if (stacking === 'replace') {
|
|
for (const old of siblings) {
|
|
this.entity.remove(old.key);
|
|
}
|
|
}
|
|
}
|
|
|
|
const stat = this.entity.get(Stat, this.state.targetStat);
|
|
if (stat) {
|
|
stat.applyModifier(this.state.delta, this.state.targetField);
|
|
this.active = true;
|
|
}
|
|
}
|
|
|
|
override onRemove(): void {
|
|
if (!this.active) return;
|
|
const stat = this.entity.get(Stat, this.state.targetStat);
|
|
if (stat) {
|
|
stat.removeModifier(this.state.delta, this.state.targetField);
|
|
}
|
|
this.active = false;
|
|
}
|
|
|
|
@action
|
|
reset(duration?: number): void {
|
|
if (duration != null) {
|
|
this.state.duration = duration;
|
|
}
|
|
this.state.remaining = this.state.duration;
|
|
this.active = true;
|
|
}
|
|
|
|
/** Mark as expired. EffectSystem will remove the component, which reverses the delta. */
|
|
@action
|
|
clear(): void {
|
|
this.state.remaining = 0;
|
|
this.emit('expired');
|
|
}
|
|
|
|
update(dt: number, ctx: EvalContext): void {
|
|
if (this.state.remaining != null) {
|
|
this.state.remaining -= dt;
|
|
if (this.state.remaining <= 0) {
|
|
this.state.remaining = 0;
|
|
this.clear();
|
|
}
|
|
} else if (this.state.condition != null) {
|
|
if (!evaluateCondition(this.state.condition, ctx)) {
|
|
this.clear();
|
|
}
|
|
}
|
|
// permanent effect (no duration, no condition): nothing to do
|
|
}
|
|
}
|
|
|
|
@component
|
|
export class EffectTemplate extends BaseEffect {
|
|
} |