Denis Cercasin

Design decisions

Table of contents

This page records the significant decisions of Room of Horror 2.0 — what we chose, what we considered, and why. Decisions inherited from 1.0 that we consciously kept are marked as such.

D1 — Extend the existing codebase instead of rebuilding

Decision: Build 2.0 on top of the inherited 1.0 application (Unity scene, Express API, React frontend) rather than starting fresh.

Why: The 1.0 system was functional and battle-tested with the Meta Quest 3; the semester timeline made a rewrite unrealistic; the client’s priority was new capability (diversity, evaluation) — not new plumbing. Trade-off: we inherited conventions we would not have chosen (query-parameter APIs, promise-chain style, no tests) and spent significant time on code analysis first (see Lessons Learned: “existing code brings its own challenges” — „Bestehender Code bringt besondere Herausforderungen mit sich”).

D2 — Migration from SQLite to PostgreSQL

Decision: Replace SQLite with PostgreSQL, accessed through a single pg pool; UUID columns use the native UUID type, flexible structures use JSONB.

Why: SQLite’s single-writer model does not fit a server receiving concurrent requests from headsets and browsers; PostgreSQL adds native UUIDs, JSONB queries/validation, and real foreign keys with ON DELETE CASCADE. The migration also unlocked the normalized game-detail tables (game_error_results etc.) used by the evaluation reports. Configuration follows the standard DATABASE_URL / PG* variables, so any hosting environment works. (Agreed with the stakeholders in week 4; completed in week 6.)

D3 — Idempotent startup migrations instead of a migration tool

Decision: createTables() runs at every API start: CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, guarded DO $$ blocks, seed upserts.

Why: For a small team without a shared migration workflow, “the running code defines the schema” removes an entire class of onboarding problems — a new developer needs only an empty database. Trade-off: no rollback and no schema history; acceptable at this project size (documented as an improvement for later).

D4 — Patient profiles as data, not code

Decision: Patient profiles live in the patients table (demographics, allowed error IDs, Unity model key, accent color), seeded at startup and served via GET /patient/get-patients. The frontend renders whatever the API returns; Unity switches models by the unity_model_id string.

Why: Diversity was the project’s core requirement — profiles had to be extensible by future teams without touching three codebases. A new patient needs: one seed entry, one Unity scene object, one avatar image. The backend validates selected errors against the patient’s allowed_error_ids, so the coupling “patient ↔ plausible errors” is enforced server-side, not by UI convention.

D5 — Central weighted error catalog + rule-based evaluation engine

Decision: All 32 errors are defined once in evaluation/ErrorCatalog.ts with category, severity weight (1–5), critical flag, and feedback hints; mirrored into error_definitions. A deterministic EvaluationEngine computes a weighted score, performance level, pass/fail, and generated feedback; results are versioned (rule-based-v1) in game_evaluations.

Why: The stakeholders required error prioritization (“higher-priority errors first”, week 6) and an evaluation the trainee can learn from. A rule-based engine (instead of the originally discussed AI evaluation) is explainable, testable, and free of external dependencies — important for an exam-adjacent context. Versioning keeps historical results comparable if the rules change. The AI variant was deliberately de-scoped (see Requirements Catalog) and documented as a feasibility study for future teams.

D6 — WebSockets for the frontend, polling kept for Unity

Decision: Add a Socket.IO layer (rooms per game, typed events) consumed by the frontend; Unity keeps its 1-second HTTP polling.

Why: The instructor dashboard needs instant feedback (error found, mistake made) — polling from the browser was wasteful and laggy. On the Unity side, polling was already reliable on the Quest, and swapping it for a socket client would have risked the stable VR build near the deadline (“stability before feature count” — „Stabilität vor Funktionsumfang”). The event contract already defines a vr client type so future teams can complete the migration.

D7 — Zod validation at the API boundary

Decision: Every endpoint validates query parameters and headers against a Zod schema before the handler runs (schemas/requests.ts + validate middleware).

Why: The inherited handlers mixed validation with business logic; malformed input could reach the database layer. Central schemas give uniform 400 responses, self-documenting request shapes, and one place to tighten rules (e.g. get-game’s “token or deviceUUID” rule as a superRefine). (Introduced together with UUIDs in week 4.)

D8 — Passwordless e-mail authentication (kept from 1.0)

Decision: Keep the inherited passwordless flow: e-mailed auth links, short-lived auth tokens exchanged for long-lived access tokens, one-time session-ID handover, token blacklist, recovery by auth_uuid rotation.

Why: No passwords to store or leak; instructors log in rarely, so link-based login is acceptable friction. DEV_MODE=true returns tokens in the response, which keeps local development mail-server-free. Trade-off: SMTP is a hard production dependency and long-lived access tokens increase the value of a stolen token (mitigated by blacklist and rotation).

D9 — Patient file as an interactive VR object

Decision: Implement the digital patient record as a clickable clipboard inside the VR room (PatientFileUI), with paging, per-patient content, and decision popups (translator, missing address, allergy conflict) — rather than showing patient data only in the web frontend.

Why: Reading the file is part of the nursing workflow being trained; several errors (26, 27, 28, 30) only make sense as documentation errors discovered in the record. Decision popups with exactly one professionally correct answer turn soft skills (communication, organization) into scoreable game events. (Requested by the stakeholders in week 6: file directly in VR, clickable, individual per patient.)

D10 — Inactive errors rendered in corrected state

Decision: Error objects that are not part of the current round are not simply hidden — correction components (MoveErrorContainer, VisualStateCorrection, TransformStateCorrection) display them in their correct state.

Why: If unselected error objects vanished, players could deduce active errors from missing furniture (“the urine bottle is gone, so it’s not a fault this time”). Rendering the corrected state keeps the room complete and forces genuine inspection.

D11 — Desktop fallback mode

Decision: Ship mouse/keyboard controls (DesktopMovement, DesktopClickInteractor, …) alongside VR input.

Why: Development, debugging, demos, and CI-less manual testing must not require a headset per developer; presentations need a projector-friendly mode. It also lowers the barrier for future contributors without VR hardware.

D12 — Accessibility: color-vision modes in the frontend

Decision: A global color-vision control (protanopia, deuteranopia, tritanopia, achromatopsia, standard — shown with German UI labels) persisted in web storage and applied as a data-color-vision attribute that CSS themes react to.

Why: Accessibility was a stakeholder requirement; a CSS-variable approach covers the whole UI at once instead of per-component fixes, and the choice survives reloads. VR-side measures (motion-sickness reduction) were addressed through teleport locomotion — the standard XR comfort technique — rather than smooth locomotion.

D13 — Documentation as GitHub Pages (Jekyll + just-the-docs)

Decision: All project documentation lives in docs/ as Markdown, published with Jekyll’s just-the-docs theme, with Mermaid for diagrams.

Why: Versioned together with the code, reviewable in PRs, zero hosting cost, and searchable navigation out of the box. Mermaid keeps diagrams editable as text instead of binary exports.


Information sources

  • Repository code as referenced per decision (API evaluation/*, schemas/*, websocket/*, database/General.ts; Unity Assets/Scripts/*; frontend src/components/ColorVisionControl.tsx)
  • Weekly Reports (stakeholder agreements in weeks 4 and 6)
  • Requirements Catalog (scope decisions)
  • External references: see Design Decisions Sources

Missing information

  • No formal ADR (Architecture Decision Record) log was kept during development; decision dates beyond the weekly reports are approximate.

Last build: 19 Aug 2026, 05:20+00:00


This site uses Just the Docs, a documentation theme for Jekyll.