Workers P2: Effekt-System mit Obergrenze
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c27f0b600b
commit
38261ecdd8
2 files changed
+350
No files matched your search
@@ -0,0 +1,188 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in new issue
Block a user