Effects
This commit is contained in:
parent
f766aee071
commit
cb2d1a099f
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { component } from "../core/registry";
|
||||||
|
import { Component, type EvalContext } from "../core/world";
|
||||||
|
import { evaluateCondition } from "../utils/conditions";
|
||||||
|
import { action, variable } from "../utils/decorators";
|
||||||
|
import { Stat } from "./stat";
|
||||||
|
|
||||||
|
@component
|
||||||
|
export class Effect 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
|
||||||
|
}> {
|
||||||
|
/** True while the effect's delta is applied to the target stat. */
|
||||||
|
@variable('.') active: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
targetStat: string,
|
||||||
|
delta: number,
|
||||||
|
targetField?: 'value' | 'max' | 'min',
|
||||||
|
duration?: number,
|
||||||
|
condition?: string,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
targetStat,
|
||||||
|
targetField: targetField ?? 'value',
|
||||||
|
delta,
|
||||||
|
duration: duration ?? null,
|
||||||
|
remaining: duration ?? null,
|
||||||
|
condition: condition ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAdd(): void {
|
||||||
|
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 {
|
||||||
|
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.active = false;
|
||||||
|
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.context)) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// permanent effect (no duration, no condition): nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ import { Component } from "../core/world";
|
||||||
import { component } from "../core/registry";
|
import { component } from "../core/registry";
|
||||||
|
|
||||||
interface StatState {
|
interface StatState {
|
||||||
value: number;
|
base: number;
|
||||||
|
modifierSums: { value: number; max: number; min: number };
|
||||||
max: number | undefined;
|
max: number | undefined;
|
||||||
min: number | undefined;
|
min: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -11,47 +12,67 @@ interface StatState {
|
||||||
@component
|
@component
|
||||||
export class Stat extends Component<StatState> {
|
export class Stat extends Component<StatState> {
|
||||||
constructor(value: number, max?: number, min?: number) {
|
constructor(value: number, max?: number, min?: number) {
|
||||||
super({ value, max, min });
|
super({ base: value, modifierSums: { value: 0, max: 0, min: 0 }, max, min });
|
||||||
}
|
}
|
||||||
|
|
||||||
@variable('.') get value(): number { return this.state.value; }
|
@variable('.') get value(): number {
|
||||||
@variable get max(): number | undefined { return this.state.max; }
|
const effMin = this.min;
|
||||||
@variable get min(): number | undefined { return this.state.min; }
|
const effMax = this.max;
|
||||||
|
let v = this.state.base + this.state.modifierSums.value;
|
||||||
|
if (effMin != null) v = Math.max(effMin, v);
|
||||||
|
if (effMax != null) v = Math.min(v, effMax);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@variable get base(): number { return this.state.base; }
|
||||||
|
@variable get max(): number | undefined {
|
||||||
|
return this.state.max != null ? this.state.max + this.state.modifierSums.max : undefined;
|
||||||
|
}
|
||||||
|
@variable get min(): number | undefined {
|
||||||
|
return this.state.min != null ? this.state.min + this.state.modifierSums.min : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
update(amount: number) {
|
update(amount: number) {
|
||||||
this.set(this.state.value + amount);
|
this.set(this.state.base + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
set(value: number) {
|
set(value: number) {
|
||||||
const prev = this.state.value;
|
const prev = this.value;
|
||||||
this.state.value = value;
|
this.state.base = value;
|
||||||
if (this.state.min != null) {
|
const next = this.value;
|
||||||
this.state.value = Math.max(this.state.min, this.state.value);
|
if (prev !== next) {
|
||||||
}
|
this.emit('set', { prev, value: next });
|
||||||
if (this.state.max != null) {
|
|
||||||
this.state.value = Math.min(this.state.value, this.state.max);
|
|
||||||
}
|
|
||||||
if (prev !== this.state.value) {
|
|
||||||
this.emit('set', { prev, value: this.state.value });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get current(): number {
|
applyModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
|
||||||
return this.state.value;
|
const prev = this.value;
|
||||||
|
this.state.modifierSums[field] += delta;
|
||||||
|
const next = this.value;
|
||||||
|
if (prev !== next) this.emit('set', { prev, value: next });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
|
||||||
|
this.applyModifier(-delta, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
get current(): number { return this.value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Health extends Stat {
|
export class Health extends Stat {
|
||||||
constructor(value: number, max?: number, min = 0) {
|
|
||||||
super(value, max, min);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
kill() {
|
kill() {
|
||||||
this.set(0);
|
this.set(0);
|
||||||
this.emit('killed');
|
this.emit('killed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@component
|
||||||
|
export class Defense extends Stat { }
|
||||||
|
|
||||||
|
@component
|
||||||
|
export class Damage extends Stat { }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Effect } from "../components/effect";
|
||||||
|
import { System, type Entity, type World } from "../core/world";
|
||||||
|
|
||||||
|
export class EffectSystem extends System {
|
||||||
|
override update(world: World, dt: number): void {
|
||||||
|
const expired: [Entity, string][] = [];
|
||||||
|
|
||||||
|
for (const [entity, key, effect] of world.query(Effect)) {
|
||||||
|
effect.update(dt, entity.context);
|
||||||
|
if (!effect.active) {
|
||||||
|
expired.push([entity, key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [entity, key] of expired) {
|
||||||
|
entity.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue