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 ----------
|
||||
|
||||
@@ -2,12 +2,12 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
import { makeCtx } from './helpers/fakeCtx.mjs'
|
||||
import { IDS, makeState } from './helpers/workersFixtures.mjs'
|
||||
import { IDS, makeState, ev } from './helpers/workersFixtures.mjs'
|
||||
|
||||
// Regie 1: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Zeichnen (Quelle: entwuerfe/scene-turm.js)
|
||||
// Regie 1: Agenten, Snapshot ohne Animation, Posen, Leerlauf, Zeichnen; Regie 2: Schritte, Aufzugfahrten, Rückstau, Wachhund (Quelle: entwuerfe/scene-turm.js)
|
||||
const D = await loadModule('src/components/workers/engine/director.js')
|
||||
const { buildLayout, homeOf } = await loadModule('src/components/workers/engine/layout.js')
|
||||
const { createLift } = await loadModule('src/components/workers/engine/lift.js')
|
||||
const { createLift, LIFT } = await loadModule('src/components/workers/engine/lift.js')
|
||||
const { createFx } = await loadModule('src/components/workers/engine/fx.js')
|
||||
const { createLook } = await loadModule('src/components/workers/engine/rooms.js')
|
||||
const { pickAgent } = await loadModule('src/components/workers/engine/camera.js')
|
||||
@@ -231,3 +231,410 @@ test('drawAgents: die Blase weicht dem „!“ und dem Auswahlpfeil aus', () =>
|
||||
assert.ok(textY(m, 'Rita') - textY(m, 'Kim') >= 27) // das „!“ ist 9 Einheiten hoch (Raster 3)
|
||||
assert.equal(textY(m, 'Rita') - textY(draw(d, 'rita'), 'Rita'), 18) // der Pfeil nimmt 6 Einheiten
|
||||
})
|
||||
|
||||
// ---------- Regie 2: Schritte, Aufzugfahrten, Rückstau, Wachhund (P2-T13) ----------
|
||||
|
||||
// Simulationszeit in Sekunden, bis keine Gruppe mehr läuft (höchstens max)
|
||||
const run = (d, max = 60) => {
|
||||
let t = 0
|
||||
while (d.pending() > 0 && t < max) {
|
||||
d.update(0.05)
|
||||
t += 0.05
|
||||
}
|
||||
return t
|
||||
}
|
||||
const at = (layout, id) => homeOf(layout, id)
|
||||
|
||||
test('Übergabe über zwei Etagen: Rita bringt Bea ein Dokument, fährt Aufzug, steht danach wieder zuhause', () => {
|
||||
const { d, lift, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt 2 Funde zu Bea' })])
|
||||
assert.ok(d.pending() >= 1)
|
||||
let ride = false
|
||||
let t = 0
|
||||
while (d.pending() > 0 && t < 60) {
|
||||
d.update(0.05)
|
||||
t += 0.05
|
||||
if (lift.state !== 'idle') ride = true
|
||||
}
|
||||
assert.ok(ride)
|
||||
assert.ok(t < 40, String(t))
|
||||
const h = at(layout, 'rita')
|
||||
assert.deepEqual([Math.round(d.byId.rita.x), Math.round(d.byId.rita.y), d.byId.rita.carrying, lift.state], [h.x, h.y, false, 'idle'])
|
||||
assert.equal(d.byId.rita.task, 'Rita bringt 2 Funde zu Bea')
|
||||
})
|
||||
|
||||
test('gleiche Etage: kein Aufzug', () => {
|
||||
const { d, lift } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' })])
|
||||
let ride = false
|
||||
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
|
||||
d.update(0.05)
|
||||
if (lift.state !== 'idle') ride = true
|
||||
}
|
||||
assert.equal(ride, false)
|
||||
assert.equal(d.pending(), 0)
|
||||
})
|
||||
|
||||
test('Rückstau: 300 Ereignisse ⇒ höchstens 6 Gruppen, Rest still, nach ≤ 60 s ruhig, letzte Aufgaben stehen (Review Focus 2)', () => {
|
||||
const { d, lift } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents(Array.from({ length: 300 }, (_, i) => ev('uebergabe', IDS[i % 19], { to: IDS[(i + 3) % 19], text: `Aufgabe ${i}` })))
|
||||
assert.ok(d.pending() <= 6, String(d.pending()))
|
||||
const t = run(d)
|
||||
assert.ok(t < 60, String(t))
|
||||
assert.equal(d.pending(), 0)
|
||||
assert.equal(lift.state, 'idle')
|
||||
for (let k = 0; k < 19; k++) {
|
||||
const last = 299 - ((299 - k) % 19)
|
||||
assert.equal(d.byId[IDS[k]].task, `Aufgabe ${last}`, IDS[k])
|
||||
}
|
||||
d.pushEvents(Array.from({ length: 10 }, (_, i) => ev('fund', 'kim', { text: `K${i}` })))
|
||||
assert.ok(d.byId.kim.groups <= 4)
|
||||
})
|
||||
|
||||
test('graue, fremde und unbekannte Ereignisse: keine Arbeit, kein Wurf (Review Focus 4)', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState({ connected: { kim: false } }))
|
||||
d.pushEvents([ev('gelesen', 'kim'), ev('fund', 'zed'), ev('neueArt', 'rita'), ev('uebergabe', 'kim', { to: 'bea' })])
|
||||
assert.equal(d.byId.kim.say, null)
|
||||
assert.equal(d.byId.kim.pose, 'sleep')
|
||||
assert.equal(d.byId.kim.groups, 0)
|
||||
})
|
||||
|
||||
test('Blasen: höchstens drei gleichzeitig', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents(['kim', 'rita', 'pia', 'finn', 'ali'].map((w) => ev('fund', w)))
|
||||
d.update(0.05)
|
||||
assert.ok(d.agents.filter((a) => a.say).length <= 3)
|
||||
})
|
||||
|
||||
test('Pause friert ein, Fortsetzen läuft weiter', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState({ paused: true }))
|
||||
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' })])
|
||||
const x0 = d.byId.kim.x
|
||||
for (let i = 0; i < 400; i++) d.update(0.05)
|
||||
assert.equal(d.byId.kim.x, x0)
|
||||
d.applyState(makeState({ paused: false }))
|
||||
for (let i = 0; i < 40; i++) d.update(0.05)
|
||||
assert.notEqual(d.byId.kim.x, x0)
|
||||
})
|
||||
|
||||
test('Aufzug zurückgesetzt mitten in der Fahrt: Fahrgast erholt sich, Türzähler 0; hängender Aufzug: Wachhund nach 45 s', () => {
|
||||
const a = setup()
|
||||
a.d.applyState(makeState())
|
||||
a.d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' })])
|
||||
let g = 0
|
||||
while (a.lift.state !== 'riding' && g++ < 400) a.d.update(0.05)
|
||||
assert.equal(a.lift.state, 'riding')
|
||||
a.lift.reset()
|
||||
assert.ok(run(a.d) < 10)
|
||||
assert.deepEqual(a.d.debug().door, new Array(10).fill(0))
|
||||
const layout = buildLayout()
|
||||
const fx = createFx()
|
||||
const look = createLook()
|
||||
const stuck = { y: 0, state: 'idle', request: (ag, f, t) => ({ a: ag, from: f, to: t, st: 'queued' }), update() {}, cancel() {}, reset() {}, at: () => false, drawBack() {}, drawFront() {} }
|
||||
const d = createDirector({ layout, lift: stuck, fx, look })
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' })])
|
||||
const t = run(d, 80)
|
||||
assert.ok(t >= 45 && t < 60, String(t))
|
||||
assert.deepEqual(d.debug().door, new Array(10).fill(0))
|
||||
})
|
||||
|
||||
test('flush: alles sofort ruhig, alle zuhause; Deal löst Münzen und Jubel aus', () => {
|
||||
const { d, lift, fx, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents(Array.from({ length: 20 }, (_, i) => ev('uebergabe', IDS[i % 19], { to: IDS[(i + 5) % 19] })))
|
||||
d.flush()
|
||||
assert.equal(d.pending(), 0)
|
||||
assert.equal(lift.state, 'idle')
|
||||
assert.equal(fx.size(), 0)
|
||||
for (const a of d.agents) {
|
||||
const h = at(layout, a.id)
|
||||
assert.deepEqual([Math.round(a.x), Math.round(a.y)], [h.x, h.y], a.id)
|
||||
}
|
||||
d.pushEvents([ev('deal', 'system')])
|
||||
d.update(0.1)
|
||||
assert.ok(fx.size() >= 3)
|
||||
assert.ok(d.byId.otto.cheer > 0)
|
||||
run(d)
|
||||
for (let i = 0; i < 100; i++) d.update(0.1)
|
||||
assert.equal(fx.size(), 0)
|
||||
})
|
||||
|
||||
// Aufzug-Attrappe: request liefert, was der Test vorgibt; alles Übrige tut nichts
|
||||
const stubLift = (request, extra = {}) => ({ y: 0, state: 'idle', request, update() {}, cancel() {}, reset() {}, at: () => false, drawBack() {}, drawFront() {}, ...extra })
|
||||
const withLift = (lift) => {
|
||||
const layout = buildLayout()
|
||||
const fx = createFx()
|
||||
const look = createLook()
|
||||
return { layout, fx, look, d: createDirector({ layout, lift, fx, look, rnd: () => 0.5 }) }
|
||||
}
|
||||
// Mittelpunkt aller gezeichneten Rechtecke in Welteinheiten (Raster 3)
|
||||
const centerOf = (rects) => ({
|
||||
x: rects.reduce((s, r) => s + r.x + r.w / 2, 0) / rects.length / 3,
|
||||
y: rects.reduce((s, r) => s + r.y + r.h / 2, 0) / rects.length / 3,
|
||||
})
|
||||
const dist = (p, q) => Math.hypot(p.x - q.x, p.y - q.y)
|
||||
|
||||
test('Aufzug antwortet cancelled: Fahrt entfällt, der Agent steht sofort wieder zuhause, Türzähler 0', () => {
|
||||
const calls = []
|
||||
const lift = stubLift((a, from, to) => {
|
||||
calls.push([a.id, from, to])
|
||||
return { a, from, to, st: 'cancelled' }
|
||||
})
|
||||
const { d, layout } = withLift(lift)
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt Bea Funde' })])
|
||||
assert.ok(run(d) < 5)
|
||||
assert.deepEqual(calls, [['rita', 1, 3]])
|
||||
const h = at(layout, 'rita')
|
||||
assert.deepEqual([d.byId.rita.x, d.byId.rita.y, d.byId.rita.floor, d.byId.rita.carrying, d.byId.rita.frame], [h.x, h.y, h.floor, false, 0])
|
||||
assert.deepEqual(d.debug().door, new Array(10).fill(0))
|
||||
assert.deepEqual([d.byId.rita.task, d.byId.bea.say, d.pending()], ['Rita bringt Bea Funde', null, 0])
|
||||
})
|
||||
|
||||
test('Wachhund: ruft lift.cancel für den Agenten, stellt ihn heim und beendet die Gruppe', () => {
|
||||
const cancelled = []
|
||||
const lift = stubLift((a, from, to) => ({ a, from, to, st: 'queued' }), { cancel: (a) => cancelled.push(a.id) })
|
||||
const { d, layout } = withLift(lift)
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('fund', 'rita')])
|
||||
assert.equal(d.byId.rita.groups, 2)
|
||||
const t = run(d, 200)
|
||||
assert.deepEqual(cancelled, ['rita'])
|
||||
assert.ok(t >= 45 && t < 60, String(t))
|
||||
const h = at(layout, 'rita')
|
||||
assert.deepEqual([d.byId.rita.x, d.byId.rita.y, d.byId.rita.carrying, d.byId.rita.groups, d.byId.rita.cur], [h.x, h.y, false, 0, null])
|
||||
})
|
||||
|
||||
test('Wartende vor dem Aufzug stehen nebeneinander (Türzähler zählt mit) und werden beim Einsteigen ausgetragen', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('uebergabe', 'kim', { to: 'max' })])
|
||||
let most = 0
|
||||
const slots = { rita: false, kim: false }
|
||||
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
|
||||
d.update(0.05)
|
||||
most = Math.max(most, d.debug().door[1])
|
||||
if (d.byId.rita.x === LIFT.slot0) slots.rita = true
|
||||
if (d.byId.kim.x === LIFT.slot0 + LIFT.slotStep) slots.kim = true
|
||||
}
|
||||
assert.equal(most, 2)
|
||||
assert.deepEqual(slots, { rita: true, kim: true })
|
||||
assert.deepEqual(d.debug().door, new Array(10).fill(0))
|
||||
})
|
||||
|
||||
test('Snapshot während einer Gruppe: Aufgabe und Pose kommen erst am Gruppenende aus der Tabelle', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia', text: 'Kim bringt Pia Funde' })])
|
||||
for (let i = 0; i < 20; i++) d.update(0.05)
|
||||
const st = makeState({ statuses: { kim: 'leerlauf' } })
|
||||
st.workers.find((w) => w.workerId === 'kim').task = 'Snapshot-Aufgabe'
|
||||
d.applyState(st)
|
||||
assert.deepEqual([d.byId.kim.status, d.byId.kim.task], ['leerlauf', 'Kim bringt Pia Funde'])
|
||||
run(d)
|
||||
assert.deepEqual([d.byId.kim.task, d.byId.kim.pose, d.byId.kim.groups], ['Snapshot-Aufgabe', 'coffee', 0])
|
||||
})
|
||||
|
||||
test('stille Gruppe für einen Worker mit laufender Gruppe: seine Aufgabe kommt am Gruppenende aus der Tabelle', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia', text: 'Kim bringt Pia Funde' })])
|
||||
d.update(0.05)
|
||||
d.pushEvents([ev('gelesen', 'kim', { text: 'Liest Mails' }), ...Array.from({ length: 6 }, (_, i) => ev('fund', 'rita', { text: `R${i}` }))])
|
||||
assert.equal(d.byId.kim.task, 'Kim bringt Pia Funde')
|
||||
run(d)
|
||||
assert.equal(d.byId.kim.task, 'Liest Mails')
|
||||
})
|
||||
|
||||
test('pro Worker höchstens 4 wartende Gruppen: die neuesten bleiben animiert, die Aufgabe des neuesten gewinnt', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents(Array.from({ length: 10 }, (_, i) => ev('fund', 'kim', { text: `K${i}` })))
|
||||
assert.equal(d.byId.kim.groups, 4)
|
||||
assert.equal(d.byId.kim.task, 'K5')
|
||||
d.pushEvents([ev('fund', 'kim', { text: 'K10' })])
|
||||
assert.equal(d.byId.kim.groups, 4)
|
||||
run(d)
|
||||
assert.equal(d.byId.kim.task, 'K10')
|
||||
})
|
||||
|
||||
test('pushEvents vor dem ersten Snapshot und mit kaputten Eingaben: nichts passiert, kein Wurf', () => {
|
||||
const { d, look } = setup()
|
||||
d.pushEvents([ev('fund', 'kim')])
|
||||
assert.deepEqual([d.pending(), d.agents.some((a) => a.grey), look.greyRooms.size, d.byId.kim.status], [0, false, 0, 'wartet'])
|
||||
d.applyState(makeState())
|
||||
for (const bad of [null, undefined, 'x', 5, {}, [], [null, 5, 'x', {}, { kind: 'fund' }]]) d.pushEvents(bad)
|
||||
assert.equal(d.pending(), 0)
|
||||
})
|
||||
|
||||
test('Ereignisse ändern den Status sofort (Fehler-Zeichen, Leerlauf, Räume), die Aufgabe setzt erst der Schritt', () => {
|
||||
const { d, look } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('fehler', 'rita', { text: 'Suche kaputt' }), ev('leerlauf', 'kim', { text: 'Pause' })])
|
||||
assert.deepEqual([d.byId.rita.status, d.byId.rita.alert, d.byId.kim.status], ['fehler', true, 'leerlauf'])
|
||||
assert.deepEqual([look.busyRooms.has('netz'), look.busyRooms.has('kleinanzeigen'), look.busyRooms.has('portale')], [false, false, true])
|
||||
assert.equal(d.byId.rita.task, '')
|
||||
d.update(0.05)
|
||||
assert.deepEqual([d.byId.rita.task, d.byId.kim.task, d.byId.kim.pose], ['Suche kaputt', 'Pause', 'coffee'])
|
||||
})
|
||||
|
||||
test('wer schläft, wird für seine Gruppe wach; danach gilt wieder der Status', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState({ statuses: { kim: 'leerlauf' } }))
|
||||
for (let i = 0; i < 300; i++) d.update(0.1)
|
||||
assert.equal(d.byId.kim.pose, 'sleep')
|
||||
d.pushEvents([ev('doppelt', 'kim')])
|
||||
d.update(0.05)
|
||||
assert.deepEqual([d.byId.kim.pose, d.byId.kim.say], ['type', 'Schon bekannt'])
|
||||
run(d)
|
||||
assert.equal(d.byId.kim.pose, 'type')
|
||||
})
|
||||
|
||||
test('Übergabe an einen nicht angeschlossenen Worker entfällt: niemand läuft, nur die Aufgabe steht', () => {
|
||||
const { d, lift } = setup()
|
||||
d.applyState(makeState({ connected: { kim: false } }))
|
||||
const x0 = d.byId.pia.x
|
||||
d.pushEvents([ev('uebergabe', 'pia', { to: 'kim', text: 'Pia an Kim' })])
|
||||
for (let i = 0; i < 20; i++) d.update(0.05)
|
||||
assert.equal(d.pending(), 0)
|
||||
assert.deepEqual([d.byId.pia.x, d.byId.pia.task, d.byId.kim.say, d.byId.kim.pose, lift.state], [x0, 'Pia an Kim', null, 'sleep', 'idle'])
|
||||
})
|
||||
|
||||
test('Übergabe auf einer Etage: Dokument in der Hand, Giver steht neben dem Empfänger (Abstand 12), blickt ihn an, Empfänger dankt', () => {
|
||||
const { d, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
const kim = d.byId.kim
|
||||
const pia = d.byId.pia
|
||||
const bea = d.byId.bea
|
||||
d.pushEvents([ev('uebergabe', 'kim', { to: 'pia' }), ev('uebergabe', 'pia', { to: 'kim' })])
|
||||
let carried = false
|
||||
const seen = {}
|
||||
for (let i = 0; i < 800 && d.pending() > 0; i++) {
|
||||
d.update(0.05)
|
||||
if (kim.carrying) carried = true
|
||||
if (!seen.kim && pia.say === 'Danke!') seen.kim = { x: kim.x, facing: kim.facing, carrying: kim.carrying }
|
||||
if (!seen.pia && kim.say === 'Danke!') seen.pia = { x: pia.x, facing: pia.facing, carrying: pia.carrying }
|
||||
}
|
||||
assert.equal(carried, true)
|
||||
assert.deepEqual(seen.kim, { x: at(layout, 'pia').x - 12, facing: 1, carrying: false })
|
||||
assert.deepEqual(seen.pia, { x: at(layout, 'kim').x + 12, facing: -1, carrying: false })
|
||||
assert.equal(bea.say, null)
|
||||
for (const a of [kim, pia]) assert.deepEqual([a.x, a.y, a.facing], [at(layout, a.id).x, at(layout, a.id).y, 1])
|
||||
})
|
||||
|
||||
test('Effekte: Deal wirft drei Münzen von außen zum Tresor (Versatz 0, 0,4 und 0,8 s), der Tresor blitzt beim Ankommen, Otto jubelt', () => {
|
||||
const calls = []
|
||||
const fx = { add: (life, draw, onEnd, delay) => calls.push({ life, draw, onEnd, delay }), update() {}, clear() {}, size: () => calls.length }
|
||||
const layout = buildLayout()
|
||||
const look = createLook()
|
||||
const d = createDirector({ layout, lift: createLift(layout), fx, look, rnd: () => 0.5 })
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('deal', 'system')])
|
||||
d.update(0.05)
|
||||
assert.deepEqual(calls.map((c) => c.delay), [0, 0.4, 0.8])
|
||||
assert.deepEqual([look.vaultFlash, d.byId.otto.say], [0, 'Deal! Super!'])
|
||||
calls[2].onEnd()
|
||||
assert.equal(look.vaultFlash, 0.7)
|
||||
const at0 = makeCtx()
|
||||
calls[0].draw(at0.ctx, 0, { t: 0, life: calls[0].life })
|
||||
const at1 = makeCtx()
|
||||
calls[0].draw(at1.ctx, 1, { t: calls[0].life, life: calls[0].life })
|
||||
assert.ok(dist(centerOf(at0.rects), layout.spots.aussen) < 4)
|
||||
assert.ok(dist(centerOf(at1.rects), layout.spots.tresor) < 4)
|
||||
})
|
||||
|
||||
test('Effekte: Ablegen ohne Ziel fliegt vom Kopf des Workers zum Archiv', () => {
|
||||
const calls = []
|
||||
const fx = { add: (life, draw, onEnd, delay) => calls.push({ life, draw, onEnd, delay }), update() {}, clear() {}, size: () => calls.length }
|
||||
const layout = buildLayout()
|
||||
const d = createDirector({ layout, lift: createLift(layout), fx, look: createLook(), rnd: () => 0.5 })
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('botlead', 'arne')])
|
||||
for (let i = 0; i < 60 && calls.length === 0; i++) d.update(0.05)
|
||||
assert.equal(calls.length, 1)
|
||||
const arne = d.byId.arne
|
||||
const start = makeCtx()
|
||||
calls[0].draw(start.ctx, 0, { t: 0, life: calls[0].life })
|
||||
const end = makeCtx()
|
||||
calls[0].draw(end.ctx, 1, { t: calls[0].life, life: calls[0].life })
|
||||
assert.ok(dist(centerOf(start.rects), { x: arne.x + 6, y: arne.y + arne.lane - 2 }) < 5)
|
||||
assert.ok(dist(centerOf(end.rects), layout.spots.archiv) < 6)
|
||||
})
|
||||
|
||||
test('flush läuft auch bei Pause durch und lässt alle zuhause', () => {
|
||||
const { d, layout, look } = setup()
|
||||
d.applyState(makeState({ paused: true }))
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea', text: 'Rita bringt Bea Funde' })])
|
||||
assert.equal(d.pending(), 1)
|
||||
d.flush()
|
||||
const h = at(layout, 'rita')
|
||||
assert.deepEqual([d.pending(), look.paused, d.byId.rita.x, d.byId.rita.y], [0, true, h.x, h.y])
|
||||
assert.equal(d.byId.rita.task, 'Rita bringt Bea Funde') // die Schritte sind durchgelaufen, nicht nur verworfen
|
||||
})
|
||||
|
||||
test('reset: Warteschlangen leer, Aufzug frei, Türzähler 0, Effekte und Blasen weg, alle zuhause', () => {
|
||||
const { d, lift, fx, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
d.pushEvents([ev('uebergabe', 'rita', { to: 'bea' }), ev('fund', 'kim')])
|
||||
let g = 0
|
||||
while (lift.state !== 'riding' && g++ < 400) d.update(0.05)
|
||||
assert.equal(lift.state, 'riding')
|
||||
fx.add(5, () => {})
|
||||
assert.ok(d.pending() > 0)
|
||||
d.reset()
|
||||
assert.deepEqual([d.pending(), lift.state, fx.size()], [0, 'idle', 0])
|
||||
assert.deepEqual(d.debug(), { door: new Array(10).fill(0), groups: 0 })
|
||||
for (const a of d.agents) {
|
||||
const h = at(layout, a.id)
|
||||
assert.deepEqual([a.x, a.y, a.carrying, a.say, a.q.length, a.cur], [h.x, h.y, false, null, 0, null], a.id)
|
||||
}
|
||||
const x = d.byId.rita.x
|
||||
for (let i = 0; i < 100; i++) d.update(0.05)
|
||||
assert.deepEqual([d.byId.rita.x, d.pending(), lift.state], [x, 0, 'idle'])
|
||||
})
|
||||
|
||||
test('graue Worker bekommen keine vorgespielte Arbeit: weder Aufgabe noch Pose noch Blase noch Gruppe', () => {
|
||||
const { d } = setup()
|
||||
d.applyState(makeState({ connected: { kim: false } }))
|
||||
d.pushEvents([ev('fund', 'kim', { text: 'Fund' }), ev('gelesen', 'kim', { text: 'Gelesen' }), ev('auftrag', 'otto', { to: 'kim', text: 'Kim, los' })])
|
||||
for (let i = 0; i < 100; i++) d.update(0.05)
|
||||
assert.deepEqual([d.byId.kim.task, d.byId.kim.pose, d.byId.kim.say, d.byId.kim.groups, d.byId.kim.status], ['', 'sleep', null, 0, 'grau'])
|
||||
assert.equal(d.byId.otto.task, 'Kim, los')
|
||||
})
|
||||
|
||||
test('Fahrgast steht beim Einsteigen schon in der Kabine, auch der hohe Otto, und kommt danach heim', () => {
|
||||
const { d, lift, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
const otto = d.byId.otto
|
||||
d.pushEvents([ev('uebergabe', 'otto', { to: 'rita' })])
|
||||
let boarded = null
|
||||
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
|
||||
d.update(0.05)
|
||||
if (!boarded && lift.state === 'closing') boarded = [otto.x, otto.y, otto.y === lift.y + 7]
|
||||
}
|
||||
assert.deepEqual(boarded, [LIFT.cabWx, boarded[1], true])
|
||||
const h = at(layout, 'otto')
|
||||
assert.deepEqual([otto.x, otto.y, otto.floor, lift.state, d.pending()], [h.x, h.y, h.floor, 'idle', 0])
|
||||
})
|
||||
|
||||
test('Übergabe vom Aufzug aus: der Giver blickt den Empfänger an, auch wenn er dafür zurücklaufen muss', () => {
|
||||
const { d, layout } = setup()
|
||||
d.applyState(makeState())
|
||||
const bea = d.byId.bea
|
||||
const rita = d.byId.rita
|
||||
d.pushEvents([ev('uebergabe', 'bea', { to: 'rita' })])
|
||||
let seen = null
|
||||
for (let i = 0; i < 1200 && d.pending() > 0; i++) {
|
||||
d.update(0.05)
|
||||
if (!seen && rita.say === 'Danke!') seen = [bea.x, bea.facing]
|
||||
}
|
||||
assert.deepEqual(seen, [at(layout, 'rita').x - 12, 1])
|
||||
assert.ok(seen[0] < LIFT.exitX) // der Weg führt vom Ausgang des Schachts nach links zurück
|
||||
})
|
||||
Reference in new issue
Block a user