Files
tickte-system/src/components/workers/engine/fx.js
T
2026-10-18 02:10:37 +00:00

189 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Effekt-System des Hustler-Hochhauses: kurzlebige Zeichnungen (Papier, Brief, Münze, Stempel, Funkeln, Warnzeichen),
// die in Weltkoordinaten von einem Punkt zum anderen laufen. Portiert aus entwuerfe/scene-turm.js (addFx/updateFx/drawFx/envelope).
// Zeit läuft nur über update(dt) der Szene, nie über setTimeout (pausierte Tabs laufen so nicht vor).
// Kein React, kein Appwrite, kein window/document; gezeichnet wird ausschließlich über die Engine-Helfer (px, drawPaper, ...).
import { px, PAL, drawPaper, drawAlertMark } from './sprites.js'
import { drawCoin, drawStamp } from './props.js'
export const FX_NAMES = Object.freeze(['paper', 'envelope', 'coin', 'stampOk', 'stampNo', 'sparkle', 'alert', 'drawer'])
export const MAX_FX = 80
// ---------- Liste der laufenden Effekte ----------
/* createFx() → { add(life, draw, onEnd?, delay = 0), update(dt), draw(ctx), clear(), size() }
add: Lebensdauer und Verzögerung in Sekunden; draw(ctx, progress 0..1, effect) mit effect = { t, life } (t = Sekunden seit dem Start).
onEnd läuft genau einmal beim Ablauf. Ist die Liste mit MAX_FX Effekten voll, wird der neue verworfen und sein onEnd sofort gerufen.
clear() verwirft alles ohne onEnd. size() zählt auch Effekte, die noch auf ihre Verzögerung warten. */
export function createFx() {
let list = []
return {
add(life, draw, onEnd, delay = 0) {
if (list.length >= MAX_FX) {
if (typeof onEnd === 'function') onEnd()
return
}
list.push({
t: Number.isFinite(delay) && delay > 0 ? -delay : 0, // negativ = wartet noch auf den Start
life: Number.isFinite(life) && life > 0 ? life : 0,
draw,
onEnd,
})
},
update(dt) {
if (!Number.isFinite(dt) || dt <= 0) return
const alive = []
const ended = []
for (const f of list) {
f.t += dt
if (f.t >= f.life) ended.push(f)
else alive.push(f)
}
// Erst die Liste ersetzen, dann die Rückrufe: Ein onEnd darf neue Effekte anlegen oder clear() rufen.
list = alive
let failure = null
for (const f of ended) {
if (typeof f.onEnd !== 'function') continue
try {
f.onEnd()
} catch (err) {
if (!failure) failure = { err } // die übrigen onEnd laufen trotzdem, der erste Fehler wird danach geworfen
}
}
if (failure) throw failure.err
},
draw(ctx) {
for (const f of list) {
if (f.t < 0 || typeof f.draw !== 'function') continue
f.draw(ctx, f.life > 0 ? Math.min(1, f.t / f.life) : 1, f)
}
},
clear() {
list = []
},
size() {
return list.length
},
}
}
// ---------- Zeichnungen ----------
// Brief 7 × 5 (x, y = linke obere Ecke)
const ENVELOPE_COLOR = '#7a5cd6'
function drawEnvelope(ctx, x, y) {
px(ctx, x, y, 7, 5, ENVELOPE_COLOR)
px(ctx, x + 1, y + 1, 5, 3, PAL.w)
px(ctx, x + 1, y + 1, 1, 1, ENVELOPE_COLOR)
px(ctx, x + 2, y + 2, 1, 1, ENVELOPE_COLOR)
px(ctx, x + 3, y + 2, 1, 1, ENVELOPE_COLOR)
px(ctx, x + 4, y + 2, 1, 1, ENVELOPE_COLOR)
px(ctx, x + 5, y + 1, 1, 1, ENVELOPE_COLOR)
}
// Vier Goldpixel in den Ecken wachsen nach außen, ein heller Pixel steht oben (um (x, y) als Mitte)
function drawSparkle(ctx, x, y, p) {
const s = Math.round(p * 6)
px(ctx, x - s - 1, y - s - 2, 1, 1, PAL.gold)
px(ctx, x + s + 1, y - s - 2, 1, 1, PAL.gold)
px(ctx, x - s - 1, y + s + 2, 1, 1, PAL.gold)
px(ctx, x + s + 1, y + s + 2, 1, 1, PAL.gold)
px(ctx, x, y - s - 4, 1, 1, '#fff3c4')
}
// Punkt auf dem Bogen von a nach b, in ganzen Einheiten (Pixel-Optik); high = Höhe des Bogens über der Geraden
function arcPoint(a, b, p, high) {
return {
x: Math.round(a.x + (b.x - a.x) * p),
y: Math.round(a.y + (b.y - a.y) * p - Math.sin(p * Math.PI) * high),
}
}
// Ziel um len Einheiten vor b (auf der Strecke a nach b); liegt b näher als len, bleibt der Start
function shortenedEnd(a, b, len) {
const dx = b.x - a.x
const dy = b.y - a.y
const dist = Math.hypot(dx, dy)
if (dist <= len) return a
return { x: b.x - (dx / dist) * len, y: b.y - (dy / dist) * len }
}
// Der Stempel hüpft dreimal über dem Punkt, die Sprünge werden flacher, am Ende liegt er auf
function stampDraw(a, ok) {
return (ctx, p) => {
const hop = Math.round(Math.abs(Math.sin(p * Math.PI * 3)) * 6 * (1 - p))
drawStamp(ctx, Math.round(a.x) - 3, Math.round(a.y) - 5 - hop, ok)
}
}
/* Je Effektname: Lebensdauer in Sekunden und eine Fabrik, die aus Start (a) und Ziel (b) die Zeichenfunktion baut.
Papier, Brief und Münze sind auf ihren Punkt zentriert (Mitte des Sprites). Stempel und Warnzeichen brauchen nur a und stehen mit
der Unterkante darauf (wie die Spitze der Sprechblase: die Regie gibt die Kopfoberkante des Workers), das Funkeln ist um a zentriert. */
const KINDS = {
paper: {
life: 0.9,
make: (a, b) => (ctx, p) => {
const q = arcPoint(a, b, p, 8)
drawPaper(ctx, q.x - 3, q.y - 3)
},
},
envelope: {
life: 3.4,
make: (a, b) => (ctx, p) => {
const q = arcPoint(a, b, p, 12)
drawEnvelope(ctx, q.x - 3, q.y - 2)
},
},
coin: {
life: 0.7,
make: (a, b) => (ctx, p, f) => {
const q = arcPoint(a, b, p, 14)
drawCoin(ctx, q.x - 2, q.y - 2, f.t)
},
},
stampOk: { life: 1.2, make: (a) => stampDraw(a, true) },
stampNo: { life: 1.2, make: (a) => stampDraw(a, false) },
sparkle: {
life: 1.0,
make: (a) => (ctx, p) => drawSparkle(ctx, Math.round(a.x), Math.round(a.y), p),
},
alert: {
life: 1.5,
// Das Zeichen blinkt (vier Wechsel je Sekunde) und beginnt sichtbar; a = Kopfoberkante des Workers
make: (a) => (ctx, p, f) => {
if (Math.floor(f.t * 4) % 2 === 0) drawAlertMark(ctx, Math.round(a.x), Math.round(a.y))
},
},
drawer: {
life: 0.9,
// Das Blatt verschwindet in der Lade: es endet 3 Einheiten vor dem Ziel
make: (a, b) => {
const end = shortenedEnd(a, b, 3)
return (ctx, p) => {
const q = arcPoint(a, end, p, 6)
drawPaper(ctx, q.x - 3, q.y - 3)
}
},
},
}
/* Startet den benannten Effekt in Weltkoordinaten. from/to = { x, y }; fehlt eines, gilt das andere, fehlen beide, (0, 0).
delay in Sekunden, onEnd genau einmal beim Ablauf. Unbekannter Name oder volle Liste: nichts wird gezeichnet,
onEnd läuft sofort (damit eine Regie, die darauf wartet, nicht hängen bleibt). */
export function spawnFx(fx, name, opts) {
const { from, to, delay = 0, onEnd } = opts || {}
const kind = FX_NAMES.includes(name) ? KINDS[name] : null
if (!kind) {
if (typeof onEnd === 'function') onEnd()
return
}
const a = from || to || { x: 0, y: 0 }
const b = to || a
fx.add(kind.life, kind.make(a, b), onEnd, delay)
}