feat: add kitchen and glitchtip wiring
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-10 12:03:12 -05:00
parent 4d9077d586
commit 26dd91947f
49 changed files with 470 additions and 53 deletions
+17 -1
View File
@@ -9,7 +9,23 @@ steps:
commands:
- npm ci
- npm run test
- npm run build
- PUBLIC_GLITCHTIP_RELEASE=$CI_PIPELINE_NUMBER npm run build
- name: upload-sourcemaps
image: rust:1
environment:
SENTRY_URL:
from_secret: glitchtip_url
SENTRY_AUTH_TOKEN:
from_secret: glitchtip_auth_token
SENTRY_ORG:
from_secret: glitchtip_org
SENTRY_PROJECT:
from_secret: glitchtip_project
commands:
- cargo install --locked --git https://gitlab.com/glitchtip/glitchtip-cli.git
- glitchtip-cli sourcemaps inject ./dist
- glitchtip-cli sourcemaps upload ./dist --release "$CI_PIPELINE_NUMBER"
- name: deploy
image: node:22
+4
View File
@@ -12,6 +12,7 @@ Played at [halfstreet.io](https://halfstreet.io).
- TypeScript engine — pure (no DOM, `Date`, `Math.random`, or console)
- World content authored in markdown (rooms, items, encounters, endings) under `src/world/`
- [Vitest](https://vitest.dev) for tests
- Optional client error reporting via [GlitchTip](https://glitchtip.com) using `PUBLIC_GLITCHTIP_DSN`
## Development
@@ -22,6 +23,9 @@ npm run dev # local dev server
npm run build # type-check + production build
```
To enable GlitchTip in the browser, set `PUBLIC_GLITCHTIP_DSN` in your environment before running or building the site.
If you want deploy-aware grouping and readable stack traces, also set `PUBLIC_GLITCHTIP_RELEASE` at build time and upload the generated source maps for that same release.
## Layout
- `src/engine/` — parser, dispatcher, encounter logic
+5
View File
@@ -6,4 +6,9 @@ export default defineConfig({
build: {
inlineStylesheets: 'auto',
},
vite: {
build: {
sourcemap: true,
},
},
})
+76
View File
@@ -9,6 +9,7 @@
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@sentry/browser": "^10.52.0",
"astro": "^6.1.9",
"yaml": "^2.8.4",
"zod": "^4.4.3"
@@ -1766,6 +1767,81 @@
"win32"
]
},
"node_modules/@sentry-internal/browser-utils": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.52.0.tgz",
"integrity": "sha512-x/yEPZdpH6NGQeoeQnV9tj8reAH8twNttiltGZl2o8Rk7sQeUfe7E8yuYP2XbJ2RqyZK5qRS3COrNyMPzf6KFA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.52.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.52.0.tgz",
"integrity": "sha512-5kAn1W8ZvCuHtEHXpq6iRkUMdNCilwww+YxaN2yofVrCivAbB3Ha5JJUMqmWOPW0pC27zGYmoJMIDvG+PczUxA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.52.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.52.0.tgz",
"integrity": "sha512-diywyuc/H7VTUR+W5ryVmLF+0X4UP1OskMqb6V8RSAvJHcj2JmIm7uP+Fc6ACTno+b6AUShwT/L4xVXzO6X9Cw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.52.0",
"@sentry/core": "10.52.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.52.0.tgz",
"integrity": "sha512-BI5ie4dxPuUJ344CXVSnAxY1xZCbghglPSCIlTOYODpR9so9yo5IZh+Mwspt0oWsUMaxWJiQSNYlbPWi7WDavg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "10.52.0",
"@sentry/core": "10.52.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.52.0.tgz",
"integrity": "sha512-ijL9jN86oXwXQWbwhPlEb70ODJSEmjxQEQdnZkC4gDWbjswcwvRsVJPYk+1xl2ir2iZixRIHipVxDcLwian35g==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.52.0",
"@sentry-internal/feedback": "10.52.0",
"@sentry-internal/replay": "10.52.0",
"@sentry-internal/replay-canvas": "10.52.0",
"@sentry/core": "10.52.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/core": {
"version": "10.52.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.52.0.tgz",
"integrity": "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@shikijs/core": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
+1
View File
@@ -15,6 +15,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@sentry/browser": "^10.52.0",
"astro": "^6.1.9",
"yaml": "^2.8.4",
"zod": "^4.4.3"
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m1065.9 452.47v-64.941h66.352v-304.94h67.766v-62.824h-78.352v54.352h-96v75.531h-105.88v105.18h-118.59v62.824h-60.707v64.941h-57.883v56.469h-76.234v108.71h-59.293v172.94h-53.648v-241.41h-52.941v-92.469h-49.41v-53.648h49.41v-67.059h97.41v-112.23h-67.059v45.176h-94.59v55.059h-48v62.824l-194.82-0.003906v-62.824h-66.352v-100.23h-67.059v112.23h54.352v67.059h51.531v53.648h-66.352v153.18h50.117v63.531h-50.117v127.77h53.648v56.469h166.59v-56.469h60.707v-60h107.29v146.12h-156v276h-17.648v86.824h280.23v-164.47h149.65v49.41h56.469v39.531h-59.293v75.531h139.77v-71.293h59.293v-93.176h45.176v93.176h67.059v71.293h176.47v-83.293h-113.65v-117.88h-62.824v-52.234h-59.293v-67.059h-60v-42.352h-121.41v-41.648h182.12v-67.059h-182.12v-45.176h280.23v-67.059h-230.12v-45.176h230.12v-50.117h-153.88v-48.707h188.47zm-750.35 151.06h-113.65v-63.531h113.65zm106.59 496.94h-40.234v-97.41h40.234z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m1200 431.4v-157.8h-77.398v49.199h-750v-49.199h-106.8v49.199h53.398v24.602h53.398v39.602h-54v13.801h-60.602v52.801h-81v60h-61.199v52.801h-57.602v85.199h-57.602v274.8h265.8v-254.4h57.602v-45h67.199v105.6h54v-105.6h43.199v67.801h54v-70.199h43.199v108h54v-64.801h54v-72h114.6v-70.199h237v-49.199h-234v-45.602h390.6zm-502.8-1.8008v96.602h-237v-121.8h237z"/>
<path d="m506.4 443.4h147v49.199h-147z"/>
<path d="m444.6 732.6h139.8v48.602h-139.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 610 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m770.13 1200v-116.11h-69.797v-100.67h-67.785v100.67h-72.484v-100.67h-67.785v100.67h-69.797v116.11z"/>
<path d="m284.9 903.36h137.58v180.54h-137.58z"/>
<path d="m770.13 903.36h137.58v180.54h-137.58z"/>
<path d="m667.45 910.74v-132.89h206.04v-176.51h-226.17v176.51h-95.973v-176.51h-226.18v176.51h204.7v132.89z"/>
<path d="m915.1 232.21v-116.11h-144.97v-116.11h-340.27v116.11h-144.97v116.11h-116.11v671.14h116.11v-555.04h116.11v-116.11h397.98v116.11h116.11v555.04h116.11v-671.14z"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m473.64 170.52h50.52v50.52h-50.52z"/>
<path d="m574.68 170.52h50.52v50.52h-50.52z"/>
<path d="m473.64 221.04h-50.52v101.04h50.52z"/>
<path d="m625.2 322.08h50.641v-101.04h-50.641z"/>
<path d="m524.16 473.64h151.68v-50.52h-101.04v-50.52h50.398v-50.523h-151.56v50.523h50.52v50.52h-101.04v50.52z"/>
<path d="m524.16 120h50.52v50.52h-50.52z"/>
<path d="m574.8 625.2v50.641h50.398v-101.04h-50.398z"/>
<path d="m877.92 928.44h-151.56v-454.8h-50.52v50.52h-50.641v50.641h50.641v353.64h-252.72v-353.64h50.52v-50.641h-50.52v-50.52h-50.52v454.8h-151.56v50.52h50.52v50.52h50.52v-50.52h505.32v50.52h50.523z"/>
<path d="m524.16 524.16h50.52v50.52h-50.52z"/>
<path d="m473.64 574.68h50.52v50.52h-50.52z"/>
<path d="m877.92 827.4h-50.523v50.523h101.04v-50.523z"/>
<path d="m928.44 928.44v101.04h50.52v-151.56h-50.52z"/>
<path d="m726.36 1029.5h-404.28v50.52h505.32v-50.52z"/>
<path d="m877.92 1029.5h50.52v50.52h-50.52z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m536.04 152.04h63.961v63.961h-63.961z"/>
<path d="m792 152.04h63.961v63.961h-63.961z"/>
<path d="m663.96 279.96v-63.961h-63.961v128.04h63.961z"/>
<path d="m600 408v-63.961h-128.04v63.961z"/>
<path d="m855.96 408v-63.961h-127.92v63.961z"/>
<path d="m408 408h63.961v63.961h-63.961z"/>
<path d="m663.96 408h63.961v63.961h-63.961z"/>
<path d="m920.04 279.96v-63.961h-64.078v128.04h64.078z"/>
<path d="m152.04 600h127.92v128.04h-127.92v63.961h192v63.961h63.961v64.078h-63.961v63.961h639.96v-63.961h-63.961v-64.078h63.961v-63.961h63.961v-255.96h-895.92zm639.96 192h63.961v-128.04h64.078v128.04h-64.078v63.961h-63.961z"/>
<path d="m87.961 663.96v64.078h64.078v-128.04h-64.078z"/>
</svg>

After

Width:  |  Height:  |  Size: 848 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m684.72 854.16h-282.36v56.398h395.28v-56.398z"/>
<path d="m684.72 684.72h-282.36v56.52h395.28v-56.52z"/>
<path d="m571.8 515.28h-169.44v56.52h395.28v-56.52z"/>
<path d="m684.72 120h-451.8v960h734.16v-734.16h-56.52v-56.398h-56.398v-56.52h-56.52v-56.398l-56.52-0.003907v-56.52zm0 112.92v169.44h225.84v621.12h-621.12v-846.96h395.28z"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m1100 274.87v-50.016h-49.969v50.016h-550.08v-50.016h-199.97v50.016h-150v50.016h-49.969v150.05h49.969v100.03h-49.969v100.03h-49.969v100.03l-50.062 0.09375v150.05h49.969v50.016h300v-50.016h-49.969v-200.06h49.969v-50.016h199.97v-50.016h-150v-100.03h49.969v50.016h49.969v-100.03h49.969v150.05h49.969v-150.05h600l0.09375-200.16h-100.03zm-900.05 600.24h-49.969v50.016h-49.969v-50.016h-49.969v-50.016h49.969v-50.016h49.969v50.016h49.969zm450.05-450.19h-49.969v-50.016h49.969zm99.984 0h-49.969v-50.016h49.969zm100.03 0h-49.969v-50.016h49.969zm99.984 0h-49.969v-50.016h49.969zm99.984 0h-49.969v-50.016h49.969zm100.03 0h-49.969v-50.016h49.969z"/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m862.4 1125v-224.95h-74.953v-75l-374.9-0.046875v75h-74.953v224.95h74.953v75h374.86v-75h74.953zm-374.81-75v-150h224.9v150z"/>
<path d="m487.6 0v75h-150v75h74.953v75l-74.953 0.046875v75h149.95v75h-149.95v75h149.95v300h149.95l0.046875-750.05z"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m555 120-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30-30-30c-9.9375 9.9844-19.969 19.969-29.953 30l30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30-30 30 30 30c30-30 59.953-60 89.953-90l-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30 30 30 30-30 30 30c20.016-20.016 39.984-39.984 59.953-60l-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30-30-30 30-30c-20.016-20.016-39.984-39.984-59.953-60l30-30c-20.016-20.016-39.984-39.984-59.953-60l30-30c-60.047-60-120.05-120-180-180l-30 30 30 30-30 30 30 30 30-30 30 30z"/>
<path d="m495.05 180 30-30-30-30-30 30 30 30z"/>
<path d="m495.05 180-30 30 30 30 30-30-30-30z"/>
<path d="m495.05 240-30 30 30 30 30-30-30-30z"/>
<path d="m495.05 300c-9.9844 10.031-20.016 20.016-30 30l30 30 30-30-30-30z"/>
<path d="m495.05 360c-9.9844 10.031-20.016 20.016-30 30l30 30 30-30-30-30z"/>
<path d="m495.05 420-30 30 30 30 30-30-30-30z"/>
<path d="m495.05 480c-9.9844 10.031-20.016 20.016-30 30l30 30 30-30-30-30z"/>
<path d="m495.05 540c-9.9844 10.031-20.016 20.016-30 30l30 30 30-30-30-30z"/>
<path d="m495.05 600-30 30 30 30 30-30-30-30z"/>
<path d="m495.05 660-30 30 30 30 30-30-30-30z"/>
<path d="m495.05 720c-9.9844 9.9844-20.016 19.969-30 30l30 30 30-30-30-30z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m971.44 257.02v-57.141h-285.74v57.188h-57.141v57.188h-57.141v114.33h-57.141v171.47h-57.141v171.47h-57.141v114.33h-57.141v57.188h-57.141v57.188h914.29v-57.188h-57.141v-57.188h-57.141v-57.188h-57.141v-171.47h-57.141v114.33h-57.141v57.188h-57.141v-57.188h-57.141v57.188h-57.141v-57.188h-57.141v-171.47h57.141v-57.188h57.141v57.188h57.141v-114.33h57.141v114.33h57.141v-57.188h57.141v114.33h57.141l0.14062-342.98h-57.141v114.33h-114.28v-57.188h57.141v-57.188h57.141v-57.188h-57.141zm-114.28 171.47h-114.28v-57.188h57.141v-57.188h57.141z"/>
<path d="m0 1000.1h57.141v-57.188l-57.141 0.046875v57.188z"/>
<path d="m114.28 1000.1h114.28v-57.188h-114.28z"/>
</svg>

After

Width:  |  Height:  |  Size: 816 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m162.94 9.0469h411.79v65.625h-411.79z"/>
<path d="m531.74 254.21h394.45v65.625h-394.45z"/>
<path d="m531.74 144.79h394.45v65.625h-394.45z"/>
<path d="m162.94 552.84h288.14v70.125h-288.14z"/>
<path d="m825.14 552.84h101.06v70.125h-101.06z"/>
<path d="m751.22 497.06v-116.16h-219.47v242.11h219.47v-70.172h-71.625v-55.781z"/>
<path d="m162.94 679.6h288.14v70.125h-288.14z"/>
<path d="m1109.5 240.61v-73.172h-83.719v-92.766h-73.172v92.766h66.375v794.21h-66.375v59.578l-866.63 0.046876v-791.95h76.172v276.05h288.14v-371.11h-288.14v-59.578h-86.719v92.766h-75.422v862.08h85.969v79.219h106.36v82.219h1008.4v-950.34zm-24.141 883.22h-778.36v-24.891h779.11v24.891z"/>
<path d="m531.74 679.6h394.45v70.125h-394.45z"/>
<path d="m531.74 788.21h394.45v70.125h-394.45z"/>
<path d="m531.74 891.52h394.45v70.125h-394.45z"/>
<path d="m162.94 805.55h288.14v156.14h-288.14z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m524.16 675.84h202.2v-50.641h101.04v-101.04h50.523v-252.6h-50.523v-101.04h-101.04v-50.52h-252.72v50.52h-101.04v101.04h-50.523v252.6h50.523v101.04h101.04v50.641z"/>
<path d="m221.04 1029.5v50.52h757.92v-101.04h-50.52v-101.04h-50.52v-101.04h-101.04v-50.52h-353.76v50.52h-101.04v101.04h-50.52v101.04h-50.52z"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m1200 202.92h-1200v794.16h1200zm-366 206.06v263.34h52.406v183h-160.64v-183h52.406v-263.34h55.875zm-206.76 0v263.34h52.406v183h-160.64v-183h52.406v-263.34h55.875zm-206.76 0v263.34h52.406v183h-160.64v-183h52.406v-263.34h55.875zm-298.26 0h90.797v263.34h52.406v183h-143.86v-446.34zm956.26 446.34h-145.26v-183h52.406v-263.34h92.906v446.34z"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m563.04 489.24v-221.52h73.922v295.32h73.801v-295.32h73.918v-147.72h-369.24v147.72h73.797v295.32h73.801z"/>
<path d="m415.44 563.04h73.801v73.801h-73.801z"/>
<path d="m710.76 563.04h73.801v73.801h-73.801z"/>
<path d="m341.52 636.96h73.801v73.801h-73.801z"/>
<path d="m784.56 636.96h73.801v73.801h-73.801z"/>
<path d="m267.72 710.76h73.801v73.801h-73.801z"/>
<path d="m858.48 710.76h73.801v73.801h-73.801z"/>
<path d="m193.8 784.56h73.801v73.801h-73.801z"/>
<path d="m784.68 784.68h-73.918v-73.918h-221.52v73.918h-73.797v73.801h-73.922v73.801h516.96v-73.801h-73.801z"/>
<path d="m932.28 784.56h73.801v73.801h-73.801z"/>
<path d="m193.8 858.48h-73.801v147.72h73.801z"/>
<path d="m1006.2 932.28v73.918h73.801v-147.72h-73.801z"/>
<path d="m858.48 1006.2h-664.68v73.801h812.4v-73.801z"/>
</svg>

After

Width:  |  Height:  |  Size: 960 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m969.3 600.06v-92.398h-46.332v-46.199h-45.961v-46.078h-92.27v-46.211h-46.309v-92.277h46.309v-184.57h-46.309v-46.078h-276.84v46.078h-46.309v184.58h46.309v92.277h-46.309v46.211h-92.281v46.078h-45.961v46.199h-46.32v92.398h-45.949v276.86h45.949v92.277h46.32v92.289h45.961v46.199h92.281v46.078h369.46v-46.078h92.27v-46.199h45.961v-92.289h46.332v-92.277h45.961v-276.86l-45.961 0.011719zm-92.293-46.32v46.32h-46.309v46.199h-45.961v46.078h-92.27v46.211h-184.92v-46.211h-92.27v-46.078h-45.961v-46.199h-46.32v-92.398h46.32v-46.199h92.27v-46.078h45.961v-184.58h-45.961v-92.277h45.961l0.003906-46.195h184.92v46.211h45.961v92.277h-45.961v184.58h45.961v46.078h92.27v46.199h46.309z"/>
<path d="m646.13 138.54h46.332v46.078h-46.332z"/>
<path d="m553.86 184.62v46.188h92.27v-46.188z"/>
<path d="m507.53 138.54h46.32v46.078h-46.32z"/>
</svg>

After

Width:  |  Height:  |  Size: 986 B

+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m540 60v-60h-59.953v60z"/>
<path d="m719.95 60h-59.953v60h59.953z"/>
<path d="m959.9 60h-59.953v60h59.953z"/>
<path d="m599.95 120v60h59.953v-60z"/>
<path d="m779.9 180v-60h-59.953v60z"/>
<path d="m360.1 240v-60h-59.953v60z"/>
<path d="m719.95 240v-60h-59.953v60z"/>
<path d="m899.86 240h-59.953v60h59.953z"/>
<path d="m779.9 300v60h59.953v-60z"/>
<path d="m959.9 300h-59.953v60h59.953z"/>
<path d="m360.1 360v60h299.9v-60z"/>
<path d="m839.9 420h59.953v-60h-59.953z"/>
<path d="m300.14 480h59.953v-60h-59.953z"/>
<path d="m660 420v60h59.953v-60z"/>
<path d="m240.1 540h59.953v-60h-59.953z"/>
<path d="m719.95 480v60h59.953v-60z"/>
<path d="m180.14 1200h539.81v-60h-479.86v-600h-59.953z"/>
<path d="m839.9 1200v-660h-59.953v660z"/>
<path d="m1019.9 540h-59.953v60h59.953z"/>
<path d="m719.95 780h-179.95v60h179.95z"/>
<path d="m899.86 840h59.953v-60h-59.953z"/>
<path d="m719.95 900h-299.9v60h299.9z"/>
<path d="m300.14 1080h419.81v-60l-419.81-0.046875z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m627.24 163.64v-54.562h-109.08v-54.562h54.516v-54.516h218.11v54.562h163.6v54.562h54.516v54.562h54.516v54.562h-54.516v54.562h109.08v54.562h-490.74v-54.562h-54.516v-54.562h109.08v-54.562h-54.516z"/>
<path d="m790.82 600v54.562h218.11v-54.562h54.516v-54.562h54.516v-163.64h-436.22v163.64h54.516v54.562zm163.6-109.08v-54.562h109.08v54.562zm-54.562 54.562h54.516v54.562h-54.516zm-109.03-54.562v-54.562h109.08v54.562z"/>
<path d="m354.61 1145.5v54.562h-163.55v-54.562h109.08v-54.562h-218.16v-54.562h54.516v-54.562h54.516v-54.562h54.516v-54.562h54.516v-54.562h54.516v-109.12h54.516v-109.12h54.516v-54.562h54.516v-54.562h54.516v-54.562h54.516v109.12h54.516v54.562h54.516v54.562h54.516v54.562h54.516v163.64h-54.516v109.12h-54.516v109.12h-54.516v54.562h-54.516v54.562h-54.516v-163.64h-54.516v54.562h-54.516v54.562h-109.08z"/>
<path d="m899.86 763.64h54.516v54.562h-54.516z"/>
<path d="m954.42 818.21h54.516v109.12h-54.516z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+73 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { dispatch, initialStateFor } from './dispatcher'
import { dispatch, getLightStatus, initialStateFor } from './dispatcher'
import type { World } from '../world/types'
import type { GameState, ParsedCommand } from './types'
import { SCHEMA_VERSION } from './types'
@@ -256,6 +256,78 @@ describe('dispatcher — inventory', () => {
})
})
describe('light status', () => {
it('shows the meter when carrying a lightable item even before it is lit', () => {
const lightWorld: World = {
...world,
items: {
...world.items,
torch: {
id: 'torch',
names: ['torch', 'lamp'],
short: 'an oil lamp',
long: 'An iron oil lamp, unlit.',
initialState: { lit: false },
takeable: true,
lightable: true,
},
},
}
const state: GameState = {
...initialStateFor(lightWorld),
inventory: [{ id: 'torch', state: { lit: false } }],
}
expect(getLightStatus(state, lightWorld)).toEqual({
itemId: 'torch',
lit: false,
turnsLeft: 0,
maxTurns: 6,
})
})
it('prefers a lit lightable item over an unlit one in inventory order', () => {
const lightWorld: World = {
...world,
items: {
...world.items,
torch: {
id: 'torch',
names: ['torch', 'lamp'],
short: 'an oil lamp',
long: 'An iron oil lamp, unlit.',
initialState: { lit: false },
takeable: true,
lightable: true,
},
candlestick: {
id: 'candlestick',
names: ['candlestick', 'candle'],
short: 'a brass candlestick',
long: 'A brass candlestick.',
initialState: { lit: false },
takeable: true,
lightable: true,
},
},
}
const state: GameState = {
...initialStateFor(lightWorld),
inventory: [
{ id: 'candlestick', state: { lit: false } },
{ id: 'torch', state: { lit: true, burn: 6 } },
],
}
expect(getLightStatus(state, lightWorld)).toEqual({
itemId: 'torch',
lit: true,
turnsLeft: 6,
maxTurns: 6,
})
})
})
describe('ambiguous → disambiguation flow', () => {
function makeAmbiguousWorld(): World {
return {
+8 -5
View File
@@ -52,19 +52,22 @@ export function initialStateFor(world: World): GameState {
}
export function getLightStatus(state: GameState, world: World): LightStatus | null {
let fallback: LightStatus | null = null
for (const inst of state.inventory) {
const def = world.items[inst.id]
if (!def?.lightable) continue
if (inst.state['lit'] !== true) continue
const turnsLeft = getLightTurnsLeft(inst)
return {
const lit = inst.state['lit'] === true
const turnsLeft = lit ? getLightTurnsLeft(inst) : 0
const status = {
itemId: inst.id,
lit: true,
lit,
turnsLeft,
maxTurns: LIGHT_TURNS_MAX,
}
if (lit) return status
fallback = fallback ?? status
}
return null
return fallback
}
function append(state: GameState, lines: TranscriptLine[]): GameState {
+15 -2
View File
@@ -405,8 +405,15 @@ body {
width: 30px;
height: 30px;
display: block;
object-fit: contain;
filter: drop-shadow(0 0 2px currentColor);
color: var(--m-dim);
opacity: 0.85;
}
.mystery-light-icon svg {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
}
.mystery-light-leds {
@@ -431,6 +438,12 @@ body {
box-shadow: 0 0 7px currentColor;
}
.mystery-light-meter[data-lit='true'] .mystery-light-icon {
color: var(--m-fg);
opacity: 1;
filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 6px currentColor);
}
.mystery-chip {
background: transparent;
color: var(--m-fg);
+15
View File
@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/browser'
const dsn = import.meta.env.PUBLIC_GLITCHTIP_DSN as string | undefined
const release = import.meta.env.PUBLIC_GLITCHTIP_RELEASE as string | undefined
export function initGlitchTip(): void {
if (!dsn) return
Sentry.init({
dsn,
release,
environment: import.meta.env.MODE,
tracesSampleRate: 0.01,
})
}
+11 -5
View File
@@ -7,12 +7,15 @@ import type { GameState, TranscriptLine } from '../engine/types'
import { TRANSCRIPT_CAP } from '../engine/types'
import { computeChips } from './chips'
import { renderChips } from './chip-render'
import LIGHT_ICON_SVG from '../assets/noun-candle-6409709.svg?raw'
import { initGlitchTip } from './glitchtip'
const transcriptEl = document.querySelector<HTMLDivElement>('[data-mystery-transcript]')
const inputEl = document.querySelector<HTMLInputElement>('[data-mystery-input]')
const inputDisplayEl = document.querySelector<HTMLSpanElement>('[data-mystery-input-display]')
const lightMeterEl = document.querySelector<HTMLDivElement>('[data-mystery-light-meter]')
const LIGHT_ICON_URL = new URL('../assets/noun-oil-lamp-8301660.svg', import.meta.url).href
initGlitchTip()
const HELP_TEXT = `You arrive at the address, but you do not remember what has happened. The road behind you is gone...
@@ -60,11 +63,10 @@ if (!transcriptEl || !inputEl || !inputDisplayEl) {
lightMeterEl.dataset['lit'] = 'true'
lightMeterEl.dataset['turnsLeft'] = String(status.turnsLeft)
const icon = document.createElement('img')
const icon = document.createElement('div')
icon.className = 'mystery-light-icon'
icon.src = LIGHT_ICON_URL
icon.alt = ''
icon.setAttribute('aria-hidden', 'true')
icon.innerHTML = LIGHT_ICON_SVG
lightMeterEl.appendChild(icon)
const leds = document.createElement('div')
@@ -73,7 +75,11 @@ if (!transcriptEl || !inputEl || !inputDisplayEl) {
for (let i = 0; i < LIGHT_TURNS_MAX; i++) {
const segment = document.createElement('span')
segment.className = 'mystery-light-segment'
segment.dataset['segmentState'] = i < turnsLeft ? 'lit' : 'dim'
const lit = i < turnsLeft
segment.dataset['segmentState'] = lit ? 'lit' : 'dim'
segment.style.backgroundColor = lit ? 'var(--m-fg)' : 'var(--m-dim)'
segment.style.boxShadow = lit ? '0 0 7px var(--m-fg)' : '0 0 0 1px rgba(0, 0, 0, 0.15) inset'
segment.style.opacity = lit ? '1' : '0.45'
leds.appendChild(segment)
}
lightMeterEl.appendChild(leds)
+7 -1
View File
@@ -25,7 +25,6 @@
- [x] If the user says "use match with letter" they should burn the letter.
- [x] There should be a lighter in the smoking room that allows unlimited lighting.
- [ ] Create a mechanic that asks "Are you sure?" before taking critical actions like attacking or other game-changing mechanics that might affect the final ending.
- [ ] Create a new item with a mechanic: whiskey bottle. When the user drinks it they get drunk and are transported to the "drunk rooms" which are a bit of a maze and things get a little topsy-turvy. The player chances to lose an item (returning to its original spot) when they get drunk and wakes up several turns later somewhere else predetermined.
- [ ] Add lightened descriptions to darkened rooms. About half the rooms should be too dark to see anything (affects ability to move forward, can't see exits or entounters, except for maybe hints at the encounters, like sounds or shapes in the dark) Add frontmatter property to all rooms: (dark: true/false). Make text in darkened rooms a grey color.
- [ ] Implement a simple "stealth mechanic", where sometimes it's advantageous to have the light out.
- [ ] Implement a simple (optional?) minimap in the UI? - Maybe tied to an item? Once you get the map the minimap appears? Can we POC it?
@@ -37,3 +36,10 @@
- [ ] Implement a carry mechanic. Decide whether we should have a limited carry ability (only able to carry a few things?) or we night need a full inventory system, where items are assigned to pockets or hand carry and we can only hand carry a couple of items?
- [ ] Implement a "drop" mechanic
- [x] We need a light indicator that shows when the light is lit and how much time is left on the light. Use the svg file I dropped in the src/assets folder for the indicator. The indicator should be a 6-segment led that runs in a dotted line underneath the light indicator and burns out right to left. The color of the indicator should be bright when it's lit and dim when it's not. The indicator should be to the right of the tiles and sized appropriately.
- [ ] FEATURE: Add an option to disable the chips in the options menu.
- [ ] BUG: The new cursor doesn't appear on mobile.
- [ ] FEATURE: Add a Safe to somewhere that it makes sense (the bedroom?) We can add a safe-cracking mini-game. The safe contains a single bullet, which can be used with the revolver.
- [ ] FEATURE: Add item Revolver, which can be used to kill the thing at the end. It comes with no bullets, so players need to defeat the safe-cracking minjgame. Place the revolver somewhere it takes the player some effort to find.
- [ ] Feature: A faceless voice that speaks in a whisper in the chapel, which demands whiskey and dispenses riddles if the player finds a bottle of whiskey (but hasn't drunk it yet). The riddles are randomly chosen from a list of 25 difficult riddles (source them). If the riddle is answered correctly there needs to be some reward and maybe a major plot point is revealed.
- [ ] Add item Whiskey bottle, half full of something smoky. In the kitchen. Drinking it gets the player drunk, which causes them to unlock the drunk rooms, which are a series of rooms where they can go many directions, but somehow end up in seemingly back in the same spot. They are essentially a maze of doors and hallways, ladders and levels. There should be boundaries established though, it shouldn't be endless. After 20 or so moves the player passes out and wakes up back in the foyer, with the whiskey bottle returned to the kitchen, somehow still half full. Add a random encounter in the dark rooms, a creaking floorboard helps you find a secret door that opens with a faceless man inside who gives you major plot info. After this encounter you pass out and wake up in the lobby as before.
- [ ]
+2 -2
View File
@@ -10,10 +10,10 @@ The wall beside your shoulder breathes in. The opposite wall answers.
## wait-resolved
You stand still.
The passage narrows, then forgets to. The breathing passes on ahead of you.
The passage narrows, then forgets to. The breathing passes on ahead of you, down the wall and out of reach.
## wrong-verb
The walls take a slower breath.
## failed
The boards lean close enough to touch your sleeves. You retreat into the music room.
The boards lean close enough to touch your sleeves. You retreat into the music room before they can close around you.
+2 -2
View File
@@ -15,7 +15,7 @@ The cage is empty. A few pale feathers cling to the wire, though no bird could h
Somewhere far above you, wings beat once inside a wall.
## wrong-verb
The cloth trembles.
The cloth trembles, then goes still again.
## failed
The rustling grows too close to your ear. You leave the room before you decide to.
The rustling grows too close to your ear. You leave the room before you decide to, and the cage is still covered when you look back.
+2 -2
View File
@@ -13,7 +13,7 @@ The shears close with a sound like teeth.
The figure falls apart leaf by leaf. Behind it, the glass is only glass.
## wrong-verb
The vines tighten without moving.
The vines tighten without moving. Their silence feels deliberate.
## failed
The ivy catches at your wrists. When you pull free, you are back among the cold plates of the dining room.
The ivy catches at your wrists. When you pull free, you are back among the cold plates of the dining room, with leaves clinging to your sleeves.
+3 -3
View File
@@ -5,7 +5,7 @@ initialPhase: hanging
---
## hanging
One hanging sheet has the weight and outline of a person standing behind it.
One hanging sheet has the weight and outline of a person standing behind it. The shape shifts when you look straight at it.
## wait-resolved
You wait.
@@ -13,7 +13,7 @@ You wait.
The sheet stirs. Nothing stands behind it. Nothing had stood behind it.
## wrong-verb
The shape seems to lean toward you.
The shape seems to lean toward you, then settles back into stillness.
## failed
You push through the hanging sheets and come out in the servants' passage, breathing hard.
You push through the hanging sheets and come out in the servants' passage, breathing hard, with damp cloth brushing your face.
+1 -1
View File
@@ -13,7 +13,7 @@ You play the waiting note.
The answer comes at once, nearer than before. A narrow part of the wall settles back into shadow.
## wrong-verb
The answering note repeats, patient and exact.
The answering note repeats, patient and exact. It sounds as if it has not moved at all.
## failed
The wrong chord goes through the floorboards. You climb back to the hallway before the echo finishes.
+3 -3
View File
@@ -5,7 +5,7 @@ initialPhase: standing-outside
---
## standing-outside
Rain touches the dining-room window from the wrong side. Someone stands beyond the glass with their head bowed.
Rain touches the dining-room window from the wrong side. Someone stands beyond the glass with their head bowed. Their coat is too dark to make out properly.
## close-window-guest-resolved
You draw the curtains together before you look closely.
@@ -13,7 +13,7 @@ You draw the curtains together before you look closely.
For a moment, cloth and rain hold the same shape. Then there is only the table behind you.
## wrong-verb
The figure outside lifts its face a little.
The figure outside lifts its face a little, as if disappointed by your hesitation.
## failed
The glass shows too much. You find yourself back in the hallway with the taste of rain in your mouth.
The glass shows too much. For one instant there is a second dining room beyond it. Then you are back in the hallway with rain in your mouth.
+3 -3
View File
@@ -8,10 +8,10 @@ initialState:
lit: false
---
A brass candlestick, heavy at the base. The candle inside it is burned low but not spent.
A brass candlestick, heavy at the base. The candle inside it is burned low but not spent. It holds a little light very steadily, as if it dislikes being noticed.
## lit
The wick takes. The flame bends toward the nearest doorway.
The wick takes. The flame stands upright for a moment, then bends toward the nearest doorway.
## extinguished
The candle gutters out. For a moment the smoke leans against your hand.
The candle gutters out. For a moment the smoke leans against your hand before it thins.
+1 -1
View File
@@ -6,4 +6,4 @@ takeable: true
initialState: {}
---
A sheet still wet from some old washing. It smells of rainwater and closed rooms.
A sheet still wet from some old washing. It smells of rainwater and closed rooms. It keeps the cold longer than cloth should.
+1 -1
View File
@@ -6,4 +6,4 @@ takeable: true
initialState: {}
---
A tiny key on a black ribbon. It is too small for any door in the house.
A tiny key on a black ribbon. It is too small for any door in the house, and yet the ribbon has been tied and retied as if someone meant to keep it safe.
+1 -1
View File
@@ -6,4 +6,4 @@ takeable: true
initialState: {}
---
Iron pruning shears with dark soil in the hinge. The handles are cold through your palm.
Iron pruning shears with dark soil in the hinge. The handles are cold through your palm. They have been used often enough to remember the shape of branches.
+1 -1
View File
@@ -7,4 +7,4 @@ lighter: true
initialState: {}
---
A silver lighter with a worn crest on one side. It opens with a small reluctant click.
A silver lighter with a worn crest on one side. It opens with a small reluctant click, then catches as if it had been waiting to be asked.
+9
View File
@@ -0,0 +1,9 @@
---
id: whiskey
names: ["whiskey", "bottle of whiskey", "bottle"]
short: "a bottle of whiskey"
takeable: true
initialState: {}
---
A bottle of whiskey with a cloudy label and a dark amber level inside. It has the look of something set down in a hurry and never touched again.
+2 -2
View File
@@ -13,7 +13,7 @@ encounter: "[[ivy-figure]]"
---
## first-visit
The conservatory roof has gone blind with moss and old rain. Vines press against the glass from both sides.
The conservatory roof has gone blind with moss and old rain. Vines press against the glass from both sides, as if the room were trying to keep something in.
Something human-shaped hangs among the ivy. The dining room is south.
@@ -21,4 +21,4 @@ Something human-shaped hangs among the ivy. The dining room is south.
The conservatory sweats in the dark.
## examined
The tiled floor is split by roots. The air smells of wet soil and cold iron. A bench has been dragged beneath the blind glass roof.
The tiled floor is split by roots. The air smells of wet soil and cold iron. A bench has been dragged beneath the blind glass roof and left at an angle, as if someone stood up too quickly.
+4 -4
View File
@@ -3,7 +3,7 @@ id: dining-room
title: "[ Dining Room ]"
exitN: "[[conservatory]]"
exitS: null
exitE: null
exitE: "[[kitchen]]"
exitW: "[[hallway]]"
exitU: null
exitD: null
@@ -14,12 +14,12 @@ encounter: "[[window-guest]]"
---
## first-visit
The dining room is laid for supper long after supper ended. Plates dull the long table. One place has not been touched.
The dining room is laid for supper long after supper ended. Plates dull the long table. One place at the far end has not been touched.
The hallway is west. A greenish light presses through glass to the north.
The hallway is west. The kitchen is east. A green light presses through the glass to the north.
## revisit
The dining room holds its place at the table.
## examined
The silver has tarnished black at the edges. The untouched plate is clean enough to show a dim shape behind you, though there is no one there.
The silver has gone black at the edges. The untouched plate is clean enough to show a shape behind you, though there is no one there when you turn.
+25
View File
@@ -0,0 +1,25 @@
---
id: kitchen
title: "[ Kitchen ]"
exitN: null
exitS: null
exitE: null
exitW: "[[dining-room]]"
exitU: null
exitD: null
items:
- "[[whiskey]]"
encounter: null
safe: true
---
## first-visit
Something recently warm. The room smells of fat, onions, and wood smoke that has not yet cleared.
The dining room is west.
## revisit
The kitchen keeps its heat a little too long.
## examined
Copper pots hang over the stove. A corkscrew lies by the sink. The bottle on the counter catches what little light there is and holds it.
+2 -2
View File
@@ -14,7 +14,7 @@ safe: true
---
## first-visit
Sheets hang unmoving in the still air. They divide the room into narrow aisles. One of them has the shape of a person behind it.
Sheets hang unmoving in the still air. They divide the room into narrow aisles. One of them has the shape of a person behind it, or the suggestion of one.
The servants' passage is west.
@@ -22,4 +22,4 @@ The servants' passage is west.
The laundry hangs white and silent.
## examined
Water darkens the floor beneath the hanging sheets, though none of them drip. An empty basket waits below the line.
Water darkens the floor beneath the hanging sheets, though none of them drip. An empty basket waits below the line, tilted as if something climbed out of it.
+1 -1
View File
@@ -22,4 +22,4 @@ Something answers from elsewhere in the house, too soft to be music. A servants'
The music room waits on the same note.
## examined
The depressed piano key does not rise when touched. The music stand is empty, but its clips remain open.
The depressed piano key does not rise when touched. The music stand is empty, but its clips remain open, as if a page had just been lifted away.
+5 -5
View File
@@ -14,14 +14,14 @@ safe: true
---
## first-visit
The parlor has been arranged for company. No chair faces another. Each one waits at a slight angle, as if the guests stood suddenly and left the room in silence.
The parlor has been set for company that never arrives. The chairs are drawn a little too far apart, as if the people meant to speak and then forgot what they had meant to say.
A stopped clock stands against the north wall. A narrow study opens beyond it. The hallway is below.
A stopped clock stands against the north wall. The study lies beyond it. The hallway is below.
A staircase climbs where the corner of the room should be.
A staircase rises where the corner should be.
## revisit
The parlor waits with its empty chairs.
The parlor keeps its arrangement.
## examined
Dust gathers on the chair arms, but not on the seats. Someone has sat here recently, or the room remembers being occupied. The grandfather clock shows no hour you can name.
Dust gathers on the chair arms, not on the seats. Something in the room has been touched often. The grandfather clock shows no hour you can name.
+2 -2
View File
@@ -12,7 +12,7 @@ encounter: "[[breathing-wall]]"
---
## first-visit
The walls here are unfinished and smell of wet wood. The passage is too narrow for two people to pass.
The walls here are unfinished and smell of wet wood. The passage is too narrow for two people to pass, though it feels made for someone else to follow.
The music room is south. A laundry lies east.
@@ -20,4 +20,4 @@ The music room is south. A laundry lies east.
The servants' passage breathes less loudly now, or you have learned when not to listen.
## examined
The boards show old finger marks in the grain. They run at shoulder height along both walls, as if someone felt their way through in the dark.
The boards show old finger marks in the grain. They run at shoulder height along both walls, as if someone felt their way through in the dark and never touched the floor.
+2 -2
View File
@@ -14,7 +14,7 @@ encounter: "[[covered-cage]]"
---
## first-visit
The smoking room smells faintly of ash, velvet, and bitter almonds. The chairs are turned toward the cold hearth.
The smoking room smells faintly of ash, velvet, and bitter almonds. The chairs are turned toward the cold hearth, though no one has sat here in years.
A cloth-covered cage hangs from a stand. The hallway is east.
@@ -22,4 +22,4 @@ A cloth-covered cage hangs from a stand. The hallway is east.
The smoking room keeps its breath.
## examined
Cigar ash lies in a clean saucer. The cold hearth reflects the chairs in a narrow black curve.
Cigar ash lies in a clean saucer. The cold hearth reflects the chairs in a narrow black curve. Nothing in the room seems to have been allowed to burn all the way down.