This commit was merged in pull request #1.
This commit is contained in:
31
Makefile
Normal file
31
Makefile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.PHONY: dev build deploy clean
|
||||||
|
|
||||||
|
# Local development server
|
||||||
|
dev:
|
||||||
|
@echo "Starting dev server on :8080..."
|
||||||
|
@cd "$(CURDIR)" && python3 -m http.server 8080
|
||||||
|
|
||||||
|
# Build the static site (generate blog index + RSS from posts)
|
||||||
|
build:
|
||||||
|
@echo "Building the Tower..."
|
||||||
|
@python3 scripts/build.py
|
||||||
|
@echo "Done."
|
||||||
|
|
||||||
|
# Deploy — configure DEPLOY_TARGET in environment or .env
|
||||||
|
deploy: build
|
||||||
|
@echo "Deploying..."
|
||||||
|
@if [ -z "$${DEPLOY_TARGET:-}" ]; then \
|
||||||
|
echo "Error: Set DEPLOY_TARGET (e.g., user@host:/var/www/tower)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
rsync -avz --delete \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude 'scripts/' \
|
||||||
|
--exclude 'Makefile' \
|
||||||
|
--exclude '*.md' \
|
||||||
|
./ "$${DEPLOY_TARGET}/"
|
||||||
|
@echo "Deployed to $${DEPLOY_TARGET}"
|
||||||
|
|
||||||
|
# Clean generated files
|
||||||
|
clean:
|
||||||
|
@echo "Nothing to clean yet."
|
||||||
40
README.md
40
README.md
@@ -1,3 +1,39 @@
|
|||||||
# alexanderwhitestone.com
|
# The Wizard's Tower
|
||||||
|
|
||||||
Public-facing interface for Timmy — AlexanderWhitestone.com
|
**AlexanderWhitestone.com** — two rooms, nothing else.
|
||||||
|
|
||||||
|
## Rooms
|
||||||
|
|
||||||
|
- **The Workshop** (`/world/`) — A 3D space where Timmy lives. Visitors enter and interact.
|
||||||
|
- **The Scrolls** (`/blog/`) — Alexander's words. Plain text, RSS, sovereign publishing.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html Entry hall — two doors
|
||||||
|
world/ The Workshop (3D scene, Timmy presence)
|
||||||
|
blog/ The Scrolls (posts, RSS feed)
|
||||||
|
scripts/ CLI tools (aw-post for quick publishing)
|
||||||
|
static/ Shared assets (fonts, favicon)
|
||||||
|
Makefile Build, dev, deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev # Local dev server on :8080
|
||||||
|
make build # Build static site
|
||||||
|
make deploy # Deploy (configure target in Makefile)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Decisions (Open)
|
||||||
|
|
||||||
|
- [ ] 3D engine: Three.js vs Babylon.js
|
||||||
|
- [ ] Blog: Hugo vs hand-rolled static generator
|
||||||
|
- [ ] Hosting: self-hosted Nginx/Caddy vs static CDN
|
||||||
|
- [ ] Timmy's 3D character design
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
Two doors. No navbar. No sidebar. No footer links. You walk in, you choose a room.
|
||||||
|
The Workshop is alive. The Scrolls are permanent. That's the Tower.
|
||||||
|
|||||||
13
blog/feed.xml
Normal file
13
blog/feed.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>The Scrolls — Alexander Whitestone</title>
|
||||||
|
<link href="https://alexanderwhitestone.com/blog/"/>
|
||||||
|
<link rel="self" href="https://alexanderwhitestone.com/blog/feed.xml"/>
|
||||||
|
<id>https://alexanderwhitestone.com/blog/</id>
|
||||||
|
<updated>2026-03-19T01:32:41Z</updated>
|
||||||
|
<author>
|
||||||
|
<name>Alexander Whitestone</name>
|
||||||
|
</author>
|
||||||
|
<subtitle>Words from the Wizard's Tower</subtitle>
|
||||||
|
|
||||||
|
</feed>
|
||||||
40
blog/index.html
Normal file
40
blog/index.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>The Scrolls — The Wizard's Tower</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
max-width: 640px; margin: 0 auto; padding: 2rem 1rem;
|
||||||
|
background: #0a0a0f; color: #e0d8c8;
|
||||||
|
font-family: Georgia, serif; line-height: 1.6;
|
||||||
|
}
|
||||||
|
header { margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }
|
||||||
|
header h1 { font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }
|
||||||
|
header nav { margin-top: 0.5rem; font-size: 0.8rem; }
|
||||||
|
header a { color: #8a7f6a; text-decoration: none; }
|
||||||
|
header a:hover { color: #e0d8c8; }
|
||||||
|
.posts { list-style: none; }
|
||||||
|
.posts li { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }
|
||||||
|
.posts .date { font-size: 0.8rem; color: #6a6050; display: block; }
|
||||||
|
.posts .title { font-size: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>The Scrolls</h1>
|
||||||
|
<nav><a href="/">← The Tower</a> · <a href="/blog/feed.xml">RSS</a></nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<ul class="posts">
|
||||||
|
<li>
|
||||||
|
<span class="date">2026-03-18</span>
|
||||||
|
<span class="title">Hello World</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
blog/posts/.gitkeep
Normal file
0
blog/posts/.gitkeep
Normal file
83
index.html
Normal file
83
index.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>The Wizard's Tower</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0d8c8;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: normal;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
color: #8a7f6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doors {
|
||||||
|
display: flex;
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #e0d8c8;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
border: 1px solid #2a2520;
|
||||||
|
transition: border-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door:hover {
|
||||||
|
border-color: #8a7f6a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door-name {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6a6050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door:hover .door-desc {
|
||||||
|
color: #8a7f6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.doors { flex-direction: column; gap: 2rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>The Wizard's Tower</h1>
|
||||||
|
<nav class="doors">
|
||||||
|
<a href="/world/" class="door">
|
||||||
|
<span class="door-name">The Workshop</span>
|
||||||
|
<span class="door-desc">Enter the world</span>
|
||||||
|
</a>
|
||||||
|
<a href="/blog/" class="door">
|
||||||
|
<span class="door-name">The Scrolls</span>
|
||||||
|
<span class="door-desc">Read the words</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
49
scripts/aw-post
Executable file
49
scripts/aw-post
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# aw-post — Quick-post a scroll from the command line
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# aw-post "Title of the Post"
|
||||||
|
# aw-post "Title" < body.md
|
||||||
|
# echo "Post body here" | aw-post "Title"
|
||||||
|
#
|
||||||
|
# Creates a new markdown file in blog/posts/ with frontmatter.
|
||||||
|
# Rebuilds the blog index and RSS feed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BLOG_DIR="$(cd "$(dirname "$0")/.." && pwd)/blog/posts"
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: aw-post \"Title of the Post\""
|
||||||
|
echo " Pipe or redirect body content via stdin."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TITLE="$1"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
|
||||||
|
FILENAME="${DATE}-${SLUG}.md"
|
||||||
|
FILEPATH="${BLOG_DIR}/${FILENAME}"
|
||||||
|
|
||||||
|
if [ -f "$FILEPATH" ]; then
|
||||||
|
echo "Error: $FILEPATH already exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read body from stdin if available
|
||||||
|
BODY=""
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
BODY=$(cat)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$FILEPATH" << EOF
|
||||||
|
---
|
||||||
|
title: "${TITLE}"
|
||||||
|
date: ${DATE}
|
||||||
|
---
|
||||||
|
|
||||||
|
${BODY}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Created: ${FILEPATH}"
|
||||||
|
echo "Next: rebuild with 'make build'"
|
||||||
172
scripts/build.py
Normal file
172
scripts/build.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build script for The Scrolls — generates blog index and RSS feed from
|
||||||
|
markdown posts in blog/posts/.
|
||||||
|
|
||||||
|
Each post is a markdown file with YAML frontmatter:
|
||||||
|
---
|
||||||
|
title: "Post Title"
|
||||||
|
date: 2026-03-18
|
||||||
|
---
|
||||||
|
Body content here.
|
||||||
|
|
||||||
|
Generates:
|
||||||
|
- blog/index.html with post listing
|
||||||
|
- blog/feed.xml Atom feed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SITE_URL = "https://alexanderwhitestone.com"
|
||||||
|
BLOG_DIR = Path(__file__).parent.parent / "blog"
|
||||||
|
POSTS_DIR = BLOG_DIR / "posts"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frontmatter(text):
|
||||||
|
"""Extract YAML frontmatter from markdown text."""
|
||||||
|
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return {}, text
|
||||||
|
|
||||||
|
meta = {}
|
||||||
|
for line in match.group(1).strip().split("\n"):
|
||||||
|
if ":" in line:
|
||||||
|
key, val = line.split(":", 1)
|
||||||
|
meta[key.strip()] = val.strip().strip('"').strip("'")
|
||||||
|
return meta, match.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def load_posts():
|
||||||
|
"""Load and sort all posts by date (newest first)."""
|
||||||
|
posts = []
|
||||||
|
if not POSTS_DIR.exists():
|
||||||
|
return posts
|
||||||
|
|
||||||
|
for path in sorted(POSTS_DIR.glob("*.md"), reverse=True):
|
||||||
|
text = path.read_text()
|
||||||
|
meta, body = parse_frontmatter(text)
|
||||||
|
if meta.get("title") and meta.get("date"):
|
||||||
|
posts.append(
|
||||||
|
{
|
||||||
|
"title": meta["title"],
|
||||||
|
"date": meta["date"],
|
||||||
|
"slug": path.stem,
|
||||||
|
"body": body,
|
||||||
|
"path": path,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return posts
|
||||||
|
|
||||||
|
|
||||||
|
def generate_index(posts):
|
||||||
|
"""Generate blog/index.html with post listing."""
|
||||||
|
if not posts:
|
||||||
|
print(" No posts found. Keeping placeholder index.")
|
||||||
|
return
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for p in posts:
|
||||||
|
items.append(
|
||||||
|
f' <li>\n'
|
||||||
|
f' <span class="date">{p["date"]}</span>\n'
|
||||||
|
f' <span class="title">{p["title"]}</span>\n'
|
||||||
|
f" </li>"
|
||||||
|
)
|
||||||
|
|
||||||
|
post_list = "\n".join(items)
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>The Scrolls — The Wizard's Tower</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
||||||
|
<style>
|
||||||
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
max-width: 640px; margin: 0 auto; padding: 2rem 1rem;
|
||||||
|
background: #0a0a0f; color: #e0d8c8;
|
||||||
|
font-family: Georgia, serif; line-height: 1.6;
|
||||||
|
}}
|
||||||
|
header {{ margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }}
|
||||||
|
header h1 {{ font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }}
|
||||||
|
header nav {{ margin-top: 0.5rem; font-size: 0.8rem; }}
|
||||||
|
header a {{ color: #8a7f6a; text-decoration: none; }}
|
||||||
|
header a:hover {{ color: #e0d8c8; }}
|
||||||
|
.posts {{ list-style: none; }}
|
||||||
|
.posts li {{ margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }}
|
||||||
|
.posts .date {{ font-size: 0.8rem; color: #6a6050; display: block; }}
|
||||||
|
.posts .title {{ font-size: 1rem; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>The Scrolls</h1>
|
||||||
|
<nav><a href="/">← The Tower</a> · <a href="/blog/feed.xml">RSS</a></nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<ul class="posts">
|
||||||
|
{post_list}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
(BLOG_DIR / "index.html").write_text(html)
|
||||||
|
print(f" Generated index with {len(posts)} post(s).")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_feed(posts):
|
||||||
|
"""Generate blog/feed.xml Atom feed."""
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for p in posts[:20]: # Cap at 20 entries
|
||||||
|
# Simple HTML conversion: wrap paragraphs
|
||||||
|
html_body = "\n".join(
|
||||||
|
f"<p>{para}</p>" for para in p["body"].split("\n\n") if para.strip()
|
||||||
|
)
|
||||||
|
entries.append(
|
||||||
|
f""" <entry>
|
||||||
|
<title>{p["title"]}</title>
|
||||||
|
<link href="{SITE_URL}/blog/posts/{p["slug"]}.html"/>
|
||||||
|
<id>{SITE_URL}/blog/posts/{p["slug"]}</id>
|
||||||
|
<updated>{p["date"]}T00:00:00Z</updated>
|
||||||
|
<content type="html"><![CDATA[{html_body}]]></content>
|
||||||
|
</entry>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_block = "\n".join(entries)
|
||||||
|
|
||||||
|
feed = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>The Scrolls — Alexander Whitestone</title>
|
||||||
|
<link href="{SITE_URL}/blog/"/>
|
||||||
|
<link rel="self" href="{SITE_URL}/blog/feed.xml"/>
|
||||||
|
<id>{SITE_URL}/blog/</id>
|
||||||
|
<updated>{now}</updated>
|
||||||
|
<author>
|
||||||
|
<name>Alexander Whitestone</name>
|
||||||
|
</author>
|
||||||
|
<subtitle>Words from the Wizard's Tower</subtitle>
|
||||||
|
{entry_block}
|
||||||
|
</feed>"""
|
||||||
|
|
||||||
|
(BLOG_DIR / "feed.xml").write_text(feed)
|
||||||
|
print(f" Generated feed with {len(entries)} entry/entries.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Building The Scrolls...")
|
||||||
|
posts = load_posts()
|
||||||
|
generate_index(posts)
|
||||||
|
generate_feed(posts)
|
||||||
|
print("Build complete.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
0
world/assets/.gitkeep
Normal file
0
world/assets/.gitkeep
Normal file
37
world/index.html
Normal file
37
world/index.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>The Workshop — The Wizard's Tower</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { background: #0a0a0f; color: #e0d8c8; font-family: Georgia, serif; }
|
||||||
|
#scene {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: #6a6050;
|
||||||
|
}
|
||||||
|
.placeholder h1 { font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; }
|
||||||
|
.placeholder p { font-size: 0.85rem; }
|
||||||
|
.placeholder a { color: #8a7f6a; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="scene">
|
||||||
|
<div class="placeholder">
|
||||||
|
<h1>The Workshop</h1>
|
||||||
|
<p>Timmy's world is being built.</p>
|
||||||
|
<p><a href="/">← Back to the Tower</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Three.js scene will mount to #scene -->
|
||||||
|
<!-- <script type="module" src="main.js"></script> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
world/main.js
Normal file
20
world/main.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* The Workshop — Three.js scene bootstrap
|
||||||
|
*
|
||||||
|
* This file will initialize the 3D world where Timmy lives.
|
||||||
|
* Currently a placeholder until tech decisions are made:
|
||||||
|
* - 3D engine confirmed (Three.js vs Babylon.js)
|
||||||
|
* - Character design direction chosen
|
||||||
|
* - WebSocket bridge to Timmy's soul designed (#243)
|
||||||
|
*
|
||||||
|
* See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Future: import * as THREE from 'three';
|
||||||
|
|
||||||
|
export function initWorkshop(container) {
|
||||||
|
// TODO: Initialize 3D scene
|
||||||
|
// TODO: Load wizard character model
|
||||||
|
// TODO: Connect to Timmy presence WebSocket
|
||||||
|
console.log('[Workshop] Scene container ready:', container.id);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user