Workers P2: Regie 2 (Schritte, Aufzugfahrten, Rückstau-Kappung, Wachhund)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e69c4355bd
commit
ad8ee9afe2
2 files changed
+848
-27
No files matched your search
@@ -1,17 +1,20 @@
|
||||
// Regie des Hustler-Hochhauses, Teil 1: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Zeichnen.
|
||||
// Portiert aus entwuerfe/scene-turm.js (Agenten Z. 596-625, Update Z. 864-905, Zeichnen Z. 925-960).
|
||||
// Schritte, Aufzugfahrten, Rückstau-Kappung und Wachhund ergänzt P2-T13. Kein React, kein Appwrite, kein window/document.
|
||||
// Regie des Hustler-Hochhauses: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Schritte (Übergaben mit Aufzugfahrten,
|
||||
// Effekte, Sprechblasen), Rückstau-Kappung, Wachhund und Zeichnen.
|
||||
// Portiert aus entwuerfe/scene-turm.js (Agenten Z. 596-625, Schritte Z. 416-464, Update Z. 864-905, Zeichnen Z. 925-960).
|
||||
// Kein React, kein Appwrite, kein window/document.
|
||||
|
||||
import { ROLES, hatForWorker } from '../../../lib/workersRoster.js'
|
||||
import { indexWorkers, cutBubble } from '../../../lib/workersEvents.js'
|
||||
import { indexWorkers, reduceEvents, eventToSteps, cutBubble } from '../../../lib/workersEvents.js'
|
||||
import { homeOf } from './layout.js'
|
||||
import { LIFT } from './lift.js'
|
||||
import { spawnFx } from './fx.js'
|
||||
import { trophyPaging } from './rooms.js'
|
||||
import { drawWorker, drawBubble, drawSideBubble, drawAlertMark, drawPaper } from './sprites.js'
|
||||
|
||||
export const BACKLOG_KEEP = 6 // je pushEvents animieren nur die neuesten so viele Ereignisse (T13)
|
||||
export const GROUPS_PER_AGENT = 4 // je Worker höchstens so viele wartende Gruppen (T13)
|
||||
export const BACKLOG_KEEP = 6 // je pushEvents animieren nur die neuesten so viele Gruppen, ältere wirken still
|
||||
export const GROUPS_PER_AGENT = 4 // je Worker höchstens so viele wartende Gruppen
|
||||
export const IDLE_COFFEE_S = 25 // so lange trinkt ein Worker im Leerlauf Kaffee, dann schläft er
|
||||
export const STEP_TIMEOUT_S = 45 // ein Schritt dauert höchstens so lange, dann greift der Wachhund (T13)
|
||||
export const STEP_TIMEOUT_S = 45 // ein Schritt dauert höchstens so lange, dann greift der Wachhund
|
||||
export const BUBBLE_MAX = 3 // höchstens so viele Sprechblasen gleichzeitig
|
||||
|
||||
const PAGE_S = 6 // Pokal-Seiten blättern alle 6 Sekunden
|
||||
@@ -22,6 +25,17 @@ const LANE_SPEED = 14 // Einheiten je Sekunde, mit der die Spur wechselt
|
||||
const HOME_EPS = 0.5 // so nah am Heimatplatz gilt ein Worker als am Platz
|
||||
const ALERT_H = 9 // Höhe des „!“ mit Umriss (sprites.js)
|
||||
|
||||
const WALK_SPEED = 46 // Einheiten je Sekunde (Vorlage WALK 38, schnell 46)
|
||||
const ARRIVE_EPS = 0.5 // so nah am Ziel gilt ein Weg als zu Ende
|
||||
const PICKUP_S = 0.4 // so lange dauert es, das Dokument aufzunehmen
|
||||
const SAY_S = 1.2 // so lange wartet die Gruppe nach einer Sprechblase, bevor der nächste Schritt kommt
|
||||
const THANKS_S = 1.0 // so lange bleibt der Giver nach der Übergabe beim Empfänger stehen
|
||||
const CHEER_S = 2 // so lange jubelt ein Worker
|
||||
const VAULT_FLASH_S = 0.7 // Tresor-Blitz, wenn eine Münze ankommt
|
||||
const BESIDE_DX = 12 // Abstand zum Empfänger bei der Übergabe
|
||||
const CAB_DY = 7 // Fahrgast: obere Kante des Körpers 7 unter der Kabinenoberkante (wie in lift.js)
|
||||
const MAX_STEPS_PER_FRAME = 32 // so viele Schritte endet ein Worker höchstens in einem Bild (Schutz vor Endlosschleifen)
|
||||
|
||||
// Blickrichtung am Heimatplatz (Vorlage FACE); alle anderen blicken nach rechts
|
||||
const FACE_LEFT = new Set(['ali', 'hanna', 'sven', 'lea', 'paul', 'clara'])
|
||||
const faceOf = (id) => (FACE_LEFT.has(id) ? -1 : 1)
|
||||
@@ -53,13 +67,47 @@ const laneTarget = (a) => (a.id !== 'otto' && atHome(a) ? LANE : 0)
|
||||
// Zähler als ganze Zahl >= 0; alles andere (NaN, Text, null, fehlend) ist kein Wert
|
||||
const kpiOf = (kpis, key) => (isRecord(kpis) && Number.isFinite(kpis[key]) ? Math.max(0, Math.floor(kpis[key])) : null)
|
||||
|
||||
/* createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Math.random }) → { agents, byId, applyState(state), update(dt), pending(), boxes(), bubble(agent, text, dur) }
|
||||
// Läuft auf (tx, ty) zu (Vorlage stepTowards); speed in Einheiten je Sekunde; true = angekommen (dann steht die Figur: frame 0)
|
||||
function stepTowards(a, tx, ty, speed, dt) {
|
||||
const dx = tx - a.x
|
||||
const dy = ty - a.y
|
||||
const dist = Math.hypot(dx, dy)
|
||||
if (dist < ARRIVE_EPS) {
|
||||
a.x = tx
|
||||
a.y = ty
|
||||
a.frame = 0
|
||||
return true
|
||||
}
|
||||
const step = Math.min(dist, speed * dt)
|
||||
a.x += (dx / dist) * step
|
||||
a.y += (dy / dist) * step
|
||||
if (Math.abs(dx) > 0.3) a.facing = dx < 0 ? -1 : 1
|
||||
a.walkT += dt
|
||||
a.frame = 1 + (Math.floor(a.walkT * 8) % 2)
|
||||
return false
|
||||
}
|
||||
|
||||
/* createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Math.random })
|
||||
→ { agents, byId, applyState(state), pushEvents(events), update(dt), flush(), reset(), pending(), debug(), boxes(), bubble(agent, text, dur) }
|
||||
Ein Agent je Rolle (Rollen ohne Heimatplatz im Layout entfallen). applyState übernimmt einen Dienst-Zustand: der erste Snapshot setzt
|
||||
alle Agenten auf ihren Platz, spätere bewegen nie und ändern Pose und Aufgabe nur bei Agenten ohne laufende Gruppe (groups = 0).
|
||||
update(dt) tut bei look.paused nichts und läuft sonst ohne Obergrenze für dt (flush in T13 ruft es mit 0,25). rnd streut die Wipp-Phase. */
|
||||
alle Agenten auf ihren Platz, spätere bewegen nie und ändern Pose und Aufgabe nur bei Agenten ohne laufende Gruppe (groups = 0); was dabei
|
||||
ausfällt, holt die Regie am Ende der letzten Gruppe aus der Tabelle nach. Die Tabelle (workers) bleibt dauerhaft: Snapshot, fortgeschrieben mit
|
||||
reduceEvents.
|
||||
pushEvents(events) übersetzt jedes Ereignis über eventToSteps in eine Gruppe von Schritten. Besitzer der Gruppe ist der Absender des ersten
|
||||
give, sonst die workerId (wenn es den Worker gibt), sonst Otto. Die neuesten BACKLOG_KEEP Gruppen laufen als Animation in der Warteschlange
|
||||
ihres Besitzers (agent.q, agent.groups), alle älteren wirken still (nur Aufgabe und Pose); still ist auch jede Gruppe eines grauen Besitzers
|
||||
und jede, die der Besitzer wegen GROUPS_PER_AGENT nicht mehr aufnimmt. Vor dem ersten Snapshot tut pushEvents nichts.
|
||||
update(dt) tut bei look.paused nichts und läuft sonst ohne Obergrenze für dt (flush ruft es mit 0,25).
|
||||
Wachhund: ein Schritt, der länger als STEP_TIMEOUT_S läuft, bricht die Gruppe ab (Aufzug freigeben, Türzähler zurücknehmen, Agent heim).
|
||||
flush() spielt alles schnell zu Ende und setzt dann alles zurück, reset() setzt sofort zurück (Warteschlangen leer, Aufzug und Effekte
|
||||
zurückgesetzt, alle Agenten heim). pending() = wartende und laufende Gruppen über alle Agenten. rnd streut die Wipp-Phase. */
|
||||
export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Math.random }) {
|
||||
const agents = []
|
||||
const byId = Object.create(null)
|
||||
const door = new Array(layout.floors.length).fill(0) // Wartende vor dem Aufzug je Etage (Türzähler)
|
||||
const counted = new Map() // Agent -> Etage, solange er im Türzähler mitgezählt wird
|
||||
const lagging = new Set() // Kennungen, bei denen Aufgabe und Pose noch aus der Tabelle nachzuholen sind
|
||||
let workers = {} // Tabelle nach workerId: letzter Snapshot, fortgeschrieben mit den Ereignissen (indexWorkers, reduceEvents)
|
||||
let first = true // der erste Snapshot stellt alle Agenten an ihren Platz
|
||||
let prevDeals = null // dealsMonth des vorigen Snapshots; null = noch keiner gesehen
|
||||
let clock = 0 // Regie-Zeit in Sekunden (nur für das Wippen)
|
||||
@@ -91,12 +139,14 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
bob: 0,
|
||||
idleT: 0,
|
||||
cheer: 0,
|
||||
q: [],
|
||||
cur: null,
|
||||
stepT: 0,
|
||||
q: [], // wartende und laufende Gruppen { steps, dead }, vorne die laufende
|
||||
cur: null, // laufender Schritt
|
||||
stepT: 0, // so lange läuft der Schritt schon (Wachhund)
|
||||
groups: 0,
|
||||
box: null,
|
||||
home,
|
||||
walkT: 0, // Laufzeit für den Schrittwechsel der Beine
|
||||
dy: home.y - layout.floors[home.floor].floorY, // Standhöhe relativ zum Boden (Otto steht höher)
|
||||
ph: rnd() * 6, // Phase des Wippens: nicht alle im Gleichschritt
|
||||
}
|
||||
a.lane = laneTarget(a)
|
||||
@@ -104,7 +154,11 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
byId[a.id] = a
|
||||
}
|
||||
|
||||
// Auf den Heimatplatz stellen (erster Snapshot, T13: reset)
|
||||
const entryOf = (a) => (Object.hasOwn(workers, a.id) ? workers[a.id] : null)
|
||||
const standY = (a, floor) => layout.floors[floor].floorY + a.dy
|
||||
const pending = () => agents.reduce((n, a) => n + a.groups, 0)
|
||||
|
||||
// Auf den Heimatplatz stellen (erster Snapshot, Abbruch, reset)
|
||||
function placeHome(a) {
|
||||
a.x = a.home.x
|
||||
a.y = a.home.y
|
||||
@@ -127,16 +181,24 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
}
|
||||
}
|
||||
|
||||
// Eintrag aus indexWorkers (oder null = fehlt im Zustand) auf den Agenten übertragen
|
||||
function adopt(a, w) {
|
||||
// Nur den Status aus einem Eintrag der Tabelle (oder null = fehlt im Zustand) übernehmen
|
||||
function adoptStatus(a, w) {
|
||||
a.status = w ? w.status : 'grau'
|
||||
a.grey = a.status === 'grau'
|
||||
a.alert = a.status === 'fehler'
|
||||
}
|
||||
|
||||
// Eintrag der Tabelle auf den Agenten übertragen; Aufgabe und Pose erst, wenn keine Gruppe läuft (sonst später per catchUp)
|
||||
function adopt(a, w) {
|
||||
adoptStatus(a, w)
|
||||
a.counters = w ? w.counters : {}
|
||||
if (!busy(a)) {
|
||||
a.task = w ? w.task : ''
|
||||
a.pose = poseFor(a)
|
||||
if (busy(a)) {
|
||||
lagging.add(a.id)
|
||||
return
|
||||
}
|
||||
lagging.delete(a.id)
|
||||
a.task = w ? w.task : ''
|
||||
a.pose = poseFor(a)
|
||||
}
|
||||
|
||||
function applyKpis(kpis) {
|
||||
@@ -153,8 +215,8 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
}
|
||||
|
||||
function applyState(state) {
|
||||
const workers = indexWorkers(state)
|
||||
for (const a of agents) adopt(a, Object.hasOwn(workers, a.id) ? workers[a.id] : null)
|
||||
workers = indexWorkers(state)
|
||||
for (const a of agents) adopt(a, entryOf(a))
|
||||
if (first) {
|
||||
first = false
|
||||
for (const a of agents) placeHome(a)
|
||||
@@ -176,7 +238,326 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------- Schritte ----------
|
||||
|
||||
// Ort für Effekte: Worker (Kopfoberkante, Mitte) oder Platz aus layout.spots; unbekannt oder fehlend -> null
|
||||
function placeOf(place) {
|
||||
if (typeof place !== 'string') return null
|
||||
if (Object.hasOwn(byId, place)) {
|
||||
const a = byId[place]
|
||||
return { x: a.x + 6 * a.scale, y: a.y + a.lane - 2 * a.scale }
|
||||
}
|
||||
return Object.hasOwn(layout.spots, place) ? layout.spots[place] : null
|
||||
}
|
||||
|
||||
// Aus dem Türzähler austragen, aber nur, wenn der Agent noch mitgezählt wird (vor dem Einsteigen)
|
||||
function uncount(a) {
|
||||
if (!counted.has(a)) return
|
||||
door[counted.get(a)] -= 1
|
||||
counted.delete(a)
|
||||
}
|
||||
|
||||
// Gruppe gilt als abgebrochen: alle weiteren Schritte entfallen, der Agent steht sofort wieder an seinem Platz
|
||||
function abort(a, group) {
|
||||
uncount(a)
|
||||
group.dead = true
|
||||
group.steps.length = 0
|
||||
placeHome(a)
|
||||
return true
|
||||
}
|
||||
|
||||
const guarded = (group, fn) => (a, dt) => (group.dead ? true : fn(a, dt))
|
||||
const instant = (fn) => (a) => {
|
||||
fn(a)
|
||||
return true
|
||||
}
|
||||
|
||||
// Wartet sec Sekunden; onStart läuft einmal beim ersten Aufruf
|
||||
function timed(sec, onStart) {
|
||||
let t = 0
|
||||
let begun = false
|
||||
return (a, dt) => {
|
||||
if (!begun) {
|
||||
begun = true
|
||||
if (onStart) onStart(a)
|
||||
}
|
||||
t += dt
|
||||
return t >= sec
|
||||
}
|
||||
}
|
||||
|
||||
// Aufzug fahren von Etage fromF nach toF (der Agent steht auf fromF); Algorithmus der Vorlage rideStep, erweitert um Abbruch
|
||||
function rideStep(group, fromF, toF) {
|
||||
let ph = 0
|
||||
let req = null
|
||||
let sx = 0
|
||||
return (a, dt) => {
|
||||
if (ph === 0) {
|
||||
sx = LIFT.slot0 + LIFT.slotStep * door[fromF]
|
||||
door[fromF] += 1
|
||||
counted.set(a, fromF)
|
||||
ph = 1
|
||||
}
|
||||
if (ph === 1) {
|
||||
if (!stepTowards(a, sx, standY(a, fromF), WALK_SPEED, dt)) return false
|
||||
a.facing = -1
|
||||
req = lift.request(a, fromF, toF) || { st: 'cancelled' }
|
||||
ph = 2
|
||||
}
|
||||
if (req.st === 'cancelled') return abort(a, group)
|
||||
if (ph === 2) {
|
||||
if (req.st !== 'open') return false
|
||||
uncount(a)
|
||||
ph = 3
|
||||
}
|
||||
if (ph === 3) {
|
||||
if (!stepTowards(a, LIFT.cabWx, standY(a, fromF), WALK_SPEED, dt)) return false
|
||||
// der Fahrgast steht vor dem Einsteigen schon in der Kabine
|
||||
a.facing = 1
|
||||
a.frame = 0
|
||||
a.x = LIFT.cabWx
|
||||
a.y = lift.y + CAB_DY
|
||||
req.st = 'boarded'
|
||||
ph = 4
|
||||
}
|
||||
if (ph === 4) {
|
||||
if (req.st !== 'arrived') return false
|
||||
a.floor = toF
|
||||
a.x = LIFT.cabWx
|
||||
a.y = standY(a, toF)
|
||||
ph = 5
|
||||
}
|
||||
if (!stepTowards(a, LIFT.exitX, standY(a, toF), WALK_SPEED, dt)) return false
|
||||
req.st = 'out'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Neben den Empfänger laufen (links oder rechts, je nach Seite des Gebers), zu ihm hinblicken
|
||||
function besideStep(r) {
|
||||
let tx = null
|
||||
let face = 1
|
||||
return (a, dt) => {
|
||||
if (tx === null) {
|
||||
const fromLeft = a.x < r.home.x
|
||||
tx = fromLeft ? r.home.x - BESIDE_DX : r.home.x + BESIDE_DX * r.scale
|
||||
face = fromLeft ? 1 : -1
|
||||
a.floor = r.home.floor
|
||||
}
|
||||
if (!stepTowards(a, tx, standY(a, r.home.floor), WALK_SPEED, dt)) return false
|
||||
a.facing = face
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Zurück zum Platz, dann Blickrichtung wie am Platz
|
||||
function homeStep() {
|
||||
return (a, dt) => {
|
||||
if (!stepTowards(a, a.home.x, a.home.y, WALK_SPEED, dt)) return false
|
||||
a.facing = faceOf(a.id)
|
||||
a.carrying = false
|
||||
a.stackN = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Übergabe: Dokument aufnehmen, (Aufzug,) zum Empfänger, Danke, (Aufzug,) heim. Ohne Empfänger, bei sich selbst oder bei grauem Empfänger entfällt sie.
|
||||
function giveSteps(op, owner, group) {
|
||||
const r = byId[op.to]
|
||||
if (!r || r === owner || r.grey) return []
|
||||
const fromF = owner.home.floor
|
||||
const toF = r.home.floor
|
||||
const steps = [
|
||||
timed(PICKUP_S, (a) => {
|
||||
a.pose = 'stand'
|
||||
a.carrying = true
|
||||
a.stackN = 1
|
||||
}),
|
||||
]
|
||||
if (fromF !== toF) steps.push(rideStep(group, fromF, toF))
|
||||
steps.push(
|
||||
besideStep(r),
|
||||
timed(THANKS_S, (a) => {
|
||||
a.carrying = false
|
||||
a.stackN = 0
|
||||
if (!r.grey) bubble(r, 'Danke!')
|
||||
}),
|
||||
)
|
||||
if (fromF !== toF) steps.push(rideStep(group, toF, fromF))
|
||||
steps.push(homeStep())
|
||||
return steps
|
||||
}
|
||||
|
||||
// Effekt starten (ohne zu warten); fehlt Start oder Ziel im Layout, entfällt er
|
||||
function fxStep(op) {
|
||||
return instant(() => {
|
||||
const from = placeOf(op.at)
|
||||
const hasTo = Object.hasOwn(op, 'to')
|
||||
const to = hasTo ? placeOf(op.to) : op.fx === 'drawer' ? layout.spots.archiv : null
|
||||
if (!from || ((hasTo || op.fx === 'drawer') && !to)) return
|
||||
spawnFx(fx, op.fx, {
|
||||
from,
|
||||
to: to || undefined,
|
||||
delay: op.delay,
|
||||
onEnd: op.fx === 'coin' ? () => { look.vaultFlash = VAULT_FLASH_S } : undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Sprechblase zeigen und kurz warten (nur, wenn sie wirklich erscheint)
|
||||
function sayStep(op) {
|
||||
let hold = -1
|
||||
let t = 0
|
||||
return (a, dt) => {
|
||||
if (hold < 0) {
|
||||
const who = byId[op.who]
|
||||
hold = who && !who.grey && bubble(who, op.text) ? SAY_S : 0
|
||||
}
|
||||
t += dt
|
||||
return t >= hold
|
||||
}
|
||||
}
|
||||
|
||||
// Schritte eines Ops; Ziel der Aufgabe, Pose, Blase und des Jubels ist op.who (nicht unbedingt der Besitzer der Gruppe)
|
||||
function stepsOf(op, owner, group) {
|
||||
const who = byId[op.who]
|
||||
switch (op.op) {
|
||||
case 'task':
|
||||
return [instant(() => {
|
||||
if (!who || who.grey) return
|
||||
if (who !== owner && busy(who)) lagging.add(who.id)
|
||||
else who.task = op.text
|
||||
})]
|
||||
case 'pose':
|
||||
return [instant(() => {
|
||||
if (!who || who.grey || (who !== owner && busy(who))) return
|
||||
who.pose = op.pose
|
||||
who.idleT = 0
|
||||
})]
|
||||
case 'cheer':
|
||||
return [instant(() => {
|
||||
if (who && !who.grey) who.cheer = CHEER_S
|
||||
})]
|
||||
case 'say':
|
||||
return [sayStep(op)]
|
||||
case 'fx':
|
||||
return [fxStep(op)]
|
||||
case 'give':
|
||||
return giveSteps(op, owner, group)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppe an die Warteschlange des Besitzers hängen; zu Beginn weckt sie ihn (wer schläft, spricht und läuft nicht im Schlaf)
|
||||
function queueGroup(owner, ops) {
|
||||
const group = { steps: [], dead: false }
|
||||
group.steps.push((a) => {
|
||||
const pose = poseFor(a)
|
||||
if (a.grey) group.dead = true
|
||||
else a.pose = pose === 'sleep' ? 'stand' : pose
|
||||
return true
|
||||
})
|
||||
for (const op of ops) group.steps.push(...stepsOf(op, owner, group).map((fn) => guarded(group, fn)))
|
||||
owner.q.push(group)
|
||||
owner.groups += 1
|
||||
}
|
||||
|
||||
// Stille Gruppe: nur Aufgabe und Pose, sofort. Bei einem Worker mit laufender Gruppe holt die Tabelle es am Gruppenende nach.
|
||||
function runQuiet(ops) {
|
||||
for (const op of ops) {
|
||||
if (op.op !== 'task' && op.op !== 'pose') continue
|
||||
const who = byId[op.who]
|
||||
if (!who || who.grey) continue
|
||||
if (busy(who)) lagging.add(who.id)
|
||||
else if (op.op === 'task') who.task = op.text
|
||||
else {
|
||||
who.pose = op.pose
|
||||
who.idleT = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Besitzer der Gruppe: Absender des ersten give, sonst die workerId (wenn es den Worker gibt), sonst Otto
|
||||
function ownerOf(e, ops) {
|
||||
const give = ops.find((s) => s.op === 'give')
|
||||
for (const id of [give && give.from, e.workerId, 'otto']) {
|
||||
if (typeof id === 'string' && Object.hasOwn(byId, id)) return byId[id]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function pushEvents(events) {
|
||||
if (first || !Array.isArray(events) || events.length === 0) return
|
||||
workers = reduceEvents(workers, events)
|
||||
for (const a of agents) adoptStatus(a, entryOf(a))
|
||||
syncRooms()
|
||||
const groups = []
|
||||
for (const e of events) {
|
||||
const ops = eventToSteps(e)
|
||||
const owner = ops.length > 0 ? ownerOf(e, ops) : null
|
||||
if (owner) groups.push({ owner, ops })
|
||||
}
|
||||
// Animiert werden die neuesten BACKLOG_KEEP Gruppen; nimmt ein Besitzer nicht alle auf, behält er die neuesten
|
||||
const animated = new Set()
|
||||
const claimed = new Map()
|
||||
for (let i = groups.length - 1; i >= 0 && i >= groups.length - BACKLOG_KEEP; i--) {
|
||||
const { owner } = groups[i]
|
||||
const held = owner.groups + (claimed.get(owner) || 0)
|
||||
if (owner.grey || held >= GROUPS_PER_AGENT) continue
|
||||
claimed.set(owner, (claimed.get(owner) || 0) + 1)
|
||||
animated.add(groups[i])
|
||||
}
|
||||
for (const g of groups) if (!animated.has(g)) runQuiet(g.ops)
|
||||
for (const g of groups) if (animated.has(g)) queueGroup(g.owner, g.ops)
|
||||
}
|
||||
|
||||
// ---------- Ablauf je Bild ----------
|
||||
|
||||
// Aufgabe und Pose aus der Tabelle nachholen (nach dem Ende der letzten Gruppe)
|
||||
function catchUp(a) {
|
||||
adopt(a, entryOf(a))
|
||||
}
|
||||
|
||||
// Nächsten Schritt der laufenden Gruppe holen; ist sie zu Ende, zählt sie ab und die nächste beginnt
|
||||
function nextStep(a) {
|
||||
a.stepT = 0
|
||||
a.cur = null
|
||||
while (a.q.length > 0) {
|
||||
const group = a.q[0]
|
||||
if (group.steps.length > 0) {
|
||||
a.cur = group.steps.shift()
|
||||
return
|
||||
}
|
||||
a.q.shift()
|
||||
a.groups = Math.max(0, a.groups - 1)
|
||||
if (a.groups === 0 && lagging.has(a.id)) catchUp(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Wachhund: der Schritt dauert zu lange, Aufzug freigeben, Türzähler zurücknehmen, Agent heim, Gruppe beenden
|
||||
function watchdog(a) {
|
||||
lift.cancel(a)
|
||||
if (a.q.length > 0) abort(a, a.q[0])
|
||||
else uncount(a)
|
||||
nextStep(a)
|
||||
}
|
||||
|
||||
function runSteps(a, dt) {
|
||||
if (!a.cur) nextStep(a)
|
||||
for (let n = 0; a.cur && n < MAX_STEPS_PER_FRAME; n++) {
|
||||
if (a.cur(a, dt)) {
|
||||
nextStep(a)
|
||||
continue
|
||||
}
|
||||
a.stepT += dt
|
||||
if (a.stepT > STEP_TIMEOUT_S) watchdog(a)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function stepAgent(a, dt) {
|
||||
runSteps(a, dt)
|
||||
a.idleT = a.status === 'leerlauf' ? a.idleT + dt : 0
|
||||
if (!busy(a)) a.pose = poseFor(a)
|
||||
a.lane += clamp(laneTarget(a) - a.lane, -dt * LANE_SPEED, dt * LANE_SPEED)
|
||||
@@ -196,8 +577,8 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (look.paused || !Number.isFinite(dt) || dt <= 0) return
|
||||
// Ein Bild weiterschalten, ohne auf Pause oder ungültiges dt zu achten (update und flush)
|
||||
function advance(dt) {
|
||||
clock += dt
|
||||
for (const a of agents) stepAgent(a, dt)
|
||||
lift.update(dt)
|
||||
@@ -211,12 +592,45 @@ export function createDirector({ layout, lift, fx, look, roles = ROLES, rnd = Ma
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (look.paused || !Number.isFinite(dt) || dt <= 0) return
|
||||
advance(dt)
|
||||
}
|
||||
|
||||
// Sofort ruhig: Warteschlangen leer, Aufzug und Effekte zurückgesetzt, alle Agenten heim, Blasen weg
|
||||
function reset() {
|
||||
for (const a of agents) {
|
||||
a.q.length = 0
|
||||
a.cur = null
|
||||
a.stepT = 0
|
||||
a.groups = 0
|
||||
a.say = null
|
||||
a.bubT = 0
|
||||
a.cheer = 0
|
||||
placeHome(a)
|
||||
if (lagging.has(a.id)) catchUp(a)
|
||||
a.pose = poseFor(a)
|
||||
}
|
||||
door.fill(0)
|
||||
counted.clear()
|
||||
lift.reset()
|
||||
fx.clear()
|
||||
}
|
||||
|
||||
// Alles schnell zu Ende spielen (höchstens 2000 Bilder à 0,25 s), danach zurücksetzen
|
||||
function flush() {
|
||||
for (let i = 0; i < 2000 && pending() > 0; i++) advance(0.25)
|
||||
reset()
|
||||
}
|
||||
|
||||
// Trefferflächen der zuletzt gezeichneten Figuren in Zeichenreihenfolge (die letzte liegt oben), für pickAgent
|
||||
function boxes() {
|
||||
return byDepth(agents).filter((a) => a.box).map((a) => ({ id: a.id, box: a.box }))
|
||||
}
|
||||
|
||||
return { agents, byId, applyState, update, pending: () => 0, boxes, bubble }
|
||||
const debug = () => ({ door: door.slice(), groups: pending() })
|
||||
|
||||
return { agents, byId, applyState, pushEvents, update, flush, reset, pending, debug, boxes, bubble }
|
||||
}
|
||||
|
||||
// ---------- Zeichnen ----------
|
||||
|
||||
Reference in new issue
Block a user