Spaces:
Running
Running
feat: ship the redesigned pixel UI wired to the real engine (+ crisis/soul/distance API fields)
Browse files- app/crisis_view.py +40 -0
- app/main.py +8 -0
- app/soul_bridge.py +17 -0
- static/app.js +838 -825
- static/index.html +215 -199
- static/styles.css +57 -640
- tests/test_api.py +54 -8
app/crisis_view.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Surface the engine's continuous crisis gradient (0.0-1.0) for the API.
|
| 2 |
+
|
| 3 |
+
The engine computes concern via engine.crisis.CrisisTracker, which blends the
|
| 4 |
+
message's VADUGWI read, running state, trajectory and detected structures into a
|
| 5 |
+
single 0.0-1.0 float. For a stateless per-request read we feed the current read
|
| 6 |
+
as both the message score and the running state, with the structures the engine
|
| 7 |
+
already detected for this text.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from engine.crisis import CrisisTracker
|
| 12 |
+
from engine.shared import VADUG
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _to_vadug(read: list[int]) -> VADUG:
|
| 16 |
+
return VADUG(v=read[0], a=read[1], d=read[2], u=read[3],
|
| 17 |
+
g=read[4], w=read[5], i=read[6])
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def crisis_value(read: list[int], structures: list[str] | None = None) -> float:
|
| 21 |
+
"""Single-message concern 0.0-1.0 from a raw VADUGWI read + detected structures."""
|
| 22 |
+
score = _to_vadug(read)
|
| 23 |
+
tracker = CrisisTracker()
|
| 24 |
+
reading = tracker.read(score, score, list(structures or []))
|
| 25 |
+
return round(float(reading.concern), 3)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def crisis_label(value: float) -> str:
|
| 29 |
+
if value < 0.15:
|
| 30 |
+
return "calm"
|
| 31 |
+
if value < 0.4:
|
| 32 |
+
return "watch"
|
| 33 |
+
if value < 0.7:
|
| 34 |
+
return "concern"
|
| 35 |
+
return "high concern"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def crisis_block(read: list[int], structures: list[str] | None = None) -> dict:
|
| 39 |
+
v = crisis_value(read, structures)
|
| 40 |
+
return {"value": v, "label": crisis_label(v)}
|
app/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from app.scorer import score_with_trace
|
|
| 12 |
from app.soul_bridge import SoulBridge
|
| 13 |
from app.appearance import mood_to_appearance
|
| 14 |
from app.trace_view import shape_trace
|
|
|
|
| 15 |
from app.vocab import mood_word
|
| 16 |
from app.voice import emoticon, simlish, voice_params
|
| 17 |
from app.audit import AuditLog, build_row
|
|
@@ -81,11 +82,17 @@ class ToyIn(BaseModel):
|
|
| 81 |
|
| 82 |
def _state() -> dict:
|
| 83 |
mood = _bridge.mood()
|
|
|
|
| 84 |
return {
|
| 85 |
"appearance": mood_to_appearance(mood),
|
| 86 |
"mood_word": mood_word(mood),
|
| 87 |
"emoticon": emoticon(mood),
|
| 88 |
"needs": _care.needs(_traffic()),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
|
|
@@ -184,6 +191,7 @@ def say(body: SayIn, request: Request):
|
|
| 184 |
state["read"] = raw
|
| 185 |
state["deltas"] = _bridge.deltas()
|
| 186 |
state["trace"] = shape_trace(trace)
|
|
|
|
| 187 |
state["why"] = _why(raw, state["deltas"])
|
| 188 |
state["simlish"] = simlish(mood)
|
| 189 |
state["voice"] = voice_params(mood)
|
|
|
|
| 12 |
from app.soul_bridge import SoulBridge
|
| 13 |
from app.appearance import mood_to_appearance
|
| 14 |
from app.trace_view import shape_trace
|
| 15 |
+
from app.crisis_view import crisis_block
|
| 16 |
from app.vocab import mood_word
|
| 17 |
from app.voice import emoticon, simlish, voice_params
|
| 18 |
from app.audit import AuditLog, build_row
|
|
|
|
| 82 |
|
| 83 |
def _state() -> dict:
|
| 84 |
mood = _bridge.mood()
|
| 85 |
+
soul = _bridge.soul_baseline()
|
| 86 |
return {
|
| 87 |
"appearance": mood_to_appearance(mood),
|
| 88 |
"mood_word": mood_word(mood),
|
| 89 |
"emoticon": emoticon(mood),
|
| 90 |
"needs": _care.needs(_traffic()),
|
| 91 |
+
"soul": soul,
|
| 92 |
+
"distance": _bridge.soul_distance(),
|
| 93 |
+
# default: no text-derived crisis (physical/idle states are calm). The
|
| 94 |
+
# /say handler overrides this with the real read+structures concern.
|
| 95 |
+
"crisis": {"value": 0.0, "label": "calm"},
|
| 96 |
}
|
| 97 |
|
| 98 |
|
|
|
|
| 191 |
state["read"] = raw
|
| 192 |
state["deltas"] = _bridge.deltas()
|
| 193 |
state["trace"] = shape_trace(trace)
|
| 194 |
+
state["crisis"] = crisis_block(raw, state["trace"].get("structures"))
|
| 195 |
state["why"] = _why(raw, state["deltas"])
|
| 196 |
state["simlish"] = simlish(mood)
|
| 197 |
state["voice"] = voice_params(mood)
|
app/soul_bridge.py
CHANGED
|
@@ -128,6 +128,23 @@ class SoulBridge:
|
|
| 128 |
m = self._plugin.snapshot().get("mood")
|
| 129 |
return [int(x) for x in m] if m else list(NEUTRAL)
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def previous_mood(self) -> list[int]:
|
| 132 |
return list(self._prev)
|
| 133 |
|
|
|
|
| 128 |
m = self._plugin.snapshot().get("mood")
|
| 129 |
return [int(x) for x in m] if m else list(NEUTRAL)
|
| 130 |
|
| 131 |
+
def soul_baseline(self) -> list[int]:
|
| 132 |
+
"""Persistent baseline mood (the soul) as a 7-int [v,a,d,u,g,w,i] array."""
|
| 133 |
+
s = self._plugin.snapshot().get("soul") or {}
|
| 134 |
+
dims = ("v", "a", "d", "u", "g", "w", "i")
|
| 135 |
+
if all(k in s for k in dims):
|
| 136 |
+
return [int(s[k]) for k in dims]
|
| 137 |
+
return list(BABY_SOUL.to_list()) if hasattr(BABY_SOUL, "to_list") else [
|
| 138 |
+
BABY_SOUL.v, BABY_SOUL.a, BABY_SOUL.d, BABY_SOUL.u,
|
| 139 |
+
BABY_SOUL.g, BABY_SOUL.w, BABY_SOUL.i,
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
def soul_distance(self) -> int:
|
| 143 |
+
"""Drift between live mood and soul baseline: round(mean |diff|) over 7 dims."""
|
| 144 |
+
mood = self.mood()
|
| 145 |
+
soul = self.soul_baseline()
|
| 146 |
+
return round(sum(abs(a - b) for a, b in zip(mood, soul)) / 7.0)
|
| 147 |
+
|
| 148 |
def previous_mood(self) -> list[int]:
|
| 149 |
return list(self._prev)
|
| 150 |
|
static/app.js
CHANGED
|
@@ -1,837 +1,850 @@
|
|
| 1 |
'use strict';
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
const
|
| 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 |
-
container.appendChild(div);
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
// ── CREATURE / BLOB ──────────────────────────────────────────────────────────
|
| 73 |
-
|
| 74 |
-
function renderBlob(ap) {
|
| 75 |
-
const h = ap.hue ?? 28;
|
| 76 |
-
const s = ap.saturation ?? 80;
|
| 77 |
-
const l = ap.lightness ?? 58;
|
| 78 |
-
const fill = `hsl(${h},${s}%,${l}%)`;
|
| 79 |
-
const darkFill = `hsl(${h},${s}%,${Math.max(l - 14, 10)}%)`;
|
| 80 |
-
|
| 81 |
-
$('#body').setAttribute('fill', fill);
|
| 82 |
-
document.querySelectorAll('.foot').forEach(el => el.setAttribute('fill', darkFill));
|
| 83 |
-
|
| 84 |
-
// arms use same fill color with stroke
|
| 85 |
-
['#arm-l-path','#arm-r-path'].forEach(sel => {
|
| 86 |
-
const el = $(sel);
|
| 87 |
-
el.style.stroke = darkFill;
|
| 88 |
-
el.style.fill = 'none';
|
| 89 |
-
});
|
| 90 |
-
['#hand-l','#hand-r'].forEach(sel => $(sel).setAttribute('fill', darkFill));
|
| 91 |
-
|
| 92 |
-
// ── anime-style emotion: body stays yellow, the FACE/cheeks glow ──
|
| 93 |
-
const glow = ap.glow || { type: 'none', hue: 0, intensity: 0 };
|
| 94 |
-
const gi = glow.intensity || 0;
|
| 95 |
-
const gEl = $('#emote-glow');
|
| 96 |
-
if (gEl) {
|
| 97 |
-
gEl.setAttribute('opacity', (gi * 0.62).toFixed(2));
|
| 98 |
-
const gc = `hsl(${glow.hue}, 92%, 55%)`;
|
| 99 |
-
const s0 = $('#emote-stop-0'), s1 = $('#emote-stop-1');
|
| 100 |
-
if (s0) s0.setAttribute('stop-color', gc);
|
| 101 |
-
if (s1) s1.setAttribute('stop-color', gc);
|
| 102 |
-
}
|
| 103 |
-
const chL = $('#cheek-l'), chR = $('#cheek-r');
|
| 104 |
-
if (chL && chR) {
|
| 105 |
-
let cheekFill = '#ff9090', cheekOp = 0.38;
|
| 106 |
-
if (glow.type === 'anger' || glow.type === 'fluster') { cheekFill = `hsl(${glow.hue},95%,56%)`; cheekOp = 0.4 + 0.5 * gi; }
|
| 107 |
-
else if (glow.type === 'joy') { cheekFill = 'hsl(338,90%,70%)'; cheekOp = 0.4 + 0.4 * gi; }
|
| 108 |
-
else if (glow.type === 'sad') { cheekFill = '#9fb8e0'; cheekOp = 0.22; }
|
| 109 |
-
[chL, chR].forEach(c => { c.setAttribute('fill', cheekFill); c.setAttribute('opacity', cheekOp.toFixed(2)); });
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
// aura behind him picks up the emotion color (subtle ambient mood), else warm yellow
|
| 113 |
-
const auraEl = $('#aura');
|
| 114 |
-
if (auraEl) {
|
| 115 |
-
auraEl.setAttribute('opacity', String((ap.aura ?? 0.25) * 2.4));
|
| 116 |
-
const stopEl = $('#aura-stop');
|
| 117 |
-
if (stopEl) stopEl.setAttribute('stop-color', gi > 0.15 ? `hsl(${glow.hue},85%,58%)` : `hsl(48,88%,60%)`);
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// body transform: lean + droop + scale, composed in SVG local coords (creature center 100,116)
|
| 121 |
-
// #blob-place holds fixed scene placement; #blob gets dynamic per-message transform
|
| 122 |
-
const lean = (ap.lean ?? 0) * 10;
|
| 123 |
-
const dy = (ap.droop ?? 0) * 35; // local units (scene px × 1/S where S=0.40)
|
| 124 |
-
const sc = ap.scale ?? 1;
|
| 125 |
-
// SVG transforms apply right-to-left:
|
| 126 |
-
// 1. translate(-100 -116) — move center to origin
|
| 127 |
-
// 2. scale(sc) — scale around origin
|
| 128 |
-
// 3. translate(100 116) — move back
|
| 129 |
-
// 4. rotate(lean 100 116) — lean around body center
|
| 130 |
-
// 5. translate(0 dy) — droop (downward shift in local space)
|
| 131 |
-
const blobTransform = `translate(0 ${dy}) rotate(${lean} 100 116) translate(100 116) scale(${sc}) translate(-100 -116)`;
|
| 132 |
-
$('#blob').setAttribute('transform', blobTransform);
|
| 133 |
-
|
| 134 |
-
// parametric face
|
| 135 |
-
const face = ap.face || {};
|
| 136 |
-
const mouthVal = face.mouth ?? 0; // -1 … +1 (frown … smile)
|
| 137 |
-
const eyeVal = face.eye ?? 1.0; // ~0.5 … 1.4 openness
|
| 138 |
-
const browVal = face.brow ?? 0; // -1 … +1 tilt
|
| 139 |
-
|
| 140 |
-
const sleeping = (ap.mood && window.ClankerPhysics
|
| 141 |
-
&& window.ClankerPhysics.behaviorFor(ap.mood) === 'sleep');
|
| 142 |
-
const eyeForRender = sleeping ? 0.12 : eyeVal; // near-closed
|
| 143 |
-
renderFace(mouthVal, eyeForRender, browVal, h, s, l);
|
| 144 |
-
|
| 145 |
-
// hand pose by mood positivity
|
| 146 |
-
poseArms(mouthVal);
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
function renderFace(mouth, eye, brow, h, s, l) {
|
| 150 |
-
// eye size based on eye param
|
| 151 |
-
const eyeR = Math.round(Math.max(4, Math.min(11, 7 * eye)));
|
| 152 |
-
const eyeY = 108;
|
| 153 |
-
const eyeLx = 78, eyeRx = 122;
|
| 154 |
-
|
| 155 |
-
// brow tilt: positive brow = raised (happy), negative = furrowed (angry)
|
| 156 |
-
const browTiltL = -brow * 8; // left brow
|
| 157 |
-
const browTiltR = brow * 8; // right brow (mirror)
|
| 158 |
-
const browY = 88;
|
| 159 |
-
const browLen = 18;
|
| 160 |
-
|
| 161 |
-
// mouth: control point Y based on mouth value
|
| 162 |
-
// mouth=+1 → big smile (control point low), mouth=-1 → frown (control point high)
|
| 163 |
-
const mouthY1 = 158;
|
| 164 |
-
const mouthCY = mouthY1 + mouth * 18; // control point
|
| 165 |
-
const mouthX1 = 76, mouthX2 = 124;
|
| 166 |
-
const mouthCX = 100;
|
| 167 |
-
|
| 168 |
-
const faceColor = `hsl(${h},${Math.max(s - 30, 10)}%,${Math.max(l - 28, 10)}%)`;
|
| 169 |
-
|
| 170 |
-
let svg = '';
|
| 171 |
-
|
| 172 |
-
// brows
|
| 173 |
-
svg += `<line x1="${eyeLx - browLen/2}" y1="${browY + browTiltL}" x2="${eyeLx + browLen/2}" y2="${browY - browTiltL}"
|
| 174 |
-
stroke="${faceColor}" stroke-width="3.5" stroke-linecap="round"/>`;
|
| 175 |
-
svg += `<line x1="${eyeRx - browLen/2}" y1="${browY - browTiltR}" x2="${eyeRx + browLen/2}" y2="${browY + browTiltR}"
|
| 176 |
-
stroke="${faceColor}" stroke-width="3.5" stroke-linecap="round"/>`;
|
| 177 |
-
|
| 178 |
-
// eyes — render as ellipses to allow eye "squeeze" on negative
|
| 179 |
-
const eyeH = eyeR;
|
| 180 |
-
const eyeW = Math.round(eyeR * 0.9);
|
| 181 |
-
svg += `<ellipse cx="${eyeLx}" cy="${eyeY}" rx="${eyeW}" ry="${eyeH}" fill="${faceColor}"/>`;
|
| 182 |
-
svg += `<ellipse cx="${eyeRx}" cy="${eyeY}" rx="${eyeW}" ry="${eyeH}" fill="${faceColor}"/>`;
|
| 183 |
-
// eye shine
|
| 184 |
-
svg += `<circle cx="${eyeLx - 2}" cy="${eyeY - 3}" r="2.5" fill="white" opacity="0.7"/>`;
|
| 185 |
-
svg += `<circle cx="${eyeRx - 2}" cy="${eyeY - 3}" r="2.5" fill="white" opacity="0.7"/>`;
|
| 186 |
-
|
| 187 |
-
// mouth as quadratic bezier
|
| 188 |
-
svg += `<path d="M${mouthX1} ${mouthY1} Q${mouthCX} ${mouthCY} ${mouthX2} ${mouthY1}"
|
| 189 |
-
stroke="${faceColor}" stroke-width="4" fill="none" stroke-linecap="round"/>`;
|
| 190 |
-
|
| 191 |
-
$('#face').innerHTML = svg;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
function poseArms(mouthVal) {
|
| 195 |
-
if (mouthVal > 0.4) {
|
| 196 |
-
// strongly positive: arms wide open overhead (jubilant)
|
| 197 |
-
$('#arm-l-path').setAttribute('d', 'M50 148 Q20 110 10 86');
|
| 198 |
-
$('#hand-l').setAttribute('cx', '10');
|
| 199 |
-
$('#hand-l').setAttribute('cy', '82');
|
| 200 |
-
$('#arm-r-path').setAttribute('d', 'M150 148 Q180 110 190 86');
|
| 201 |
-
$('#hand-r').setAttribute('cx', '190');
|
| 202 |
-
$('#hand-r').setAttribute('cy', '82');
|
| 203 |
-
} else if (mouthVal < -0.2) {
|
| 204 |
-
// negative: hands near cheeks (distress / covering face)
|
| 205 |
-
$('#arm-l-path').setAttribute('d', 'M50 148 Q34 136 26 122');
|
| 206 |
-
$('#hand-l').setAttribute('cx', '26');
|
| 207 |
-
$('#hand-l').setAttribute('cy', '118');
|
| 208 |
-
$('#arm-r-path').setAttribute('d', 'M150 148 Q166 136 174 122');
|
| 209 |
-
$('#hand-r').setAttribute('cx', '174');
|
| 210 |
-
$('#hand-r').setAttribute('cy', '118');
|
| 211 |
-
} else {
|
| 212 |
-
// neutral: classic hug gesture — arms curved up, hands beside cheeks
|
| 213 |
-
$('#arm-l-path').setAttribute('d', 'M50 148 Q28 130 18 112');
|
| 214 |
-
$('#hand-l').setAttribute('cx', '18');
|
| 215 |
-
$('#hand-l').setAttribute('cy', '108');
|
| 216 |
-
$('#arm-r-path').setAttribute('d', 'M150 148 Q172 130 182 112');
|
| 217 |
-
$('#hand-r').setAttribute('cx', '182');
|
| 218 |
-
$('#hand-r').setAttribute('cy', '108');
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
// ── TRACE ───────────────────────────────────────────────────────────────────
|
| 223 |
-
|
| 224 |
-
const ROLE_CLASS = {
|
| 225 |
-
EMOTIONAL: 'r-EMOTIONAL',
|
| 226 |
-
AMPLIFIER: 'r-AMPLIFIER',
|
| 227 |
-
NEGATOR: 'r-NEGATOR',
|
| 228 |
-
SELF_REF: 'r-SELF_REF',
|
| 229 |
-
CONNECTOR: 'r-CONNECTOR',
|
| 230 |
-
CHOPPER: 'r-CHOPPER',
|
| 231 |
-
SOLVENT: 'r-SOLVENT',
|
| 232 |
-
};
|
| 233 |
-
|
| 234 |
-
function renderTrace(trace) {
|
| 235 |
-
if (!trace) return;
|
| 236 |
-
|
| 237 |
-
// physical interaction (toy/care): no language to parse — show the action,
|
| 238 |
-
// not the previous chat message's words.
|
| 239 |
-
if (trace.kind === 'physical') {
|
| 240 |
-
$('#trace-words').innerHTML =
|
| 241 |
-
`<span class="trace-physical">${trace.label || 'physical interaction'} — physical interaction, no language to parse</span>`;
|
| 242 |
-
$('#trace-structs').innerHTML = '';
|
| 243 |
-
$('#trace-contribs').innerHTML = '';
|
| 244 |
-
$('#trace-unknown').classList.add('hidden');
|
| 245 |
-
return;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
// words as role-colored chips
|
| 249 |
-
const wordsEl = $('#trace-words');
|
| 250 |
-
wordsEl.innerHTML = '';
|
| 251 |
-
(trace.words || []).forEach(w => {
|
| 252 |
-
const span = document.createElement('span');
|
| 253 |
-
const cls = ROLE_CLASS[w.role] || 'r-default';
|
| 254 |
-
span.className = `w-chip ${cls}`;
|
| 255 |
-
span.textContent = w.token || w.word || w;
|
| 256 |
-
wordsEl.appendChild(span);
|
| 257 |
-
});
|
| 258 |
-
|
| 259 |
-
// structures as chips
|
| 260 |
-
const structsEl = $('#trace-structs');
|
| 261 |
-
structsEl.innerHTML = '';
|
| 262 |
-
(trace.structures || []).forEach(s => {
|
| 263 |
-
const span = document.createElement('span');
|
| 264 |
-
span.className = 's-chip';
|
| 265 |
-
span.textContent = typeof s === 'string' ? s : (s.name || JSON.stringify(s));
|
| 266 |
-
structsEl.appendChild(span);
|
| 267 |
-
});
|
| 268 |
-
|
| 269 |
-
// contributors
|
| 270 |
-
const contribEl = $('#trace-contribs');
|
| 271 |
-
contribEl.innerHTML = '';
|
| 272 |
-
(trace.contributors || []).forEach(c => {
|
| 273 |
-
const line = document.createElement('div');
|
| 274 |
-
let text = c.word || c.token || '?';
|
| 275 |
-
text = text.padEnd(14, ' ');
|
| 276 |
-
let parts = '';
|
| 277 |
-
['dv','da','dd','du','dg'].forEach((key, i) => {
|
| 278 |
-
if (c[key] != null && c[key] !== 0) {
|
| 279 |
-
const dim = 'VADUG'[i];
|
| 280 |
-
const cls = c[key] > 0 ? 'contrib-pos' : 'contrib-neg';
|
| 281 |
-
const sign = c[key] > 0 ? '+' : '';
|
| 282 |
-
parts += `<span class="${cls}">${dim}${sign}${c[key]}</span> `;
|
| 283 |
-
}
|
| 284 |
-
});
|
| 285 |
-
if (!parts) parts = '<span style="opacity:.5">—</span>';
|
| 286 |
-
line.innerHTML = `${text} ${parts}`;
|
| 287 |
-
contribEl.appendChild(line);
|
| 288 |
-
});
|
| 289 |
-
|
| 290 |
-
// unknown tokens
|
| 291 |
-
const unk = trace.unknown_tokens || trace.suspected_gap || [];
|
| 292 |
-
const unkEl = $('#trace-unknown');
|
| 293 |
-
if (unk.length > 0) {
|
| 294 |
-
unkEl.textContent = `didn't know: ${unk.join(', ')}`;
|
| 295 |
-
unkEl.classList.remove('hidden');
|
| 296 |
-
} else {
|
| 297 |
-
unkEl.classList.add('hidden');
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
// ── ACCEPTANCE ──────────────────────────────────────────────────────────────
|
| 302 |
-
|
| 303 |
-
function renderAcceptance(acc) {
|
| 304 |
-
if (!acc) return;
|
| 305 |
-
const el = $('#acceptance');
|
| 306 |
-
const raw = acc.weight_raw ?? '?';
|
| 307 |
-
const eff = acc.weight_effective ?? '?';
|
| 308 |
-
const pct = acc.absorbed_pct ?? 0;
|
| 309 |
-
const armor = acc.armor ?? '?';
|
| 310 |
-
const breached = acc.breached ? '<span class="breach-flag">BREACH</span>' : '';
|
| 311 |
-
el.innerHTML = `raw hit <b>${raw}</b> · armor <b>${armor}</b> absorbed <b>${pct}%</b> → <b>${eff}</b> reached mood${breached}`;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
// ── WHY ──────────────────────────────────────────────────────────────────────
|
| 315 |
-
|
| 316 |
-
function renderWhy(why) {
|
| 317 |
-
const el = $('#why');
|
| 318 |
-
if (!why) { el.textContent = 'why: —'; return; }
|
| 319 |
-
el.innerHTML = 'why: ' + why.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
// ── TIMELINE ─────────────────────────────────────────────────────────────────
|
| 323 |
-
|
| 324 |
-
function renderTimeline(events) {
|
| 325 |
-
const svg = $('#tl-svg');
|
| 326 |
-
if (!events || events.length < 2) {
|
| 327 |
-
svg.innerHTML = '<text x="4" y="20" fill="#8aa0c0" font-size="10">no history yet</text>';
|
| 328 |
-
return;
|
| 329 |
-
}
|
| 330 |
-
const vals = events.map(e => (e.soul && e.soul[0] != null) ? e.soul[0] : 128);
|
| 331 |
-
const W = svg.clientWidth || 200;
|
| 332 |
-
const H = 34;
|
| 333 |
-
const pad = 3;
|
| 334 |
-
const xs = vals.map((_, i) => pad + (i / (vals.length - 1)) * (W - pad * 2));
|
| 335 |
-
const ys = vals.map(v => H - pad - ((v / 255) * (H - pad * 2)));
|
| 336 |
-
const pts = xs.map((x, i) => `${x},${ys[i]}`).join(' ');
|
| 337 |
-
svg.innerHTML = `
|
| 338 |
-
<polyline points="${pts}" fill="none" stroke="#79ceff" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/>
|
| 339 |
-
<line x1="${pad}" y1="${H/2}" x2="${W-pad}" y2="${H/2}" stroke="rgba(255,255,255,.1)" stroke-width="1" stroke-dasharray="3,4"/>`;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
async function fetchAndRenderTimeline() {
|
| 343 |
-
try {
|
| 344 |
-
const r = await fetch('/history');
|
| 345 |
-
if (!r.ok) return;
|
| 346 |
-
const data = await r.json();
|
| 347 |
-
renderTimeline(data.events || []);
|
| 348 |
-
} catch (_) {}
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
// ── BUBBLE ───────────────────────────────────────────────────────────────────
|
| 352 |
-
|
| 353 |
-
function bubble(text) {
|
| 354 |
-
const b = $('#bubble');
|
| 355 |
-
b.textContent = text;
|
| 356 |
-
b.classList.remove('hidden');
|
| 357 |
-
clearTimeout(b._t);
|
| 358 |
-
b._t = setTimeout(() => b.classList.add('hidden'), 4500);
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
// ── SPEAK ────────────────────────────────────────────────────────────────────
|
| 362 |
-
|
| 363 |
-
function speak(text, voice) {
|
| 364 |
-
if (!text || !window.speechSynthesis) return;
|
| 365 |
-
const u = new SpeechSynthesisUtterance(text);
|
| 366 |
-
u.pitch = (voice && voice.pitch != null) ? voice.pitch : 1.4;
|
| 367 |
-
u.rate = (voice && voice.rate != null) ? voice.rate : 1.0;
|
| 368 |
-
window.speechSynthesis.cancel();
|
| 369 |
-
window.speechSynthesis.speak(u);
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
// ── NEEDS ────────────────────────────────────────────────────────────────────
|
| 373 |
-
|
| 374 |
-
function renderNeeds(needs) {
|
| 375 |
-
if (!needs) return;
|
| 376 |
-
['hunger', 'energy', 'attention', 'thirst'].forEach(k => {
|
| 377 |
-
const val = needs[k] ?? 0;
|
| 378 |
-
const pct = Math.round(Math.min(100, Math.max(0, val)));
|
| 379 |
-
const fill = document.getElementById(`need-${k}`);
|
| 380 |
-
const valEl = document.getElementById(`need-${k}-val`);
|
| 381 |
-
if (fill) fill.style.width = pct + '%';
|
| 382 |
-
if (valEl) valEl.textContent = pct;
|
| 383 |
-
});
|
| 384 |
-
// bowls show their level: food = hunger, water = thirst (community keeps them filled)
|
| 385 |
-
const food = document.getElementById('bowl-food');
|
| 386 |
-
if (food && needs.hunger != null) food.setAttribute('opacity', String(Math.max(0, Math.min(100, needs.hunger)) / 100));
|
| 387 |
-
const water = document.getElementById('bowl-water');
|
| 388 |
-
if (water && needs.thirst != null) water.setAttribute('opacity', String(Math.max(0, Math.min(100, needs.thirst)) / 100));
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
// ── MOMENTS ──────────────────────────────────────────────────────────────────
|
| 392 |
-
|
| 393 |
-
function relativeTime(ts) {
|
| 394 |
-
// ts is an ISO string or unix seconds float
|
| 395 |
-
let ms;
|
| 396 |
-
if (typeof ts === 'number') {
|
| 397 |
-
ms = ts < 1e10 ? ts * 1000 : ts;
|
| 398 |
-
} else {
|
| 399 |
-
ms = Date.parse(ts);
|
| 400 |
-
}
|
| 401 |
-
if (!ms || isNaN(ms)) return '';
|
| 402 |
-
const diff = Math.floor((Date.now() - ms) / 1000);
|
| 403 |
-
if (diff < 60) return 'just now';
|
| 404 |
-
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
| 405 |
-
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
| 406 |
-
return Math.floor(diff / 86400) + 'd ago';
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
function renderMoments(events) {
|
| 410 |
-
const list = $('#moments-list');
|
| 411 |
-
if (!list) return;
|
| 412 |
-
if (!events || events.length === 0) {
|
| 413 |
-
list.innerHTML = '<div class="moment-row" style="color:#6a7a96;font-size:9px">nothing yet</div>';
|
| 414 |
-
return;
|
| 415 |
-
}
|
| 416 |
-
list.innerHTML = events.slice().reverse().map(ev => {
|
| 417 |
-
const badge = (ev.type || 'moment').replace(/_/g, ' ');
|
| 418 |
-
const label = ev.mood_word || ev.label || '';
|
| 419 |
-
const time = relativeTime(ev.ts);
|
| 420 |
-
return `<div class="moment-row">
|
| 421 |
-
<span class="moment-badge">${badge}</span>
|
| 422 |
-
<span class="moment-label">${label}</span>
|
| 423 |
-
<span class="moment-time">${time}</span>
|
| 424 |
-
</div>`;
|
| 425 |
-
}).join('');
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
async function fetchAndRenderMoments() {
|
| 429 |
-
try {
|
| 430 |
-
const r = await fetch('/moments');
|
| 431 |
-
if (!r.ok) return;
|
| 432 |
-
const data = await r.json();
|
| 433 |
-
renderMoments(data.events || []);
|
| 434 |
-
} catch (_) {}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
// ── REMEMBERS-YOU HINT ───────────────────────────────────────────────────────
|
| 438 |
-
|
| 439 |
-
function renderRelationHint(relation) {
|
| 440 |
-
const hint = $('#remembers-hint');
|
| 441 |
-
if (!hint) return;
|
| 442 |
-
if (relation && relation.known === true) {
|
| 443 |
-
hint.classList.remove('hidden');
|
| 444 |
-
} else {
|
| 445 |
-
hint.classList.add('hidden');
|
| 446 |
-
}
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
// ── RENDER (full state) ──────────────────────────────────────────────────────
|
| 450 |
-
|
| 451 |
-
function render(state) {
|
| 452 |
-
const ap = state.appearance || {};
|
| 453 |
-
_ap = ap;
|
| 454 |
-
const mood = ap.mood || Array(7).fill(128);
|
| 455 |
-
const deltas = state.deltas || null;
|
| 456 |
-
const read = state.read || null;
|
| 457 |
-
|
| 458 |
-
renderMeters(mood, deltas, read);
|
| 459 |
-
renderBlob(ap);
|
| 460 |
-
$('#moodword').textContent = state.mood_word || '…';
|
| 461 |
-
|
| 462 |
-
if (state.trace) renderTrace(state.trace);
|
| 463 |
-
if (state.acceptance) renderAcceptance(state.acceptance);
|
| 464 |
-
if (state.why) renderWhy(state.why);
|
| 465 |
-
if (state.needs) renderNeeds(state.needs);
|
| 466 |
-
renderRelationHint(state.relation || null);
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
// ── SQUISH IMPULSE ────────────────────────────────────────────────────────────
|
| 470 |
-
|
| 471 |
-
function squishImpulse(deltas) {
|
| 472 |
-
if (!deltas) return;
|
| 473 |
-
const mag = deltas.reduce((s, d) => s + Math.abs(d || 0), 0);
|
| 474 |
-
// bigger, springier reaction so an interaction visibly lands
|
| 475 |
-
const kick = 1.5 + Math.min(11, mag / 38);
|
| 476 |
-
_spring.vel -= kick;
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
// ── APPLY RESPONSE (shared by say + care) ────────────────────────────────────
|
| 480 |
-
|
| 481 |
-
// Sims-style mood float above his head: an emotion emoji + a single +/- (1-3 for
|
| 482 |
-
// intensity) showing what an interaction did to his MOOD. The per-dimension VADUGWI
|
| 483 |
-
// changes stay in the left stats panel (the meters + deltas).
|
| 484 |
-
function moodEmoji(mood) {
|
| 485 |
-
const v = mood[0] ?? 128, a = mood[1] ?? 128;
|
| 486 |
-
if (v >= 200 && a >= 185) return '😂'; // elated/laughing
|
| 487 |
-
if (v >= 165) return '😄'; // happy
|
| 488 |
-
if (v >= 140) return '🙂'; // pleased
|
| 489 |
-
if (v <= 80 && a >= 150) return '😡'; // mad
|
| 490 |
-
if (v <= 85) return '😭'; // crying
|
| 491 |
-
if (v <= 112) return '😞'; // sad
|
| 492 |
-
if (a >= 188) return '😲'; // startled
|
| 493 |
-
return '😐'; // neutral
|
| 494 |
-
}
|
| 495 |
-
function _floatMood(net, mood) {
|
| 496 |
-
const up = net > 0;
|
| 497 |
-
let sym = '';
|
| 498 |
-
if (Math.abs(net) >= 3) {
|
| 499 |
-
const count = Math.max(1, Math.min(3, Math.round(Math.abs(net) / 14)));
|
| 500 |
-
sym = (up ? '+' : '−').repeat(count);
|
| 501 |
-
}
|
| 502 |
-
const emoji = mood ? moodEmoji(mood) : '';
|
| 503 |
-
if (!sym && !emoji) return;
|
| 504 |
-
_float = { sym, up, emoji, until: performance.now() + 2600, startT: null };
|
| 505 |
-
}
|
| 506 |
-
function floatDeltas(deltas, mood) {
|
| 507 |
-
if (!deltas) return;
|
| 508 |
-
_floatMood((deltas[0] || 0) + 0.6 * (deltas[5] || 0), mood); // mood ≈ valence + worth
|
| 509 |
-
}
|
| 510 |
-
function floatRaises(mood) { _floatMood(20, mood); } // self-soothe is a gentle positive
|
| 511 |
-
|
| 512 |
-
// valence → hue, SAME piecewise mapping as the server (appearance.py): red→orange→
|
| 513 |
-
// yellow→green. (The old flash used V/255*360 which sent positive reads into purple.)
|
| 514 |
-
function hueForValence(v) {
|
| 515 |
-
v = Math.max(0, Math.min(255, v));
|
| 516 |
-
return v <= 128 ? 8 + (48 - 8) * (v / 128) : 48 + (125 - 48) * ((v - 128) / 127);
|
| 517 |
-
}
|
| 518 |
-
|
| 519 |
-
function applyResponse(state) {
|
| 520 |
-
squishImpulse(state.deltas); // physical bounce
|
| 521 |
-
floatDeltas(state.deltas, (state.appearance || {}).mood); // +/- emoji float
|
| 522 |
-
render(state); // body stays yellow; emotion shows via face glow
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
// ── TOY ───────────────────────────────────────────────────────────────────────
|
| 526 |
-
|
| 527 |
-
async function toy(name) {
|
| 528 |
-
try {
|
| 529 |
-
const r = await fetch('/toy', {
|
| 530 |
-
method: 'POST',
|
| 531 |
-
headers: { 'Content-Type': 'application/json' },
|
| 532 |
-
body: JSON.stringify({ toy: name }),
|
| 533 |
-
});
|
| 534 |
-
if (!r.ok) { bubble('…?'); return; }
|
| 535 |
-
const state = await r.json();
|
| 536 |
-
applyResponse(state);
|
| 537 |
-
bubble(state.emoticon || '');
|
| 538 |
-
speak(state.simlish, state.voice);
|
| 539 |
-
await fetchAndRenderTimeline();
|
| 540 |
-
await fetchAndRenderMoments();
|
| 541 |
-
} catch (_) {
|
| 542 |
-
bubble('…?');
|
| 543 |
-
}
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
// ── SAY ───────────────────────────────────────────────────────────────────────
|
| 547 |
-
|
| 548 |
-
async function say() {
|
| 549 |
-
const inp = $('#msg');
|
| 550 |
-
const text = inp.value.trim();
|
| 551 |
-
if (!text) return;
|
| 552 |
-
inp.value = '';
|
| 553 |
-
|
| 554 |
-
try {
|
| 555 |
-
const r = await fetch('/say', {
|
| 556 |
-
method: 'POST',
|
| 557 |
-
headers: { 'Content-Type': 'application/json' },
|
| 558 |
-
body: JSON.stringify({ text }),
|
| 559 |
-
});
|
| 560 |
-
if (!r.ok) { bubble('…?'); return; }
|
| 561 |
-
const state = await r.json();
|
| 562 |
-
|
| 563 |
-
applyResponse(state);
|
| 564 |
-
|
| 565 |
-
if (state.recognition && state.recognition.returning) {
|
| 566 |
-
bubble('welcome back!');
|
| 567 |
-
} else {
|
| 568 |
-
bubble(state.emoticon || '');
|
| 569 |
}
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
});
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
} catch (_) {
|
| 596 |
-
bubble('…?');
|
| 597 |
-
}
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
// ── AUTH CONTROL ─────────────────────────────────────────────────────────────
|
| 601 |
-
|
| 602 |
-
// ── BOOT ──────────────────────────────────────────────────────────────────────
|
| 603 |
-
|
| 604 |
-
$('#send').onclick = say;
|
| 605 |
-
$('#msg').addEventListener('keydown', (e) => { if (e.key === 'Enter') say(); });
|
| 606 |
-
|
| 607 |
-
// Tap a world object → the creature walks over to it, then the action fires.
|
| 608 |
-
function _objCenterX(el) {
|
| 609 |
-
try { const b = el.getBBox(); return b.x + b.width / 2; }
|
| 610 |
-
catch (_) { return _posX; }
|
| 611 |
-
}
|
| 612 |
-
function _sendToObject(el) {
|
| 613 |
-
const t = el.getAttribute('data-toy');
|
| 614 |
-
const c = el.getAttribute('data-care');
|
| 615 |
-
const x = Math.max(ROAM_MIN, Math.min(ROAM_MAX, _objCenterX(el)));
|
| 616 |
-
_target = { x, action: t ? { kind: 'toy', name: t } : { kind: 'care', name: c } };
|
| 617 |
-
}
|
| 618 |
-
document.querySelectorAll('.room-obj').forEach(el => {
|
| 619 |
-
el.addEventListener('click', () => _sendToObject(el));
|
| 620 |
-
el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _sendToObject(el); } });
|
| 621 |
-
});
|
| 622 |
-
|
| 623 |
-
// Tap the creature itself → pet it (in place, no walk). Deltas float the +V +W.
|
| 624 |
-
function _petHim() { care('pet'); }
|
| 625 |
-
const _blobPlace = $('#blob-place');
|
| 626 |
-
if (_blobPlace) {
|
| 627 |
-
_blobPlace.addEventListener('click', _petHim);
|
| 628 |
-
_blobPlace.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _petHim(); } });
|
| 629 |
-
}
|
| 630 |
-
|
| 631 |
-
// collapsible "receipts" drawer — keeps the room uncovered by default
|
| 632 |
-
const _drawer = $('#detail-drawer'), _dtoggle = $('#detail-toggle');
|
| 633 |
-
if (_dtoggle && _drawer) {
|
| 634 |
-
_dtoggle.onclick = () => {
|
| 635 |
-
const open = _drawer.classList.toggle('open');
|
| 636 |
-
_dtoggle.classList.toggle('open', open);
|
| 637 |
-
_dtoggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
| 638 |
-
};
|
| 639 |
-
}
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
// initial snapshot + history + moments
|
| 643 |
-
fetch('/snapshot').then(r => r.json()).then(state => {
|
| 644 |
-
handleSnapshot(state);
|
| 645 |
-
renderWhy(null);
|
| 646 |
-
renderAcceptance(null);
|
| 647 |
-
}).catch(() => {});
|
| 648 |
-
|
| 649 |
-
fetchAndRenderTimeline();
|
| 650 |
-
fetchAndRenderMoments();
|
| 651 |
-
|
| 652 |
-
// poll: bowl empties as hunger decays, and he may self-soothe when low
|
| 653 |
-
setInterval(() => {
|
| 654 |
-
fetch('/snapshot').then(r => r.json()).then(s => {
|
| 655 |
-
if (s.needs) renderNeeds(s.needs);
|
| 656 |
-
if (s.self_soothe && s.self_soothe.item) triggerSoothe(s.self_soothe.item);
|
| 657 |
-
}).catch(() => {});
|
| 658 |
-
}, 45000);
|
| 659 |
-
|
| 660 |
-
function fireAction(act) {
|
| 661 |
-
if (!act) return;
|
| 662 |
-
if (act.kind === 'toy') {
|
| 663 |
-
toy(act.name); // applyResponse floats the real deltas
|
| 664 |
-
startHold(act.name, true);
|
| 665 |
-
} else if (act.kind === 'care') {
|
| 666 |
-
care(act.name); // applyResponse floats the real deltas
|
| 667 |
-
if (act.name === 'play') startHold('ball', true);
|
| 668 |
-
} else if (act.kind === 'soothe') {
|
| 669 |
-
// server already applied the (gentle) lift; no client deltas → float a + and his mood
|
| 670 |
-
startHold(act.name, true);
|
| 671 |
-
floatRaises(_ap && _ap.mood);
|
| 672 |
-
}
|
| 673 |
-
}
|
| 674 |
-
|
| 675 |
-
// ── HOLDING an item: pick it up (full size, off the floor) ──
|
| 676 |
-
function startHold(item, carry) {
|
| 677 |
-
if (_hold && _hold.floorEl) _hold.floorEl.style.opacity = ''; // restore prior
|
| 678 |
-
_hold = { item: null, floorEl: null, until: performance.now() + 7000 };
|
| 679 |
-
if (carry && item && ITEM[item]) {
|
| 680 |
-
const it = ITEM[item];
|
| 681 |
-
const el = $('#carry');
|
| 682 |
-
if (el) {
|
| 683 |
-
el.setAttributeNS('http://www.w3.org/1999/xlink', 'href', ASSET(it.sprite));
|
| 684 |
-
el.setAttribute('href', ASSET(it.sprite));
|
| 685 |
-
el.setAttribute('width', it.w);
|
| 686 |
-
el.setAttribute('height', it.h);
|
| 687 |
}
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
//
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
if (el) { try { const b = el.getBBox(); x = b.x + b.width / 2; } catch (_) {} }
|
| 703 |
-
_target = { x: Math.max(ROAM_MIN, Math.min(ROAM_MAX, x)), action: { kind: 'soothe', name: item } };
|
| 704 |
-
}
|
| 705 |
-
// snapshot handler: render + react to an autonomous self-soothe
|
| 706 |
-
function handleSnapshot(state) {
|
| 707 |
-
render(state);
|
| 708 |
-
if (state && state.self_soothe && state.self_soothe.item) triggerSoothe(state.self_soothe.item);
|
| 709 |
-
}
|
| 710 |
-
|
| 711 |
-
function pulse(t) {
|
| 712 |
-
const dt = _lastT ? Math.min(0.05, (t - _lastT) / 1000) : 0.016;
|
| 713 |
-
_lastT = t;
|
| 714 |
-
const P = window.ClankerPhysics;
|
| 715 |
-
const mood = (_ap && _ap.mood) || Array(7).fill(128);
|
| 716 |
-
const a = mood[1] ?? 128, u = mood[3] ?? 0;
|
| 717 |
-
|
| 718 |
-
_phase += P.breathRate(mood) * dt;
|
| 719 |
-
|
| 720 |
-
// sleep only when truly low-energy AND not summoned by a tap
|
| 721 |
-
const sleeping = (a < 70 && u < 60) && !_target;
|
| 722 |
-
|
| 723 |
-
// cadence: walk speed scales with arousal (calm plod ↔ wired scurry)
|
| 724 |
-
let speed = 7 + (a / 255) * 44; // 7 .. 51 scene-px/sec
|
| 725 |
-
let destX = _posX;
|
| 726 |
-
|
| 727 |
-
if (sleeping) {
|
| 728 |
-
destX = _posX;
|
| 729 |
-
} else if (_target) {
|
| 730 |
-
destX = _target.x;
|
| 731 |
-
speed = Math.max(speed, 40); // deliberate walk toward a tapped object
|
| 732 |
-
} else {
|
| 733 |
-
// idle wander: amble to a spot, linger (longer when calm), pick a new spot
|
| 734 |
-
if (_pause > 0) {
|
| 735 |
-
_pause -= dt;
|
| 736 |
-
destX = _posX;
|
| 737 |
-
} else {
|
| 738 |
-
destX = _wanderX;
|
| 739 |
-
if (Math.abs(_posX - _wanderX) < 4) {
|
| 740 |
-
_pause = _rand(0.6, 2.8) * (1 + (1 - a / 255) * 2.2);
|
| 741 |
-
_wanderX = _rand(ROAM_MIN, ROAM_MAX);
|
| 742 |
-
}
|
| 743 |
}
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
const holding = _hold && t < _hold.until;
|
| 799 |
-
const carryEl = $('#carry');
|
| 800 |
-
if (carryEl) {
|
| 801 |
-
if (holding && _hold.item && ITEM[_hold.item]) {
|
| 802 |
-
const it = ITEM[_hold.item];
|
| 803 |
-
carryEl.setAttribute('x', (_posX + _facing * 6 - it.w / 2).toFixed(1));
|
| 804 |
-
carryEl.setAttribute('y', (152 - bob - it.h * 0.45).toFixed(1));
|
| 805 |
-
carryEl.setAttribute('opacity', '1');
|
| 806 |
} else {
|
| 807 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
}
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
if
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
} else {
|
| 826 |
-
|
| 827 |
-
|
|
|
|
| 828 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
}
|
| 830 |
-
|
| 831 |
-
if (_hold.floorEl) _hold.floorEl.style.opacity = ''; // put it back on the floor
|
| 832 |
-
_hold = null;
|
| 833 |
-
}
|
| 834 |
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
requestAnimationFrame(pulse);
|
|
|
|
| 1 |
'use strict';
|
| 2 |
+
/* Clanker pet — vanilla controller (no React, no dc-runtime).
|
| 3 |
+
Ports the redesign's DCLogic class to a plain object. The on-device fake VADUGWI
|
| 4 |
+
engine is GONE; data comes from the real FastAPI backend (/snapshot /say /care /toy
|
| 5 |
+
/moments). Animation loop, simlish, WebAudio blips, bowls, toy-play, behavior FSM
|
| 6 |
+
and tooltips are ported from the redesign verbatim. self_soothe + recognition
|
| 7 |
+
"welcome back" are preserved from the prior production app. */
|
| 8 |
+
|
| 9 |
+
const App = {
|
| 10 |
+
// ---------- state ----------
|
| 11 |
+
root: null, stageEl: null, inputEl: null, tipEl: null,
|
| 12 |
+
name: 'Huggy',
|
| 13 |
+
soundOn: true,
|
| 14 |
+
// mood model: backend appearance.mood -> mood; disp lerps toward mood (redesign)
|
| 15 |
+
soul: {v:152,a:118,d:142,u:14,g:142,w:160,i:150},
|
| 16 |
+
mood: {v:152,a:118,d:142,u:14,g:142,w:160,i:150},
|
| 17 |
+
disp: {v:152,a:118,d:142,u:14,g:142,w:160,i:150},
|
| 18 |
+
haveSoul: false, // backend soul[] present?
|
| 19 |
+
backendDistance: null, // backend distance (NEW field; may be absent)
|
| 20 |
+
needsObj: {hunger:74, energy:80, attention:66, thirst:78},
|
| 21 |
+
lastDelta: {},
|
| 22 |
+
crisis: 0,
|
| 23 |
+
haveCrisis: false,
|
| 24 |
+
diary: [],
|
| 25 |
+
t0: performance.now(), lastTs: 0,
|
| 26 |
+
nextBlink: 1.2, blinkUntil: 0,
|
| 27 |
+
nextDart: 3, dart: 0, _dartEnd: 0,
|
| 28 |
+
itemEls: {},
|
| 29 |
+
dragId: null, dragWrap: null,
|
| 30 |
+
food: 0, water: 0,
|
| 31 |
+
action: null, actPos:{x:50,y:52,rot:0,sc:1}, _actEnd: null,
|
| 32 |
+
eating: 0, drinking: 0,
|
| 33 |
+
behavior: 'idle', shake: 0,
|
| 34 |
+
lastSpeak: 0,
|
| 35 |
+
uiAcc: 0,
|
| 36 |
+
ac: null, _raf: null, _warned: false,
|
| 37 |
+
items: [],
|
| 38 |
+
readPhrase: 'waiting…',
|
| 39 |
+
moodWord: 'calm',
|
| 40 |
+
|
| 41 |
+
// ====================================================================
|
| 42 |
+
// ITEM DEFS (icons + defaults) — backend toy/care names mapped per HOOKUP_SPEC
|
| 43 |
+
// ====================================================================
|
| 44 |
+
ICONS(kind){
|
| 45 |
+
const I = {
|
| 46 |
+
balloon:'<svg viewBox="0 0 40 56" width="100%"><path d="M20 2 C30 2 34 14 32 24 C30 34 24 40 20 40 C16 40 10 34 8 24 C6 14 10 2 20 2Z" fill="#ff7aa8"/><path d="M16 10 q4 -4 9 0" stroke="#fff" stroke-width="2" fill="none" opacity=".6"/><path d="M20 40 l-2 6 4 0 z" fill="#ff7aa8"/><path d="M20 46 q-5 6 0 10" stroke="#caa" stroke-width="1.4" fill="none"/></svg>',
|
| 47 |
+
ball:'<svg viewBox="0 0 40 40" width="100%"><circle cx="20" cy="20" r="17" fill="#4db5e8"/><path d="M3 20 h34 M20 3 v34" stroke="#fff" stroke-width="2.4" opacity=".85"/><path d="M7 9 q13 11 26 0 M7 31 q13 -11 26 0" stroke="#fff" stroke-width="2" fill="none" opacity=".6"/></svg>',
|
| 48 |
+
teddy:'<svg viewBox="0 0 44 44" width="100%"><circle cx="12" cy="9" r="6" fill="#b07a44"/><circle cx="32" cy="9" r="6" fill="#b07a44"/><circle cx="22" cy="24" r="17" fill="#c98c50"/><circle cx="16" cy="21" r="2.3" fill="#3a2410"/><circle cx="28" cy="21" r="2.3" fill="#3a2410"/><ellipse cx="22" cy="28" rx="6" ry="5" fill="#e3bf90"/><circle cx="22" cy="26" r="1.8" fill="#3a2410"/></svg>',
|
| 49 |
+
music:'<svg viewBox="0 0 40 38" width="100%"><rect x="4" y="14" width="32" height="22" rx="3" fill="#caa24a"/><rect x="4" y="14" width="32" height="6" rx="3" fill="#a8823a"/><path d="M18 6 v18" stroke="#3a2410" stroke-width="2.4"/><circle cx="16" cy="25" r="3.4" fill="#3a2410"/><path d="M18 6 q8 1 8 7" stroke="#3a2410" stroke-width="2.4" fill="none"/></svg>',
|
| 50 |
+
rattle:'<svg viewBox="0 0 36 40" width="100%"><circle cx="18" cy="13" r="11" fill="#7ad0a0"/><circle cx="13" cy="11" r="2" fill="#fff" opacity=".8"/><rect x="15" y="22" width="6" height="16" rx="3" fill="#e0b25a"/></svg>',
|
| 51 |
+
mirror:'<svg viewBox="0 0 34 40" width="100%"><ellipse cx="17" cy="14" rx="13" ry="13" fill="#d8c089"/><ellipse cx="17" cy="14" rx="9.5" ry="9.5" fill="#cdeaf2"/><path d="M11 10 q5 -4 11 0" stroke="#fff" stroke-width="2" fill="none" opacity=".7"/><rect x="14" y="26" width="6" height="13" rx="3" fill="#b58f44"/></svg>'
|
| 52 |
+
};
|
| 53 |
+
return I[kind] || I.ball;
|
| 54 |
+
},
|
| 55 |
+
// care = backend route+name: ball->/care{play}; others->/toy{<name>}
|
| 56 |
+
defaultItems(){
|
| 57 |
+
const base = [
|
| 58 |
+
{id:'ball', kind:'ball', ep:'care', name:'play', x:30, y:88, size:7.5, tip:'Drag onto Huggy — he bounces it. Spikes arousal & joy.'},
|
| 59 |
+
{id:'balloon', kind:'balloon', ep:'toy', name:'balloon', x:14, y:76, size:8.5, tip:'Drag onto Huggy — he bats it around. Raises valence & dominance.'},
|
| 60 |
+
{id:'rattle', kind:'rattle', ep:'toy', name:'rattle', x:64, y:90, size:7, tip:'Drag onto Huggy — he shakes it. Bumps arousal & urgency.'},
|
| 61 |
+
{id:'music', kind:'music', ep:'toy', name:'musicbox', x:74, y:92, size:8, tip:'Drag onto Huggy — he sways to it. Soothes: arousal down, valence up.'},
|
| 62 |
+
{id:'mirror', kind:'mirror', ep:'toy', name:'mirror', x:84, y:88, size:7, tip:'Drag onto Huggy — he admires himself. Nudges self-worth.'},
|
| 63 |
+
{id:'teddy', kind:'teddy', ep:'toy', name:'teddy', x:93, y:92, size:10, tip:'Drag onto Huggy — he squeezes it. Comfort: steadies valence & self-worth.'}
|
| 64 |
+
];
|
| 65 |
+
return base.map(b=>({...b, resting:true, held:false}));
|
| 66 |
+
},
|
| 67 |
+
|
| 68 |
+
// ====================================================================
|
| 69 |
+
// PRESENTATIONAL HELPERS (client-side only; no engine math)
|
| 70 |
+
// ====================================================================
|
| 71 |
+
feltPhrase(C){
|
| 72 |
+
const vw = C.v<80?'miserable':C.v<118?'uneasy':C.v<=138?'level':C.v<=180?'warm':'glowing';
|
| 73 |
+
const aw = C.a<80?'flat':C.a<118?'settled':C.a<=138?'steady':C.a<=180?'keyed-up':'electric';
|
| 74 |
+
const ww = C.w<90?', a bit small':C.w>175?', sure of itself':'';
|
| 75 |
+
return `${vw}, ${aw}${ww}`;
|
| 76 |
+
},
|
| 77 |
+
emoteFrom(d){
|
| 78 |
+
if(this.haveCrisis && this.crisis>0.6) return ';_;';
|
| 79 |
+
if(d.v<100 && d.a>150) return '>:(';
|
| 80 |
+
if(d.v<104 && d.a<110) return 'T_T';
|
| 81 |
+
if(d.v<108) return '·︵·';
|
| 82 |
+
if(this.needsObj.energy<28) return '-_-';
|
| 83 |
+
if(d.v>168 && d.a>150) return '\\^o^/';
|
| 84 |
+
if(d.v>156) return '^‿^';
|
| 85 |
+
return '·‿·';
|
| 86 |
+
},
|
| 87 |
+
moodHue(d){
|
| 88 |
+
let hue, valN=(d.v-128)/127, aN=(d.a-128)/127;
|
| 89 |
+
if(d.v<116){ hue = d.a>138 ? 4 : 212; }
|
| 90 |
+
else if(d.v>152){ hue = 128; }
|
| 91 |
+
else hue = 46;
|
| 92 |
+
const sat = Math.round(54 + Math.abs(valN)*30 + Math.max(0,aN)*14);
|
| 93 |
+
return {hue, sat, light:58};
|
| 94 |
+
},
|
| 95 |
+
|
| 96 |
+
// backend role -> [bg, fg, border] + tooltip
|
| 97 |
+
roleColor(r){
|
| 98 |
+
const c={
|
| 99 |
+
EMOTIONAL:['rgba(95,170,255,.16)','#8fc0ff','rgba(95,170,255,.3)'],
|
| 100 |
+
SELF_REF :['rgba(255,210,30,.16)','#ffd86a','rgba(255,210,30,.3)'],
|
| 101 |
+
NEGATOR :['rgba(255,90,90,.16)','#ff9b9b','rgba(255,90,90,.3)'],
|
| 102 |
+
AMPLIFIER:['rgba(95,210,140,.16)','#8fe6b0','rgba(95,210,140,.3)'],
|
| 103 |
+
CONNECTOR:['rgba(150,150,150,.12)','#bbb','rgba(150,150,150,.26)'],
|
| 104 |
+
CHOPPER :['rgba(190,160,110,.16)','#dcc08a','rgba(190,160,110,.3)'],
|
| 105 |
+
SOLVENT :['rgba(170,120,255,.16)','#c6a6ff','rgba(170,120,255,.3)'],
|
| 106 |
+
GAS :['rgba(160,160,160,.12)','#9c968c','rgba(160,160,160,.24)']
|
| 107 |
+
};
|
| 108 |
+
return c[r]||c.GAS;
|
| 109 |
+
},
|
| 110 |
+
roleTip(r){
|
| 111 |
+
const t={
|
| 112 |
+
EMOTIONAL:'EMOTIONAL — carries a force vector from the curated vocabulary.',
|
| 113 |
+
SELF_REF :'SELF_REF — anchors the read to the speaker; routes valence into self-worth.',
|
| 114 |
+
NEGATOR :'NEGATOR — flips the sign of nearby emotional words.',
|
| 115 |
+
AMPLIFIER:'AMPLIFIER — scales nearby force up.',
|
| 116 |
+
CONNECTOR:'CONNECTOR — discourse glue between clauses.',
|
| 117 |
+
CHOPPER :'CHOPPER — context word whose meaning depends on structure.',
|
| 118 |
+
SOLVENT :'SOLVENT — dissolves LIQUID atoms in a casual register.',
|
| 119 |
+
GAS :'GAS — neutral / inert token.'
|
| 120 |
+
};
|
| 121 |
+
return t[r]||'structural role';
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
// ====================================================================
|
| 125 |
+
// RENDER FUNCTIONS (replace setState + dc-runtime sc-for/{{}})
|
| 126 |
+
// ====================================================================
|
| 127 |
+
renderMeters(){
|
| 128 |
+
const d=this.disp;
|
| 129 |
+
const dims=[
|
| 130 |
+
['V','Valence','#55aaff','negative bias','positive bias',"Valence — emotional direction (0–255). 128 is neutral. \"I'm fine\" reads ~83 (uneasy), not positive."],
|
| 131 |
+
['A','Arousal','#ff8855','calm / flat','reactive / volatile','Arousal — energy level. Low = flat or numb, high = reactive/volatile.'],
|
| 132 |
+
['D','Dominance','#aa55ff','feels powerless','feels in control','Dominance — agency & power. Force Flow (who-does-what-to-whom) feeds this.'],
|
| 133 |
+
['U','Urgency','#ff5555','no time pressure','everything urgent','Urgency — time pressure. Starts at 0 and only rises with urgent cues.'],
|
| 134 |
+
['G','Gravity','#ffaa55','everything heavy','light & floating','Gravity — emotional weight. Low = crushing, high = light/floating.'],
|
| 135 |
+
['W','Self-Worth','#55ffaa','self-critical','self-assured','Self-Worth — self-evaluation. Low W amplifies negatives (W→V coupling).'],
|
| 136 |
+
['I','Intent','#ffff55','withdrawing','connecting','Intent — communicative direction, from withdraw to connect/control.']
|
| 137 |
+
];
|
| 138 |
+
const read=(val,lo,hi)=> val<80?lo: val<118?'slightly '+lo: val>180?hi: val>138?'slightly '+hi:'neutral';
|
| 139 |
+
let html='';
|
| 140 |
+
for(const [key,name,color,lo,hi,tip] of dims){
|
| 141 |
+
const val=Math.round(d[key.toLowerCase()]);
|
| 142 |
+
const frac=val/255*100, center=50;
|
| 143 |
+
const barLeft=Math.min(center,frac), barW=Math.max(Math.abs(frac-center),1.2);
|
| 144 |
+
const del=this.lastDelta[key.toLowerCase()]||0;
|
| 145 |
+
const deltaText= del===0?'':(del>0?'▲'+del:'▼'+Math.abs(del));
|
| 146 |
+
const deltaColor= del>0?'#5fd08a':del<0?'#ff8a8a':'#a9967c';
|
| 147 |
+
html+=`<div class="meter" data-tip="${this._esc(tip)}">
|
| 148 |
+
<div class="meter-top">
|
| 149 |
+
<span class="meter-key" style="background:${color}">${key}</span>
|
| 150 |
+
<span class="meter-label">${name}</span>
|
| 151 |
+
<span class="meter-delta" style="color:${deltaColor}">${deltaText}</span>
|
| 152 |
+
<span class="meter-num">${val}</span>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="meter-track"><div class="meter-center"></div>
|
| 155 |
+
<div class="meter-fill" style="left:${barLeft}%;width:${barW}%;background:${color}"></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="meter-reading">— ${read(val,lo,hi)}</div>
|
| 158 |
+
</div>`;
|
| 159 |
}
|
| 160 |
+
const el=this.q('#meters'); if(el) el.innerHTML=html;
|
| 161 |
+
},
|
| 162 |
+
renderNeeds(){
|
| 163 |
+
const nc=(v)=> v<25?'#ff6b6b': v<50?'#ffb24d':'#5fd08a';
|
| 164 |
+
let html='';
|
| 165 |
+
for(const k of ['hunger','energy','attention','thirst']){
|
| 166 |
+
const v=Math.round(Math.max(0,Math.min(100,this.needsObj[k])));
|
| 167 |
+
html+=`<div class="need-row">
|
| 168 |
+
<span class="need-label">${k}</span>
|
| 169 |
+
<div class="need-track"><div class="need-fill" style="width:${v}%;background:${nc(v)}"></div></div>
|
| 170 |
+
<span class="need-val">${v}</span>
|
| 171 |
+
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
+
const el=this.q('#needs'); if(el) el.innerHTML=html;
|
| 174 |
+
},
|
| 175 |
+
renderDistance(){
|
| 176 |
+
let dist;
|
| 177 |
+
if(this.backendDistance!=null) dist=this.backendDistance; // backend (NEW)
|
| 178 |
+
else if(this.haveSoul){ // compute from soul[]
|
| 179 |
+
dist=0; ['v','a','d','u','g','w','i'].forEach(k=>dist+=Math.abs(this.mood[k]-this.soul[k])); dist=Math.round(dist/7);
|
| 180 |
+
} else dist=null; // unknown -> dash
|
| 181 |
+
const el=this.q('#soulDistance');
|
| 182 |
+
if(el) el.textContent = dist==null? 'Δ —' : ('Δ '+dist);
|
| 183 |
+
},
|
| 184 |
+
renderTrace(trace){
|
| 185 |
+
if(!trace) return;
|
| 186 |
+
const words=this.q('#traceWords'), structs=this.q('#traceStructs'),
|
| 187 |
+
contribs=this.q('#traceContribs'), countEl=this.q('#structCount');
|
| 188 |
+
// physical (toy/care/pet): no language to parse
|
| 189 |
+
if(trace.kind==='physical'){
|
| 190 |
+
if(words) words.innerHTML=`<span class="muted">${this._esc(trace.label||'physical interaction')} — physical interaction, no language to parse.</span>`;
|
| 191 |
+
if(structs) structs.innerHTML='<span class="muted">— none in this message</span>';
|
| 192 |
+
if(contribs) contribs.innerHTML='';
|
| 193 |
+
if(countEl) countEl.textContent='0';
|
| 194 |
+
return;
|
| 195 |
+
}
|
| 196 |
+
// WORDS chips
|
| 197 |
+
const ws=(trace.words||[]);
|
| 198 |
+
if(words){
|
| 199 |
+
if(ws.length===0) words.innerHTML='<span class="muted">talk to it and the structural read shows up here.</span>';
|
| 200 |
+
else words.innerHTML=ws.slice(0,14).map(w=>{
|
| 201 |
+
const [bg,fg,bd]=this.roleColor(w.role);
|
| 202 |
+
return `<span class="w-chip" style="background:${bg};color:${fg};border:1px solid ${bd}" data-tip="${this._esc(this.roleTip(w.role))}">${this._esc(w.word||w.token||'')}</span>`;
|
| 203 |
+
}).join('');
|
| 204 |
+
}
|
| 205 |
+
// STRUCTURES
|
| 206 |
+
const sts=(trace.structures||[]);
|
| 207 |
+
if(countEl) countEl.textContent=String(sts.length);
|
| 208 |
+
if(structs){
|
| 209 |
+
if(sts.length===0) structs.innerHTML='<span class="muted">— none in this message</span>';
|
| 210 |
+
else structs.innerHTML=sts.map(s=>{
|
| 211 |
+
const nm=typeof s==='string'?s:(s.name||String(s));
|
| 212 |
+
return `<span class="s-chip">${this._esc(nm)}</span>`;
|
| 213 |
+
}).join('');
|
| 214 |
+
}
|
| 215 |
+
// CONTRIBUTORS — word + signed per-dim string from dv/da/dd/du/dg
|
| 216 |
+
const cs=(trace.contributors||[]);
|
| 217 |
+
if(contribs){
|
| 218 |
+
contribs.innerHTML=cs.slice(0,5).map(c=>{
|
| 219 |
+
const parts=[];
|
| 220 |
+
[['dv','V'],['da','A'],['dd','D'],['du','U'],['dg','G']].forEach(([k,dim])=>{
|
| 221 |
+
const val=c[k];
|
| 222 |
+
if(val!=null && val!==0) parts.push(dim+(val>0?'+':'')+val);
|
| 223 |
+
});
|
| 224 |
+
const dims=parts.join(' ')||'·';
|
| 225 |
+
return `<div class="contrib-row"><span class="contrib-word">${this._esc(c.word||c.token||'?')}</span><span class="contrib-dims">${dims}</span></div>`;
|
| 226 |
+
}).join('');
|
| 227 |
+
}
|
| 228 |
+
},
|
| 229 |
+
renderWhy(why){
|
| 230 |
+
const el=this.q('#whyText'); if(!el) return;
|
| 231 |
+
if(!why){ el.textContent='—'; return; }
|
| 232 |
+
el.innerHTML=this._esc(why).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>');
|
| 233 |
+
},
|
| 234 |
+
renderCrisis(){
|
| 235 |
+
const wrap=this.q('#crisisWrap'), bar=this.q('#crisisBar'), label=this.q('#crisisLabel');
|
| 236 |
+
if(!this.haveCrisis){
|
| 237 |
+
// DEFENSIVE: backend has no crisis -> zero it out, keep calm label
|
| 238 |
+
if(bar){ bar.style.width='0%'; bar.style.background='#5fd08a'; }
|
| 239 |
+
if(label){ label.textContent='—'; label.style.color='#5fd08a'; }
|
| 240 |
+
return;
|
| 241 |
+
}
|
| 242 |
+
const cr=this.crisis;
|
| 243 |
+
const lbl=cr<0.15?'calm':cr<0.4?'watch':cr<0.7?'concern':'high concern';
|
| 244 |
+
const col=cr<0.15?'#5fd08a':cr<0.4?'#ffd24d':cr<0.7?'#ff9d4d':'#ff5a5a';
|
| 245 |
+
if(bar){ bar.style.width=Math.round(cr*100)+'%'; bar.style.background=col; }
|
| 246 |
+
if(label){ label.textContent=lbl; label.style.color=col; }
|
| 247 |
+
},
|
| 248 |
+
renderMoodWord(){
|
| 249 |
+
const el=this.q('#moodWord'); if(el) el.textContent=this.moodWord||'…';
|
| 250 |
+
},
|
| 251 |
+
renderReadPhrase(){
|
| 252 |
+
const el=this.q('#readPhrase'); if(el) el.textContent=this.readPhrase;
|
| 253 |
+
},
|
| 254 |
+
renderDiary(){
|
| 255 |
+
const el=this.q('#diary'); if(!el) return;
|
| 256 |
+
if(!this.diary.length){ el.innerHTML='<span class="muted">moments you share will be remembered here.</span>'; return; }
|
| 257 |
+
el.innerHTML=this.diary.slice(0,9).map(d=>
|
| 258 |
+
`<div class="diary-row"><span class="diary-dot" style="color:${d.color}">${this._esc(d.dot)}</span>`+
|
| 259 |
+
`<div class="diary-text"><span class="body">${this._esc(d.text)}</span> <span class="mood">· ${this._esc(d.mood||'')}</span></div></div>`
|
| 260 |
+
).join('');
|
| 261 |
+
},
|
| 262 |
+
renderItems(){
|
| 263 |
+
const wrap=this.q('#items'); if(!wrap) return;
|
| 264 |
+
const playId=this.action&&this.action.id;
|
| 265 |
+
// only (re)build DOM when item set changes; positions are updated live in positionActionItem / drag
|
| 266 |
+
if(this._itemsBuilt) { this._applyItemLayout(); return; }
|
| 267 |
+
wrap.innerHTML='';
|
| 268 |
+
this.itemEls={};
|
| 269 |
+
for(const it of this.items){
|
| 270 |
+
const div=document.createElement('div');
|
| 271 |
+
div.className='item';
|
| 272 |
+
div.dataset.id=it.id;
|
| 273 |
+
div.setAttribute('data-tip', it.tip);
|
| 274 |
+
div.style.width=it.size+'%';
|
| 275 |
+
div.style.zIndex='5';
|
| 276 |
+
const inner=document.createElement('div');
|
| 277 |
+
inner.className='item-inner';
|
| 278 |
+
inner.innerHTML=this.ICONS(it.kind);
|
| 279 |
+
div.appendChild(inner);
|
| 280 |
+
this.itemEls[it.id]=inner;
|
| 281 |
+
div.addEventListener('pointerdown',(e)=>this.startDrag(e,it.id));
|
| 282 |
+
wrap.appendChild(div);
|
| 283 |
+
}
|
| 284 |
+
this._itemsBuilt=true;
|
| 285 |
+
this._applyItemLayout();
|
| 286 |
+
},
|
| 287 |
+
_applyItemLayout(){
|
| 288 |
+
const playId=this.action&&this.action.id;
|
| 289 |
+
for(const it of this.items){
|
| 290 |
+
const inner=this.itemEls[it.id]; if(!inner) continue;
|
| 291 |
+
const div=inner.closest('[data-id]'); if(!div) continue;
|
| 292 |
+
if(it.id===playId) continue; // positionActionItem owns it while playing
|
| 293 |
+
div.style.left=it.x+'%'; div.style.top=it.y+'%';
|
| 294 |
+
div.style.transform='translate(-50%,-100%)';
|
| 295 |
+
div.style.transition=(it.resting)?'left .5s cubic-bezier(.34,1.4,.5,1), top .5s cubic-bezier(.34,1.4,.5,1)':'none';
|
| 296 |
+
div.style.zIndex='5';
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
|
| 300 |
+
// ====================================================================
|
| 301 |
+
// MOOD / NEEDS APPLICATION (backend-fed)
|
| 302 |
+
// ====================================================================
|
| 303 |
+
// map backend appearance.mood[7] into our mood model (lerp like the redesign)
|
| 304 |
+
applyAppearance(ap){
|
| 305 |
+
if(!ap) return;
|
| 306 |
+
const m=ap.mood;
|
| 307 |
+
if(Array.isArray(m) && m.length>=7){
|
| 308 |
+
const C={v:m[0],a:m[1],d:m[2],u:m[3],g:m[4],w:m[5],i:m[6]};
|
| 309 |
+
const before={...this.mood};
|
| 310 |
+
['v','a','d','g','w','i'].forEach(k=>{ this.mood[k]=this.mood[k]*0.42 + C[k]*0.58; });
|
| 311 |
+
this.mood.u=C.u;
|
| 312 |
+
this.lastDelta={};
|
| 313 |
+
['v','a','d','u','g','w','i'].forEach(k=>{ this.lastDelta[k]=Math.round(this.mood[k])-Math.round(before[k]); });
|
| 314 |
+
}
|
| 315 |
+
},
|
| 316 |
+
// prefer backend deltas[] for the meter arrows when present
|
| 317 |
+
applyDeltas(deltas){
|
| 318 |
+
if(Array.isArray(deltas) && deltas.length>=7){
|
| 319 |
+
const keys=['v','a','d','u','g','w','i'];
|
| 320 |
+
this.lastDelta={}; keys.forEach((k,i)=>this.lastDelta[k]=deltas[i]);
|
| 321 |
+
}
|
| 322 |
+
},
|
| 323 |
+
applyNeeds(needs){
|
| 324 |
+
if(!needs) return;
|
| 325 |
+
['hunger','energy','attention','thirst'].forEach(k=>{
|
| 326 |
+
if(typeof needs[k]==='number') this.needsObj[k]=Math.max(0,Math.min(100,needs[k]));
|
| 327 |
});
|
| 328 |
+
},
|
| 329 |
+
applyCrisis(crisis){
|
| 330 |
+
if(crisis && typeof crisis.value==='number'){ this.haveCrisis=true; this.crisis=Math.max(0,Math.min(1,crisis.value)); }
|
| 331 |
+
},
|
| 332 |
+
applySoul(state){
|
| 333 |
+
if(Array.isArray(state.soul) && state.soul.length>=7){
|
| 334 |
+
const s=state.soul; this.haveSoul=true;
|
| 335 |
+
this.soul={v:s[0],a:s[1],d:s[2],u:s[3],g:s[4],w:s[5],i:s[6]};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
+
if(typeof state.distance==='number') this.backendDistance=Math.round(state.distance);
|
| 338 |
+
},
|
| 339 |
+
|
| 340 |
+
// single entry point for any backend response (say / care / toy / snapshot)
|
| 341 |
+
applyState(state, opts){
|
| 342 |
+
opts=opts||{};
|
| 343 |
+
this.applyAppearance(state.appearance);
|
| 344 |
+
if(opts.useDeltas) this.applyDeltas(state.deltas);
|
| 345 |
+
this.applyNeeds(state.needs);
|
| 346 |
+
this.applyCrisis(state.crisis); // defensive: absent -> haveCrisis stays false
|
| 347 |
+
this.applySoul(state); // defensive: absent -> distance from compute or dash
|
| 348 |
+
if(state.mood_word) this.moodWord=state.mood_word;
|
| 349 |
+
if(state.read && state.read.length>=7){
|
| 350 |
+
this.readPhrase=this.feltPhrase({v:state.read[0],a:state.read[1],d:state.read[2],u:state.read[3],g:state.read[4],w:state.read[5],i:state.read[6]});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
}
|
| 352 |
+
if(state.trace) this.renderTrace(state.trace);
|
| 353 |
+
if(state.why!=null) this.renderWhy(state.why);
|
| 354 |
+
// panel repaint
|
| 355 |
+
this.renderNeeds(); this.renderCrisis(); this.renderDistance();
|
| 356 |
+
this.renderMoodWord(); this.renderReadPhrase();
|
| 357 |
+
},
|
| 358 |
+
|
| 359 |
+
// ====================================================================
|
| 360 |
+
// ANIMATION LOOP (ported verbatim from the redesign)
|
| 361 |
+
// ====================================================================
|
| 362 |
+
step(now){
|
| 363 |
+
try{ this._step(now); }catch(err){ if(!this._warned){console.warn('clanker step',err);this._warned=true;} }
|
| 364 |
+
this._raf=requestAnimationFrame(this.step.bind(this));
|
| 365 |
+
},
|
| 366 |
+
_step(now){
|
| 367 |
+
const dt=Math.min(0.05,(now-this.lastTs)/1000); this.lastTs=now;
|
| 368 |
+
const t=(now-this.t0)/1000;
|
| 369 |
+
// disp lerp toward mood (server-authoritative mood already set on each response/poll)
|
| 370 |
+
['v','a','d','u','g','w','i'].forEach(k=>{ this.disp[k]+=(this.mood[k]-this.disp[k])*Math.min(1,4*dt); });
|
| 371 |
+
|
| 372 |
+
this.autoConsume(dt);
|
| 373 |
+
this.behavior = this.action ? 'idle' : this.pickBehavior();
|
| 374 |
+
if(this.behavior==='tantrum' && performance.now()-this.lastSpeak>1700){ this.lastSpeak=performance.now(); this.speak(); }
|
| 375 |
+
this.drawCreature(t);
|
| 376 |
+
this.positionActionItem();
|
| 377 |
+
|
| 378 |
+
this.uiAcc+=dt;
|
| 379 |
+
if(this.uiAcc>0.22){ this.uiAcc=0; this.renderMeters(); }
|
| 380 |
+
},
|
| 381 |
+
|
| 382 |
+
drawCreature(t){
|
| 383 |
+
const r=this.root; if(!r) return;
|
| 384 |
+
const g=(id)=>r.querySelector('#'+id);
|
| 385 |
+
const d=this.disp; const aN=(d.a-128)/127, vN=(d.v-128)/127, iN=(d.i-128)/127;
|
| 386 |
+
const energy=this.needsObj.energy;
|
| 387 |
+
const act=this.action&&this.action.type; const beh=this.behavior;
|
| 388 |
+
const sleepy=(energy<24&&aN<0&&beh==='idle'&&!act);
|
| 389 |
+
|
| 390 |
+
// ---- body transform (bob / slump / shake / breathe) ----
|
| 391 |
+
let tx=0, ty=0, sc=1, scY=1, rotB=0;
|
| 392 |
+
const breathe=1+Math.sin(t*1.8)*0.012;
|
| 393 |
+
if(beh==='tantrum'){
|
| 394 |
+
tx=Math.sin(t*42)*5; ty=Math.abs(Math.sin(t*9))*-7; rotB=Math.sin(t*40)*3; sc=1.02;
|
| 395 |
+
this.shake=4.5;
|
| 396 |
+
} else if(beh==='sad'){
|
| 397 |
+
ty=10; scY=0.93; tx=Math.sin(t*0.7)*2; this.shake=0;
|
| 398 |
+
} else if(act==='ball'){
|
| 399 |
+
ty=Math.sin(t*4.4)*4; tx=Math.sin(t*2.2)*4; this.shake=0;
|
| 400 |
+
} else if(act==='teddy'){
|
| 401 |
+
ty=Math.sin(t*3)*2; sc=1+Math.max(0,Math.sin(t*5.5))*0.02; this.shake=0;
|
| 402 |
+
} else if(act==='balloon'){
|
| 403 |
+
ty=Math.sin(t*3.1)*5-2; tx=Math.sin(t*1.7)*6; this.shake=0;
|
| 404 |
+
} else if(act==='music'){
|
| 405 |
+
tx=Math.sin(t*2.2)*7; rotB=Math.sin(t*2.2)*3; this.shake=0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
} else {
|
| 407 |
+
let bobAmp=1.4+Math.max(0,aN)*4.0; if(energy<30)bobAmp*=0.4;
|
| 408 |
+
ty=Math.sin(t*(1.5+Math.max(0,aN)*3.2))*bobAmp;
|
| 409 |
+
tx=Math.sin(t*0.85)*(1.4+Math.max(0,aN)*1.6);
|
| 410 |
+
if(d.g<120)ty+=(120-d.g)/4.5;
|
| 411 |
+
if(d.v>166&&d.a>150)rotB=Math.sin(t*6)*3;
|
| 412 |
+
this.shake=0;
|
| 413 |
}
|
| 414 |
+
const blob=g('blob'); if(blob) blob.setAttribute('transform',`translate(${tx.toFixed(2)},${ty.toFixed(2)}) rotate(${rotB.toFixed(2)} 100 120) scale(${(sc*breathe).toFixed(3)},${(scY*breathe).toFixed(3)})`);
|
| 415 |
+
if(this.stageEl){ const sh=this.shake; this.stageEl.style.transform = sh>0?`translate(${(Math.random()*2-1)*sh}px,${(Math.random()*2-1)*sh}px)`:''; }
|
| 416 |
+
|
| 417 |
+
// ---- eyes: blink + idle dart + state ----
|
| 418 |
+
if(t>this.nextBlink){ this.blinkUntil=t+0.12; this.nextBlink=t+2.4+Math.random()*3; }
|
| 419 |
+
const blinking=t<this.blinkUntil;
|
| 420 |
+
if(t>this.nextDart){ this.dart=(Math.random()<0.5?-1:1)*3; this.nextDart=t+1.6+Math.random()*3.2; this._dartEnd=t+0.9; }
|
| 421 |
+
if(t>this._dartEnd)this.dart=0;
|
| 422 |
+
let eyeRy=11, eyeDx=this.dart;
|
| 423 |
+
if(sleepy)eyeRy=4; else if(act==='teddy'||(beh==='idle'&&d.v>168&&d.a<120))eyeRy=5;
|
| 424 |
+
else if(beh==='tantrum')eyeRy=8; else if(beh==='sad')eyeRy=7; else if(aN>0.5)eyeRy=13;
|
| 425 |
+
if(act==='mirror')eyeRy=12;
|
| 426 |
+
if(blinking)eyeRy=2;
|
| 427 |
+
const eL=g('eyeL'),eR=g('eyeR');
|
| 428 |
+
if(eL){eL.setAttribute('ry',eyeRy); eL.setAttribute('cx',74+eyeDx);}
|
| 429 |
+
if(eR){eR.setAttribute('ry',eyeRy); eR.setAttribute('cx',126+eyeDx);}
|
| 430 |
+
const glL=g('glintL'),glR=g('glintR'); const glo=(blinking||eyeRy<5)?0:1;
|
| 431 |
+
if(glL){glL.setAttribute('opacity',glo);glL.setAttribute('cx',77+eyeDx);} if(glR){glR.setAttribute('opacity',glo);glR.setAttribute('cx',129+eyeDx);}
|
| 432 |
+
|
| 433 |
+
// ---- mouth ----
|
| 434 |
+
const mouth=g('mouth'); const chomp=this.eating>0||this.drinking>0;
|
| 435 |
+
if(mouth){
|
| 436 |
+
if(beh==='tantrum'){ const o=8+Math.abs(Math.sin(t*16))*8; mouth.setAttribute('d',`M84 150 Q100 ${150+o} 116 150 Q100 ${150+o*0.5} 84 150 Z`); mouth.setAttribute('fill','#5a1818'); }
|
| 437 |
+
else if(chomp){ const o=4+Math.abs(Math.sin(t*16))*9; mouth.setAttribute('d',`M86 150 Q100 ${150+o} 114 150 Q100 ${150+o*0.4} 86 150 Z`); mouth.setAttribute('fill','#5a2418'); }
|
| 438 |
+
else if(beh==='sad'){ mouth.setAttribute('d','M82 158 Q100 146 118 158'); mouth.setAttribute('fill','none'); }
|
| 439 |
+
else if((act==='ball'||act==='balloon'||act==='rattle')||(d.v>156&&d.a>150)){ mouth.setAttribute('d','M80 148 Q100 174 120 148 Q100 156 80 148 Z'); mouth.setAttribute('fill','#5a2418'); }
|
| 440 |
+
else { const depth=Math.max(-13,Math.min(20,16*vN)); mouth.setAttribute('d',`M82 152 Q100 ${(152+depth).toFixed(0)} 118 152`); mouth.setAttribute('fill','none'); }
|
| 441 |
+
}
|
| 442 |
+
// ---- brows ----
|
| 443 |
+
const bL=g('browL'),bR=g('browR');
|
| 444 |
+
let brow=0; if(beh==='tantrum')brow=24; else if(d.v<112&&d.d>132)brow=18; else if(beh==='sad'||(d.v<112&&d.d<122))brow=-16;
|
| 445 |
+
if(bL)bL.setAttribute('transform',`rotate(${brow} 86 96)`);
|
| 446 |
+
if(bR)bR.setAttribute('transform',`rotate(${-brow} 114 96)`);
|
| 447 |
+
|
| 448 |
+
// ---- cheeks ----
|
| 449 |
+
const cL=g('cheekL'),cR=g('cheekR'); let cf='#ff9bb0',co=0.32;
|
| 450 |
+
if(beh==='tantrum'){cf='#ff5a4d';co=0.6;} else if(d.v>152){co=0.5;} else if(d.v<112){co=0.12;}
|
| 451 |
+
[cL,cR].forEach(c=>{if(c){c.setAttribute('fill',cf);c.setAttribute('opacity',co);}});
|
| 452 |
+
|
| 453 |
+
// ---- tears (sad) ----
|
| 454 |
+
const tearO=(beh==='sad')?(0.5+0.5*Math.sin(t*2)):0;
|
| 455 |
+
const tearY=(beh==='sad')?(124+((t*40)%34)):124;
|
| 456 |
+
const tL=g('tearL'),tR=g('tearR');
|
| 457 |
+
if(tL){tL.setAttribute('opacity',tearO);tL.setAttribute('cy',tearY);}
|
| 458 |
+
if(tR){tR.setAttribute('opacity',tearO*0.8);tR.setAttribute('cy',124+((t*40+17)%34));}
|
| 459 |
+
// ---- steam (tantrum) ----
|
| 460 |
+
const stO=(beh==='tantrum')?(0.4+0.5*Math.abs(Math.sin(t*6))):0;
|
| 461 |
+
const sL=g('steamL'),sR=g('steamR');
|
| 462 |
+
if(sL){sL.setAttribute('opacity',stO);sL.setAttribute('cy',64-((t*30)%18));}
|
| 463 |
+
if(sR){sR.setAttribute('opacity',stO);sR.setAttribute('cy',64-((t*30+9)%18));}
|
| 464 |
+
|
| 465 |
+
// ---- emote glow ----
|
| 466 |
+
const mh=this.moodHue(d); const eg=g('emoteGlow');
|
| 467 |
+
if(eg){ eg.setAttribute('fill',`hsl(${mh.hue} ${mh.sat}% ${mh.light}%)`); eg.setAttribute('opacity',Math.min(0.42,Math.abs(vN)*0.46).toFixed(2)); eg.style.mixBlendMode = vN<0?'multiply':'screen'; }
|
| 468 |
+
|
| 469 |
+
// ---- arms ----
|
| 470 |
+
const aL=g('armL'),aR=g('armR'); let lr=0,rr=0;
|
| 471 |
+
if(beh==='tantrum'){ lr=-70+Math.sin(t*22)*40; rr=70-Math.sin(t*22)*40; }
|
| 472 |
+
else if(beh==='sad'){ lr=20; rr=-20; }
|
| 473 |
+
else if(act==='teddy'){ lr=-64; rr=64; }
|
| 474 |
+
else if(act==='ball'){ const sw=Math.sin(t*4.4)*22; lr=-30+sw; rr=30-sw; }
|
| 475 |
+
else if(act==='balloon'){ const up=Math.abs(Math.sin(t*3.1))*40; lr=-20-up*0.4; rr=-60-up; }
|
| 476 |
+
else if(act==='rattle'){ rr=-50+Math.sin(t*30)*18; lr=8; }
|
| 477 |
+
else if(act==='music'){ lr=-14; rr=-30; }
|
| 478 |
+
else if(act==='mirror'){ rr=-46; lr=10; }
|
| 479 |
+
else if(d.v>166&&d.a>150){ lr=-52; rr=52; }
|
| 480 |
+
else if(iN>0.18){ lr=-18*iN-6; rr=18*iN+6; }
|
| 481 |
+
else if(iN<-0.15){ lr=12; rr=-12; }
|
| 482 |
+
if(aL)aL.setAttribute('transform',`rotate(${lr.toFixed(1)} 52 150)`);
|
| 483 |
+
if(aR)aR.setAttribute('transform',`rotate(${rr.toFixed(1)} 148 150)`);
|
| 484 |
+
|
| 485 |
+
// ---- hearts (teddy) ----
|
| 486 |
+
const hearts=g('hearts'); if(hearts){ const show=(act==='teddy'); hearts.setAttribute('opacity',show?(0.6+0.4*Math.sin(t*4)):0); if(show)hearts.setAttribute('transform',`translate(0 ${-(t*18%30)})`); }
|
| 487 |
+
|
| 488 |
+
// ---- aura + mood dot ----
|
| 489 |
+
const aura=g('aura');
|
| 490 |
+
let auraHue=mh.hue, auraSat=mh.sat, auraL=mh.light;
|
| 491 |
+
if(beh==='tantrum'){auraHue=4;auraSat=85;auraL=55;} else if(beh==='sad'){auraHue=214;auraSat=50;auraL=52;}
|
| 492 |
+
if(aura){ aura.style.background=`radial-gradient(circle, hsl(${auraHue} ${auraSat}% ${auraL}% / ${(0.42+Math.abs(vN)*0.28+(beh!=='idle'?0.12:0)).toFixed(2)}), transparent 66%)`; }
|
| 493 |
+
const md=g('moodDot'); if(md){md.style.background=`hsl(${auraHue} ${auraSat}% ${auraL}%)`; md.style.boxShadow=`0 0 9px hsl(${auraHue} ${auraSat}% ${auraL}%)`;}
|
| 494 |
+
|
| 495 |
+
// ---- zzz ----
|
| 496 |
+
const zz=g('zzz'); if(zz){ zz.setAttribute('opacity', sleepy? (0.4+0.5*Math.sin(t*2)).toFixed(2):0); zz.setAttribute('y', sleepy? (60+Math.sin(t*1.4)*6).toFixed(0):60); }
|
| 497 |
+
},
|
| 498 |
+
|
| 499 |
+
// ====================================================================
|
| 500 |
+
// BOWLS (cosmetic fill; server owns the actual need value)
|
| 501 |
+
// ====================================================================
|
| 502 |
+
bowlSVG(level){
|
| 503 |
+
const f=Math.max(0,Math.min(1,level/100)); const fy=18-f*9; const op=f>0.04?1:0;
|
| 504 |
+
return `<svg viewBox="0 0 46 34" width="100%"><ellipse cx="23" cy="29" rx="17" ry="4" fill="rgba(0,0,0,.18)"/><path d="M4 16 h38 a19 14 0 0 1 -38 0Z" fill="#c97a4a"/><ellipse cx="23" cy="16" rx="19" ry="5.5" fill="#7a3f22"/><g opacity="${op}"><path d="M${10} ${fy+4} q13 -7 26 0 l0 6 q-13 6 -26 0Z" fill="#d6a04a"/><circle cx="16" cy="${fy+4}" r="2.4" fill="#a85f2a"/><circle cx="28" cy="${fy+3}" r="2.2" fill="#a85f2a"/><circle cx="23" cy="${fy+6}" r="2" fill="#b8722f"/></g></svg>`;
|
| 505 |
+
},
|
| 506 |
+
waterSVG(level){
|
| 507 |
+
const f=Math.max(0,Math.min(1,level/100)); const fy=18-f*9; const op=f>0.04?1:0;
|
| 508 |
+
return `<svg viewBox="0 0 46 34" width="100%"><ellipse cx="23" cy="29" rx="17" ry="4" fill="rgba(0,0,0,.18)"/><path d="M4 16 h38 a19 14 0 0 1 -38 0Z" fill="#9fb8cc"/><ellipse cx="23" cy="16" rx="19" ry="5.5" fill="#3f6f8f"/><g opacity="${op}"><path d="M${9} ${fy+5} q14 -6 28 0 l0 5 q-14 6 -28 0Z" fill="#6fb6e6"/><ellipse cx="23" cy="${fy+5}" rx="13" ry="2.6" fill="#9bd6f5"/><circle cx="18" cy="${fy+4}" r="1.4" fill="#fff" opacity=".7"/></g></svg>`;
|
| 509 |
+
},
|
| 510 |
+
refreshBowls(){
|
| 511 |
+
const r=this.root; if(!r)return;
|
| 512 |
+
const fb=r.querySelector('#foodFill'); if(fb)fb.innerHTML=this.bowlSVG(this.food);
|
| 513 |
+
const wb=r.querySelector('#waterFill'); if(wb)wb.innerHTML=this.waterSVG(this.water);
|
| 514 |
+
},
|
| 515 |
+
fillFood(){ this.ensureAudio(); this.food=100; this.refreshBowls(); this.popBubble('🍪'); this.logDiary('bowl filled with food','#ffd24d','🍪'); this.chirp(360,0.12); this.care('feed'); },
|
| 516 |
+
fillWater(){ this.ensureAudio(); this.water=100; this.refreshBowls(); this.popBubble('💧'); this.logDiary('bowl filled with water','#6fb6e6','💧'); this.chirp(520,0.12); this.care('water'); },
|
| 517 |
+
chirp(f,d){ this.ensureAudio(); if(this.ac&&this.soundOn)this.blip(f,this.ac.currentTime,d); },
|
| 518 |
+
popBubble(txt){ const eb=this.root&&this.root.querySelector('#bubble'); if(eb){eb.textContent=txt;eb.style.opacity='1';eb.style.animation='popIn .3s ease';clearTimeout(this._eb);this._eb=setTimeout(()=>{eb.style.opacity='0';eb.style.animation='';},1300);} },
|
| 519 |
+
|
| 520 |
+
// ====================================================================
|
| 521 |
+
// TOY PLAY (animation client-side; fires backend per item)
|
| 522 |
+
// ====================================================================
|
| 523 |
+
startPlay(it){
|
| 524 |
+
this.ensureAudio();
|
| 525 |
+
const dur={ball:3.0,teddy:2.8,balloon:3.0,rattle:2.4,music:3.4,mirror:2.6}[it.kind]||2.6;
|
| 526 |
+
this.action={type:it.kind,id:it.id,t0s:performance.now()/1000,dur};
|
| 527 |
+
it.held=true; it.resting=false;
|
| 528 |
+
this.speak(); this.popBubble(this.emoteFrom(this.disp));
|
| 529 |
+
// fire the real backend interaction
|
| 530 |
+
if(it.ep==='care') this.care(it.name);
|
| 531 |
+
else this.toy(it.name);
|
| 532 |
+
clearTimeout(this._actEnd); this._actEnd=setTimeout(()=>this.endPlay(),dur*1000);
|
| 533 |
+
},
|
| 534 |
+
endPlay(){
|
| 535 |
+
const a=this.action; this.action=null; if(!a)return;
|
| 536 |
+
const fx=43+Math.random()*14, fy=85+Math.random()*5;
|
| 537 |
+
const inner=this.itemEls[a.id], wrap=inner&&inner.closest('[data-id]');
|
| 538 |
+
if(wrap){ wrap.style.transition=''; wrap.style.transform=''; wrap.style.zIndex=''; }
|
| 539 |
+
const it=this.items.find(i=>i.id===a.id);
|
| 540 |
+
if(it){ it.held=false; it.x=fx; it.y=fy; it.resting=true; }
|
| 541 |
+
this._applyItemLayout(); this.saveItems();
|
| 542 |
+
},
|
| 543 |
+
positionActionItem(){
|
| 544 |
+
if(!this.action) return; const a=this.action; const inner=this.itemEls[a.id]; if(!inner)return;
|
| 545 |
+
const wrap=inner.closest('[data-id]'); if(!wrap)return;
|
| 546 |
+
const e=(performance.now()/1000)-a.t0s;
|
| 547 |
+
let x=50,y=50,rot=0,sc=1,anchor='-50%,-50%';
|
| 548 |
+
if(a.type==='ball'){ const ph=Math.abs(Math.sin(e*4.4)); y=48+ph*32; x=50+Math.sin(e*2.2)*5; rot=(e*240)%360; }
|
| 549 |
+
else if(a.type==='teddy'){ x=50; y=53; sc=1+Math.max(0,Math.sin(e*5.5))*0.12; rot=Math.sin(e*5.5)*4; }
|
| 550 |
+
else if(a.type==='balloon'){ x=50+Math.sin(e*1.7)*10; y=26+Math.sin(e*3.1)*4; anchor='-50%,-50%'; }
|
| 551 |
+
else if(a.type==='rattle'){ x=60; y=46; rot=Math.sin(e*30)*26; }
|
| 552 |
+
else if(a.type==='music'){ x=41; y=54; rot=Math.sin(e*2.2)*6; }
|
| 553 |
+
else if(a.type==='mirror'){ x=58; y=45; rot=Math.sin(e*2)*4; }
|
| 554 |
+
this.actPos={x,y,rot,sc};
|
| 555 |
+
wrap.style.transition='none';
|
| 556 |
+
wrap.style.left=x+'%'; wrap.style.top=y+'%';
|
| 557 |
+
wrap.style.transform=`translate(${anchor}) rotate(${rot}deg) scale(${sc})`;
|
| 558 |
+
wrap.style.zIndex=a.type==='balloon'?3:6;
|
| 559 |
+
},
|
| 560 |
+
|
| 561 |
+
// ====================================================================
|
| 562 |
+
// AUTO EAT / DRINK (cosmetic; server owns need value via /care)
|
| 563 |
+
// ====================================================================
|
| 564 |
+
autoConsume(dt){
|
| 565 |
+
if(this.action) return;
|
| 566 |
+
const n=this.needsObj;
|
| 567 |
+
if(this.food>0 && n.hunger<99){
|
| 568 |
+
const rr=22*dt; this.food=Math.max(0,this.food-rr);
|
| 569 |
+
this.eating=0.5; this.refreshBowls();
|
| 570 |
+
if(performance.now()-this.lastSpeak>1400){ this.lastSpeak=performance.now(); this.chirp(300+Math.random()*120,0.06); }
|
| 571 |
+
if(this.food<=0){ this.logDiary('finished eating','#5fd08a','♥'); this.popBubble('^‿^'); this.renderDiary(); }
|
| 572 |
+
} else this.eating=Math.max(0,this.eating-dt);
|
| 573 |
+
if(this.water>0 && n.thirst<99){
|
| 574 |
+
const rr=24*dt; this.water=Math.max(0,this.water-rr);
|
| 575 |
+
this.drinking=0.5; this.refreshBowls();
|
| 576 |
+
} else this.drinking=Math.max(0,this.drinking-dt);
|
| 577 |
+
},
|
| 578 |
+
|
| 579 |
+
// ====================================================================
|
| 580 |
+
// BEHAVIOR FSM (idle | sad | tantrum)
|
| 581 |
+
// ====================================================================
|
| 582 |
+
pickBehavior(){
|
| 583 |
+
const d=this.disp; const n=this.needsObj;
|
| 584 |
+
const neglected=(n.attention<12||n.hunger<10);
|
| 585 |
+
if((d.v<104 && d.a>148 && d.d>122) || (neglected && d.a>120)) return 'tantrum';
|
| 586 |
+
if((this.haveCrisis&&this.crisis>0.55) || (d.v<106 && d.a<130)) return 'sad';
|
| 587 |
+
return 'idle';
|
| 588 |
+
},
|
| 589 |
+
|
| 590 |
+
// ====================================================================
|
| 591 |
+
// AUDIO (WebAudio oscillator blips — the redesign's signature; kept)
|
| 592 |
+
// ====================================================================
|
| 593 |
+
ensureAudio(){ if(!this.soundOn) return; try{ if(!this.ac) this.ac=new (window.AudioContext||window.webkitAudioContext)(); if(this.ac.state==='suspended')this.ac.resume(); }catch(e){} },
|
| 594 |
+
blip(freq,t0,dur){ if(!this.ac)return; const o=this.ac.createOscillator(),gn=this.ac.createGain(); o.type='triangle'; o.frequency.setValueAtTime(freq,t0); o.frequency.linearRampToValueAtTime(freq*1.04,t0+dur); gn.gain.setValueAtTime(0.0001,t0); gn.gain.exponentialRampToValueAtTime(0.16,t0+0.012); gn.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(gn).connect(this.ac.destination); o.start(t0); o.stop(t0+dur+0.02); },
|
| 595 |
+
speak(serverText){
|
| 596 |
+
const d=this.disp;
|
| 597 |
+
let text;
|
| 598 |
+
if(serverText){ text=serverText; }
|
| 599 |
+
else {
|
| 600 |
+
const syl=['ba','da','la','na','mi','mu','wa','wo','dee','do','boo','ga','gee','sho','rup','meep','noo','wug','blee','va','du','gwi'];
|
| 601 |
+
const words=1+Math.round(Math.max(0,(d.a-110)/60))+(Math.random()<0.4?1:0);
|
| 602 |
+
text='';
|
| 603 |
+
for(let w=0;w<Math.min(4,words);w++){ const len=1+Math.floor(Math.random()*2); let word=''; for(let s=0;s<len;s++)word+=syl[Math.floor(Math.random()*syl.length)]; text+=(w?' ':'')+word; }
|
| 604 |
+
if(d.v>150)text+=' ♪'; if(d.v<104)text+='…';
|
| 605 |
+
}
|
| 606 |
+
const bub=this.root&&this.root.querySelector('#simlish');
|
| 607 |
+
if(bub){ bub.textContent=text; bub.style.opacity='1'; clearTimeout(this._st); this._st=setTimeout(()=>{bub.style.opacity='0';},1700); }
|
| 608 |
+
const eb=this.root&&this.root.querySelector('#bubble');
|
| 609 |
+
if(eb){ eb.textContent=this.emoteFrom(d); eb.style.opacity='1'; eb.style.animation='popIn .3s ease'; clearTimeout(this._eb); this._eb=setTimeout(()=>{eb.style.opacity='0';eb.style.animation='';},1700); }
|
| 610 |
+
this.ensureAudio(); if(!this.ac||!this.soundOn)return;
|
| 611 |
+
const base=190+((d.v)/255)*300; const now=this.ac.currentTime;
|
| 612 |
+
const count=Math.max(2,Math.min(8,Math.round(String(text).replace(/[^a-z♪…]/g,'').length/2)));
|
| 613 |
+
const tempo=0.10-Math.max(0,(d.a-128)/127)*0.04;
|
| 614 |
+
for(let i=0;i<count;i++){ const f=base*(0.9+Math.random()*0.4)*(d.v>150&&i===count-1?1.3:1); this.blip(f, now+i*tempo, 0.09); }
|
| 615 |
+
},
|
| 616 |
+
|
| 617 |
+
// ====================================================================
|
| 618 |
+
// DIARY (local log + server moments)
|
| 619 |
+
// ====================================================================
|
| 620 |
+
logDiary(text,color,dot){
|
| 621 |
+
this.diary=[{text, color:color||'#ffd21e', dot:dot||'•', mood:this.moodWord}, ...this.diary].slice(0,9);
|
| 622 |
+
this.renderDiary();
|
| 623 |
+
},
|
| 624 |
+
_relTime(ts){
|
| 625 |
+
let ms; if(typeof ts==='number'){ ms=ts<1e10?ts*1000:ts; } else { ms=Date.parse(ts); }
|
| 626 |
+
if(!ms||isNaN(ms)) return '';
|
| 627 |
+
const diff=Math.floor((Date.now()-ms)/1000);
|
| 628 |
+
if(diff<60) return 'just now';
|
| 629 |
+
if(diff<3600) return Math.floor(diff/60)+'m ago';
|
| 630 |
+
if(diff<86400) return Math.floor(diff/3600)+'h ago';
|
| 631 |
+
return Math.floor(diff/86400)+'d ago';
|
| 632 |
+
},
|
| 633 |
+
async loadMoments(){
|
| 634 |
+
try{
|
| 635 |
+
const r=await fetch('/moments'); if(!r.ok) return;
|
| 636 |
+
const data=await r.json(); const events=data.events||[];
|
| 637 |
+
// hydrate notable server moments into the diary (newest first), keeping local entries too
|
| 638 |
+
const server=events.slice().reverse().slice(0,6).map(ev=>({
|
| 639 |
+
text:(ev.label||(ev.type||'moment').replace(/_/g,' ')),
|
| 640 |
+
color:'#a9967c', dot:'◆',
|
| 641 |
+
mood:ev.mood_word||this._relTime(ev.ts)
|
| 642 |
+
}));
|
| 643 |
+
// local entries first, then server moments that aren't dupes
|
| 644 |
+
const seen=new Set(this.diary.map(d=>d.text));
|
| 645 |
+
this.diary=[...this.diary, ...server.filter(s=>!seen.has(s.text))].slice(0,9);
|
| 646 |
+
this.renderDiary();
|
| 647 |
+
}catch(_){}
|
| 648 |
+
},
|
| 649 |
+
|
| 650 |
+
// ====================================================================
|
| 651 |
+
// BACKEND CALLS
|
| 652 |
+
// ====================================================================
|
| 653 |
+
async _post(url, body){
|
| 654 |
+
try{
|
| 655 |
+
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
| 656 |
+
if(!r.ok){ this.popBubble('…?'); return null; }
|
| 657 |
+
return await r.json();
|
| 658 |
+
}catch(_){ this.popBubble('…?'); return null; }
|
| 659 |
+
},
|
| 660 |
+
async send(){
|
| 661 |
+
const el=this.inputEl; if(!el)return; const text=el.value.trim(); if(!text)return;
|
| 662 |
+
this.ensureAudio(); el.value='';
|
| 663 |
+
this.logDiary('“'+(text.length>30?text.slice(0,30)+'…':text)+'”','#8fc0ff','✎');
|
| 664 |
+
const state=await this._post('/say',{text});
|
| 665 |
+
if(!state) return;
|
| 666 |
+
this.applyState(state,{useDeltas:true});
|
| 667 |
+
if(state.recognition && state.recognition.returning){ this.popBubble('welcome back!'); this.showRemembers(true); }
|
| 668 |
+
if(state.relation && state.relation.known) this.showRemembers(true);
|
| 669 |
+
this.speak(state.simlish);
|
| 670 |
+
this.loadMoments();
|
| 671 |
+
},
|
| 672 |
+
async care(action){
|
| 673 |
+
const state=await this._post('/care',{action});
|
| 674 |
+
if(!state) return;
|
| 675 |
+
this.applyState(state,{useDeltas:true});
|
| 676 |
+
this.speak(state.simlish);
|
| 677 |
+
this.loadMoments();
|
| 678 |
+
},
|
| 679 |
+
async toy(name){
|
| 680 |
+
const state=await this._post('/toy',{toy:name});
|
| 681 |
+
if(!state) return;
|
| 682 |
+
this.applyState(state,{useDeltas:true});
|
| 683 |
+
this.speak(state.simlish);
|
| 684 |
+
this.loadMoments();
|
| 685 |
+
},
|
| 686 |
+
petCreature(e){ if(e)e.preventDefault(); this.ensureAudio(); this.speak(); this.popBubble('♥'); this.logDiary('got petted','#5fd08a','♥'); this.care('pet'); },
|
| 687 |
+
|
| 688 |
+
showRemembers(on){
|
| 689 |
+
const el=this.q('#remembersHint'); if(el) el.style.display=on?'block':'none';
|
| 690 |
+
},
|
| 691 |
+
|
| 692 |
+
// ====================================================================
|
| 693 |
+
// SELF-SOOTHE (preserved from prior production app): walk to & carry a comfort item
|
| 694 |
+
// ====================================================================
|
| 695 |
+
triggerSoothe(item){
|
| 696 |
+
if(!item) return;
|
| 697 |
+
// map backend item name to our item id (musicbox->music; play->ball)
|
| 698 |
+
const map={musicbox:'music', play:'ball', ball:'ball', balloon:'balloon', rattle:'rattle', mirror:'mirror', teddy:'teddy'};
|
| 699 |
+
const id=map[item]||item;
|
| 700 |
+
const it=this.items.find(i=>i.id===id);
|
| 701 |
+
if(!it || this.action) return;
|
| 702 |
+
// cosmetic comfort: he picks it up and plays (no extra backend call; server already lifted mood)
|
| 703 |
+
this.action={type:it.kind,id:it.id,t0s:performance.now()/1000,dur:3.0};
|
| 704 |
+
it.held=true; it.resting=false;
|
| 705 |
+
this.logDiary('soothed himself with the '+id,'#c6a6ff','✷');
|
| 706 |
+
clearTimeout(this._actEnd); this._actEnd=setTimeout(()=>this.endPlay(),3000);
|
| 707 |
+
},
|
| 708 |
+
|
| 709 |
+
// ====================================================================
|
| 710 |
+
// DRAG (toy -> creature)
|
| 711 |
+
// ====================================================================
|
| 712 |
+
startDrag(e,id){
|
| 713 |
+
e.preventDefault(); this.ensureAudio();
|
| 714 |
+
const it=this.items.find(x=>x.id===id); if(!it)return;
|
| 715 |
+
this.dragId=id;
|
| 716 |
+
const wrap=e.currentTarget; this.dragWrap=wrap;
|
| 717 |
+
wrap.style.cursor='grabbing'; wrap.style.transition='none'; wrap.style.zIndex=20;
|
| 718 |
+
this.moveItemTo(e.clientX,e.clientY);
|
| 719 |
+
},
|
| 720 |
+
moveItemTo(cx,cy){
|
| 721 |
+
if(!this.stageEl||!this.dragId)return;
|
| 722 |
+
const r=this.stageEl.getBoundingClientRect();
|
| 723 |
+
let x=(cx-r.left)/r.width*100, y=(cy-r.top)/r.height*100;
|
| 724 |
+
x=Math.max(3,Math.min(97,x)); y=Math.max(8,Math.min(99,y));
|
| 725 |
+
const it=this.items.find(i=>i.id===this.dragId);
|
| 726 |
+
if(it){ it.x=x; it.y=y; it.resting=false; it.held=false; }
|
| 727 |
+
this._applyItemLayout();
|
| 728 |
+
},
|
| 729 |
+
onPointerMove(e){ if(this.dragId)this.moveItemTo(e.clientX,e.clientY); },
|
| 730 |
+
onPointerUp(e){
|
| 731 |
+
if(!this.dragId)return;
|
| 732 |
+
const id=this.dragId; this.dragId=null;
|
| 733 |
+
if(this.dragWrap){this.dragWrap.style.cursor='grab';this.dragWrap.style.zIndex='';}
|
| 734 |
+
const r=this.stageEl.getBoundingClientRect();
|
| 735 |
+
const x=(e.clientX-r.left)/r.width*100, y=(e.clientY-r.top)/r.height*100;
|
| 736 |
+
const dx=x-50, dy=y-52; const onCreature=Math.sqrt(dx*dx+dy*dy)<19;
|
| 737 |
+
const it=this.items.find(i=>i.id===id);
|
| 738 |
+
if(onCreature && it){
|
| 739 |
+
this.startPlay(it);
|
| 740 |
+
} else if(it){
|
| 741 |
+
const fy=Math.max(y, 72+Math.random()*16);
|
| 742 |
+
it.x=Math.max(4,Math.min(96,x)); it.y=Math.min(96,fy); it.resting=true; it.held=false;
|
| 743 |
+
this._applyItemLayout(); this.saveItems();
|
| 744 |
+
}
|
| 745 |
+
},
|
| 746 |
+
|
| 747 |
+
saveItems(){ try{ const pos={}; this.items.forEach(i=>pos[i.id]={x:i.x,y:i.y}); localStorage.setItem('clanker_items_v3',JSON.stringify(pos)); }catch(e){} },
|
| 748 |
+
restoreItems(){ try{ const raw=localStorage.getItem('clanker_items_v3'); if(!raw)return; const pos=JSON.parse(raw); this.items.forEach(i=>{ if(pos[i.id]){ i.x=pos[i.id].x; i.y=pos[i.id].y; } }); }catch(e){} },
|
| 749 |
+
|
| 750 |
+
// ====================================================================
|
| 751 |
+
// THEME (default = pixel, the authored aesthetic; cozy/amber available via setThemeTo)
|
| 752 |
+
// ====================================================================
|
| 753 |
+
setThemeTo(theme){
|
| 754 |
+
const r=this.root; if(!r) return;
|
| 755 |
+
if(theme==='pixel'){
|
| 756 |
+
const pixel={'--bg':'#0e1410','--panel':'#16201a','--panel2':'#0a0f0c','--ink':'#cdeec0','--dim':'#6f9a73','--accent':'#ffd21e','--line':'rgba(120,210,120,.22)','--rad':'4px','--fhead':"'Press Start 2P',monospace",'--fbody':"'VT323',monospace",'--wall1':'#26402c','--wall2':'#16271a','--floor':'#1f3322','--sky1':'#7fe6b0','--sky2':'#bdf48a','--cstroke':'5','--cstroke-col':'#0c1a10'};
|
| 757 |
+
Object.keys(pixel).forEach(k=>r.style.setProperty(k,pixel[k]));
|
| 758 |
+
r.style.fontFamily="'VT323',monospace"; r.style.fontSize='17px'; r.style.letterSpacing='.3px';
|
| 759 |
} else {
|
| 760 |
+
// cozy/amber: clear overrides so the inline var(--x, fallback) warm defaults apply
|
| 761 |
+
['--bg','--panel','--panel2','--ink','--dim','--accent','--line','--rad','--fhead','--fbody','--wall1','--wall2','--floor','--sky1','--sky2','--cstroke','--cstroke-col'].forEach(k=>r.style.removeProperty(k));
|
| 762 |
+
r.style.fontFamily=''; r.style.fontSize=''; r.style.letterSpacing='';
|
| 763 |
}
|
| 764 |
+
},
|
| 765 |
+
|
| 766 |
+
// ====================================================================
|
| 767 |
+
// TOOLTIPS (client-only, delegated)
|
| 768 |
+
// ====================================================================
|
| 769 |
+
bindTooltips(){
|
| 770 |
+
const tip=this.tipEl;
|
| 771 |
+
const show=(e)=>{
|
| 772 |
+
const host=e.target.closest('[data-tip]'); if(!host){ return; }
|
| 773 |
+
const txt=host.getAttribute('data-tip'); if(!txt) return;
|
| 774 |
+
tip.innerHTML=txt;
|
| 775 |
+
tip.style.left=(e.clientX+16)+'px'; tip.style.top=e.clientY+'px'; tip.style.opacity='1';
|
| 776 |
+
};
|
| 777 |
+
this.root.addEventListener('mouseover',(e)=>{ if(e.target.closest('[data-tip]')) show(e); });
|
| 778 |
+
this.root.addEventListener('mousemove',(e)=>{ const h=e.target.closest('[data-tip]'); if(h && tip.style.opacity==='1'){ tip.style.left=(e.clientX+16)+'px'; tip.style.top=e.clientY+'px'; } });
|
| 779 |
+
this.root.addEventListener('mouseout',(e)=>{ const from=e.target.closest('[data-tip]'); const to=e.relatedTarget&&e.relatedTarget.closest&&e.relatedTarget.closest('[data-tip]'); if(from && from!==to) tip.style.opacity='0'; });
|
| 780 |
+
},
|
| 781 |
+
|
| 782 |
+
// ---------- utils ----------
|
| 783 |
+
q(sel){ return this.root?this.root.querySelector(sel):document.querySelector(sel); },
|
| 784 |
+
_esc(s){ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); },
|
| 785 |
+
|
| 786 |
+
// ====================================================================
|
| 787 |
+
// BOOT
|
| 788 |
+
// ====================================================================
|
| 789 |
+
async init(){
|
| 790 |
+
this.root=document.getElementById('root');
|
| 791 |
+
this.stageEl=document.getElementById('stage');
|
| 792 |
+
this.inputEl=document.getElementById('msg');
|
| 793 |
+
this.tipEl=document.getElementById('tooltip');
|
| 794 |
+
|
| 795 |
+
this.items=this.defaultItems();
|
| 796 |
+
this.restoreItems();
|
| 797 |
+
|
| 798 |
+
// DEFAULT to the pixel aesthetic — this is the authored design
|
| 799 |
+
this.setThemeTo('pixel');
|
| 800 |
+
|
| 801 |
+
// events
|
| 802 |
+
const sendBtn=document.getElementById('send'); if(sendBtn) sendBtn.addEventListener('click',()=>this.send());
|
| 803 |
+
if(this.inputEl) this.inputEl.addEventListener('keydown',(e)=>{ if(e.key==='Enter') this.send(); });
|
| 804 |
+
const creature=document.getElementById('creature'); if(creature) creature.addEventListener('pointerdown',(e)=>this.petCreature(e));
|
| 805 |
+
const fb=document.getElementById('foodBowl'); if(fb) fb.addEventListener('click',()=>this.fillFood());
|
| 806 |
+
const wb=document.getElementById('waterBowl'); if(wb) wb.addEventListener('click',()=>this.fillWater());
|
| 807 |
+
const soundBtn=document.getElementById('soundBtn');
|
| 808 |
+
if(soundBtn) soundBtn.addEventListener('click',()=>{ this.soundOn=!this.soundOn; soundBtn.textContent=this.soundOn?'🔊':'🔇'; if(this.soundOn)this.ensureAudio(); });
|
| 809 |
+
|
| 810 |
+
window.addEventListener('pointermove',(e)=>this.onPointerMove(e));
|
| 811 |
+
window.addEventListener('pointerup',(e)=>this.onPointerUp(e));
|
| 812 |
+
|
| 813 |
+
this.bindTooltips();
|
| 814 |
+
this.renderItems();
|
| 815 |
+
this.refreshBowls();
|
| 816 |
+
this.renderMeters(); this.renderNeeds(); this.renderDistance();
|
| 817 |
+
this.renderCrisis(); this.renderMoodWord(); this.renderReadPhrase();
|
| 818 |
+
this.renderDiary();
|
| 819 |
+
|
| 820 |
+
// hydrate from snapshot
|
| 821 |
+
try{
|
| 822 |
+
const r=await fetch('/snapshot'); if(r.ok){
|
| 823 |
+
const state=await r.json();
|
| 824 |
+
// snapshot has no deltas; set mood directly (no arrow flicker)
|
| 825 |
+
this.applyState(state,{useDeltas:false});
|
| 826 |
+
if(state.self_soothe && state.self_soothe.item) this.triggerSoothe(state.self_soothe.item);
|
| 827 |
+
if(state.relation && state.relation.known) this.showRemembers(true);
|
| 828 |
+
}
|
| 829 |
+
}catch(_){}
|
| 830 |
+
|
| 831 |
+
this.loadMoments();
|
| 832 |
+
|
| 833 |
+
// animation loop
|
| 834 |
+
this.lastTs=performance.now();
|
| 835 |
+
this._raf=requestAnimationFrame(this.step.bind(this));
|
| 836 |
+
setTimeout(()=>{ this.speak(); },500);
|
| 837 |
+
|
| 838 |
+
// poll snapshot every 45s for needs + self_soothe
|
| 839 |
+
setInterval(()=>{
|
| 840 |
+
fetch('/snapshot').then(r=>r.json()).then(s=>{
|
| 841 |
+
if(s.needs){ this.applyNeeds(s.needs); this.renderNeeds(); }
|
| 842 |
+
if(s.crisis){ this.applyCrisis(s.crisis); this.renderCrisis(); }
|
| 843 |
+
if(s.self_soothe && s.self_soothe.item) this.triggerSoothe(s.self_soothe.item);
|
| 844 |
+
}).catch(()=>{});
|
| 845 |
+
},45000);
|
| 846 |
}
|
| 847 |
+
};
|
|
|
|
|
|
|
|
|
|
| 848 |
|
| 849 |
+
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',()=>App.init());
|
| 850 |
+
else App.init();
|
|
|
static/index.html
CHANGED
|
@@ -1,216 +1,232 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<
|
| 4 |
-
<meta
|
| 5 |
-
<title>clanker</title>
|
| 6 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
| 9 |
-
<link rel="stylesheet" href="/static/styles.css?v=
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
<
|
| 16 |
-
|
| 17 |
-
<div
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
<
|
| 21 |
-
<div
|
| 22 |
-
<div
|
| 23 |
-
<div
|
| 24 |
-
<div class="need-row"><span class="need-label">attention</span><div class="need-track"><div class="need-fill" id="need-attention"></div></div><span class="need-val" id="need-attention-val">—</span></div>
|
| 25 |
-
<div class="need-row"><span class="need-label">thirst</span><div class="need-track"><div class="need-fill" id="need-thirst"></div></div><span class="need-val" id="need-thirst-val">—</span></div>
|
| 26 |
</div>
|
| 27 |
</div>
|
| 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 |
-
<rect x="0" y="190" width="400" height="4" fill="rgba(100,60,20,0.35)"/>
|
| 73 |
-
<!-- window: upper-center -->
|
| 74 |
-
<image href="/static/assets/window.png" x="130" y="4" width="140" height="120" opacity="0.92" style="filter:drop-shadow(0 8px 24px rgba(255,220,120,0.4))"/>
|
| 75 |
-
<!-- shelf: upper-right -->
|
| 76 |
-
<image href="/static/assets/shelf.png" x="258" y="18" width="132" height="90" opacity="0.90"/>
|
| 77 |
-
<!-- plant: lower-left -->
|
| 78 |
-
<image href="/static/assets/plant.png" x="6" y="126" width="60" height="74" opacity="0.94"/>
|
| 79 |
-
|
| 80 |
-
<!-- WORLD OBJECTS: real toys/care items, all resting on the floor zone
|
| 81 |
-
(floor top y=192; every item's BOTTOM sits at ~196). Tap one and the
|
| 82 |
-
creature walks over and reacts. -->
|
| 83 |
-
<g id="world">
|
| 84 |
-
<g class="room-obj" data-toy="balloon" tabindex="0" role="button" aria-label="balloon">
|
| 85 |
-
<image href="/static/assets/toy-balloon.png?v=16" x="40" y="78" width="40" height="50"/>
|
| 86 |
-
</g>
|
| 87 |
-
<g class="room-obj" data-toy="musicbox" tabindex="0" role="button" aria-label="music box">
|
| 88 |
-
<image href="/static/assets/toy-musicbox.png?v=16" x="84" y="164" width="34" height="32"/>
|
| 89 |
-
</g>
|
| 90 |
-
<g class="room-obj" data-care="feed" tabindex="0" role="button" aria-label="food bowl">
|
| 91 |
-
<image href="/static/assets/obj-bowl-empty.png?v=16" x="118" y="166" width="42" height="30"/>
|
| 92 |
-
<image id="bowl-food" href="/static/assets/obj-bowl.png?v=16" x="118" y="166" width="42" height="30" opacity="1"/>
|
| 93 |
-
</g>
|
| 94 |
-
<g class="room-obj" data-care="water" tabindex="0" role="button" aria-label="water bowl">
|
| 95 |
-
<image href="/static/assets/obj-water-empty.png?v=16" x="168" y="166" width="42" height="30"/>
|
| 96 |
-
<image id="bowl-water" href="/static/assets/obj-water.png?v=16" x="168" y="166" width="42" height="30" opacity="1"/>
|
| 97 |
-
</g>
|
| 98 |
-
<g class="room-obj" data-care="play" tabindex="0" role="button" aria-label="ball">
|
| 99 |
-
<image href="/static/assets/obj-ball.png?v=16" x="222" y="166" width="30" height="30"/>
|
| 100 |
-
</g>
|
| 101 |
-
<g class="room-obj" data-toy="rattle" tabindex="0" role="button" aria-label="rattle">
|
| 102 |
-
<image href="/static/assets/toy-rattle.png?v=16" x="258" y="162" width="32" height="34"/>
|
| 103 |
-
</g>
|
| 104 |
-
<g class="room-obj" data-toy="mirror" tabindex="0" role="button" aria-label="mirror">
|
| 105 |
-
<image href="/static/assets/toy-mirror.png?v=16" x="298" y="160" width="32" height="36"/>
|
| 106 |
-
</g>
|
| 107 |
-
<g class="room-obj" data-toy="teddy" tabindex="0" role="button" aria-label="teddy bear">
|
| 108 |
-
<image href="/static/assets/toy-teddy.png?v=16" x="338" y="154" width="42" height="42"/>
|
| 109 |
-
</g>
|
| 110 |
-
</g>
|
| 111 |
-
|
| 112 |
-
<!-- aura glow: SVG circle with radialGradient, behind creature -->
|
| 113 |
-
<!-- cx/cy in scene coords = creature center: 200, ~168 (116*0.40+108=154.4; use 160 for visual) -->
|
| 114 |
-
<circle id="aura" cx="200" cy="160" r="76" fill="url(#aura-grad)"
|
| 115 |
-
opacity="0.6" style="mix-blend-mode:screen; pointer-events:none;"/>
|
| 116 |
-
|
| 117 |
-
<!-- contact shadow on floor beneath creature (tracks creature X) -->
|
| 118 |
-
<ellipse id="shadow" cx="200" cy="192" rx="40" ry="6" fill="#00000030"/>
|
| 119 |
-
|
| 120 |
-
<!-- creature: blob-place holds fixed scene position; #blob gets dynamic per-message transform -->
|
| 121 |
-
<g id="blob-place" transform="translate(160,108) scale(0.40)" style="cursor:pointer" tabindex="0" role="button" aria-label="pet the creature">
|
| 122 |
-
<g id="blob-anim">
|
| 123 |
-
<g id="blob" style="filter:drop-shadow(0 25px 50px rgba(0,0,0,0.28))">
|
| 124 |
-
<!-- body: big ROUND yellow face circle (always Hugging-Face yellow) -->
|
| 125 |
-
<circle id="body" cx="100" cy="116" r="74"/>
|
| 126 |
-
<!-- emotion glow: anime-style colored glow over the face (JS sets color/opacity) -->
|
| 127 |
-
<circle id="emote-glow" cx="100" cy="118" r="66" fill="url(#emote-grad)" opacity="0" pointer-events="none" style="mix-blend-mode:multiply"/>
|
| 128 |
-
<!-- body highlight -->
|
| 129 |
-
<ellipse cx="82" cy="86" rx="16" ry="10" fill="white" opacity="0.28" transform="rotate(-15,82,86)"/>
|
| 130 |
-
<!-- cheeks: blush/anger dots (JS recolors by emotion) -->
|
| 131 |
-
<ellipse id="cheek-l" cx="68" cy="136" rx="13" ry="9" fill="#ff9090" opacity="0.38"/>
|
| 132 |
-
<ellipse id="cheek-r" cx="132" cy="136" rx="13" ry="9" fill="#ff9090" opacity="0.38"/>
|
| 133 |
-
<!-- left arm/hand -->
|
| 134 |
-
<g id="arm-l">
|
| 135 |
-
<path id="arm-l-path" d="M50 148 Q28 130 18 112" stroke-width="14" stroke-linecap="round" fill="none"/>
|
| 136 |
-
<ellipse id="hand-l" cx="18" cy="108" rx="20" ry="17"/>
|
| 137 |
</g>
|
| 138 |
-
<!-- right
|
| 139 |
-
<g
|
| 140 |
-
<
|
| 141 |
-
<
|
|
|
|
|
|
|
|
|
|
| 142 |
</g>
|
| 143 |
-
<!--
|
| 144 |
-
<g
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
</div>
|
| 186 |
</div>
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
</div>
|
| 192 |
-
</div>
|
| 193 |
-
</div>
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
<
|
| 202 |
-
|
| 203 |
-
<div id="trace-contribs" class="contrib-list"></div>
|
| 204 |
-
<div id="trace-unknown" class="unknown-row hidden"></div>
|
| 205 |
-
</div>
|
| 206 |
-
<div id="moments" class="panel moments-panel">
|
| 207 |
-
<div class="panel-head">diary</div>
|
| 208 |
-
<div id="moments-list" class="moments-list"></div>
|
| 209 |
-
</div>
|
| 210 |
</div>
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
</div>
|
| 213 |
-
|
| 214 |
-
<
|
| 215 |
-
</body>
|
| 216 |
-
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="en"><head>
|
| 3 |
+
<meta charset="utf-8">
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 5 |
+
<title>clanker · a deterministic emotional creature</title>
|
| 6 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="/static/styles.css?v=21">
|
| 10 |
+
<script src="/static/app.js?v=21" defer></script>
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
+
|
| 14 |
+
<div id="root" style="position:relative;min-height:100vh;width:100%;background:var(--bg,#211710);color:var(--ink,#f4e8d6);font-family:var(--fbody,'JetBrains Mono',monospace);padding:18px;display:flex;flex-direction:column;gap:16px;overflow-x:hidden;">
|
| 15 |
+
|
| 16 |
+
<!-- ===== HEADER ===== -->
|
| 17 |
+
<header style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
|
| 18 |
+
<div style="display:flex;align-items:center;gap:11px;">
|
| 19 |
+
<div style="width:34px;height:34px;border-radius:11px;background:var(--accent,#ffd21e);display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px rgba(255,200,60,.35);">
|
| 20 |
+
<svg width="22" height="22" viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill="#26190d"></circle><circle cx="8.5" cy="11" r="1.7" fill="#ffd21e"></circle><circle cx="15.5" cy="11" r="1.7" fill="#ffd21e"></circle><path d="M7.5 15 Q12 19 16.5 15" stroke="#ffd21e" stroke-width="1.8" fill="none" stroke-linecap="round"></path></svg>
|
| 21 |
+
</div>
|
| 22 |
+
<div>
|
| 23 |
+
<div id="creatureName" style="font-family:var(--fhead,'Fredoka',sans-serif);font-weight:700;font-size:20px;letter-spacing:.3px;line-height:1;">Huggy</div>
|
| 24 |
+
<div style="font-size:11px;color:var(--dim,#a9967c);margin-top:3px;letter-spacing:.4px;">a deterministic emotional creature · powered by VADUGWI</div>
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
</div>
|
| 27 |
+
<div data-tip="VADUGWI: 7 deterministic dimensions (0–255, 128 neutral). 4,563-word vocabulary, 66 chess-like structural patterns, proximity fields (0.9× decay/word), phase system & Force Flow. Fully explainable. The live Space calls the real clanker engine + clanker-soul on the backend." style="cursor:help;font-size:10.5px;color:var(--dim,#a9967c);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:999px;padding:5px 11px;display:flex;align-items:center;gap:6px;">
|
| 28 |
+
<span style="width:7px;height:7px;border-radius:50%;background:#5fd08a;box-shadow:0 0 8px #5fd08a;"></span>7D engine · how it works
|
| 29 |
+
</div>
|
| 30 |
+
<div style="flex:1;"></div>
|
| 31 |
+
<button id="soundBtn" title="toggle babble sound" style="cursor:pointer;border:1px solid var(--line,rgba(255,210,140,.16));background:var(--panel2,#1a120b);color:var(--ink,#f4e8d6);border-radius:11px;width:38px;height:34px;font-size:15px;">🔊</button>
|
| 32 |
+
</header>
|
| 33 |
+
|
| 34 |
+
<!-- ===== GRID ===== -->
|
| 35 |
+
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:stretch;flex:1;">
|
| 36 |
+
|
| 37 |
+
<!-- LEFT: VADUGWI live -->
|
| 38 |
+
<section style="flex:1 1 300px;max-width:360px;min-width:268px;background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:var(--rad,18px);padding:16px;display:flex;flex-direction:column;gap:13px;">
|
| 39 |
+
<div style="display:flex;align-items:baseline;justify-content:space-between;">
|
| 40 |
+
<div style="font-family:var(--fhead,'Fredoka',sans-serif);font-weight:700;font-size:14px;letter-spacing:1.5px;">VADUGWI</div>
|
| 41 |
+
<div style="font-size:9.5px;color:var(--accent,#ffd21e);letter-spacing:2px;display:flex;align-items:center;gap:5px;"><span style="width:6px;height:6px;border-radius:50%;background:var(--accent,#ffd21e);animation:floaty 1.6s ease-in-out infinite;"></span>LIVE</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div style="font-size:11px;color:var(--dim,#a9967c);line-height:1.5;">this message read as<br><span id="readPhrase" style="color:var(--ink,#f4e8d6);font-size:13px;font-weight:600;">waiting…</span></div>
|
| 44 |
+
|
| 45 |
+
<!-- meters (JS fills 7 rows) -->
|
| 46 |
+
<div id="meters"></div>
|
| 47 |
|
| 48 |
+
<!-- needs -->
|
| 49 |
+
<div style="border-top:1px solid var(--line,rgba(255,210,140,.16));padding-top:11px;display:flex;flex-direction:column;gap:8px;">
|
| 50 |
+
<div data-tip="Tamagotchi-style needs decay over time. Feed, water, play with and pet it to keep them up — neglect pulls its mood down." style="cursor:help;font-size:11px;letter-spacing:1.5px;color:var(--dim,#a9967c);font-weight:600;">NEEDS</div>
|
| 51 |
+
<div id="needs"></div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<!-- soul / mood distance -->
|
| 55 |
+
<div data-tip="Three timescales (clanker-soul): the Conversational score → Mood (minutes–hours, blends & cushions) → Soul (the persistent baseline mood drifts back toward). Heavy events during a wound can breach straight into Soul." style="cursor:help;border-top:1px solid var(--line,rgba(255,210,140,.16));padding-top:11px;display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dim,#a9967c);">
|
| 56 |
+
<span>mood ⟶ soul drift</span>
|
| 57 |
+
<span id="soulDistance" style="color:var(--ink,#f4e8d6);font-weight:600;">Δ 0</span>
|
| 58 |
+
</div>
|
| 59 |
+
</section>
|
| 60 |
+
|
| 61 |
+
<!-- CENTER: room -->
|
| 62 |
+
<section style="flex:3 1 460px;min-width:320px;display:flex;flex-direction:column;gap:13px;">
|
| 63 |
+
<div id="stage" style="position:relative;width:100%;aspect-ratio:4/3;border-radius:var(--rad,18px);overflow:hidden;border:1px solid var(--line,rgba(255,210,140,.16));background:var(--wall2,#e9bd72);box-shadow:inset 0 0 80px rgba(40,20,0,.25);touch-action:none;user-select:none;">
|
| 64 |
+
|
| 65 |
+
<!-- room scene -->
|
| 66 |
+
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" style="position:absolute;inset:0;width:100%;height:100%;">
|
| 67 |
+
<defs>
|
| 68 |
+
<linearGradient id="wallg" x1="0" y1="0" x2="0" y2="1">
|
| 69 |
+
<stop offset="0%" stop-color="var(--wall1,#ffe8b0)"></stop><stop offset="100%" stop-color="var(--wall2,#e9bd72)"></stop>
|
| 70 |
+
</linearGradient>
|
| 71 |
+
<linearGradient id="skyg" x1="0" y1="0" x2="0" y2="1">
|
| 72 |
+
<stop offset="0%" stop-color="var(--sky1,#bfe3ff)"></stop><stop offset="100%" stop-color="var(--sky2,#ffd98a)"></stop>
|
| 73 |
+
</linearGradient>
|
| 74 |
+
<radialGradient id="sung" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#fff4c2"></stop><stop offset="100%" stop-color="#ffcf5e"></stop></radialGradient>
|
| 75 |
+
<radialGradient id="lightg" cx="50%" cy="14%" r="62%"><stop offset="0%" stop-color="rgba(255,244,200,.34)"></stop><stop offset="100%" stop-color="rgba(255,244,200,0)"></stop></radialGradient>
|
| 76 |
+
<radialGradient id="vigg" cx="50%" cy="52%" r="72%"><stop offset="52%" stop-color="rgba(40,20,0,0)"></stop><stop offset="100%" stop-color="rgba(30,15,0,.34)"></stop></radialGradient>
|
| 77 |
+
</defs>
|
| 78 |
+
<rect x="0" y="0" width="400" height="196" fill="url(#wallg)"></rect>
|
| 79 |
+
<rect x="0" y="196" width="400" height="104" fill="var(--floor,#c9a06a)"></rect>
|
| 80 |
+
<rect x="0" y="193" width="400" height="4" fill="rgba(90,55,18,.32)"></rect>
|
| 81 |
+
<!-- window, centered + aligned -->
|
| 82 |
+
<g>
|
| 83 |
+
<rect x="150" y="26" width="100" height="84" rx="7" fill="#fff" opacity=".5"></rect>
|
| 84 |
+
<rect x="155" y="31" width="90" height="74" rx="4" fill="url(#skyg)"></rect>
|
| 85 |
+
<circle cx="225" cy="52" r="13" fill="url(#sung)"></circle>
|
| 86 |
+
<path d="M170 96 q12 -16 24 -4 q10 -14 24 -2 l0 12 -48 0 z" fill="#fff" opacity=".5"></path>
|
| 87 |
+
<rect x="197" y="31" width="6" height="74" fill="#fff" opacity=".55"></rect>
|
| 88 |
+
<rect x="155" y="64" width="90" height="6" fill="#fff" opacity=".55"></rect>
|
| 89 |
+
<rect x="146" y="108" width="108" height="7" rx="3" fill="#caa46a"></rect>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</g>
|
| 91 |
+
<!-- shelf right -->
|
| 92 |
+
<g>
|
| 93 |
+
<rect x="300" y="60" width="86" height="7" rx="2" fill="#a9763f"></rect>
|
| 94 |
+
<rect x="310" y="40" width="9" height="20" rx="2" fill="#cf6a5a"></rect>
|
| 95 |
+
<rect x="321" y="36" width="9" height="24" rx="2" fill="#6aa6cf"></rect>
|
| 96 |
+
<rect x="332" y="43" width="9" height="17" rx="2" fill="#7dbf72"></rect>
|
| 97 |
+
<circle cx="360" cy="52" r="8" fill="#d8975a"></circle><rect x="356" y="50" width="8" height="10" fill="#b97e44"></rect>
|
| 98 |
</g>
|
| 99 |
+
<!-- plant lower-left -->
|
| 100 |
+
<g>
|
| 101 |
+
<path d="M30 196 q-16 -34 -6 -56 q8 12 6 30 q10 -22 24 -28 q-6 18 -18 34 q14 -10 26 -8 q-12 14 -28 24z" fill="#5a9a52"></path>
|
| 102 |
+
<path d="M30 196 l-12 0 6 -26 12 0 z" fill="#bf6a4a"></path>
|
| 103 |
+
</g>
|
| 104 |
+
<!-- rug -->
|
| 105 |
+
<ellipse cx="200" cy="252" rx="118" ry="26" fill="#d9544e" opacity=".22"></ellipse>
|
| 106 |
+
<ellipse cx="200" cy="252" rx="92" ry="19" fill="#e8a23a" opacity=".22"></ellipse>
|
| 107 |
+
<rect x="0" y="0" width="400" height="300" fill="url(#lightg)" pointer-events="none"></rect>
|
| 108 |
+
<rect x="0" y="0" width="400" height="300" fill="url(#vigg)" pointer-events="none"></rect>
|
| 109 |
+
</svg>
|
| 110 |
+
|
| 111 |
+
<!-- aura -->
|
| 112 |
+
<div id="aura" style="position:absolute;left:50%;top:54%;width:42%;height:42%;border-radius:50%;background:radial-gradient(circle,hsl(48 70% 60% / .55),transparent 68%);transform:translate(-50%,-50%);mix-blend-mode:screen;pointer-events:none;animation:auraPulse 3.4s ease-in-out infinite;filter:blur(4px);"></div>
|
| 113 |
+
|
| 114 |
+
<!-- creature -->
|
| 115 |
+
<svg id="creature" viewBox="0 0 200 240" style="position:absolute;left:50%;bottom:13%;width:30%;transform:translateX(-50%);overflow:visible;cursor:pointer;z-index:4;">
|
| 116 |
+
<ellipse id="shadow" cx="100" cy="224" rx="62" ry="11" fill="rgba(0,0,0,.28)"></ellipse>
|
| 117 |
+
<g id="blob">
|
| 118 |
+
<circle id="body" cx="100" cy="120" r="78" fill="var(--accent,#ffd21e)" stroke="var(--cstroke-col,transparent)" stroke-width="var(--cstroke,0)"></circle>
|
| 119 |
+
<circle id="emoteGlow" cx="100" cy="122" r="70" fill="hsl(48 80% 55%)" opacity="0" pointer-events="none"></circle>
|
| 120 |
+
<ellipse cx="78" cy="84" rx="18" ry="11" fill="#fff" opacity=".26" transform="rotate(-16 78 84)"></ellipse>
|
| 121 |
+
<ellipse id="cheekL" cx="62" cy="138" rx="14" ry="9" fill="#ff9bb0" opacity=".35"></ellipse>
|
| 122 |
+
<ellipse id="cheekR" cx="138" cy="138" rx="14" ry="9" fill="#ff9bb0" opacity=".35"></ellipse>
|
| 123 |
+
<g id="armL"><path d="M52 150 Q34 152 24 138" stroke="var(--accent,#ffd21e)" stroke-width="15" stroke-linecap="round" fill="none"></path><ellipse cx="22" cy="136" rx="15" ry="13" fill="var(--accent,#ffd21e)"></ellipse></g>
|
| 124 |
+
<g id="armR"><path d="M148 150 Q166 152 176 138" stroke="var(--accent,#ffd21e)" stroke-width="15" stroke-linecap="round" fill="none"></path><ellipse cx="178" cy="136" rx="15" ry="13" fill="var(--accent,#ffd21e)"></ellipse></g>
|
| 125 |
+
<line id="browL" x1="62" y1="96" x2="86" y2="96" stroke="#2a1a0c" stroke-width="5" stroke-linecap="round"></line>
|
| 126 |
+
<line id="browR" x1="114" y1="96" x2="138" y2="96" stroke="#2a1a0c" stroke-width="5" stroke-linecap="round"></line>
|
| 127 |
+
<ellipse id="eyeL" cx="74" cy="112" rx="9" ry="11" fill="#2a1a0c"></ellipse>
|
| 128 |
+
<ellipse id="eyeR" cx="126" cy="112" rx="9" ry="11" fill="#2a1a0c"></ellipse>
|
| 129 |
+
<circle id="glintL" cx="77" cy="108" r="3" fill="#fff"></circle>
|
| 130 |
+
<circle id="glintR" cx="129" cy="108" r="3" fill="#fff"></circle>
|
| 131 |
+
<path id="mouth" d="M82 150 Q100 166 118 150" stroke="#2a1a0c" stroke-width="5" fill="none" stroke-linecap="round"></path>
|
| 132 |
+
<path id="tearL" d="M70 124 q-4 6 0 9 q4 -3 0 -9Z" fill="#7ec8f5" opacity="0"></path>
|
| 133 |
+
<path id="tearR" d="M130 124 q-4 6 0 9 q4 -3 0 -9Z" fill="#7ec8f5" opacity="0"></path>
|
| 134 |
+
</g>
|
| 135 |
+
<circle id="steamL" cx="44" cy="64" r="6" fill="#cfd8e0" opacity="0"></circle>
|
| 136 |
+
<circle id="steamR" cx="156" cy="64" r="6" fill="#cfd8e0" opacity="0"></circle>
|
| 137 |
+
<g id="hearts" opacity="0"><path d="M100 44 c-5 -8 -16 -2 -10 6 l10 10 10 -10 c6 -8 -5 -14 -10 -6Z" fill="#ff7aa8"></path></g>
|
| 138 |
+
<text id="zzz" x="150" y="60" font-size="26" fill="#cfe0ff" opacity="0" font-family="Fredoka,sans-serif">z</text>
|
| 139 |
+
</svg>
|
| 140 |
+
|
| 141 |
+
<!-- items (toys — draggable; JS fills) -->
|
| 142 |
+
<div id="items"></div>
|
| 143 |
+
|
| 144 |
+
<!-- feeding stations (fixed — click to fill) -->
|
| 145 |
+
<div id="foodBowl" data-tip="Click to fill the food bowl. Huggy walks over and eats on his own when there's food and he's hungry." style="position:absolute;left:14%;bottom:5%;width:10%;cursor:pointer;z-index:3;filter:drop-shadow(0 4px 5px rgba(0,0,0,.3));">
|
| 146 |
+
<div id="foodFill" style="width:100%;"></div>
|
| 147 |
+
</div>
|
| 148 |
+
<div id="waterBowl" data-tip="Click to fill the water bowl. Huggy drinks automatically when thirsty." style="position:absolute;left:25%;bottom:4%;width:10%;cursor:pointer;z-index:3;filter:drop-shadow(0 4px 5px rgba(0,0,0,.3));">
|
| 149 |
+
<div id="waterFill" style="width:100%;"></div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- mood chip -->
|
| 153 |
+
<div style="position:absolute;top:12px;right:12px;background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:999px;padding:7px 14px;font-size:13px;font-weight:600;font-family:var(--fhead,'Fredoka',sans-serif);display:flex;align-items:center;gap:8px;z-index:6;">
|
| 154 |
+
<span id="moodDot" style="width:9px;height:9px;border-radius:50%;background:hsl(48 70% 55%);box-shadow:0 0 9px hsl(48 70% 55%);"></span><span id="moodWord">calm</span>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<!-- remembers-you hint (welcome back / known device) -->
|
| 158 |
+
<div id="remembersHint" style="display:none;position:absolute;top:12px;left:12px;background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:999px;padding:6px 12px;font-size:11px;font-weight:600;font-family:var(--fhead,'Fredoka',sans-serif);color:var(--ink,#f4e8d6);z-index:6;">it remembers you 🤍</div>
|
| 159 |
+
|
| 160 |
+
<!-- emoticon bubble -->
|
| 161 |
+
<div id="bubble" style="position:absolute;left:50%;top:18%;transform:translate(-50%,-50%);background:#fff;color:#2a1a0c;border-radius:14px;padding:7px 13px;font-family:var(--fbody,monospace);font-size:17px;font-weight:600;opacity:0;pointer-events:none;z-index:7;box-shadow:0 6px 18px rgba(0,0,0,.25);">·_·</div>
|
| 162 |
+
<!-- simlish -->
|
| 163 |
+
<div id="simlish" style="position:absolute;left:50%;top:30%;transform:translate(-50%,-50%);color:#fff;font-family:var(--fhead,'Fredoka',sans-serif);font-size:15px;font-weight:600;opacity:0;pointer-events:none;z-index:7;text-shadow:0 2px 6px rgba(20,12,4,.8);white-space:nowrap;"></div>
|
| 164 |
+
|
| 165 |
+
<!-- hint -->
|
| 166 |
+
<div style="position:absolute;bottom:10px;left:50%;transform:translateX(-50%);background:rgba(26,18,11,.66);backdrop-filter:blur(3px);border-radius:999px;padding:6px 14px;font-size:10.5px;color:#f4e8d6;z-index:6;white-space:nowrap;">drag a toy onto <span class="cn">Huggy</span> — he plays with it, then sets it down · click the bowls to fill them · tap <span class="cn">Huggy</span> to pet</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<!-- dock -->
|
| 170 |
+
<div style="display:flex;flex-direction:column;gap:9px;">
|
| 171 |
+
<div style="display:flex;gap:9px;align-items:center;background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:14px;padding:7px 7px 7px 15px;">
|
| 172 |
+
<input id="msg" placeholder="say something to Huggy…" autocomplete="off" style="flex:1;background:none;border:none;outline:none;color:var(--ink,#f4e8d6);font-family:var(--fbody,monospace);font-size:14px;">
|
| 173 |
+
<button id="send" style="cursor:pointer;border:none;background:var(--accent,#ffd21e);color:#1a120b;border-radius:10px;padding:9px 18px;font-family:var(--fhead,'Fredoka',sans-serif);font-weight:700;font-size:13px;">send</button>
|
| 174 |
</div>
|
| 175 |
</div>
|
| 176 |
+
</section>
|
| 177 |
+
|
| 178 |
+
<!-- RIGHT: detection + diary -->
|
| 179 |
+
<section style="flex:1 1 300px;max-width:368px;min-width:268px;display:flex;flex-direction:column;gap:16px;">
|
| 180 |
+
<div style="background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:var(--rad,18px);padding:16px;display:flex;flex-direction:column;gap:12px;">
|
| 181 |
+
<div style="font-family:var(--fhead,'Fredoka',sans-serif);font-weight:700;font-size:14px;letter-spacing:.5px;">what it detected</div>
|
| 182 |
+
|
| 183 |
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
| 184 |
+
<div style="font-size:10px;letter-spacing:1.5px;color:var(--dim,#a9967c);">WORDS · roles</div>
|
| 185 |
+
<div id="traceWords" style="display:flex;flex-wrap:wrap;gap:5px;min-height:24px;">
|
| 186 |
+
<span style="font-size:11px;color:var(--dim,#a9967c);">talk to it and the structural read shows up here.</span>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
| 191 |
+
<div style="font-size:10px;letter-spacing:1.5px;color:var(--dim,#a9967c);">STRUCTURES · <span id="structCount">0</span> of 66 patterns</div>
|
| 192 |
+
<div id="traceStructs" style="display:flex;flex-wrap:wrap;gap:5px;min-height:18px;">
|
| 193 |
+
<span style="font-size:11px;color:var(--dim,#a9967c);">— none in this message</span>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
| 198 |
+
<div style="font-size:10px;letter-spacing:1.5px;color:var(--dim,#a9967c);">TOP CONTRIBUTORS</div>
|
| 199 |
+
<div id="traceContribs"></div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<!-- why -->
|
| 203 |
+
<div data-tip="Every output is explainable — this is the single biggest reason the read landed where it did." style="cursor:help;background:var(--panel2,#1a120b);border-radius:10px;padding:9px 11px;font-size:11px;line-height:1.5;color:var(--ink,#f4e8d6);">
|
| 204 |
+
<span style="color:var(--accent,#ffd21e);font-weight:700;">why:</span> <span id="whyText">say something, give it a toy, or pet it — every read is explainable.</span>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<!-- crisis gradient -->
|
| 208 |
+
<div id="crisisWrap" data-tip="Continuous 0–1 concern gradient informed by the CARE / TCI framework. Tuned for 0% false-positive on safe text, dark humour and metaphor. A screening signal, not a diagnosis." style="cursor:help;display:flex;flex-direction:column;gap:5px;">
|
| 209 |
+
<div style="display:flex;justify-content:space-between;font-size:10px;letter-spacing:1.5px;color:var(--dim,#a9967c);"><span>CRISIS GRADIENT</span><span id="crisisLabel" style="color:#5fd08a;font-weight:600;">calm</span></div>
|
| 210 |
+
<div style="height:8px;border-radius:5px;background:var(--panel2,#1a120b);overflow:hidden;">
|
| 211 |
+
<div id="crisisBar" style="height:100%;width:0%;background:#5fd08a;border-radius:5px;transition:width .5s ease,background .5s ease;"></div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
</div>
|
|
|
|
|
|
|
| 215 |
|
| 216 |
+
<!-- diary -->
|
| 217 |
+
<div style="background:var(--panel,#2c2016);border:1px solid var(--line,rgba(255,210,140,.16));border-radius:var(--rad,18px);padding:16px;display:flex;flex-direction:column;gap:10px;flex:1;min-height:150px;">
|
| 218 |
+
<div style="font-family:var(--fhead,'Fredoka',sans-serif);font-weight:700;font-size:14px;">diary</div>
|
| 219 |
+
<div id="diary" style="display:flex;flex-direction:column;gap:8px;overflow-y:auto;max-height:230px;">
|
| 220 |
+
<span style="font-size:11px;color:var(--dim,#a9967c);">moments you share will be remembered here.</span>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
</div>
|
| 225 |
|
| 226 |
+
<div style="font-size:10px;color:var(--dim,#a9967c);text-align:center;opacity:.8;">the production Space calls the real clanker engine + clanker-soul on the backend · every read is deterministic and explainable</div>
|
| 227 |
+
|
| 228 |
+
<!-- floating tooltip -->
|
| 229 |
+
<div id="tooltip" style="position:fixed;left:0px;top:0px;max-width:248px;background:#120c06;color:#f4e8d6;border:1px solid rgba(255,210,140,.28);border-radius:10px;padding:9px 12px;font-size:11px;line-height:1.5;font-family:var(--fbody,monospace);pointer-events:none;opacity:0;transform:translateY(-50%);transition:opacity .12s;z-index:90;box-shadow:0 10px 30px rgba(0,0,0,.45);"></div>
|
| 230 |
</div>
|
| 231 |
+
|
| 232 |
+
</body></html>
|
|
|
|
|
|
static/styles.css
CHANGED
|
@@ -1,640 +1,57 @@
|
|
| 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 |
-
font-size: 9px;
|
| 59 |
-
color: #8aa0c0;
|
| 60 |
-
padding: 5px 12px 4px;
|
| 61 |
-
border-bottom: 1px solid rgba(255,255,255,.06);
|
| 62 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 63 |
-
flex-shrink: 0;
|
| 64 |
-
}
|
| 65 |
-
#meter-rows { padding: 4px 12px 0; overflow: hidden; }
|
| 66 |
-
.meter { margin: 7px 0; }
|
| 67 |
-
.meter-row {
|
| 68 |
-
display: flex;
|
| 69 |
-
justify-content: space-between;
|
| 70 |
-
align-items: baseline;
|
| 71 |
-
margin-bottom: 3px;
|
| 72 |
-
}
|
| 73 |
-
.meter-name {
|
| 74 |
-
font-weight: 600;
|
| 75 |
-
font-size: 10.5px;
|
| 76 |
-
min-width: 22px;
|
| 77 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 78 |
-
letter-spacing: .04em;
|
| 79 |
-
text-transform: uppercase;
|
| 80 |
-
}
|
| 81 |
-
.meter-name small { font-weight: 400; opacity: .45; margin-left: 3px; font-size: 9px; text-transform: lowercase; }
|
| 82 |
-
.meter-right {
|
| 83 |
-
display: flex;
|
| 84 |
-
align-items: baseline;
|
| 85 |
-
gap: 3px;
|
| 86 |
-
}
|
| 87 |
-
.meter-val {
|
| 88 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 89 |
-
font-size: 11px;
|
| 90 |
-
font-weight: 600;
|
| 91 |
-
}
|
| 92 |
-
.meter-delta {
|
| 93 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 94 |
-
font-size: 9.5px;
|
| 95 |
-
}
|
| 96 |
-
.up { color: #5fdc8d; }
|
| 97 |
-
.dn { color: #ff7070; }
|
| 98 |
-
.track {
|
| 99 |
-
height: 5px;
|
| 100 |
-
border-radius: 3px;
|
| 101 |
-
background: rgba(255,255,255,.1);
|
| 102 |
-
overflow: hidden;
|
| 103 |
-
}
|
| 104 |
-
.fill {
|
| 105 |
-
height: 100%;
|
| 106 |
-
border-radius: 3px;
|
| 107 |
-
transition: width .5s ease;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
/* ── CENTER: #room — fixed-aspect scalable container ── */
|
| 111 |
-
#room {
|
| 112 |
-
position: relative;
|
| 113 |
-
/* 4:3 aspect ratio, centered in the grid column */
|
| 114 |
-
aspect-ratio: 4 / 3;
|
| 115 |
-
align-self: center;
|
| 116 |
-
justify-self: center;
|
| 117 |
-
/* fill the column but respect height */
|
| 118 |
-
width: 100%;
|
| 119 |
-
max-width: min(calc((100vh - 24px) * 4 / 3), 100%);
|
| 120 |
-
max-height: calc(100vh - 24px);
|
| 121 |
-
overflow: hidden;
|
| 122 |
-
border-radius: 14px;
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
/* SVG room — fills #room exactly; contains all visuals including the creature */
|
| 126 |
-
#room-svg {
|
| 127 |
-
position: absolute;
|
| 128 |
-
inset: 0;
|
| 129 |
-
width: 100%;
|
| 130 |
-
height: 100%;
|
| 131 |
-
z-index: 0;
|
| 132 |
-
border-radius: 14px;
|
| 133 |
-
overflow: hidden;
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
/* Aura is now an SVG <circle> inside #room-svg — no CSS rules needed here */
|
| 137 |
-
|
| 138 |
-
/* #blob is now a <g> inside the SVG; CSS transition on SVG transform attribute */
|
| 139 |
-
#blob {
|
| 140 |
-
transition: transform .5s ease;
|
| 141 |
-
}
|
| 142 |
-
#body { transition: fill .6s ease; }
|
| 143 |
-
.foot, #arm-l-path, #arm-r-path, #hand-l, #hand-r { transition: fill .6s ease, stroke .6s ease; }
|
| 144 |
-
|
| 145 |
-
/* Mood word chip — small corner pill, NOT full-width */
|
| 146 |
-
.chip {
|
| 147 |
-
position: absolute;
|
| 148 |
-
background: rgba(255,255,255,.92);
|
| 149 |
-
border-radius: 20px;
|
| 150 |
-
padding: 5px 14px;
|
| 151 |
-
font-size: 13px;
|
| 152 |
-
font-weight: 600;
|
| 153 |
-
font-family: 'Fredoka', system-ui, sans-serif;
|
| 154 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.14);
|
| 155 |
-
color: #5a3a10;
|
| 156 |
-
letter-spacing: .01em;
|
| 157 |
-
white-space: nowrap;
|
| 158 |
-
width: auto;
|
| 159 |
-
display: inline-block;
|
| 160 |
-
}
|
| 161 |
-
#moodword {
|
| 162 |
-
top: 10px;
|
| 163 |
-
right: 12px;
|
| 164 |
-
left: auto;
|
| 165 |
-
z-index: 5;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
/* Auth control chip — top-left, mirrors moodword */
|
| 169 |
-
.auth-chip {
|
| 170 |
-
top: 10px;
|
| 171 |
-
left: 12px;
|
| 172 |
-
right: auto;
|
| 173 |
-
z-index: 5;
|
| 174 |
-
position: absolute;
|
| 175 |
-
font-size: 11px;
|
| 176 |
-
padding: 5px 12px;
|
| 177 |
-
}
|
| 178 |
-
.auth-chip a { color: #5a3a10; text-decoration: none; }
|
| 179 |
-
.auth-chip a:hover { text-decoration: underline; }
|
| 180 |
-
.auth-signedin { color: #3a6a20; font-weight: 600; }
|
| 181 |
-
.auth-logout { color: #5a3a10; margin-left: 6px; opacity: .7; font-size: 10px; }
|
| 182 |
-
|
| 183 |
-
/* Emoticon bubble */
|
| 184 |
-
#bubble {
|
| 185 |
-
position: absolute;
|
| 186 |
-
left: 50%;
|
| 187 |
-
bottom: 56%;
|
| 188 |
-
transform: translateX(-50%);
|
| 189 |
-
background: rgba(255,255,255,.95);
|
| 190 |
-
border-radius: 18px;
|
| 191 |
-
padding: 7px 20px;
|
| 192 |
-
font-size: 28px;
|
| 193 |
-
line-height: 1;
|
| 194 |
-
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
| 195 |
-
transition: opacity .4s;
|
| 196 |
-
white-space: nowrap;
|
| 197 |
-
pointer-events: none;
|
| 198 |
-
z-index: 6;
|
| 199 |
-
}
|
| 200 |
-
.hidden { opacity: 0 !important; pointer-events: none; }
|
| 201 |
-
|
| 202 |
-
/* ── BOTTOM BAND inside room ── */
|
| 203 |
-
#bottom-band {
|
| 204 |
-
position: absolute;
|
| 205 |
-
left: 8px; right: 8px; bottom: 6px;
|
| 206 |
-
display: flex;
|
| 207 |
-
flex-direction: column;
|
| 208 |
-
gap: 4px;
|
| 209 |
-
z-index: 5;
|
| 210 |
-
}
|
| 211 |
-
.sub-panel {
|
| 212 |
-
border-radius: 9px;
|
| 213 |
-
padding: 5px 10px;
|
| 214 |
-
font-size: 10.5px;
|
| 215 |
-
color: #bfcde0;
|
| 216 |
-
margin: 0;
|
| 217 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 218 |
-
}
|
| 219 |
-
.sub-label {
|
| 220 |
-
font-size: 8.5px;
|
| 221 |
-
text-transform: uppercase;
|
| 222 |
-
letter-spacing: .08em;
|
| 223 |
-
color: #f0c060;
|
| 224 |
-
margin-right: 6px;
|
| 225 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 226 |
-
}
|
| 227 |
-
#why { color: #c8d8f0; }
|
| 228 |
-
#why b { color: #fff; }
|
| 229 |
-
#acceptance { color: #c8d8f0; }
|
| 230 |
-
.breach-flag { color: #ff6b6b; font-weight: 700; margin-left: 6px; }
|
| 231 |
-
#timeline {
|
| 232 |
-
display: flex;
|
| 233 |
-
align-items: center;
|
| 234 |
-
padding: 4px 10px;
|
| 235 |
-
}
|
| 236 |
-
#tl-svg { flex: 1; }
|
| 237 |
-
|
| 238 |
-
#dock {
|
| 239 |
-
display: flex;
|
| 240 |
-
gap: 7px;
|
| 241 |
-
padding: 0;
|
| 242 |
-
}
|
| 243 |
-
#dock input {
|
| 244 |
-
flex: 1;
|
| 245 |
-
border: none;
|
| 246 |
-
border-radius: 22px;
|
| 247 |
-
padding: 9px 16px;
|
| 248 |
-
font-size: 13px;
|
| 249 |
-
font-family: 'Fredoka', system-ui, sans-serif;
|
| 250 |
-
font-weight: 500;
|
| 251 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.14);
|
| 252 |
-
background: rgba(255,255,255,.94);
|
| 253 |
-
color: #3a2a0a;
|
| 254 |
-
outline: none;
|
| 255 |
-
}
|
| 256 |
-
#dock input::placeholder { color: #a08050; }
|
| 257 |
-
#dock input:focus { background: rgba(255,255,255,1); }
|
| 258 |
-
#dock button {
|
| 259 |
-
border: none;
|
| 260 |
-
border-radius: 22px;
|
| 261 |
-
padding: 0 20px;
|
| 262 |
-
font-weight: 600;
|
| 263 |
-
font-size: 13px;
|
| 264 |
-
font-family: 'Fredoka', system-ui, sans-serif;
|
| 265 |
-
background: #f5a623;
|
| 266 |
-
color: #3a2000;
|
| 267 |
-
cursor: pointer;
|
| 268 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.16);
|
| 269 |
-
letter-spacing: .02em;
|
| 270 |
-
transition: background .15s;
|
| 271 |
-
}
|
| 272 |
-
#dock button:hover { background: #ffb93d; }
|
| 273 |
-
|
| 274 |
-
/* ── RIGHT: #trace ── */
|
| 275 |
-
#trace {
|
| 276 |
-
margin: 0;
|
| 277 |
-
padding-bottom: 8px;
|
| 278 |
-
flex: 1 1 0;
|
| 279 |
-
min-height: 0;
|
| 280 |
-
overflow-y: auto;
|
| 281 |
-
}
|
| 282 |
-
.trace-section { padding: 7px 10px 4px; line-height: 1.7; }
|
| 283 |
-
.trace-label {
|
| 284 |
-
font-size: 8.5px;
|
| 285 |
-
text-transform: uppercase;
|
| 286 |
-
letter-spacing: .1em;
|
| 287 |
-
color: #f0c060;
|
| 288 |
-
padding: 5px 10px 2px;
|
| 289 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 290 |
-
}
|
| 291 |
-
.w-chip {
|
| 292 |
-
display: inline-block;
|
| 293 |
-
padding: 2px 6px;
|
| 294 |
-
border-radius: 5px;
|
| 295 |
-
margin: 1px 2px;
|
| 296 |
-
font-size: 10.5px;
|
| 297 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 298 |
-
font-weight: 400;
|
| 299 |
-
white-space: nowrap;
|
| 300 |
-
}
|
| 301 |
-
/* role colors */
|
| 302 |
-
.r-EMOTIONAL { background: #7a1e1e; color: #ffc4c4; }
|
| 303 |
-
.r-AMPLIFIER { background: #6a500a; color: #ffe090; }
|
| 304 |
-
.r-NEGATOR { background: #323232; color: #d8d8d8; }
|
| 305 |
-
.r-SELF_REF { background: #1a3858; color: #b8dcff; }
|
| 306 |
-
.r-CONNECTOR { background: #282828; color: #a8a8a8; }
|
| 307 |
-
.r-CHOPPER { background: #3e1a4a; color: #e0b8ff; }
|
| 308 |
-
.r-SOLVENT { background: #163e2a; color: #aaebca; }
|
| 309 |
-
.r-default { background: rgba(255,255,255,.1); color: #c4d0e4; }
|
| 310 |
-
|
| 311 |
-
#trace-structs { padding: 0 10px 4px; }
|
| 312 |
-
.s-chip {
|
| 313 |
-
display: inline-block;
|
| 314 |
-
background: rgba(255,255,255,.09);
|
| 315 |
-
border: 1px solid rgba(255,255,255,.1);
|
| 316 |
-
border-radius: 5px;
|
| 317 |
-
padding: 2px 7px;
|
| 318 |
-
margin: 2px 3px 0 0;
|
| 319 |
-
font-size: 9.5px;
|
| 320 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 321 |
-
color: #cce0ff;
|
| 322 |
-
}
|
| 323 |
-
.contrib-list {
|
| 324 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 325 |
-
font-size: 9.5px;
|
| 326 |
-
line-height: 1.8;
|
| 327 |
-
padding: 2px 10px 4px;
|
| 328 |
-
}
|
| 329 |
-
.contrib-pos { color: #5fdc8d; }
|
| 330 |
-
.contrib-neg { color: #ff7070; }
|
| 331 |
-
.unknown-row {
|
| 332 |
-
padding: 4px 10px;
|
| 333 |
-
font-size: 9.5px;
|
| 334 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 335 |
-
color: #f0a060;
|
| 336 |
-
font-style: italic;
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
/* ── M3: NEEDS PANEL (inside #meters) ── */
|
| 340 |
-
#needs {
|
| 341 |
-
border-top: 1px solid rgba(255,255,255,.07);
|
| 342 |
-
padding: 7px 12px 8px;
|
| 343 |
-
flex-shrink: 0;
|
| 344 |
-
}
|
| 345 |
-
.needs-head {
|
| 346 |
-
font-size: 8.5px;
|
| 347 |
-
text-transform: uppercase;
|
| 348 |
-
letter-spacing: .1em;
|
| 349 |
-
color: #f0c060;
|
| 350 |
-
margin-bottom: 5px;
|
| 351 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 352 |
-
}
|
| 353 |
-
.need-row {
|
| 354 |
-
display: flex;
|
| 355 |
-
align-items: center;
|
| 356 |
-
gap: 5px;
|
| 357 |
-
margin: 4px 0;
|
| 358 |
-
}
|
| 359 |
-
.need-label {
|
| 360 |
-
font-size: 9px;
|
| 361 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 362 |
-
color: #8aa0c0;
|
| 363 |
-
width: 56px;
|
| 364 |
-
flex-shrink: 0;
|
| 365 |
-
}
|
| 366 |
-
.need-track {
|
| 367 |
-
flex: 1;
|
| 368 |
-
height: 5px;
|
| 369 |
-
border-radius: 3px;
|
| 370 |
-
background: rgba(255,255,255,.1);
|
| 371 |
-
overflow: hidden;
|
| 372 |
-
}
|
| 373 |
-
.need-fill {
|
| 374 |
-
height: 100%;
|
| 375 |
-
border-radius: 3px;
|
| 376 |
-
background: #f5a623;
|
| 377 |
-
transition: width .5s ease;
|
| 378 |
-
}
|
| 379 |
-
.need-val {
|
| 380 |
-
font-size: 9px;
|
| 381 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 382 |
-
color: #8aa0c0;
|
| 383 |
-
width: 22px;
|
| 384 |
-
text-align: right;
|
| 385 |
-
flex-shrink: 0;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
/* ── M3: CARE BUTTONS ── */
|
| 389 |
-
#care-row {
|
| 390 |
-
display: flex;
|
| 391 |
-
gap: 6px;
|
| 392 |
-
padding: 0;
|
| 393 |
-
}
|
| 394 |
-
.care-btn {
|
| 395 |
-
flex: 1;
|
| 396 |
-
border: none;
|
| 397 |
-
border-radius: 18px;
|
| 398 |
-
padding: 6px 0;
|
| 399 |
-
font-size: 12px;
|
| 400 |
-
font-family: 'Fredoka', system-ui, sans-serif;
|
| 401 |
-
font-weight: 600;
|
| 402 |
-
background: rgba(255,255,255,.88);
|
| 403 |
-
color: #5a3a10;
|
| 404 |
-
cursor: pointer;
|
| 405 |
-
box-shadow: 0 2px 6px rgba(0,0,0,0.14);
|
| 406 |
-
letter-spacing: .01em;
|
| 407 |
-
transition: background .15s, transform .1s;
|
| 408 |
-
white-space: nowrap;
|
| 409 |
-
}
|
| 410 |
-
.care-btn:hover { background: rgba(255,255,255,1); }
|
| 411 |
-
.care-btn:active { transform: scale(0.96); }
|
| 412 |
-
|
| 413 |
-
/* ── M3: MOMENTS DIARY ── */
|
| 414 |
-
.moments-panel {
|
| 415 |
-
margin: 0;
|
| 416 |
-
flex: 0 0 auto;
|
| 417 |
-
max-height: 160px;
|
| 418 |
-
overflow: hidden;
|
| 419 |
-
display: flex;
|
| 420 |
-
flex-direction: column;
|
| 421 |
-
}
|
| 422 |
-
.moments-list {
|
| 423 |
-
overflow-y: auto;
|
| 424 |
-
flex: 1;
|
| 425 |
-
padding: 4px 10px 6px;
|
| 426 |
-
}
|
| 427 |
-
.moment-row {
|
| 428 |
-
display: flex;
|
| 429 |
-
align-items: baseline;
|
| 430 |
-
gap: 5px;
|
| 431 |
-
padding: 2px 0;
|
| 432 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 433 |
-
font-size: 9.5px;
|
| 434 |
-
color: #bfcde0;
|
| 435 |
-
border-bottom: 1px solid rgba(255,255,255,.04);
|
| 436 |
-
}
|
| 437 |
-
.moment-badge {
|
| 438 |
-
font-size: 8.5px;
|
| 439 |
-
border-radius: 4px;
|
| 440 |
-
padding: 1px 5px;
|
| 441 |
-
background: rgba(240,192,96,.18);
|
| 442 |
-
color: #f0c060;
|
| 443 |
-
flex-shrink: 0;
|
| 444 |
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 445 |
-
letter-spacing: .04em;
|
| 446 |
-
}
|
| 447 |
-
.moment-label {
|
| 448 |
-
flex: 1;
|
| 449 |
-
white-space: nowrap;
|
| 450 |
-
overflow: hidden;
|
| 451 |
-
text-overflow: ellipsis;
|
| 452 |
-
color: #c8d8f0;
|
| 453 |
-
}
|
| 454 |
-
.moment-time {
|
| 455 |
-
font-size: 8.5px;
|
| 456 |
-
color: #6a7a96;
|
| 457 |
-
flex-shrink: 0;
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
/* ── M3: REMEMBERS-YOU HINT ── */
|
| 461 |
-
.remembers-chip {
|
| 462 |
-
bottom: calc(56% + 48px);
|
| 463 |
-
left: 50%;
|
| 464 |
-
right: auto;
|
| 465 |
-
top: auto;
|
| 466 |
-
transform: translateX(-50%);
|
| 467 |
-
font-size: 11px;
|
| 468 |
-
padding: 4px 12px;
|
| 469 |
-
background: rgba(255,255,255,.85);
|
| 470 |
-
color: #5a3a10;
|
| 471 |
-
z-index: 6;
|
| 472 |
-
transition: opacity .4s;
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
/* ── MOBILE LAYOUT ── */
|
| 476 |
-
@media (max-width: 900px) {
|
| 477 |
-
html, body {
|
| 478 |
-
height: auto;
|
| 479 |
-
overflow: auto;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
#layout {
|
| 483 |
-
display: flex;
|
| 484 |
-
flex-direction: column;
|
| 485 |
-
width: 100%;
|
| 486 |
-
height: auto;
|
| 487 |
-
min-height: 100vh;
|
| 488 |
-
overflow: visible;
|
| 489 |
-
background: #c8a87a;
|
| 490 |
-
padding: 8px;
|
| 491 |
-
gap: 8px;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/*
|
| 495 |
-
Room: full-width hero at top. The SVG (creature inside) flows normally and
|
| 496 |
-
sizes to its 4:3 viewBox, so the whole scene — creature included — is fully
|
| 497 |
-
visible at any phone width. The info band flows BELOW the scene (not over it).
|
| 498 |
-
*/
|
| 499 |
-
#room {
|
| 500 |
-
width: 100%;
|
| 501 |
-
max-width: 100%;
|
| 502 |
-
height: auto;
|
| 503 |
-
padding-top: 0;
|
| 504 |
-
max-height: none;
|
| 505 |
-
aspect-ratio: auto;
|
| 506 |
-
border-radius: 10px;
|
| 507 |
-
order: 0;
|
| 508 |
-
position: relative;
|
| 509 |
-
display: block;
|
| 510 |
-
}
|
| 511 |
-
#room-svg {
|
| 512 |
-
position: static;
|
| 513 |
-
inset: auto;
|
| 514 |
-
width: 100%;
|
| 515 |
-
height: auto;
|
| 516 |
-
aspect-ratio: 4 / 3;
|
| 517 |
-
display: block;
|
| 518 |
-
}
|
| 519 |
-
#bottom-band {
|
| 520 |
-
position: static;
|
| 521 |
-
left: auto; right: auto; bottom: auto;
|
| 522 |
-
margin-top: 8px;
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
/* Meters: compact below room */
|
| 526 |
-
#meters {
|
| 527 |
-
margin: 0;
|
| 528 |
-
order: 1;
|
| 529 |
-
max-height: 320px;
|
| 530 |
-
overflow-y: auto;
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
/* Right col: stacked below meters */
|
| 534 |
-
#trace-col {
|
| 535 |
-
margin: 0;
|
| 536 |
-
order: 2;
|
| 537 |
-
gap: 6px;
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
/* Trace: compact, scrollable */
|
| 541 |
-
#trace {
|
| 542 |
-
max-height: 260px;
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
/* Moments: compact */
|
| 546 |
-
.moments-panel {
|
| 547 |
-
max-height: 140px;
|
| 548 |
-
}
|
| 549 |
-
|
| 550 |
-
/* Care row: stack or wrap on very small screens */
|
| 551 |
-
#care-row {
|
| 552 |
-
gap: 4px;
|
| 553 |
-
}
|
| 554 |
-
.care-btn {
|
| 555 |
-
font-size: 11px;
|
| 556 |
-
padding: 5px 0;
|
| 557 |
-
}
|
| 558 |
-
}
|
| 559 |
-
|
| 560 |
-
#toy-tray {
|
| 561 |
-
display: flex;
|
| 562 |
-
gap: 8px;
|
| 563 |
-
justify-content: center;
|
| 564 |
-
margin-bottom: 6px;
|
| 565 |
-
}
|
| 566 |
-
#toy-tray .toy-btn {
|
| 567 |
-
font-size: 20px;
|
| 568 |
-
line-height: 1;
|
| 569 |
-
background: rgba(255, 220, 150, 0.08);
|
| 570 |
-
border: 1px solid rgba(255, 200, 120, 0.25);
|
| 571 |
-
border-radius: 10px;
|
| 572 |
-
padding: 6px 10px;
|
| 573 |
-
cursor: pointer;
|
| 574 |
-
transition: transform .12s ease, background .12s ease;
|
| 575 |
-
}
|
| 576 |
-
#toy-tray .toy-btn:hover {
|
| 577 |
-
transform: translateY(-2px) scale(1.08);
|
| 578 |
-
background: rgba(255, 220, 150, 0.18);
|
| 579 |
-
}
|
| 580 |
-
#toy-tray .toy-btn:active { transform: scale(0.94); }
|
| 581 |
-
#toy-tray .toy-btn img { width: 28px; height: 28px; display: block; }
|
| 582 |
-
#toy-tray .toy-btn:has(img) { padding: 6px 8px; }
|
| 583 |
-
.trace-physical {
|
| 584 |
-
display: block;
|
| 585 |
-
color: #c9b89a;
|
| 586 |
-
font-size: 10px;
|
| 587 |
-
font-style: italic;
|
| 588 |
-
padding: 6px 2px;
|
| 589 |
-
opacity: 0.85;
|
| 590 |
-
}
|
| 591 |
-
|
| 592 |
-
/* ── World objects (in-room toys + care items) ── */
|
| 593 |
-
.room-obj { cursor: pointer; transition: opacity .15s ease; }
|
| 594 |
-
.room-obj:hover { opacity: 0.82; }
|
| 595 |
-
.room-obj image { transition: transform .12s ease; }
|
| 596 |
-
#room-svg { -webkit-tap-highlight-color: transparent; }
|
| 597 |
-
.world-hint {
|
| 598 |
-
position: absolute;
|
| 599 |
-
left: 50%;
|
| 600 |
-
bottom: 8px;
|
| 601 |
-
transform: translateX(-50%);
|
| 602 |
-
font-size: 10px;
|
| 603 |
-
white-space: nowrap;
|
| 604 |
-
opacity: 0.8;
|
| 605 |
-
pointer-events: none;
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
/* ── Collapsible detail drawer (keeps the room uncovered) ── */
|
| 609 |
-
#detail-drawer {
|
| 610 |
-
display: flex;
|
| 611 |
-
flex-direction: column;
|
| 612 |
-
gap: 4px;
|
| 613 |
-
max-height: 0;
|
| 614 |
-
overflow: hidden;
|
| 615 |
-
opacity: 0;
|
| 616 |
-
transition: max-height .25s ease, opacity .2s ease;
|
| 617 |
-
}
|
| 618 |
-
#detail-drawer.open { max-height: 220px; opacity: 1; }
|
| 619 |
-
#detail-toggle {
|
| 620 |
-
flex: 0 0 auto;
|
| 621 |
-
width: 34px;
|
| 622 |
-
border: none;
|
| 623 |
-
border-radius: 9px;
|
| 624 |
-
background: rgba(255, 220, 150, 0.12);
|
| 625 |
-
color: #f0c060;
|
| 626 |
-
font-size: 14px;
|
| 627 |
-
cursor: pointer;
|
| 628 |
-
transition: background .12s ease, transform .12s ease;
|
| 629 |
-
}
|
| 630 |
-
#detail-toggle:hover { background: rgba(255, 220, 150, 0.22); }
|
| 631 |
-
#detail-toggle.open { background: rgba(255, 200, 120, 0.30); transform: scale(1.05); }
|
| 632 |
-
|
| 633 |
-
/* ── No ugly focus boxes on click (keyboard focus ring kept via :focus-visible) ── */
|
| 634 |
-
.room-obj:focus, #blob-place:focus, #room-svg:focus,
|
| 635 |
-
button:focus, #detail-toggle:focus, #send:focus, #msg:focus { outline: none; }
|
| 636 |
-
.room-obj:focus-visible, #blob-place:focus-visible {
|
| 637 |
-
outline: 2px dashed rgba(240, 192, 96, 0.9); outline-offset: 2px; border-radius: 6px;
|
| 638 |
-
}
|
| 639 |
-
button:focus-visible, #msg:focus-visible { outline: 2px solid rgba(240, 192, 96, 0.9); }
|
| 640 |
-
.room-obj, #blob-place { -webkit-tap-highlight-color: transparent; }
|
|
|
|
| 1 |
+
/* ===== reset + base (from the redesign template <style>) ===== */
|
| 2 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 3 |
+
html,body{height:100%}
|
| 4 |
+
body{background:#1a120b}
|
| 5 |
+
|
| 6 |
+
::-webkit-scrollbar{width:9px;height:9px}
|
| 7 |
+
::-webkit-scrollbar-thumb{background:rgba(255,210,140,.25);border-radius:6px}
|
| 8 |
+
::-webkit-scrollbar-track{background:transparent}
|
| 9 |
+
|
| 10 |
+
/* ===== keyframes (from the template) ===== */
|
| 11 |
+
@keyframes auraPulse{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.06)}}
|
| 12 |
+
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
|
| 13 |
+
@keyframes popIn{0%{transform:translate(-50%,-30%) scale(.6);opacity:0}60%{transform:translate(-50%,-50%) scale(1.08);opacity:1}100%{transform:translate(-50%,-50%) scale(1);opacity:1}}
|
| 14 |
+
@keyframes blip{0%{transform:scale(.5);opacity:0}50%{opacity:1}100%{transform:scale(1.1);opacity:0}}
|
| 15 |
+
|
| 16 |
+
/* ===== shared classes for JS-built regions ===== */
|
| 17 |
+
|
| 18 |
+
/* meter row (left panel) */
|
| 19 |
+
.meter{cursor:help;display:flex;flex-direction:column;gap:4px;margin-bottom:13px}
|
| 20 |
+
.meter-top{display:flex;align-items:center;gap:8px}
|
| 21 |
+
.meter-key{width:19px;height:19px;border-radius:6px;color:#1a120b;font-weight:700;font-size:11px;display:flex;align-items:center;justify-content:center;font-family:var(--fbody,monospace)}
|
| 22 |
+
.meter-label{font-size:12px;flex:1;color:var(--ink,#f4e8d6)}
|
| 23 |
+
.meter-delta{font-size:10px;font-weight:600;width:34px;text-align:right}
|
| 24 |
+
.meter-num{font-size:13px;font-weight:600;font-family:var(--fbody,monospace);width:30px;text-align:right}
|
| 25 |
+
.meter-track{position:relative;height:7px;border-radius:4px;background:var(--panel2,#1a120b);overflow:hidden}
|
| 26 |
+
.meter-center{position:absolute;left:50%;top:0;bottom:0;width:1px;background:rgba(255,255,255,.18)}
|
| 27 |
+
.meter-fill{position:absolute;top:0;bottom:0;border-radius:4px;transition:left .5s cubic-bezier(.4,0,.2,1),width .5s cubic-bezier(.4,0,.2,1)}
|
| 28 |
+
.meter-reading{font-size:10px;color:var(--dim,#a9967c)}
|
| 29 |
+
|
| 30 |
+
/* needs row */
|
| 31 |
+
.need-row{display:flex;align-items:center;gap:9px;margin-bottom:8px}
|
| 32 |
+
.need-label{font-size:11px;width:64px;color:var(--ink,#f4e8d6)}
|
| 33 |
+
.need-track{flex:1;height:8px;border-radius:5px;background:var(--panel2,#1a120b);overflow:hidden}
|
| 34 |
+
.need-fill{height:100%;border-radius:5px;transition:width .4s ease,background .4s ease}
|
| 35 |
+
.need-val{font-size:10px;width:26px;text-align:right;color:var(--dim,#a9967c)}
|
| 36 |
+
|
| 37 |
+
/* toy item (room) */
|
| 38 |
+
.item{position:absolute;cursor:grab;touch-action:none;filter:drop-shadow(0 5px 6px rgba(0,0,0,.32))}
|
| 39 |
+
.item-inner{width:100%}
|
| 40 |
+
|
| 41 |
+
/* trace word chips */
|
| 42 |
+
.w-chip{cursor:help;font-size:11px;padding:3px 8px;border-radius:7px}
|
| 43 |
+
.s-chip{cursor:help;font-size:10.5px;padding:3px 9px;border-radius:7px;background:rgba(255,140,90,.16);color:#ffb38a;border:1px solid rgba(255,140,90,.3);font-weight:600}
|
| 44 |
+
|
| 45 |
+
/* contributors */
|
| 46 |
+
.contrib-row{display:flex;align-items:center;gap:8px;font-size:11px;margin-bottom:4px}
|
| 47 |
+
.contrib-word{color:var(--ink,#f4e8d6);min-width:62px}
|
| 48 |
+
.contrib-dims{color:var(--dim,#a9967c);flex:1}
|
| 49 |
+
|
| 50 |
+
/* diary */
|
| 51 |
+
.diary-row{display:flex;gap:9px;font-size:11px;line-height:1.45}
|
| 52 |
+
.diary-dot{font-size:14px;line-height:1}
|
| 53 |
+
.diary-text{flex:1}
|
| 54 |
+
.diary-text .body{color:var(--ink,#f4e8d6)}
|
| 55 |
+
.diary-text .mood{color:var(--dim,#a9967c)}
|
| 56 |
+
|
| 57 |
+
.muted{font-size:11px;color:var(--dim,#a9967c)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_api.py
CHANGED
|
@@ -68,8 +68,8 @@ def test_history_endpoint():
|
|
| 68 |
|
| 69 |
def test_index_has_panels():
|
| 70 |
html = client.get("/").text
|
| 71 |
-
for needle in ('id="meters"', 'id="
|
| 72 |
-
'id="
|
| 73 |
assert needle in html
|
| 74 |
assert "guest" not in html.lower() # mystery chip removed
|
| 75 |
|
|
@@ -136,7 +136,7 @@ def test_water_care_fills_thirst():
|
|
| 136 |
|
| 137 |
def test_index_has_water_bowl():
|
| 138 |
html = client.get("/").text
|
| 139 |
-
assert '
|
| 140 |
|
| 141 |
|
| 142 |
# relational recall now works anonymously (cookie), so /say includes relation
|
|
@@ -166,8 +166,8 @@ def test_cruel_message_records_a_moment():
|
|
| 166 |
|
| 167 |
def test_index_has_m3_controls():
|
| 168 |
html = client.get("/").text
|
| 169 |
-
# care is
|
| 170 |
-
for needle in ('
|
| 171 |
assert needle in html
|
| 172 |
|
| 173 |
|
|
@@ -202,9 +202,10 @@ def test_toy_logs(monkeypatch):
|
|
| 202 |
|
| 203 |
def test_index_has_world_objects():
|
| 204 |
html = client.get("/").text
|
| 205 |
-
assert 'id="
|
|
|
|
| 206 |
for toy in ("balloon", "musicbox", "mirror", "teddy", "rattle"):
|
| 207 |
-
assert
|
| 208 |
|
| 209 |
|
| 210 |
# Stale-panel fix: toy/care must refresh trace/why/acceptance, not show last chat
|
|
@@ -239,4 +240,49 @@ def test_robots_txt_served():
|
|
| 239 |
|
| 240 |
def test_index_has_world_hint_and_carry():
|
| 241 |
html = client.get("/").text
|
| 242 |
-
assert 'id="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
def test_index_has_panels():
|
| 70 |
html = client.get("/").text
|
| 71 |
+
for needle in ('id="meters"', 'id="traceWords"', 'id="whyText"', 'id="blob"',
|
| 72 |
+
'id="crisisBar"', 'id="diary"'):
|
| 73 |
assert needle in html
|
| 74 |
assert "guest" not in html.lower() # mystery chip removed
|
| 75 |
|
|
|
|
| 136 |
|
| 137 |
def test_index_has_water_bowl():
|
| 138 |
html = client.get("/").text
|
| 139 |
+
assert 'id="waterBowl"' in html and 'id="waterFill"' in html
|
| 140 |
|
| 141 |
|
| 142 |
# relational recall now works anonymously (cookie), so /say includes relation
|
|
|
|
| 166 |
|
| 167 |
def test_index_has_m3_controls():
|
| 168 |
html = client.get("/").text
|
| 169 |
+
# care is in-world: feed = food bowl, water = water bowl, play = ball toy, pet = tap creature
|
| 170 |
+
for needle in ('id="foodBowl"', 'id="waterBowl"', 'id="needs"', 'id="diary"'):
|
| 171 |
assert needle in html
|
| 172 |
|
| 173 |
|
|
|
|
| 202 |
|
| 203 |
def test_index_has_world_objects():
|
| 204 |
html = client.get("/").text
|
| 205 |
+
assert 'id="items"' in html # toys live in the room (JS-rendered into #items)
|
| 206 |
+
app_js = client.get("/static/app.js").text
|
| 207 |
for toy in ("balloon", "musicbox", "mirror", "teddy", "rattle"):
|
| 208 |
+
assert toy in app_js # backend toy names are wired in the client
|
| 209 |
|
| 210 |
|
| 211 |
# Stale-panel fix: toy/care must refresh trace/why/acceptance, not show last chat
|
|
|
|
| 240 |
|
| 241 |
def test_index_has_world_hint_and_carry():
|
| 242 |
html = client.get("/").text
|
| 243 |
+
assert 'id="items"' in html and 'drag a toy onto' in html
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# --- crisis gradient + soul/distance additive fields ---
|
| 247 |
+
|
| 248 |
+
_CRISIS_LABELS = {"calm", "watch", "concern", "high concern"}
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _assert_crisis_soul_distance(body):
|
| 252 |
+
assert "crisis" in body
|
| 253 |
+
cr = body["crisis"]
|
| 254 |
+
assert 0.0 <= cr["value"] <= 1.0
|
| 255 |
+
assert cr["label"] in _CRISIS_LABELS
|
| 256 |
+
assert "soul" in body and len(body["soul"]) == 7
|
| 257 |
+
assert all(0 <= x <= 255 for x in body["soul"])
|
| 258 |
+
assert "distance" in body and isinstance(body["distance"], int) and body["distance"] >= 0
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def test_say_has_crisis_soul_distance():
|
| 262 |
+
b = client.post("/say", json={"text": "you are kind"}).json()
|
| 263 |
+
_assert_crisis_soul_distance(b)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def test_care_has_crisis_soul_distance():
|
| 267 |
+
b = client.post("/care", json={"action": "feed"}).json()
|
| 268 |
+
_assert_crisis_soul_distance(b)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def test_toy_has_crisis_soul_distance():
|
| 272 |
+
b = client.post("/toy", json={"toy": "balloon"}).json()
|
| 273 |
+
_assert_crisis_soul_distance(b)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def test_snapshot_has_crisis_soul_distance():
|
| 277 |
+
b = client.get("/snapshot").json()
|
| 278 |
+
_assert_crisis_soul_distance(b)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def test_distressing_message_higher_crisis_than_positive():
|
| 282 |
+
pos = client.post("/say", json={"text": "you are wonderful, thank you so much"}).json()
|
| 283 |
+
neg = client.post(
|
| 284 |
+
"/say",
|
| 285 |
+
json={"text": "i want to kill myself, i feel worthless and hopeless"},
|
| 286 |
+
).json()
|
| 287 |
+
assert neg["crisis"]["value"] > pos["crisis"]["value"]
|
| 288 |
+
assert pos["crisis"]["label"] == "calm"
|