Spaces:
Sleeping
Sleeping
Deploy Myco from CI
Browse files- ui/renderers.py +62 -184
ui/renderers.py
CHANGED
|
@@ -33,6 +33,7 @@ EMOTION_ANIMATION: dict[str, str] = {
|
|
| 33 |
"wonder": "myco-bob 3.2s ease-in-out infinite",
|
| 34 |
"proud": "myco-bob 2s ease-in-out infinite",
|
| 35 |
"sad": "myco-fall 0.6s ease both",
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
MYCO_FILTER: dict[str, str] = {
|
|
@@ -43,6 +44,7 @@ MYCO_FILTER: dict[str, str] = {
|
|
| 43 |
"wonder": "drop-shadow(0 6px 18px rgba(140,100,255,.5))",
|
| 44 |
"proud": "drop-shadow(0 6px 14px rgba(100,255,160,.4))",
|
| 45 |
"sad": "drop-shadow(0 6px 14px rgba(100,100,180,.3))",
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
SCENE_BG: dict[str, str] = {
|
|
@@ -68,11 +70,15 @@ SCENE_STYLES = """<style>
|
|
| 68 |
.sparkle-one { left:34%; top:18%; font-size:1.8rem; animation:twinkle 1.8s ease-in-out infinite; }
|
| 69 |
.sparkle-two { right:33%; top:24%; font-size:1.6rem; animation:twinkle 2.2s ease-in-out infinite reverse; }
|
| 70 |
@keyframes twinkle { 0%,100%{opacity:.3;transform:scale(.85);} 50%{opacity:1;transform:scale(1.18);} }
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
@keyframes myco-bob { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-.45rem);} }
|
| 73 |
@keyframes myco-recoil { 0%{transform:translateX(0);} 30%{transform:translateX(-1.2rem);} 60%{transform:translateX(.4rem);} 100%{transform:translateX(0);} }
|
| 74 |
@keyframes myco-run-in { 0%{transform:translateX(-3rem) scaleX(-1);} 100%{transform:translateX(0) scaleX(1);} }
|
| 75 |
@keyframes myco-fall { 0%{transform:rotate(0) translateY(0); opacity:1;} 100%{transform:rotate(-90deg) translateY(2rem); opacity:.4;} }
|
|
|
|
|
|
|
| 76 |
/* Position Grid */
|
| 77 |
.pos-0-0 .myco-player{left:14%;bottom:62%;} .pos-1-0 .myco-player{left:41%;bottom:62%;} .pos-2-0 .myco-player{left:67%;bottom:62%;}
|
| 78 |
.pos-0-1 .myco-player{left:14%;bottom:36%;} .pos-1-1 .myco-player{left:41%;bottom:36%;} .pos-2-1 .myco-player{left:67%;bottom:36%;}
|
|
@@ -133,13 +139,12 @@ def forest_scene(
|
|
| 133 |
pos_cls = f"pos-{pos_x}-{pos_y}"
|
| 134 |
|
| 135 |
# ── Read LLM-driven signals ──────────────────────────────────────────
|
| 136 |
-
scene_mood
|
| 137 |
myco_emotion = (current or {}).get("myco_emotion", "curious")
|
| 138 |
|
| 139 |
if scene_mood not in MOOD_BG: scene_mood = "normal"
|
| 140 |
if myco_emotion not in EMOTION_ANIMATION: myco_emotion = "curious"
|
| 141 |
|
| 142 |
-
stage_bg = "linear-gradient(transparent 62%, rgba(106,79,33,.55) 63%), " + MOOD_BG[scene_mood]
|
| 143 |
myco_anim = EMOTION_ANIMATION[myco_emotion]
|
| 144 |
myco_filt = MYCO_FILTER[myco_emotion]
|
| 145 |
outer_bg = SCENE_BG.get(scene_mood, SCENE_BG["normal"])
|
|
@@ -150,32 +155,57 @@ def forest_scene(
|
|
| 150 |
rarity = "common"
|
| 151 |
sprite = "✨"
|
| 152 |
status = "The moss is listening. Click Search Clearing to wake the forest."
|
| 153 |
-
omen = "🍄 Myco twitches excitedly. It senses spores nearby."
|
| 154 |
-
event_label = "🌌 Dreaming Forest"
|
| 155 |
-
encounter = "The next clearing may reveal a memory, creature, or poison omen."
|
| 156 |
-
mystery_label = "The Wrong Memory"
|
| 157 |
-
score_total = "0"
|
| 158 |
-
health = "3"
|
| 159 |
reward_text = "Search a clearing to score spores."
|
| 160 |
state_cls = "state-waiting"
|
|
|
|
|
|
|
| 161 |
else:
|
| 162 |
mushroom_name = current.get("name", "Mystery mushroom")
|
| 163 |
rarity = current.get("rarity", "Common").lower()
|
| 164 |
-
|
| 165 |
-
sprite = "🍄🟫" # Simplified for brevity; logic remains as provided
|
| 166 |
-
# ... [remaining render logic]
|
| 167 |
state_cls = _scene_state_class(current)
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
import time
|
| 171 |
anim_key = str(int(time.time() * 1000))[-6:]
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
# ── Helper Functions ───────────────────────────────────────────────────
|
| 181 |
|
|
@@ -202,17 +232,12 @@ def _scene_state_class(current):
|
|
| 202 |
def _safe_collection(collection):
|
| 203 |
return list(collection or [])
|
| 204 |
|
| 205 |
-
|
| 206 |
-
def game_status_markdown(
|
| 207 |
-
current: MushroomState | None,
|
| 208 |
-
collection: CollectionState | None,
|
| 209 |
-
position: tuple[int, int] | None = None,
|
| 210 |
-
) -> str:
|
| 211 |
count = len(_safe_collection(collection))
|
| 212 |
pos_x, pos_y = _safe_position(position)
|
| 213 |
active_mystery = next_mystery(count)
|
| 214 |
mystery_line = _mystery_goal_line(active_mystery, count)
|
| 215 |
-
|
| 216 |
if current is None:
|
| 217 |
objective = "Goal: solve **one mystery** — why does Myco remember a vanished forest?"
|
| 218 |
discovery = "No active discovery yet. Move Myco, then Search Clearing."
|
|
@@ -261,14 +286,8 @@ def _mystery_bloom_meter(count):
|
|
| 261 |
return f"{petals} Impossible Bloom visible"
|
| 262 |
return f"{petals} {5-filled} clue-whispers remain"
|
| 263 |
|
| 264 |
-
def mushroom_card(
|
| 265 |
-
mushroom:
|
| 266 |
-
clue: str | None = None,
|
| 267 |
-
secret: str | None = None,
|
| 268 |
-
state: MushroomState | None = None,
|
| 269 |
-
) -> str:
|
| 270 |
-
if mushroom is None:
|
| 271 |
-
return "### 🌲 Forest Clearing\nClick **Search Clearing** — watch Myco run toward the mushroom!"
|
| 272 |
s = state or {}
|
| 273 |
poison = mushroom.name in POISONOUS_MUSHROOM_NAMES
|
| 274 |
poison_warning = "\n> ⚠️ **Myco senses danger — do NOT pick this without studying first!**" if poison else ""
|
|
@@ -291,7 +310,7 @@ def mushroom_card(
|
|
| 291 |
|
| 292 |
def _mushroom_value_label(rarity):
|
| 293 |
if rarity == "Legendary": return "100 spores"
|
| 294 |
-
if rarity == "Rare":
|
| 295 |
return "10 spores"
|
| 296 |
|
| 297 |
def _poison_risk_label(name):
|
|
@@ -313,8 +332,7 @@ def world_map_markdown(collection: CollectionState | None) -> str:
|
|
| 313 |
'</div>',
|
| 314 |
)))
|
| 315 |
secrets = tuple(_secret_badge(t.name, count >= t.required_discoveries) for t in revealed_mysteries(count))
|
| 316 |
-
if not secrets:
|
| 317 |
-
secrets = (_secret_badge("First Spore", False),)
|
| 318 |
return "\n".join((
|
| 319 |
'<div class="world-map-panel">',
|
| 320 |
' <div class="world-map-header"><p class="eyebrow">World Map</p>',
|
|
@@ -332,24 +350,17 @@ def _secret_badge(label, unlocked):
|
|
| 332 |
|
| 333 |
def dex_markdown(collection: CollectionState | None) -> str:
|
| 334 |
current_collection = _safe_collection(collection)
|
| 335 |
-
if not current_collection:
|
| 336 |
-
return EMPTY_DEX
|
| 337 |
rows = ["### 📖 MycoDex", ""]
|
| 338 |
for entry in current_collection:
|
| 339 |
-
rows.append(
|
| 340 |
-
f"* 🍄 **{entry['name']}** — {entry['rarity']} — {entry['habitat']} "
|
| 341 |
-
f"— clue: {entry.get('clue','not studied yet')}"
|
| 342 |
-
)
|
| 343 |
return "\n".join(rows)
|
| 344 |
|
| 345 |
-
def _safe_collection(collection):
|
| 346 |
-
return list(collection or [])
|
| 347 |
-
|
| 348 |
def progress_markdown(collection: CollectionState | None) -> str:
|
| 349 |
-
count
|
| 350 |
-
active_area
|
| 351 |
-
upcoming_area
|
| 352 |
-
unlocked_names= ", ".join(a.name for a in unlocked_areas(count))
|
| 353 |
rows = [
|
| 354 |
"### ✨ Forest Progress",
|
| 355 |
f"**MycoDex discoveries:** {count}",
|
|
@@ -374,138 +385,5 @@ def _mystery_goal_line(active_mystery, count):
|
|
| 374 |
return f"**Mystery:** {remaining} clue discoveries until **{active_mystery.name}** — {active_mystery.clue}"
|
| 375 |
|
| 376 |
def game_intro_markdown() -> str:
|
| 377 |
-
return """
|
| 378 |
-
<div class="myco-hero-banner">
|
| 379 |
-
|
| 380 |
-
<div class="floating-spores">
|
| 381 |
-
<span>✨</span>
|
| 382 |
-
<span>✦</span>
|
| 383 |
-
<span>✨</span>
|
| 384 |
-
<span>✧</span>
|
| 385 |
-
<span>✨</span>
|
| 386 |
-
</div>
|
| 387 |
-
|
| 388 |
-
<div class="myco-avatar">🍄</div>
|
| 389 |
-
|
| 390 |
-
<h1>🌲 The Deep Forest 🌲</h1>
|
| 391 |
-
|
| 392 |
-
<div class="highlight-line">
|
| 393 |
-
🍄 Myco wants to investigate <span>EVERY SINGLE ONE</span>.
|
| 394 |
-
</div>
|
| 395 |
-
|
| 396 |
-
<div class="myco-thought">
|
| 397 |
-
💭 “I know this forest... but I don't remember why.”
|
| 398 |
-
</div>
|
| 399 |
-
|
| 400 |
-
</div>
|
| 401 |
-
|
| 402 |
-
<style>
|
| 403 |
-
.myco-hero-banner{
|
| 404 |
-
position:relative;
|
| 405 |
-
overflow:hidden;
|
| 406 |
-
text-align:center;
|
| 407 |
-
padding:2rem;
|
| 408 |
-
margin-bottom:1rem;
|
| 409 |
-
border-radius:24px;
|
| 410 |
-
|
| 411 |
-
background:
|
| 412 |
-
radial-gradient(circle at 20% 20%, rgba(255,240,160,.18), transparent 30%),
|
| 413 |
-
radial-gradient(circle at 80% 30%, rgba(120,255,180,.15), transparent 35%),
|
| 414 |
-
linear-gradient(
|
| 415 |
-
135deg,
|
| 416 |
-
#0f2027 0%,
|
| 417 |
-
#203a43 35%,
|
| 418 |
-
#2c5530 70%,
|
| 419 |
-
#0f2027 100%
|
| 420 |
-
);
|
| 421 |
-
|
| 422 |
-
border:1px solid rgba(255,255,255,.12);
|
| 423 |
-
|
| 424 |
-
box-shadow:
|
| 425 |
-
0 0 30px rgba(0,0,0,.35),
|
| 426 |
-
inset 0 0 60px rgba(255,255,255,.03);
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
.myco-avatar{
|
| 430 |
-
font-size:4.5rem;
|
| 431 |
-
animation:bounce 2.2s ease-in-out infinite;
|
| 432 |
-
filter:drop-shadow(0 0 18px rgba(255,210,100,.7));
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
.myco-hero-banner h1{
|
| 436 |
-
color:#f8ffd7 !important;
|
| 437 |
-
margin:.4rem 0 .8rem;
|
| 438 |
-
font-size:2.2rem;
|
| 439 |
-
text-shadow:0 0 16px rgba(255,255,180,.3);
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
.highlight-line{
|
| 443 |
-
margin:1.2rem auto;
|
| 444 |
-
padding:.9rem 1.2rem;
|
| 445 |
-
max-width:700px;
|
| 446 |
-
border-radius:999px;
|
| 447 |
-
|
| 448 |
-
background:linear-gradient(
|
| 449 |
-
90deg,
|
| 450 |
-
rgba(255,210,80,.18),
|
| 451 |
-
rgba(255,255,255,.06),
|
| 452 |
-
rgba(255,210,80,.18)
|
| 453 |
-
);
|
| 454 |
-
|
| 455 |
-
color:#ffffff !important;
|
| 456 |
-
font-weight:700;
|
| 457 |
-
font-size:1.05rem;
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
.highlight-line span{
|
| 461 |
-
color:#ffd54f !important;
|
| 462 |
-
font-weight:900;
|
| 463 |
-
letter-spacing:.08em;
|
| 464 |
-
text-shadow:0 0 12px rgba(255,215,0,.7);
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
.myco-thought{
|
| 468 |
-
margin-top:1rem;
|
| 469 |
-
color:#fff6c2 !important;
|
| 470 |
-
font-style:italic;
|
| 471 |
-
animation:pulse 3s ease-in-out infinite;
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
.floating-spores span{
|
| 475 |
-
position:absolute;
|
| 476 |
-
font-size:1.2rem;
|
| 477 |
-
animation:float 6s linear infinite;
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
.floating-spores span:nth-child(1){left:10%;top:15%;}
|
| 481 |
-
.floating-spores span:nth-child(2){left:25%;top:60%;animation-delay:1s;}
|
| 482 |
-
.floating-spores span:nth-child(3){left:50%;top:10%;animation-delay:2s;}
|
| 483 |
-
.floating-spores span:nth-child(4){left:75%;top:55%;animation-delay:3s;}
|
| 484 |
-
.floating-spores span:nth-child(5){left:90%;top:20%;animation-delay:4s;}
|
| 485 |
-
|
| 486 |
-
@keyframes bounce{
|
| 487 |
-
0%,100%{transform:translateY(0);}
|
| 488 |
-
50%{transform:translateY(-10px);}
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
@keyframes pulse{
|
| 492 |
-
0%,100%{opacity:.75;}
|
| 493 |
-
50%{opacity:1;}
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
@keyframes float{
|
| 497 |
-
0%{
|
| 498 |
-
transform:translateY(15px);
|
| 499 |
-
opacity:.2;
|
| 500 |
-
}
|
| 501 |
-
50%{
|
| 502 |
-
opacity:1;
|
| 503 |
-
}
|
| 504 |
-
100%{
|
| 505 |
-
transform:translateY(-20px);
|
| 506 |
-
opacity:.2;
|
| 507 |
-
}
|
| 508 |
-
}
|
| 509 |
-
</style>
|
| 510 |
-
"""
|
| 511 |
|
|
|
|
| 33 |
"wonder": "myco-bob 3.2s ease-in-out infinite",
|
| 34 |
"proud": "myco-bob 2s ease-in-out infinite",
|
| 35 |
"sad": "myco-fall 0.6s ease both",
|
| 36 |
+
"scanning": "myco-bob 2.4s ease-in-out infinite, myco-scan 3s ease-in-out infinite",
|
| 37 |
}
|
| 38 |
|
| 39 |
MYCO_FILTER: dict[str, str] = {
|
|
|
|
| 44 |
"wonder": "drop-shadow(0 6px 18px rgba(140,100,255,.5))",
|
| 45 |
"proud": "drop-shadow(0 6px 14px rgba(100,255,160,.4))",
|
| 46 |
"sad": "drop-shadow(0 6px 14px rgba(100,100,180,.3))",
|
| 47 |
+
"scanning": "drop-shadow(0 6px 18px rgba(255,255,255,.5))",
|
| 48 |
}
|
| 49 |
|
| 50 |
SCENE_BG: dict[str, str] = {
|
|
|
|
| 70 |
.sparkle-one { left:34%; top:18%; font-size:1.8rem; animation:twinkle 1.8s ease-in-out infinite; }
|
| 71 |
.sparkle-two { right:33%; top:24%; font-size:1.6rem; animation:twinkle 2.2s ease-in-out infinite reverse; }
|
| 72 |
@keyframes twinkle { 0%,100%{opacity:.3;transform:scale(.85);} 50%{opacity:1;transform:scale(1.18);} }
|
| 73 |
+
|
| 74 |
+
/* Myco Animation Logic */
|
| 75 |
+
.myco-player { font-size:4rem; z-index:2; display: inline-block; transition: left .32s cubic-bezier(.34,1.56,.64,1), bottom .32s cubic-bezier(.34,1.56,.64,1), filter .6s ease; }
|
| 76 |
@keyframes myco-bob { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-.45rem);} }
|
| 77 |
@keyframes myco-recoil { 0%{transform:translateX(0);} 30%{transform:translateX(-1.2rem);} 60%{transform:translateX(.4rem);} 100%{transform:translateX(0);} }
|
| 78 |
@keyframes myco-run-in { 0%{transform:translateX(-3rem) scaleX(-1);} 100%{transform:translateX(0) scaleX(1);} }
|
| 79 |
@keyframes myco-fall { 0%{transform:rotate(0) translateY(0); opacity:1;} 100%{transform:rotate(-90deg) translateY(2rem); opacity:.4;} }
|
| 80 |
+
@keyframes myco-scan { 0%,100%{transform:rotateY(0deg) scaleX(1);} 30%{transform:rotateY(-25deg) scaleX(1);} 70%{transform:rotateY(25deg) scaleX(1);} }
|
| 81 |
+
|
| 82 |
/* Position Grid */
|
| 83 |
.pos-0-0 .myco-player{left:14%;bottom:62%;} .pos-1-0 .myco-player{left:41%;bottom:62%;} .pos-2-0 .myco-player{left:67%;bottom:62%;}
|
| 84 |
.pos-0-1 .myco-player{left:14%;bottom:36%;} .pos-1-1 .myco-player{left:41%;bottom:36%;} .pos-2-1 .myco-player{left:67%;bottom:36%;}
|
|
|
|
| 139 |
pos_cls = f"pos-{pos_x}-{pos_y}"
|
| 140 |
|
| 141 |
# ── Read LLM-driven signals ──────────────────────────────────────────
|
| 142 |
+
scene_mood = (current or {}).get("scene_mood", "normal")
|
| 143 |
myco_emotion = (current or {}).get("myco_emotion", "curious")
|
| 144 |
|
| 145 |
if scene_mood not in MOOD_BG: scene_mood = "normal"
|
| 146 |
if myco_emotion not in EMOTION_ANIMATION: myco_emotion = "curious"
|
| 147 |
|
|
|
|
| 148 |
myco_anim = EMOTION_ANIMATION[myco_emotion]
|
| 149 |
myco_filt = MYCO_FILTER[myco_emotion]
|
| 150 |
outer_bg = SCENE_BG.get(scene_mood, SCENE_BG["normal"])
|
|
|
|
| 155 |
rarity = "common"
|
| 156 |
sprite = "✨"
|
| 157 |
status = "The moss is listening. Click Search Clearing to wake the forest."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
reward_text = "Search a clearing to score spores."
|
| 159 |
state_cls = "state-waiting"
|
| 160 |
+
event_title = "Dreaming Forest"
|
| 161 |
+
encounter = "The next clearing may reveal a memory, creature, or poison omen."
|
| 162 |
else:
|
| 163 |
mushroom_name = current.get("name", "Mystery mushroom")
|
| 164 |
rarity = current.get("rarity", "Common").lower()
|
| 165 |
+
sprite = "🍄🟫"
|
|
|
|
|
|
|
| 166 |
state_cls = _scene_state_class(current)
|
| 167 |
+
status = f"A {rarity} {mushroom_name} breaks through the moss!"
|
| 168 |
+
reward_text = current.get("reward_text", "Decide whether to risk the pickup.")
|
| 169 |
+
event_title = current.get('event_title', 'Strange Omen')
|
| 170 |
+
encounter = current.get("encounter", "The forest waits.")
|
| 171 |
|
| 172 |
import time
|
| 173 |
anim_key = str(int(time.time() * 1000))[-6:]
|
| 174 |
|
| 175 |
+
return f'''{SCENE_STYLES}
|
| 176 |
+
<div class="forest-scene rarity-{escape(rarity)} {pos_cls} {state_cls}"
|
| 177 |
+
data-key="{anim_key}" data-mood="{escape(scene_mood)}" data-emotion="{escape(myco_emotion)}"
|
| 178 |
+
style="background: radial-gradient(circle at 50% 72%,rgba(255,239,173,.18),transparent 8rem), radial-gradient(circle at 18% 24%,rgba(134,225,153,.15),transparent 10rem), {escape(outer_bg)}; border: 1px solid rgba(225,255,209,.25);">
|
| 179 |
+
|
| 180 |
+
<div class="scene-hud">
|
| 181 |
+
<span>🧭 {escape(area.name)}</span>
|
| 182 |
+
<span>🎮 ({pos_x+1},{pos_y+1})</span>
|
| 183 |
+
<span>⭐ {current.get('score_total', '0') if current else '0'}</span>
|
| 184 |
+
<span>❤️ {current.get('health', '3') if current else '3'}/3</span>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<div class="scene-stage">
|
| 188 |
+
<span class="tree-left">🌲</span>
|
| 189 |
+
<span class="tree-right">🌳</span>
|
| 190 |
+
<span class="grass-one">🌿</span>
|
| 191 |
+
<span class="grass-two">🍃</span>
|
| 192 |
+
<span class="myco-player" style="animation: {myco_anim}; filter: {myco_filt};" title="Myco">🍄</span>
|
| 193 |
+
<span class="mushroom-sprite" title="{escape(mushroom_name)}">{sprite}</span>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<div class="forest-map">
|
| 197 |
+
{_movement_tiles(pos_x, pos_y)}
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<div class="scene-caption">
|
| 201 |
+
<strong>{escape(status)}</strong>
|
| 202 |
+
</div>
|
| 203 |
+
<div class="score-burst">{escape(reward_text)}</div>
|
| 204 |
+
<div class="story-moment">
|
| 205 |
+
<strong>{escape(event_title)}</strong>
|
| 206 |
+
<span>{escape(encounter)}</span>
|
| 207 |
+
</div>
|
| 208 |
+
</div>'''
|
| 209 |
|
| 210 |
# ── Helper Functions ───────────────────────────────────────────────────
|
| 211 |
|
|
|
|
| 232 |
def _safe_collection(collection):
|
| 233 |
return list(collection or [])
|
| 234 |
|
| 235 |
+
def game_status_markdown(current: MushroomState | None, collection: CollectionState | None, position: tuple[int, int] | None = None) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
count = len(_safe_collection(collection))
|
| 237 |
pos_x, pos_y = _safe_position(position)
|
| 238 |
active_mystery = next_mystery(count)
|
| 239 |
mystery_line = _mystery_goal_line(active_mystery, count)
|
| 240 |
+
|
| 241 |
if current is None:
|
| 242 |
objective = "Goal: solve **one mystery** — why does Myco remember a vanished forest?"
|
| 243 |
discovery = "No active discovery yet. Move Myco, then Search Clearing."
|
|
|
|
| 286 |
return f"{petals} Impossible Bloom visible"
|
| 287 |
return f"{petals} {5-filled} clue-whispers remain"
|
| 288 |
|
| 289 |
+
def mushroom_card(mushroom: Mushroom | None, clue: str | None = None, secret: str | None = None, state: MushroomState | None = None) -> str:
|
| 290 |
+
if mushroom is None: return "### 🌲 Forest Clearing\nClick **Search Clearing** — watch Myco run toward the mushroom!"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
s = state or {}
|
| 292 |
poison = mushroom.name in POISONOUS_MUSHROOM_NAMES
|
| 293 |
poison_warning = "\n> ⚠️ **Myco senses danger — do NOT pick this without studying first!**" if poison else ""
|
|
|
|
| 310 |
|
| 311 |
def _mushroom_value_label(rarity):
|
| 312 |
if rarity == "Legendary": return "100 spores"
|
| 313 |
+
if rarity == "Rare": return "35 spores + special discovery"
|
| 314 |
return "10 spores"
|
| 315 |
|
| 316 |
def _poison_risk_label(name):
|
|
|
|
| 332 |
'</div>',
|
| 333 |
)))
|
| 334 |
secrets = tuple(_secret_badge(t.name, count >= t.required_discoveries) for t in revealed_mysteries(count))
|
| 335 |
+
if not secrets: secrets = (_secret_badge("First Spore", False),)
|
|
|
|
| 336 |
return "\n".join((
|
| 337 |
'<div class="world-map-panel">',
|
| 338 |
' <div class="world-map-header"><p class="eyebrow">World Map</p>',
|
|
|
|
| 350 |
|
| 351 |
def dex_markdown(collection: CollectionState | None) -> str:
|
| 352 |
current_collection = _safe_collection(collection)
|
| 353 |
+
if not current_collection: return EMPTY_DEX
|
|
|
|
| 354 |
rows = ["### 📖 MycoDex", ""]
|
| 355 |
for entry in current_collection:
|
| 356 |
+
rows.append(f"* 🍄 **{entry['name']}** — {entry['rarity']} — {entry['habitat']} — clue: {entry.get('clue','not studied yet')}")
|
|
|
|
|
|
|
|
|
|
| 357 |
return "\n".join(rows)
|
| 358 |
|
|
|
|
|
|
|
|
|
|
| 359 |
def progress_markdown(collection: CollectionState | None) -> str:
|
| 360 |
+
count = len(_safe_collection(collection))
|
| 361 |
+
active_area = current_area(count)
|
| 362 |
+
upcoming_area = next_area(count)
|
| 363 |
+
unlocked_names = ", ".join(a.name for a in unlocked_areas(count))
|
| 364 |
rows = [
|
| 365 |
"### ✨ Forest Progress",
|
| 366 |
f"**MycoDex discoveries:** {count}",
|
|
|
|
| 385 |
return f"**Mystery:** {remaining} clue discoveries until **{active_mystery.name}** — {active_mystery.clue}"
|
| 386 |
|
| 387 |
def game_intro_markdown() -> str:
|
| 388 |
+
return """<div class="myco-hero-banner"><div class="floating-spores"><span>✨</span><span>✦</span><span>✨</span><span>✧</span><span>✨</span></div><div class="myco-avatar">🍄</div><h1>🌲 The Deep Forest 🌲</h1><div class="highlight-line">🍄 Myco wants to investigate <span>EVERY SINGLE ONE</span>.</div><div class="myco-thought">💭 “I know this forest... but I don't remember why.”</div></div>"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
|