1
0
Fork 0
This commit is contained in:
Pabloader 2026-04-29 12:24:20 +00:00
parent f766aee071
commit cb2d1a099f
3 changed files with 145 additions and 22 deletions

View File

@ -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
}
}

View File

@ -3,7 +3,8 @@ import { Component } from "../core/world";
import { component } from "../core/registry";
interface StatState {
value: number;
base: number;
modifierSums: { value: number; max: number; min: number };
max: number | undefined;
min: number | undefined;
}
@ -11,47 +12,67 @@ interface StatState {
@component
export class Stat extends Component<StatState> {
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 max(): number | undefined { return this.state.max; }
@variable get min(): number | undefined { return this.state.min; }
@variable('.') get value(): number {
const effMin = this.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
update(amount: number) {
this.set(this.state.value + amount);
this.set(this.state.base + amount);
}
@action
set(value: number) {
const prev = this.state.value;
this.state.value = value;
if (this.state.min != null) {
this.state.value = Math.max(this.state.min, this.state.value);
}
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 });
const prev = this.value;
this.state.base = value;
const next = this.value;
if (prev !== next) {
this.emit('set', { prev, value: next });
}
}
get current(): number {
return this.state.value;
applyModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
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
export class Health extends Stat {
constructor(value: number, max?: number, min = 0) {
super(value, max, min);
}
@action
kill() {
this.set(0);
this.emit('killed');
}
}
@component
export class Defense extends Stat { }
@component
export class Damage extends Stat { }

View File

@ -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);
}
}
}