189 lines
6.7 KiB
JavaScript
189 lines
6.7 KiB
JavaScript
// 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)
|
||
}
|