PageSourceSearch

https://suzyeth.github.io/

html suzyeth.github.io collected 2026-10-03 10:01:46 UTC 63,448 bytes, 1,130 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Suzy Su — Game Designer · Systems, Economy, Solo Prototypes</title>
7<meta name="description" content="Suzy Su — London-based game designer focused on F2P systems, economy and monetisation, with shipped mobile titles plus solo games, tools and prototypes.">
8<meta name="author" content="Suzy Su">
9<meta name="robots" content="index, follow">
10<meta name="theme-color" content="#ffffff">
11
12<!-- Update this to the live URL once deployed (used for canonical + social images) -->
13<link rel="canonical" href="https://suzyeth.github.io/">
14
15<!-- Open Graph (LinkedIn / Slack / Facebook previews) -->
16<meta property="og:type" content="website">
17<meta property="og:title" content="Suzy Su — Game Designer">
18<meta property="og:description" content="London-based game designer focused on F2P systems, economy and monetisation, with shipped mobile titles plus solo games, tools and prototypes.">
19<meta property="og:url" content="https://suzyeth.github.io/">
20<meta property="og:image" content="https://suzyeth.github.io/assets/og-card.jpg">
21<meta property="og:image:width" content="1200">
22<meta property="og:image:height" content="630">
23<meta property="og:image:alt" content="Suzy Su — Game Designer, London / Remote">
24<meta property="og:locale" content="en_GB">
25<meta property="og:locale:alternate" content="zh_CN">
26
27<!-- Twitter / X card -->
28<meta name="twitter:card" content="summary_large_image">
29<meta name="twitter:title" content="Suzy Su — Game Designer">
30<meta name="twitter:description" content="F2P systems, economy and monetisation designer with shipped mobile work plus solo games, tools and prototypes.">
31<meta name="twitter:image" content="https://suzyeth.github.io/assets/og-card.jpg">
32
33<!-- Inline SVG favicon: salmon monogram on warm-black, no extra request -->
34<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230d0c0a'/%3E%3Ctext x='50%25' y='52%25' dominant-baseline='central' text-anchor='middle' font-family='Georgia,serif' font-size='40' font-style='italic' fill='%23e8623a'%3ES%3C/text%3E%3C/svg%3E">
35<link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230d0c0a'/%3E%3Ctext x='50%25' y='52%25' dominant-baseline='central' text-anchor='middle' font-family='Georgia,serif' font-size='40' font-style='italic' fill='%23e8623a'%3ES%3C/text%3E%3C/svg%3E">
36<link rel="manifest" href="site.webmanifest">
37<meta name="apple-mobile-web-app-title" content="Suzy Su">
38<meta name="apple-mobile-web-app-capable" content="yes">
39<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
40
41<!-- Self-hosted fonts (no Google Fonts CDN → GDPR-friendly, one fewer 3rd-party, faster first paint) -->
42<link rel="preload" href="assets/fonts/Fraunces-normal-300-latin.woff2" as="font" type="font/woff2" crossorigin>
43<link rel="preload" href="assets/fonts/Inter-normal-400-latin.woff2" as="font" type="font/woff2" crossorigin>
44<link rel="stylesheet" href="assets/fonts.css">
45
46<link rel="stylesheet" href="style.css?v=20260701-review-fixes">
47
48<!-- Optional smooth-scroll enhancement (page works without it) -->
49<script src="assets/lenis.min.js" defer></script>
49
50
51<!-- Structured data: helps Google show a richer result for the name -->
52<script type="application/ld+json">
53{
54  "@context": "https://schema.org",
55  "@type": "Person",
56  "name": "Suzy Su",
57  "alternateName": ["Ziwei Su", "苏紫薇", "suzysu"],
58  "jobTitle": "Game Designer",
59  "url": "https://suzyeth.github.io/",
60  "image": "https://suzyeth.github.io/assets/og-card.jpg",
61  "email": "mailto:[email protected]",
62  "address": { "@type": "PostalAddress", "addressLocality": "London", "addressCountry": "GB" },
63  "alumniOf": [
64    { "@type": "CollegeOrUniversity", "name": "University of the Arts London — London College of Communication" },
65    { "@type": "CollegeOrUniversity", "name": "Jilin University" }
66  ],
67  "knowsLanguage": ["zh-CN", "en"],
68  "sameAs": [
69    "https://www.linkedin.com/in/suzy99-su/",
70    "https://github.com/suzyeth",
71    "https://ssu997.itch.io/"
72  ]
73}
74</script>
74
75</head>
76<body>
77
78<a class="skip-link" href="#main"><span lang="en">Skip to content</span><span lang="zh">跳到主内容</span></a>
79
80<!-- Top nav ============================================ -->
81<header class="hud">
82  <a href="#" class="brand">SUSU / PORTFOLIO</a>
83  <nav class="hud-r" aria-label="Primary">
84    <div class="hud-links" id="hudLinks">
85      <a href="#work"><span lang="en">Work</span><span lang="zh">作品</span></a>
86      <a href="#about"><span lang="en">About</span><span lang="zh">关于</span></a>
87      <a href="#contact"><span lang="en">Contact</span><span lang="zh">联系</span></a>
88    </div>
89    <button id="langToggle" class="lang-toggle" aria-label="Toggle language">EN / 中</button>
90    <button id="menuToggle" class="menu-toggle" aria-label="Menu" aria-expanded="false" aria-controls="hudLinks">
91      <span class="menu-bars" aria-hidden="true"></span>
92    </button>
93  </nav>
94</header>
95
96<main id="main">
97
98<!-- HERO =========================================== -->
99<section class="hero" id="top">
100  <div class="hero-veil"></div>
101  <div class="hero-letterbox top"></div>
102  <div class="hero-letterbox bot"></div>
103  <div class="hero-frame container">
104    <div class="hero-spacer"></div>
105    <div class="hero-bottom">
106      <h1 class="hero-name">
107        <span lang="en">Suzy <em>Su</em></span>
108        <span lang="zh">苏紫薇</span>
109      </h1>
110      <div class="hero-meta">
111        <p class="hero-role">
112          <span lang="en">Game Designer <span class="hero-role-mute">— F2P Systems · Economy · Monetisation · Solo Prototypes — London</span></span>
113          <span lang="zh">游戏设计师 <span class="hero-role-mute">— F2P 系统 · 经济 · 商业化 · 独立原型 — 伦敦</span></span>
114        </p>
115        <p class="hero-scroll" aria-hidden="true"><span lang="en">Scroll · 9 Projects</span><span lang="zh">向下 · 9 个项目</span></p>
116      </div>
117    </div>
118  </div>
119</section>
120
121<!-- CREDENTIALS ===================================== -->
122<section class="creds" id="creds">
123  <div class="container">
124    <p class="creds-line" data-reveal>
125      <span lang="en">London-based · 19 months in F2P mobile · 2 shipped titles · 9 personal projects · MA UAL</span>
126      <span lang="zh">现居伦敦 · F2P 手游 19 个月 · 2 款已上线 · 9 个个人项目 · UAL 硕士</span>
127    </p>
128  </div>
129</section>
130
131<!-- ABOUT =========================================== -->
132<section class="about" id="about">
133  <div class="container about-grid">
134    <div class="about-photo" data-reveal>
135      <span class="about-photo-placeholder" aria-hidden="true">ZS</span>
136      <picture>
137        <source type="image/webp" srcset="assets/profile.webp" />
138        <img class="about-photo-img" src="assets/profile.jpg" alt="Suzy Su" width="600" height="800" loading="lazy" decoding="async" onerror="this.closest('picture').remove()" />
139      </picture>
140    </div>
141    <div class="about-body" data-reveal>
142      <h2 class="about-h">
143        <span lang="en">Hi <span class="wave">👋</span> — I'm <em>Suzy.</em></span>
144        <span lang="zh">嗨 <span class="wave">👋</span> — 我是 <em>紫薇.</em></span>
145      </h2>
146      <div lang="en" class="about-text">
147        <p>I'm a London-based Game Designer focused on F2P systems, economy and monetisation, with two shipped mobile titles. Alongside that work, I build my own games, tools and prototypes — they're what you can play and read here, built on the same design instincts.</p>
148        <p>I like systems with pressure in them — resource flows, reward pacing, risk/reward loops, and designer-facing tools that make tuning faster. I also like getting from idea to playable quickly: prototyping solo, end to end, so a design can be felt and tested rather than just written down.</p>
149      </div>
150      <div lang="zh" class="about-text">
151        <p>我是一名现居伦敦的游戏设计师,专注 F2P 系统、经济与商业化,已有两款上线手游。除了这些工作,我也自己做游戏、工具和原型——网站展示的就是这部分你能直接玩到、读到的内容,背后是同一套设计能力。</p>
152        <p>我喜欢有张力的系统——资源流、奖励节奏、风险/回报循环,以及能让调参更快的策划工具。我也喜欢快速把想法做成可玩版本:独立、端到端地做原型,让设计能被亲手感受和验证,而不只是写在文档里。</p>
153      </div>
154      <dl class="about-meta">
155        <div class="about-meta-row"><dt lang="en">Experience</dt><dt lang="zh">经历</dt><dd><span lang="en">19 months in F2P mobile · 2 shipped titles · monetisation ownership</span><span lang="zh">F2P 手游 19 个月 · 2 款已上线 · 负责商业化设计</span></dd></div>
156        <div class="about-meta-row"><dt lang="en">Focus</dt><dt lang="zh">方向</dt><dd><span lang="en">Systems · economy · gacha · battle pass · progression · rapid prototyping</span><span lang="zh">系统 · 经济 · 抽卡 · 战令 · 成长 · 快速原型</span></dd></div>
157        <div class="about-meta-row"><dt lang="en">Education</dt><dt lang="zh">教育</dt><dd>MA Game Design — UAL London College of Communication, 2023<br>BE Engineering Geology — Jilin University, 2020</dd></div>
158        <div class="about-meta-row"><dt lang="en">Languages</dt><dt lang="zh">语言</dt><dd lang="en">Mandarin (native) · English (working)</dd><dd lang="zh">中文(母语)· 英文(工作流利)</dd></div>
159      </dl>
160    </div>
161  </div>
162</section>
163
164<!-- BACKSTORY ======================================= -->
165<section class="backstory" id="backstory">
166  <div class="bs-bg" aria-hidden="true"></div>
167  <div class="container">
168    <header class="bs-head" data-reveal>
169      <p class="eyebrow"><span lang="en">My Backstory</span><span lang="zh">我的旅程</span></p>
170      <h2 class="bs-title">
171        <span lang="en">The <em>path</em> so far.</span>
172        <span lang="zh">走过的 <em>路</em>。</span>
173      </h2>
174    </header>
175
176    <div class="timeline">
177      <span class="timeline-rail" aria-hidden="true"></span>
178
179      <article class="tl-entry right" data-reveal>
180        <div class="tl-card tl-card-feature">
181          <p class="tl-meta">2024 — Present · London</p>
182          <h3 class="tl-role"><span lang="en">Game Designer</span><span lang="zh">游戏设计师</span></h3>
183          <div class="tl-body">
184            <p lang="en">End-to-end ownership of monetisation design across two F2P mobile titles, both shipped — one in East Asia, one live since May 2026.</p>
185            <p lang="zh">两款 F2P 手游的商业化端到端负责 — 两款均已上线,一款在东亚,一款于 2026 年 5 月上线。</p>
186          </div>
187          <div class="tl-domains">
188            <div class="tl-domain">
189              <h4 class="tl-d-h"><span lang="en">Monetisation Systems</span><span lang="zh">商业化系统</span></h4>
190              <ul>
191                <li lang="en">Gacha banner design — limited-time UP probability &amp; pity systems</li>
192                <li lang="en">Battle-pass architecture, login event series (7-day / 30-day)</li>
193                <li lang="en">Affinity rewards, monthly subscription, daily / weekly bundles</li>
194                <li lang="en">Shop with VIP membership tier · pricing models · treasure-chest config</li>
195                <li lang="zh">限时抽卡 — UP 概率与保底机制设计</li>
196                <li lang="zh">战令架构 · 签到活动序列(7 / 30 日)</li>
197                <li lang="zh">好感度奖励 · 月卡 · 每日 / 每周礼包</li>
198                <li lang="zh">商城 + VIP 会员 · 定价模型 · 宝箱配置</li>
199              </ul>
200            </div>
201            <div class="tl-domain">
202              <h4 class="tl-d-h"><span lang="en">Numerical Design</span><span lang="zh">数值设计</span></h4>
203              <ul>
204                <li lang="en">Game-wide master numerics — iterated across multiple versions</li>
205                <li lang="en">Economy resource-flow model — multiple iterations</li>
206                <li lang="en">Items, equipment, skill progression &amp; formula tables</li>
207                <li lang="zh">全局数值总表 — 多轮迭代</li>
208                <li lang="zh">资源流转模型 — 多轮迭代</li>
209                <li lang="zh">物品 / 装备 / 技能成长 / 公式表</li>
210              </ul>
211            </div>
212            <div class="tl-domain">
213              <h4 class="tl-d-h"><span lang="en">Documentation</span><span lang="zh">文档与交付</span></h4>
214              <ul>
215                <li lang="en">Interaction system design docs — lead author</li>
216                <li lang="en">Combat configuration tables</li>
217                <li lang="en">Gacha system design documentation, concept → launch</li>
218                <li lang="zh">交互系统设计文档 — 主笔</li>
219                <li lang="zh">战斗配置表</li>
220                <li lang="zh">抽卡系统设计文档(概念 → 上线)</li>
221              </ul>
222            </div>
223          </div>
224          <p class="tl-tags">F2P RPG (East Asia, shipped) · F2P Roguelike Shooter (live since May 2026)</p>
225        </div>
226        <span class="tl-dot" aria-hidden="true"></span>
227      </article>
228
229      <article class="tl-entry left" data-reveal>
230        <span class="tl-dot" aria-hidden="true"></span>
231        <div class="tl-card">
232          <p class="tl-meta">2024 — Present · After Hours</p>
233          <h3 class="tl-role"><span lang="en">Solo Maker</span><span lang="zh">独立作者</span></h3>
234          <p class="tl-org"><span lang="en">Side projects · always-on</span><span lang="zh">副业 · 持续进行</span></p>
235          <div class="tl-body">
236            <p lang="en">Started shipping side projects after joining the studio — jam games, automation tools, and fast solo prototypes. Each one a small bet on how far one designer can get from idea to playable.</p>
237            <p lang="zh">入职后开始做独立项目 — jam 游戏、自动化工具,以及快速的独立原型。每一个都是在押注:一个设计师能多快把想法做成可玩。</p>
238          </div>
239          <ul class="tl-projects">
240            <li><strong>tarduck</strong> · <span lang="en">Three.js extraction shooter · Cursor Vibe Jam 2026</span><span lang="zh">Three.js 撤离射击 · Cursor Vibe Jam 2026</span></li>
241            <li><strong>Robot Karesansui</strong> · <span lang="en">R3F zen sim · shader-drawn sand garden with a tireless rake bot</span><span lang="zh">R3F 枯山水模拟 · 全 shader 沙地与一只不知疲倦的耙园机器人</span></li>
242            <li><strong>Respond to Signal 404</strong> · <span lang="en">Unity 2D horror puzzle · LD59 (sole programmer)</span><span lang="zh">Unity 2D 恐怖解谜 · LD59(唯一程序)</span></li>
243            <li><strong>gridbot</strong> · <span lang="en">Python Android automation · MIT · install from source</span><span lang="zh">Python Android 自动化 · MIT · 从源码安装</span></li>
244          </ul>
245          <a href="#work" class="tl-cta"><span lang="en">See Selected Work ↓</span><span lang="zh">查看精选作品 ↓</span></a>
246        </div>
247      </article>
248
249      <article class="tl-entry right" data-reveal>
250        <div class="tl-card">
251          <p class="tl-meta">2022 — 2023 · London</p>
252          <h3 class="tl-role"><span lang="en">MA Game Design</span><span lang="zh">硕士 · 游戏设计</span></h3>
253          <p class="tl-org">UAL · London College of Communication</p>
254          <div class="tl-body">
255            <p lang="en">Three Unity prototypes — an RPG puzzle, a meta-style platformer, and Hypha — covering mechanics design, level design, and end-to-e
255nd implementation in C#.</p>
256            <p lang="zh">三款 Unity 原型 — RPG 解谜、元叙事平台跳跃、Hypha 单键菌丝小游戏 — 覆盖机制设计、关卡设计与 C# 完整实现。</p>
257          </div>
258        </div>
259        <span class="tl-dot" aria-hidden="true"></span>
260      </article>
261
262      <article class="tl-entry left" data-reveal>
263        <span class="tl-dot" aria-hidden="true"></span>
264        <div class="tl-card">
265          <p class="tl-meta">2015 — 2020 · Jilin</p>
266          <h3 class="tl-role"><span lang="en">BE Engineering Geology</span><span lang="zh">本科 · 工程地质</span></h3>
267          <p class="tl-org">Jilin University</p>
268          <div class="tl-body">
269            <p lang="en">Five years of engineering analysis and field research. Pivoted to game design afterwards — same systems thinking, different domain.</p>
270            <p lang="zh">五年的工程分析与野外研究。之后转向游戏设计 — 同样的系统思维,换一个领域。</p>
271          </div>
272        </div>
273      </article>
274
275    </div>
276  </div>
277</section>
278
279<!-- SELECTED WORK =================================== -->
280<section class="work" id="work">
281  <div class="container">
282    <header class="work-head" data-reveal>
283      <p class="eyebrow"><span lang="en">Selected Work</span><span lang="zh">精选作品</span></p>
284      <h2 class="work-title">
285        <span lang="en">Playable proof of <em>systems thinking</em>.</span>
286        <span lang="zh">用可玩的作品证明 <em>系统思维</em>。</span>
287      </h2>
288      <div class="work-filter" role="group" aria-label="Filter projects by type">
289        <button type="button" class="filter-btn is-active" data-filter="all" aria-pressed="true"><span lang="en">All</span><span lang="zh">全部</span></button>
290        <button type="button" class="filter-btn" data-filter="games" aria-pressed="false"><span lang="en">Games</span><span lang="zh">游戏</span></button>
291        <button type="button" class="filter-btn" data-filter="tools" aria-pressed="false"><span lang="en">Tech &amp; Tools</span><span lang="zh">技术与工具</span></button>
292      </div>
293    </header>
294
295    <div class="work-carousel" aria-label="Featured project carousel">
296
297      <!-- 01 tarduck (featured) -->
298      <article class="card" data-reveal data-id="tarduck" data-cat="games">
299        <div class="card-media tarduck-art">
300          <video loop muted playsinline preload="none" poster="assets/tarduck-poster.jpg" data-lazy-video aria-label="tarduck — browser extraction shooter, isometric 3D raid overview with mission HUD">
301            <source src="assets/tarduck-demo.mp4" type="video/mp4" />
302          </video>
303        </div>
304        <div class="card-text">
305          <p class="card-id">01 · <span lang="en">Featured · Cursor Vibe Jam 2026</span><span lang="zh">特色项目 · Cursor Vibe Jam 2026</span></p>
306          <h3 class="card-title">tarduck</h3>
307          <p class="card-tag">Solo · Three.js · Browser Extraction Shooter</p>
308          <p class="card-blurb">
309            <span lang="en">A compact extraction loop built to test risk/reward pacing: push deeper for loot, or leave early and bank the run.</span>
310            <span lang="zh">一个用于验证风险/回报节奏的撤离循环:深入搜刮更高收益,或提前撤离保住本局成果。</span>
311          </p>
312          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-tarduck"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
313        </div>
314        <div class="card-detail" id="card-detail-tarduck" hidden>
315          <div class="detail-grid">
316            <div>
317              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
318              <p lang="en">Solo entry — game design, creative direction, prompt decomposition, code review and integration.</p>
319              <p lang="zh">独立项目 — 游戏设计、创意方向、prompt 拆解、代码审阅与整合。</p>
320            </div>
321            <div>
322              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
323              <ul>
324                <li lang="en">5 weapons · 5 enemy types · body-part hitbox damage</li>
325                <li lang="en">Grid inventory · 12 talents · 8 crafting recipes</li>
326                <li lang="en">3 extract points · fog-of-war · wave spawning · difficulty scaling</li>
327                <li lang="zh">5 武器 · 5 敌人类型 · 部位 hitbox 伤害</li>
328                <li lang="zh">网格背包 · 12 天赋 · 8 合成配方</li>
329                <li lang="zh">3 个撤离点 · 战争迷雾 · 波次生成 · 难度缩放</li>
330              </ul>
331            </div>
332            <div>
333              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
334              <p lang="en">30 days · 57 commits · solo, under jam rules.</p>
335              <p lang="zh">30 天 · 57 commits · 独立完成,遵循 jam 规则。</p>
336            </div>
337            <div>
338              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
339              <p>Three.js · Vite · lil-gui · ACES tone mapping · Bloom</p>
340            </div>
341          </div>
342          <div class="detail-links">
343            <a class="case" href="projects/tarduck.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
344            <a href="https://suzyeth.github.io/escape-from-duckkov/" target="_blank" rel="noopener" class="primary"><span lang="en">▶ Play in browser</span><span lang="zh">▶ 浏览器试玩</span></a>
345            <a href="https://github.com/suzyeth/escape-from-duckkov" target="_blank" rel="noopener">GitHub ↗</a>
346          </div>
347        </div>
348      </article>
349
350      <!-- Kitchen Bar — liquid & glass rendering (tech art case study) -->
351      <article class="card" data-reveal data-id="cocktail" data-cat="tools">
352        <div class="card-media">
353          <img src="assets/cocktail/cover.png" alt="Kitchen Bar — procedural liquid and glass rendering showcase: refraction, color presets, layered pour" width="800" height="600" loading="lazy" decoding="async" />
354        </div>
355        <div class="card-text">
356          <p class="card-id">02 · <span lang="en">Tech Art · Unity · Personal</span><span lang="zh">技术美术 · Unity · 个人项目</span></p>
357          <h3 class="card-title"><span lang="en">Kitchen Bar</span><span lang="zh">厨房调酒</span></h3>
358          <p class="card-tag">Unity · URP · Procedural Liquid/Glass · Custom Shader</p>
359          <p class="card-blurb">
360            <span lang="en">Making a glass of liquid look real in a 2D game — the stylized-water recipe (surface, refraction, lighting, foam, buoyancy) transposed into a glass. Six chapters, each with the fix and the code.</span>
361            <span lang="zh">让 2D 游戏里的一杯酒看起来像真的——把程式化水体的配方(液面、折射、光照、泡沫、浮力)移植进杯子。六章拆解,每章都带修法和代码。</span>
362          </p>
363          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-cocktail"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
364        </div>
365        <div class="card-detail" id="card-detail-cocktail" hidden>
366          <div class="detail-grid">
367            <div>
368              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
369              <p lang="en">Solo — liquid/glass/cup rendering, custom shaders, art tooling.</p>
370              <p lang="zh">独立 — 液体/玻璃/酒杯渲染、自写 shader、美术工具。</p>
371            </div>
372            <div>
373              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
374              <ul>
375                <li lang="en">Procedural liquid · meniscus · foam · ripples</li>
376                <li lang="en">GrabPass glass refraction · URP Light2D arc-light</li>
377                <li lang="en">EffLevel gameplay sync · Python/PIL cup pipeline</li>
378                <li lang="zh">程序化液体 · 弯月液面 · 泡沫 · 涟漪</li>
379                <li lang="zh">GrabPass 折射 · URP Light2D 弧光</li>
380                <li lang="zh">EffLevel 玩法同步 · Python/PIL 杯子管线</li>
381              </ul>
382            </div>
383            <div>
384              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
385              <p>Unity 6 · URP 17 · C# · HLSL · SpriteMask · Python/PIL</p>
386            </div>
387            <div>
388              <h4><span lang="en">Highlight</span><span lang="zh">亮点</span></h4>
389              <p lang="en">6 chapters, each ending in a reusable takeaway and real code.</p>
390              <p lang="zh">6 章拆解,每章都落到一条可复用的经验和真实代码。</p>
391            </div>
392          </div>
393          <div class="detail-links">
394            <a class="case" href="projects/cocktail-rendering.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
395          </div>
396        </div>
397      </article>
398
399      <!-- 02 sand-garden -->
400      <article class="card" data-reveal data-id="sand-garden" data-cat="games">
401        <div class="card-media sand-garden-art">
402          <video loop muted playsinline preload="none" poster="assets/sandgarden-poster.jpg" data-lazy-video aria-label="Robot Karesansui — a robot raking concentric ripple patterns into a shader-drawn zen garden">
403            <source src="assets/sandgarden-demo.mp4" type="video/mp4" />
404          </video>
405        </div>
406        <div class="card-text">
407          <p class="card-id">03 · <span lang="en">Solo · MIT</span><span lang="zh">独立项目 · MIT 协议</span></p>
408          <h3 class="card-title">Robot <em>Karesansui</em></h3>
409          <p class="card-tag">Solo · R3F · WebGL Zen Sim</p>
410          <p class="card-blurb">
411            <span lang="en">A small robot perpetually rakes wave patterns into a dry garden. Battery FSM, day/night cycle, weather tiers, eight fauna species — all shader-drawn, no asset pipeline.</span>
412            <span lang="zh">一个小机器人不停在枯山水里耙出波纹。电量状态机、昼夜循环、天气分级、八种生物 — 全 shader 绘制,没有资源管线。</span>
413          </p>
414          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-sand-garden"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
415        </div>
416        <div class="card-detail" id="card-detail-sand-garden" hidden>
417          <div class="detail-grid">
418            <div>
419              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
420              <p lang="en">Solo — sim engine, shaders, robot FSM, synthesised audio.</p>
421              <p lang="zh">独立 — 模拟引擎、shader、机器人状态机、合成音频。</p>
422            </div>
423            <div>
424              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
425              <ul>
426                <li lang="en">Heightfield sand sim (1024×1024) with diffusion + drift; rake teeth etch real grooves that slowly fade</li>
427                <li lang="en">Pre-computed Catmull-Rom path aligned to the shader's concentric ripples</li>
428                <li lang="en">Battery FSM with two-leg dock approach; day/night cycle (3-min loop); weather state machine with three rain tiers</li>
429                <li lang="en">Eight fauna species (dragonfly, gecko, frog, sparrow, beetle, butterfly, fireflies, falling leaves)</li>
430                <li lang="zh">1024×1024 heightfield 沙地模拟,含扩散与漂移;耙齿留下真实凹痕并缓慢消退</li>
431                <li lang="zh">预计算 Catmull-Rom 路径,与 shader 同心波纹像素对齐</li>
432                <li lang="zh">两段式归坞的电量状态机;3 分钟昼夜循环;含三档雨势的天气状态机</li>
433                <li lang="zh">八种生物(蜻蜓 / 壁虎 / 青蛙 / 麻雀 / 甲虫 / 蝴蝶 / 萤火虫 / 落叶)</li>
434              </ul>
435            </div>
436            <div>
437              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
438              <p lang="en">~5k LOC TypeScript · single-page R3F scene · zero asset pipeline (everything in-shader except ambient audio).</p>
439              <p lang="zh">约 5k 行 TypeScript · 单页 R3F 场景 · 零资源管线(环境音之外全部 shader 绘制)。</p>
440            </div>
441            <div>
442              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
443              <p>Vite · React 18 · TypeScript · Three.js (R3F + drei) · zustand · Web Audio API</p>
444            </div>
445          </div>
446          <div class="detail-links">
447            <a class="case" href="projects/sand-garden.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
448            <a href="https://suzyeth.github.io/sand-garden/" target="_blank" rel="noopener" class="primary"><span lang="en">▶ Play in browser</span><span lang="zh">▶ 浏览器试玩</span></a>
449            <a href="https://github.com/suzyeth/sand-garden" target="_blank" rel="noopener">GitHub ↗</a>
450          </div>
451        </div>
452      </article>
453
454      <!-- 03 LD59 -->
455      <article class="card" data-reveal data-id="ld59" data-cat="games">
456        <div class="card-media ld59-art">
457          <picture>
458            <source type="image/webp" srcset="assets/ld59-cover.webp" />
459            <img src="assets/ld59-cover.jpg" alt="Respond to Signal 404 — 2D pixel-art horror puzzle game" width="800" height="600" loading="lazy" decoding="async" />
460          </picture>
461        </div>
462        <div class="card-text">
463          <p class="card-id">04 · <span lang="en">Ludum Dare 59 Jam</span><span lang="zh">Ludum Dare 59 Jam</span></p>
464          <h3 class="card-title">Respond to <em>Signal 404</em></h3>
465          <p class="card-tag">Sole Programmer · Unity · 2D Pixel Horror</p>
466          <p class="card-blurb">
467            <span lang="en">Designed the signal-detection puzzle, then shipped it as sole programmer — 6-person team, 72-hour jam, live on itch.io.</span>
468            <span lang="zh">设计了信号检测解谜机制,并作为唯一程序员落地——6 人团队,72 小时 jam,已上线 itch.io。</span>
469          </p>
470          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-ld59"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
471        </div>
472        <div class="card-detail" id="card-detail-ld59" hidden>
473          <div class="detail-grid">
474            <div>
475              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
476              <p lang="en">Co-designer (with Cheng Min); sole programmer on a 6-person team; co-handled audio.</p>
477              <p lang="zh">联合设计师(与 Cheng Min);6 人团队中唯一程序员;联合负责音频。</p>
478            </div>
479            <div>
480              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
481              <p lang="en">Designed signal-detection mechanic and puzzle progression. Built every gameplay system in Unity, shipped HTML5 build to itch.io.</p>
482              <p lang="zh">设计信号检测机制和解谜流程。Unity 中实现所有玩法系统,HTML5 构建发布到 itch.io。</p>
483            </div>
484            <div>
485              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
486              <p lang="en">72-hour jam · 6-person team · public on itch + Ludum Dare for rating.</p>
487              <p lang="zh">72 小时 jam · 6 人团队 · itch + LD 公开评分。</p>
488            </div>
489            <div>
490              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
491              <p>Unity 2022 · C# · WebGL build</p>
492            </div>
493          </div>
494          <div class="detail-links">
495            <a class="case" href="projects/ld59.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
496            <a href="https://cheng-min.itch.io/respond-to-signal-404" target="_blank" rel="noopener">itch.io ↗</a>
497          </div>
498        </div>
499      </article>
500
501      <!-- 04 gridbot -->
502      <article class="card" data-reveal data-id="gridbot" data-cat="tools">
503        <div class="card-media terminal-art" aria-hidden="true">
504<pre>$ pip install -e ".[ocr]"
505▸ capture   ✓
506▸ input     ✓
507▸ ocr       ✓ paddle
508▸ navigator ✓
509▸ recorder  ✓
510
511› ready_state: idle
512_</pre>
513        </div>
514        <div class="card-text">
515          <p class="card-id">05 · <span lang="en">Solo · MIT</span><span lang="zh">独立项目 · MIT 协议</span></p>
516          <h3 class="card-title">
516gridbot — <em>Black-box QA</em></h3>
517          <p class="card-tag">Python · ADB · PaddleOCR · alpha (from source)</p>
518          <p class="card-blurb">
519            <span lang="en">Android automation toolkit aimed at designers and QA — works where UIAutomator and Appium don't.</span>
520            <span lang="zh">面向策划与 QA 的 Android 自动化工具包 — 在 UIAutomator/Appium 失效的场合也能用。</span>
521          </p>
522          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-gridbot"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
523        </div>
524        <div class="card-detail" id="card-detail-gridbot" hidden>
525          <div class="detail-grid">
526            <div>
527              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
528              <p lang="en">Solo — architecture, API design, packaging, docs.</p>
529              <p lang="zh">独立 — 架构、API 设计、打包、文档。</p>
530            </div>
531            <div>
532              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
533              <p lang="en">Modular package: <code>capture · input · ocr · navigator · recorder · state</code>. Designed for use cases where UIAutomator and Appium fail (custom rendering engines, third-party games).</p>
534              <p lang="zh">模块化包:<code>capture · input · ocr · navigator · recorder · state</code>。针对 UIAutomator/Appium 失效场景(自定义渲染引擎、第三方游戏)。</p>
535            </div>
536            <div>
537              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
538              <p lang="en">Alpha 0.1.x · MIT · install from source (not yet on PyPI).</p>
539              <p lang="zh">Alpha 0.1.x · MIT · 从源码安装(暂未发布到 PyPI)。</p>
540            </div>
541            <div>
542              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
543              <p>Python 3.9+ · ADB · PaddleOCR · YAML state graphs</p>
544            </div>
545          </div>
546          <div class="detail-links">
547            <a class="case" href="projects/gridbot.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
548            <a href="https://github.com/suzyeth/gridbot" target="_blank" rel="noopener">GitHub ↗</a>
549          </div>
550        </div>
551      </article>
552
553      <!-- 05 gacha-sim -->
554      <article class="card" data-reveal data-id="gacha" data-cat="tools">
555        <div class="card-media gacha-art">
556          <video loop muted playsinline preload="none" poster="assets/gacha-poster.jpg" data-lazy-video aria-label="Gacha economy simulator — target-driven pull-cost percentile dashboard, running a 1000-player simulation">
557            <source src="assets/gacha-demo.mp4" type="video/mp4" />
558          </video>
559        </div>
560        <div class="card-text">
561          <p class="card-id">06 · <span lang="en">Solo · Live tool</span><span lang="zh">独立项目 · 在线工具</span></p>
562          <h3 class="card-title">Gacha <em>Economy Sim</em></h3>
563          <p class="card-tag">React · TypeScript · Monte Carlo · Web Worker</p>
564          <p class="card-blurb">
565            <span lang="en">A real-time gacha-economy simulator for numerical designers — edit pool weights and pity periods, see P50/P90 pull-cost distributions instantly. No engineer round-trips.</span>
566            <span lang="zh">面向数值策划的实时抽卡经济模拟器 — 改池子权重与保底周期,立即看到 P50/P90 抽数成本分布。无需工程师介入。</span>
567          </p>
568          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-gacha"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
569        </div>
570        <div class="card-detail" id="card-detail-gacha" hidden>
571          <div class="detail-grid">
572            <div>
573              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
574              <p lang="en">Solo — engine architecture, plugin-rule design, UI, test suite.</p>
575              <p lang="zh">独立 — 引擎架构、插件式规则设计、UI、测试。</p>
576            </div>
577            <div>
578              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
579              <ul>
580                <li lang="en">Pure 2-level draw engine with a 4-hook plugin-rule interface (pity, scripted first-draw)</li>
581                <li lang="en">Target-driven simulation: 1000 players pull until they hit item X → P50/P75/P90
581/P99 cost</li>
582                <li lang="en">Surfaces that the common spreadsheet drop-rate formula overstates true rates by ~20%</li>
583                <li lang="en">Inline-editable weights / % / pity periods; multi-scheme save + JSON / xlsx import</li>
584                <li lang="zh">纯函数二级抽卡引擎 + 4-hook 插件式规则接口(保底、首抽脚本)</li>
585                <li lang="zh">目标驱动模拟:1000 玩家各自抽到目标为止 → P50/P75/P90/P99 成本分位</li>
586                <li lang="zh">揭示常见 Excel 掉率公式比真实概率高估约 20%</li>
587                <li lang="zh">权重 / 概率 / 保底周期全部 click-to-edit;多方案保存 + JSON / xlsx 导入</li>
588              </ul>
589            </div>
590            <div>
591              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
592              <p lang="en">60 tests; engine math validated within ±0.4% of closed-form values over 200k Monte Carlo pulls; 100k-pull batches off-thread in ~500ms.</p>
593              <p lang="zh">60 个测试;引擎数学经 20 万次蒙特卡洛验证,与闭式解误差 ±0.4% 内;10 万抽批量在 Worker 中约 500ms 完成。</p>
594            </div>
595            <div>
596              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
597              <p>React 19 · TypeScript · Zustand · Zod · Web Worker · ECharts · Vitest</p>
598            </div>
599          </div>
600          <div class="detail-links">
601            <a class="case" href="projects/gacha.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
602            <a href="https://gacha-simulator-ten.vercel.app" target="_blank" rel="noopener" class="primary"><span lang="en">▶ Live demo</span><span lang="zh">▶ 在线试用</span></a>
603          </div>
604        </div>
605      </article>
606
607      <!-- 06 MA -->
608      <article class="card" data-reveal data-id="ma" data-cat="games">
609        <div class="card-media ma-art" aria-hidden="true">
610          <video src="assets/ma-ending.mp4" loop muted playsinline preload="none" data-lazy-video></video>
611        </div>
612        <div class="card-text">
613          <p class="card-id">07 · <span lang="en">UAL · 2022 — 2023</span><span lang="zh">UAL · 2022 — 2023</span></p>
614          <h3 class="card-title">MA Game <em>Design</em></h3>
615          <p class="card-tag">Unity · C# · 3 Prototypes</p>
616          <p class="card-blurb">
617            <span lang="en">Two Unity prototypes from the MA — an RPG puzzle and a meta-narrative platformer — plus Hypha, featured separately below.</span>
618            <span lang="zh">硕士期间的两款 Unity 原型——RPG 解谜与元叙事平台跳跃——以及单独展示的 Hypha(见下)。</span>
619          </p>
620          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-ma"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
621        </div>
622        <div class="card-detail" id="card-detail-ma" hidden>
623          <div class="detail-grid">
624            <div>
625              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
626              <p lang="en">Owned game mechanics design and Unity implementation across three prototypes; level design and parameter tuning for the platformer.</p>
627              <p lang="zh">三款原型中负责玩法设计与 Unity 实现;平台跳跃部分负责关卡设计与数值调优。</p>
628            </div>
629            <div>
630              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
631              <ul>
632                <li lang="en">RPG Puzzle — 4 mechanics (teleport / box pull / ice break / thorn cross)</li>
633                <li lang="en">Meta Platformer — level design + parameter tuning</li>
634                <li lang="en">Hypha — one-button mycelium game: click to collect nutrients, dodge hazards</li>
635                <li lang="zh">RPG 解谜 — 4 个机制(传送 / 推箱 / 破冰 / 穿刺)</li>
636                <li lang="zh">元叙事平台 — 关卡 + 参数调优</li>
637                <li lang="zh">Hypha — 单键菌丝游戏:点击收集养分、躲避伤害</li>
638              </ul>
639            </div>
640            <div>
641              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
642              <p lang="en">3 Unity prototypes during MA Game Design at UAL LCC, 2022–2023.</p>
643              <p lang="zh">UAL LCC 硕士期间三款 Unity 原型,2022–2023。</p>
644            </div>
645            <div>
646              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
647              <p>Unity 2022 · C#</p>
648            </div>
649          </div>
650          <div class="detail-links">
651            <a class="case" href="projects/ma.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
652          </div>
653        </div>
654      </article>
655
656      <!-- 06 hypha -->
657      <article class="card" data-reveal data-id="hypha" data-cat="games">
658        <div class="card-media hypha-art">
659          <picture>
660            <source type="image/webp" srcset="assets/hypha-cover.webp" />
661            <img src="assets/hypha-cover.jpg" alt="Hypha — one-button web game about a mycelium reaching toward the surface" width="800" height="600" loading="lazy" decoding="async" />
662          </picture>
663        </div>
664        <div class="card-text">
665          <p class="card-id">08 · <span lang="en">GGJ 2023 · Team</span><span lang="zh">GGJ 2023 · 团队</span></p>
666          <h3 class="card-title"><em>Hypha</em></h3>
667          <p class="card-tag">GGJ 2023 · Unity · One-Button Web Game</p>
668          <p class="card-blurb">
669            <span lang="en">A one-button game about a mycelium reaching toward the surface. Click the centre to collect nutrients or dodge what harms the hyphae.</span>
670            <span lang="zh">一款单键小游戏:菌丝在地下伸出边缘,点击中心吸收养分或躲开有害物质。</span>
671          </p>
672          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-hypha"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
673        </div>
674        <div class="card-detail" id="card-detail-hypha" hidden>
675          <div class="detail-grid">
676            <div>
677              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
678              <p lang="en">Game designer + Unity dev; art by @Cheng Min.</p>
679              <p lang="zh">游戏设计 + Unity 开发;美术 @Cheng Min。</p>
680            </div>
681            <div>
682              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
683              <p lang="en">Single-input timing puzzle around organic mushroom growth. 48-hour jam scope.</p>
684              <p lang="zh">围绕菌丝生长的单键节奏玩法。48 小时 jam 体量。</p>
685            </div>
686            <div>
687              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
688              <p lang="en">Global Game Jam 2023 · 2-person team · HTML5 web build on itch.</p>
689              <p lang="zh">Global Game Jam 2023 · 2 人团队 · HTML5 网页版发布到 itch。</p>
690            </div>
691            <div>
692              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
693              <p>Unity · C# · WebGL</p>
694            </div>
695          </div>
696          <div class="detail-links">
697            <a class="case" href="projects/hypha.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
698            <a href="https://ssu997.itch.io/hypha" target="_blank" rel="noopener" class="primary"><span lang="en">▶ Play in browser</span><span lang="zh">▶ 浏览器试玩</span></a>
699          </div>
700        </div>
701      </article>
702
703      <!-- 07 metagame -->
704      <article class="card" data-reveal data-id="metagame" data-cat="games">
705        <div class="card-media metagame-art">
706          <picture>
707            <source type="image/webp" srcset="assets/metagame-cover.webp" />
708            <img src="assets/metagame-cover.jpg" alt="MetaGame — a meta-narrative platformer about playtesting a platformer" width="800" height="600" loading="lazy" decoding="async" />
709          </picture>
710        </div>
711        <div class="card-text">
712          <p class="card-id">09 · <span lang="en">UAL · MA Term 3</span><span lang="zh">UAL · 硕士第三学期</span></p>
713          <h3 class="card-title"><em>MetaGame</em></h3>
714          <p class="card-tag">MA Term 3 · Unity · Meta-narrative Platformer</p>
715          <p class="card-blurb">
716            <span lang="en">A metagame about making a platformer. You're a tester moving through three iterative versions of the same game, leaving feedback on each — the playtest loop turned into the level.</span>
717            <span lang="zh">一款"做平台跳跃"的元游戏:玩家扮演测试员,依次跑通三个迭代版本并留下反馈 — 把 playtest 循环本身做成关卡。</span>
718          </p>
719          <button class="card-expand" aria-expanded="false" aria-controls="card-detail-metagame"><span lang="en">More details</span><span lang="zh">展开详情</span></button>
720        </div>
721        <div class="card-detail" id="card-detail-metagame" hidden>
722          <div class="detail-grid">
723            <div>
724              <h4><span lang="en">Role</span><span lang="zh">角色</span></h4>
725              <p lang="en">Solo — concept, three-stage level design, Unity implementation.</p>
726              <p lang="zh">独立 — 概念、三段式关卡设计、Unity 实现。</p>
727            </div>
728            <div>
729              <h4><span lang="en">Scope</span><span lang="zh">范围</span></h4>
730              <p lang="en">Three iterative platformer "versions" wrapped in a tester loop; combat + spike obstacles + in-fiction feedback prompts.</p>
731              <p lang="zh">三个迭代平台跳跃版本包在一个测试员循环里;战斗 + 尖刺障碍 + 角色叙事内的反馈机制。</p>
732            </div>
733            <div>
734              <h4><span lang="en">Scale</span><span lang="zh">规模</span></h4>
735              <p lang="en">
735MA Term 3 at UAL LCC, 2023 · HTML5 web build · public on itch.</p>
736              <p lang="zh">UAL LCC 硕士第三学期, 2023 · HTML5 网页版 · 发布到 itch。</p>
737            </div>
738            <div>
739              <h4><span lang="en">Stack</span><span lang="zh">技术栈</span></h4>
740              <p>Unity · C# · WebGL</p>
741            </div>
742          </div>
743          <div class="detail-links">
744            <a class="case" href="projects/metagame.html"><span lang="en">Full case study →</span><span lang="zh">完整案例 →</span></a>
745            <a href="https://ssu997.itch.io/metagame" target="_blank" rel="noopener" class="primary"><span lang="en">▶ Play in browser</span><span lang="zh">▶ 浏览器试玩</span></a>
746          </div>
747        </div>
748      </article>
749
750    </div>
751
752    <div class="work-list-head" data-reveal>
753      <p class="eyebrow"><span lang="en">Project Grid</span><span lang="zh">项目网格</span></p>
754    </div>
755    <div class="work-list" aria-label="All projects grid"></div>
756  </div>
757</section>
758
759<!-- SKILLS =========================================== -->
760<section class="skills" id="skills">
761  <div class="container">
762    <header class="skills-head" data-reveal>
763      <h2 class="skills-eyebrow">
764        <span lang="en">Skills</span><span lang="zh">能力</span>
765      </h2>
766    </header>
767    <div class="skills-grid">
768      <div class="skill-col" data-reveal>
769        <h3 class="skill-h"><span lang="en">Game Design</span><span lang="zh">游戏设计</span></h3>
770        <ul>
771          <li>Systems Design</li>
772          <li>Game Economy</li>
773          <li>Monetisation · Gacha · Battle Pass</li>
774          <li>Live Ops · Login Events</li>
775          <li>Numerical Balancing</li>
776          <li>Progression Design</li>
777          <li>Spec / GDD authoring</li>
778        </ul>
779      </div>
780      <div class="skill-col" data-reveal>
781        <h3 class="skill-h"><span lang="en">Programming</span><span lang="zh">程序</span></h3>
782        <ul>
783          <li>Python (intermediate)</li>
784          <li>C# (working — Unity scripting)</li>
785          <li>JavaScript (working)</li>
786          <li>Three.js (jam project)</li>
787          <li>REST APIs · JSON config</li>
788          <li>Cron / launchd orchestration</li>
789          <li>Rapid solo prototyping</li>
790        </ul>
791      </div>
792      <div class="skill-col" data-reveal>
793        <h3 class="skill-h"><span lang="en">Tools</span><span lang="zh">工具</span></h3>
794        <ul>
795          <li>Unity 2022.3+</li>
796          <li>Excel / Google Sheets modelling</li>
797          <li>JSON / CSV configs</li>
798          <li>Figma</li>
799          <li>JIRA / Confluence</li>
800          <li>Cursor · Claude Code</li>
801          <li>Git</li>
802        </ul>
803      </div>
804    </div>
805  </div>
806</section>
807
808<!-- CONTACT ========================================= -->
809<section class="contact" id="contact">
810  <div class="container">
811    <p class="eyebrow"><span lang="en">Contact</span><span lang="zh">联系</span></p>
812    <h2 class="contact-title" data-reveal>
813      <span lang="en">Open to <em>Game Designer</em> roles.</span>
814      <span lang="zh">正在寻找 <em>游戏设计</em> 机会。</span>
815    </h2>
816    <p class="contact-sub" data-reveal>
817      <span lang="en">London / Remote · systems, economy &amp; monetisation · available now</span>
818      <span lang="zh">伦敦 / 远程 · 系统、经济与商业化 · 即时到岗</span>
819    </p>
820    <a class="contact-email" href="mailto:[email protected]?subject=Game%20Designer%20opportunity%20—%20suzysu" data-reveal>[email protected]</a>
821    <ul class="contact-links" data-reveal>
822      <li><a href="https://www.linkedin.com/in/suzy99-su/" target="_blank" rel="noopener">LinkedIn ↗</a></li>
823      <li><a href="https://github.com/suzyeth" target="_blank" rel="noopener">GitHub ↗</a></li>
824      <li><a href="https://ssu997.itch.io/" target="_blank" rel="noopener">itch.io ↗</a></li>
825    </ul>
826  </div>
827</section>
828
829<!-- FOOTER ========================================== -->
830</main>
831
832<footer class="foot">
833  <div class="container foot-inner">
834    <span>© 2026 Suzy Su · London</span>
835    <span class="foot-mute" lang="en">Last updated June 2026</span>
836    <span class="foot-mute" lang="zh">2026 年 6 月更新</span>
837  </div>
838</footer>
839
840<script>
841// CRITICAL: language toggle must work even if every other script fails.
842(function () {
843  const KEY = 'lang';
844  const btn = document.getElementById('langToggle');
845  const apply = (l) => {
846    document.body.classList.toggle('zh', l === 'zh');
847    document.documentElement.lang = (l === 'zh') ? 'zh-CN' : 'en';
848    if (btn) btn.setAttribute('aria-pressed', String(l === 'zh'));
849  };
850  apply(localStorage.getItem(KEY) || 'en');
851  if (btn) btn.addEventListener('click', () => {
852    const next = document.body.classList.contains('zh') ? 'en' : 'zh';
853    localStorage.setItem(KEY, next);
854    apply(next);
855  });
856})();
857
858// Mobile nav menu (progressive — links still work if this fails) -----
859(function () {
860  const hud = document.querySelector('.hud');
861  const btn = document.getElementById('menuToggle');
862  if (!hud || !btn) return;
863  const close = () => { hud.classList.remove('menu-open'); btn.setAttribute('aria-expanded', 'false'); };
864  btn.addEventListener('click', () => {
865    const open = hud.classList.toggle('menu-open');
866    btn.setAttribute('aria-expanded', String(open));
867  });
868  hud.querySelectorAll('.hud-links a').forEach(a => a.addEventListener('click', close));
869  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
870  document.addEventListener('click', (e) => {
871    if (hud.classList.contains('menu-open') && !hud.contains(e.target)) close();
872  });
873})();
874
875// Project carousel + grid ---------------------------------
876(function () {
877  const carousel = document.querySelector('.work-carousel');
878  const list = document.querySelector('.work-list');
879  if (!carousel || !list) return;
880
881  const sourceCards = Array.from(carousel.querySelectorAll('.card'));
882
883  sourceCards.forEach((card, index) => {
884    const clone = card.cloneNode(true);
885    clone.classList.remove('is-open');
886    clone.classList.add('card-compact');
887    clone.removeAttribute('data-reveal');
888
889    const detail = clone.querySelector('.card-detail');
890    const btn = clone.querySelector('.card-expand');
891    if (detail) detail.remove();
892    if (btn) btn.remove();
893
894    const blurb = clone.querySelector('.card-blurb');
895    if (blurb) {
896      blurb.querySelectorAll('span').forEach(span => {
897        const text = span.textContent.trim();
898        const short = text.length > 92 ? `${text.slice(0, 92).trim()}...` : text;
899        span.textContent = short;
900      });
901    }
902    list.appendChild(clone);
903  });
904
905  sourceCards.forEach((card) => {
906    const clone = card.cloneNode(true);
907    clone.classList.add('marquee-clone');
908    clone.setAttribute('aria-hidden', 'true');
909    clone.removeAttribute('data-reveal');
910    clone.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
911    clone.querySelectorAll('a, button, video').forEach(el => {
912      el.setAttribute('tabindex', '-1');
913      if (el.tagName === 'BUTTON') el.setAttribute('disabled', '');
914    });
915    const detail = clone.querySelector('.card-detail');
916    if (detail) detail.setAttribute('hidden', '');
917    carousel.appendChild(clone);
918  });
919
920  const setMarqueeDistance = () => {
921    const originals = sourceCards.filter(card => getComputedStyle(card).display !== 'none');
922    if (!originals.length) return;
923    const gap = parseFloat(getComputedStyle(carousel).columnGap || getComputedStyle(carousel).gap) || 0;
924    const distance = originals.reduce((sum, card) => sum + card.getBoundingClientRect().width + gap, 0);
925    carousel.style.setProperty('--marquee-distance', `${Math.max(1, distance)}px`);
926    carousel.classList.add('is-marquee-ready');
927  };
928
929  const getCurrentOffset = () => {
930    const first = carousel.querySelector('.card:not(.marquee-clone)');
931    if (!first) return 0;
932    const transform = getComputedStyle(first).transform;
933    if (!transform || transform === 'none') return 0;
934    const values = transform.match(/matrix.*\((.+)\)/);
935    if (!values) return 0;
936    const parts = values[1].split(',').map(v => parseFloat(v.trim()));
937    return Number.isFinite(parts[4]) ? parts[4] : 0;
938  };
939
940  let dragStartX = 0;
941  let dragStartOffset = 0;
942  let isDragging = false;
943
944  const setManualOffset = (value) => {
945    const distance = parseFloat(getComputedStyle(carousel).getPropertyValue('--marquee-distance')) || 1;
946    const min = -distance;
947    const max = 0;
948    const next = Math.max(min, Math.min(max, value));
949    carousel.style.setProperty('--marquee-offset', `${next}px`);
950    return next;
951  };
952
953  const isTouchCarousel = () => window.matchMedia('(max-width: 640px)').matches;
954
955  carousel.addEventListener('pointerdown', (e) => {
956    if (e.button !== 0) return;
957    if (isTouchCarousel()) return;   // phones use native scroll-snap, not custom drag
958    isDragging = true;
959    dragStartX = e.clientX;
960    dragStartOffset = carousel.classList.contains('is-user-controlled')
961      ? parseFloat(getComputedStyle(carousel).getPropertyValue('--marquee-offset')) || 0
962      : getCurrentOffset();
963    carousel.classList.add('is-user-controlled', 'is-dragging');
964    setManualOffset(dragStartOffset);
965    carousel.setPointerCapture(e.pointerId);
966  });
967
968  carousel.addEventListener('pointermove', (e) => {
969    if (!isDragging) return;
970    setManualOffset(dragStartOffset + e.clientX - dragStartX);
971  });
972
973  const endDrag = (e) => {
974    if (!isDragging) return;
975    isDragging = false;
976    const offset = parseFloat(getComputedStyle(carousel).getPropertyValue('--marquee-offset')) || 0;
977    const distance = parseFloat(getComputedStyle(carousel).getPropertyValue('--marquee-distance')) || 1;
978    const progress = Math.min(1, Math.max(0, Math.abs(offset) / distance));
979    carousel.style.setProperty('--marquee-delay', `${-(progress * 52)}s`);
980    carousel.classList.remove('is-user-controlled');
981    carousel.classList.remove('is-dragging');
982    if (carousel.hasPointerCapture(e.pointerId)) carousel.releasePointerCapture(e.pointerId);
983  };
984
985  carousel.addEventListener('pointerup', endDrag);
986  carousel.addEventListener('pointercancel', endDrag);
987
988  window.addEventListener('load', setMarqueeDistance);
989  window.addEventListener('resize', setMarqueeDistance);
990  setMarqueeDistance();
991  window.setWorkMarqueeDistance = setMarqueeDistance;
992})();
993
994// Project card expand/collapse ---------------------------------
995(function () {
996  const cards = () => document.querySelectorAll('.card:not(.marquee-clone)');
997  let openCard = null;
998
999  function close(card) {
1000    if (!card) return;
1001    const btn = card.querySelector('.card-expand');
1002    const detail = card.querySelector('.card-detail');
1003    if (btn) btn.setAttribute('aria-expanded', 'false');
1004    if (detail) detail.setAttribute('hidden', '');
1005    card.classList.remove('is-open');
1006  }
1007
1008  function open(card) {
1009    if (openCard && openCard !== card) close(openCard);
1010    const btn = card.querySelector('.card-expand');
1011    const detail = card.querySelector('.card-detail');
1012    if (btn) btn.setAttribute('aria-expanded', 'true');
1013    if (detail) detail.removeAttribute('hidden');
1014    card.classList.add('is-open');
1015    openCard = card;
1016  }
1017
1018  document.addEventListener('click', (e) => {
1019    const btn = e.target.closest('.card-expand');
1020    if (!btn || btn.disabled) return;
1021    const card = btn.closest('.card');
1022    if (!card || card.classList.contains('marquee-clone')) return;
1023    e.preventDefault();
1024    const isOpen = btn.getAttribute('aria-expanded') === 'true';
1025    if (isOpen) { close(card); openCard = null; }
1026    else open(card);
1027  });
1028
1029  document.addEventListener('keydown', (e) => {
1030    if (e.key === 'Escape' && openCard) { close(openCard); openCard = null; }
1031  });
1032})();
1033
1034// Make project cards clickable → open the full case study.
1035// Distinguishes a tap from a carousel drag, and ignores inner links/buttons.
1036(function () {
1037  const SLUG = { cocktail: 'cocktail-rendering' };
1038  function hrefFor(card) {
1039    const a = card.querySelector('a.case');
1040    if (a && a.getAttribute('href')) return a.getAttribute('href');
1041    const id = card.dataset.id;
1042    return id ? 'projects/' + (SLUG[id] || id) + '.html' : null;
1043  }
1044  let downX = 0, downY = 0, downCard = null;
1045  document.addEventListener('pointerdown', (e) => {
1046    downCard = e.target.closest('.card');
1047    downX = e.clientX; downY = e.clientY;
1048  }, true);
1049  document.addEventListener('pointerup', (e) => {
1050    const card = e.target.closest('.card');
1051    if (!card || card !== downCard) return;                              // started elsewhere
1052    if (Math.abs(e.clientX - downX) > 8 || Math.abs(e.clientY - downY) > 8) return; // was a drag
1053    if (e.target.closest('a, button, video')) return;                    // let real controls work
1054    const href = hrefFor(card);
1055    if (href) window.location.href = href;
1056  });
1057})();
1058
1059// Work filter (All / Games / Tools) -------------------------
1060(function () {
1061  const carousel = document.querySelector('.work-carousel');
1062  const list = document.querySelector('.work-list');
1063  const btns = document.querySelectorAll('.filter-btn');
1064  if (!carousel || !list || !btns.length) return;
1065  btns.forEach(btn => {
1066    btn.addEventListener('click', () => {
1067      const f = btn.dataset.filter;
1068      carousel.classList.remove('filter-games', 'filter-tools');
1069      list.classList.remove('filter-games', 'filter-tools');
1070      if (f !== 'all') {
1071        carousel.classList.add('filter-' + f);
1072        list.classList.add('filter-' + f);
1073      }
1074      carousel.classList.remove('is-user-controlled', 'is-dragging');
1075      carousel.style.removeProperty('--marquee-offset');
1076      carousel.style.removeProperty('--marquee-delay');
1077      btns.forEach(b => {
1078        const on = b === btn;
1079        b.classList.toggle('is-active', on);
1080        b.setAttribute('aria-pressed', String(on));
1081      });
1082      requestAnimationFrame(() => window.setWorkMarqueeDistance && window.setWorkMarqueeDistance());
1083    });
1084  });
1085})();
1086
1087// Scroll reveals (IntersectionObserver, no GSAP required) ----
1088(function () {
1089  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
1090  const targets = document.querySelectorAll('[data-reveal]');
1091  if (!('IntersectionObserver' in window)) {
1092    targets.forEach(t => t.classList.add('is-in'));
1093    return;
1094  }
1095  const io = new IntersectionObserver((entries) => {
1096    entries.forEach(e => {
1097      if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
1098    });
1099  }, { threshold: .15, rootMargin: '0px 0px -8% 0px' });
1100  targets.forEach(t => io.observe(t));
1101})();
1102
1103// Lazy-play decorative videos only while in view (battery + data) ---
1104(function () {
1105  const vids = document.querySelectorAll('video[data-lazy-video]');
1106  if (!vids.length) return;
1107  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1108  if (reduce) return;                       // honour reduced-motion: stay paused
1109  if (!('IntersectionObserver' in window)) { vids.forEach(v => v.play().catch(()=>{})); return; }
1110  const io = new IntersectionObserver((entries) => {
1111    entries.forEach(e => {
1112      const v = e.target;
1113      if (e.isIntersecting) { v.play().catch(()=>{}); }
1114      else { v.pause(); }
1115    });
1116  }, { threshold: .25 });
1117  vids.forEach(v => io.observe(v));
1118})();
1119
1120// Lenis smooth scroll (optional enhancement) -----------------
1121window.addEventListener('load', () => {
1122  if (typeof Lenis === 'undefined') return;
1123  const lenis = new Lenis({ lerp: .08, smoothWheel: true });
1124  function raf(t) { lenis.raf(t); requestAnimationFrame(raf); }
1125  requestAnimationFrame(raf);
1126});
1127</script>
1127
1128
1129</body>
1130</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.