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 & 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 & 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 & 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 & 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.