File size: 4,763 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Render game data to themed, animated HTML.

The suspect's hidden ``internal`` state is never rendered; only ``spoken`` text and a
few derived in-world cues reach the player. The stage and notebook are deterministic
CSS-animated graphics - no model runs to draw them.
"""

from __future__ import annotations

import html

from ..engine.game_state import GameState
from ..projections.player_view import PlayerCaseView
from ..schemas.accusation import Verdict
from ..schemas.case import CaseFile


def _esc(text: str) -> str:
    return html.escape(text or "")


def how_to_play_html() -> str:
    steps = [
        ("1", "INTERROGATE", "Pick a suspect and ask anything. They remember, and they lie."),
        ("2", "INVESTIGATE", "Search the rooms to collect physical evidence."),
        ("3", "CONFRONT", "Present a clue that contradicts a suspect - watch their alibi crack."),
        ("4", "ACCUSE", "Name the killer and cite your proof. One of them did it."),
    ]
    cards = "".join(
        f"<div class='cz-step'><span class='cz-step-n'>{n}</span>"
        f"<b>{title}</b><div class='cz-muted'>{body}</div></div>"
        for n, title, body in steps
    )
    return (
        "<div class='cz-howto'><div class='cz-howto-h'>HOW TO PLAY</div>"
        f"<div class='cz-steps'>{cards}</div>"
        "<div class='cz-howto-cta'>Press <b>NEW CASE</b> to open a fresh file, "
        "then question your suspects.</div></div>"
    )


def briefing_html(view: PlayerCaseView) -> str:
    return (
        f"<div class='cz-briefing'><b>{_esc(view.title)}</b><br><br>{_esc(view.briefing)}"
        f"<br><br><i>Victim:</i> {_esc(view.victim.name)} ({_esc(view.victim.role)}) - "
        f"{_esc(view.victim.cause_of_death)}.</div>"
    )


def stage_html(scene_uri: str, sprite_uri: str, name: str, room_name: str = "") -> str:
    room = f"<div class='cz-roomlabel'>{_esc(room_name)}</div>" if room_name else ""
    return (
        "<div class='cz-stage'>"
        f"<div class='cz-room' style=\"background-image:url('{scene_uri}')\"></div>"
        f"<div class='cz-nameplate'>{_esc(name)}</div>{room}"
        f"<div class='cz-actor'><div id='cz-sprite' class='cz-sprite' "
        f"style=\"background-image:url('{sprite_uri}')\"></div></div>"
        "</div>"
    )


def dialogue_html(name: str, line: str, streaming: bool = False) -> str:
    caret = "<span class='cz-caret'>&#9608;</span>" if streaming else ""
    body = _esc(line) if line else "<span class='cz-muted'>...</span>"
    who = _esc(name.upper()) if name else "&nbsp;"
    return f"<div class='cz-dialogue'><div class='who'>{who}</div><div class='line'>{body}{caret}</div></div>"


def scene_panel_html(scene_uri: str, caption: str) -> str:
    return (
        f"<div class='cz-scene'><img src='{scene_uri}' alt=''>"
        f"<div class='cap'>{_esc(caption)}</div></div>"
    )


def evidence_html(items: list[tuple[str, str, str]]) -> str:
    """items: (name, reveal, image_uri)."""
    if not items:
        return "<div class='cz-muted'>No evidence yet. Search the rooms.</div>"
    # Each card is a <details>: the description is clamped to 3 lines, click to expand the
    # full text (native HTML, no JS needed). The "+"/"-" corner marker shows it is toggleable.
    slots = [
        "<details class='cz-slot'><summary>"
        f"<img src='{uri}' alt=''>"
        f"<div class='bd'><div class='nm'>{_esc(name)}</div>"
        f"<div class='ds'>{_esc(reveal[:300])}</div></div>"
        "</summary></details>"
        for name, reveal, uri in items
    ]
    return f"<div class='cz-evi'>{''.join(slots)}</div>"


def notebook_html(state: GameState, case: CaseFile) -> str:
    if not state.notebook:
        return "<div class='cz-notebook'><div class='nb cz-muted'>The page is blank. Question them and search the rooms.</div></div>"
    rows = []
    for entry in reversed(state.notebook):
        cls = entry.kind.value
        rows.append(f"<div class='nb {cls}'><b>{entry.kind.value.upper()}</b> &middot; {_esc(entry.text)}</div>")
    return f"<div class='cz-notebook'>{''.join(rows)}</div>"


def verdict_html(case: CaseFile, verdict: Verdict) -> str:
    head_color = "#7bbf73" if verdict.solved else "#d2603f"
    head = "CASE SOLVED" if verdict.solved else "CASE UNSOLVED"
    parts = [f"<div class='cz-verdict'><div style='color:{head_color};font-size:1.2rem'>{head}</div>",
             f"<div style='margin:8px 0'>Score: <b style='color:#e0ab4a'>{verdict.score}/100</b></div>",
             f"<div style='margin-bottom:8px'>{_esc(verdict.rationale)}</div>",
             "<div class='cz-muted'><b>Director's Cut:</b><ul>"]
    parts += [f"<li>{_esc(step)}</li>" for step in verdict.deduction_chain]
    parts.append("</ul></div></div>")
    return "".join(parts)