Cloudwiki

Web sandbox 프로젝트/구상/엔진 개선안

주의:  이 문서는 구상이지 구현 기록이 아니다
여기 적힌 제안 중 **구현된 것은 하나도 없다**. 각 절의 "현황" 표는 저장소의 지금 상태를 기술한 것이고, `제안` 표시가 붙은 항목은 전부 후보다. 이미 있는 기능의 정본 설명은 저장소 `docs/` 폴더(`ARCHITECTURE.md`, `PHYSICS.md`, `MATERIAL-SYSTEMS.md`, `OBJECTS.md`, `ELECTRICITY.md`)에 있다.

성능·최적화는 이 문서의 범위가 아니다 — [[Web sandbox 프로젝트/구상/엔진 최적화 방안]]으로 분리했다. 여기서는 **"같은 결과를 더 싸게"가 아니라 "지금은 표현할 수 없는 것"** 만 다룬다.


## 이 문서가 다루는 것

엔진이 **지원하는 물리·화학 상호작용의 폭**과 **엔진 차원의 기능**을 넓히는 후보를 모았다. 판단 기준은 프로젝트 우선순위 그대로다.


① 재미
새 놀이가 열리는가. 플레이어가 "이걸로 뭘 만들지"를 떠올리게 되는가. 물리적으로 정확하지만 아무 놀이도 안 여는 것은 순위가 낮다.

② 편의성
만들던 것을 더 적은 삽질로 만들 수 있게 되는가. 기존 물질 60여 종이 손으로 반복하던 것을 데이터 한 줄로 줄이는가.

③ 과학적 고증
고증이 재미를 **거들 때만** 채택한다. 고증을 위해 재미를 깎는 항목은 이 문서에서 기각 사유가 된다.



모든 제안에는 세 축의 근거를 붙였다 — **(a) 지금 무엇이 비어 있는가 · (b) 어떤 기존 인프라를 재사용하는가 · (c) 비용/리스크**. 근거 없이 나열한 항목은 없다.

---

## 현재 틱 구조

새 물리량을 넣는다는 것은 결국 셋 중 하나다. **패스를 하나 더 붙이거나**, **`updateCell` 우선순위 사슬에 한 칸 끼우거나**, **셀당 배열을 늘리는 것**. 비용이 셋 다 다르므로 먼저 지금 구조를 못박는다.

### 한 틱의 5패스

`src/game/engine/Simulation.ts`의 `step()`이 정확히 5개 패스다. 프로파일러가 그 경계를 그대로 계측한다.

```mermaid
flowchart TD
  A["틱 시작 — moved / overlayMoved 전체 0으로 리셋"]
  B["패스 1 · 열<br/>diffuseHeat ×3 서브스텝 + radiateHeat ×1"]
  C["전역 필드 리셋<br/>wind 클리어(lazy) · magnetFields 비움 · tick 동기화"]
  D["패스 2 · CA 물질 스캔<br/>중력 기준 정렬 스캔 + 교차축 flip 반전"]
  E["패스 3 · 오브젝트<br/>stepObjects — 자유 강체 5종"]
  F["패스 4 · 드리프트<br/>배경 틴트 OU 스텝 — 순수 코스메틱"]
  G["틱 끝"]
  H["렌더 — Game.frame (틱 밖, 디스플레이 주사율)"]
  A --> B --> C --> D --> E --> F --> G
  G -.-> H
```

| 패스 | 하는 일 | 확장 시 알아야 할 것 |
|---|---|---|
| 준비 | `moved`/`overlayMoved` 리셋 | 틱당 1회 이동 가드의 기준선 |
| 1 · 열 | 4방향 직교 전도 3서브스텝(WASM 커널 있으면 한 번에) + 500°↑ 셀의 3칸 복사 보조 | **빈칸은 전도도 0의 완벽 단열재** — 공기가 열을 안 나른다 |
| 1.5 · 필드 리셋 | `wind`·`magnetFields`를 매 틱 버림 | 새 전역 필드는 여기에 리셋 지점이 필요 |
| 2 · CA | 중력 방향으로 정렬해 셀마다 `updateCell` | 활성 타일 스캔이 기본이고 전면 스캔과 **비트 동일**이어야 함 |
| 3 · 오브젝트 | 강체 물리 — CA가 **끝난 뒤** 완전히 분리 | 그리드를 읽기만 하고 거의 안 쓴다(§오브젝트 레이어) |
| 4 · 드리프트 | 액체 렌더용 배경 필드를 흔듦 | **시뮬레이션이 절대 안 읽음** |

기본 시뮬레이션 주기는 30 Hz(`SIM_HZ_AT_1X`), 기본 격자는 360×203, 격자 상한은 `MAX_CELLS = 600_000`이다(`src/game/config.ts`).

### `updateCell` 내부 우선순위

셀 하나가 자기 차례에 밟는 순서. **각 단계가 앞 단계를 조기 종료시킨다** — 이 사슬 어디에 끼우느냐가 곧 새 기능의 의미론이다.

```mermaid
flowchart TD
  S["셀 차례"] --> Q1{"통전 전자석이 잡고 있나"}
  Q1 -- "예" --> X1["턴 통째로 건너뜀<br/>낙하도 반응도 없음"]
  Q1 -- "아니오" --> Q2{"이번 틱에 이미 moved인가?"}
  Q2 -- "예" --> X2["스킵"]
  Q2 -- "아니오" --> R["방사선 구동<br/>radiationP[id] > 0 이면 irradiate"]
  R --> Q3{"life 수명 붕괴?"}
  Q3 -- "예" --> X3["붕괴 — 반응·update 모두 스킵"]
  Q3 -- "아니오" --> Q4{"선언형 반응 tryReact 성공?"}
  Q4 -- "예" --> X4["update 스킵"]
  Q4 -- "아니오" --> U["물질 자신의 update<br/>없으면 phase 기본 거동"]
  U --> O["겹침 유체의 독자 이동<br/>별도 overlayMoved 가드"]
```

노트:
방사선이 `update`보다 **앞**에 있는 것은 의도적이다 — 녹아내리는 우라늄이 물질이 바뀌는 그 틱에 마지막 선량을 빠뜨리지 않게 하려고. 같은 이유로 **엔진이 id 테이블로 평탄화해 직접 구동하는 태그**(`thermal.conductivity`, `life`, `magnetic`, `radiation`)는 물질이 자기 `update`에서 호출을 빠뜨릴 수가 없다. 새 엔진 태그를 설계할 때 이 자리가 제일 튼튼한 자리다.


---

## 지원 물리량과 그 단순화 지점

지금 있는 물리량 전수와, **각각이 무엇을 일부러 안 했는가**. 확장 제안은 전부 이 표의 "일부러 안 한 것" 칸에서 나온다.


| 물리량 | 모델 | 일부러 안 한 것 |
|---|---|---|
| **온도** | 셀당 `Float32` 스칼라. 4방향 직교 전도(rate 0.2 × 3서브스텝) + 500°↑ 셀의 3칸 복사 보조 | **대류 없음. 빈칸 전도도 0**(공기가 절연체). **열용량·비열 없음** → "큰 덩어리가 천천히 식는다"를 표현 못 함. **잠열 없음**. 대각 직접 전도 없음(복사 패스가 우회) |
| **밀도** | 이동 시점의 이웃 1:1 밀도 비교 + 확률적 드래그 게이트 | **압력이 아니라 국소 스왑**. 정수 격자라 부분 침수 없음(뗏목 흘수는 컬럼 단위 에드혹 계산). 밀어내기는 빈칸으로만 — **재귀 변위 없음** |
| **중력** | 단위벡터 + 수직축. 모든 이동 프리미티브가 통과 | **방향은 상하좌우 4방향뿐**(임의 각도 불가). **세기는 가속도가 아니라 "이번 틱에 이동을 시도할 확률"** → 속도가 아니라 빈도가 바뀜. 관성·종단속도 없음(탄도 파티클 제외) |
| **전기** | 1틱 수명 Spark 파티클. `aux` 하위 8비트=도체 클래스, 상위 8비트=세기 | **전압·전류·저항 없음**. "세기"는 곧 **남은 도달 거리**. 회로·분기 전류 분배 없음. 장치는 "연결 부위 전역 즉시 활성화" 규약이라 세기와 무관하게 통째로 켜짐 |
| **자기장** | 자석 몸체 윤곽에서 나가는 측지(octile) 최단경로 스윕. 셀이 "어디서 도달했는지"를 기억해 되짚어 당김 | **벡터장이 아니라 거리장**. 극성(N/S)·척력 없음(인력 전용). 사거리 안 감쇠 없음. 고체가 완전 차폐. **붙잡힌 셀은 반응도 안 함** |
| **빛/레이저** | 8방향 직진 셀 파티클. 비행 상태를 `temp`에 패킹 | **파장·강도·간섭·굴절률 없음**. 물질별 응답이 하드코딩 분기표. Heat Ray는 **파괴력 0**(가열만) |
| **충격파** | 크레이터 플러드 → 림 바깥으로 비파괴 압력링 | **한 틱 내 즉시 전파**(전파 속도 없음). 감쇠 없이 사거리에서 뚝 끊김. 파괴 판정이 `destructivePower` vs `durability` **스칼라 비교** — 응력 누적 아님 |
| **방사선** | 선원에서 너비우선 측지 플러드, 거리 d에 `dose/d` | **감쇠가 1/d — 역제곱이 아니다**(명시적으로 "1순위 재미"). **고체만 차폐**, 가루·액체·기체 전부 통과. 반감기·핵종·선량 누적 없음 |
| **바람** | 셀당 1바이트 = 방향+1. 매 틱 전량 클리어 후 재스탬프 | **완전 단방향** — CA는 이 필드를 안 읽는다(물질은 Fan 자신의 swap으로 밀림). **속도 크기 없이 방향만**. 오브젝트와 렌더러만 소비 |
| **점도** | `Material.viscosity` — 확률 게이트 | **옆 퍼짐만 억제, 수직 낙하는 절대 못 막음**. 낙하까지 늦추려면 물질이 자기 `updateLiquid` 호출을 확률로 감싸는 **별도 관용구**가 필요 |
| **표면장력** | `Material.surfaceTension` — 접촉 이웃 수 극대화 이동 | 8이웃 4 미만인 **낙오 셀만**. 곡률·라플라스 압력·접촉각 없음 |
| **마찰** | `Material.friction` 가루 안식각 / 오브젝트 접촉 임펄스 | 가루 쪽은 **대각 미끄럼 확률**일 뿐 실제 마찰력이 아님. 고무공에는 **회전 마찰이 아예 없음** |
| **탄성** | `Material.elasticity` 파편 반발계수 / 오브젝트 `restitution` | 파편은 `aux`에 실린 원물질 id에서 읽음. 변형·소성 없음 |
| **어는점** | `Material.freeze` — 온도 ≤ 임계면 **제자리 고체** | 별도 물질 전환이 아님(경량). **잠열·과냉각 없음** |
| **겹침(소킹)** | 셀당 유체 1개 동거. 별도 `overlayAux` 16비트 | 포화도가 아니라 **0/1**. 어느 알갱이가 흡수하는지는 고정 `tint` 바이트 기준의 **이진 분류**. 겹침 유체의 이동 방향은 **화면 기준 고정**(중력 회전을 안 따라감) |

주의:  가장 오해하기 쉬운 지점 — 점도·표면장력·마찰·탄성
이 넷은 **"없는 것"이 아니라 "확률 게이트로 얕게 있는 것"** 이다. `Material` 필드로 버젓이 존재하지만 구현은 전부 확률 롤이다.

· **점도**는 옆 퍼짐 확률일 뿐 낙하 속도가 아니다 — 꿀이 물과 똑같은 속도로 떨어지던 버그가 정확히 이 오해에서 나왔고, 지금도 낙하 지연은 물질 자기 관용구로 따로 처리한다.

· **마찰**은 대각 미끄럼 확률이지 힘이 아니다.

· **표면장력**은 낙오 셀만 건드리는 휴리스틱이라 벌크 레벨링에 영향이 없다.

진짜 연속체 물리를 기대하고 이 필드들을 확장하면 반드시 어긋난다. **확률 게이트를 연속량으로 승격하는 것은 "값 조정"이 아니라 "새 물리량 추가"** 로 취급해야 한다.


---

## 미지원 물리량 — 넣으면 무엇이 열리는가

`src/` 전체 grep으로 부재를 확인한 항목들이다. 각 행의 **판정**은 ①재미 ②편의성 ③고증 순서로 매겼다.


| 물리량 | 왜 지금 없나 | 넣으면 무엇이 열리나 | 비용 | 판정 |
|---|---|---|---|---|
| **압력 / 기압** | 셀당 배열이 하나 더 늘고 세이브 포맷이 바뀐다. **빈칸으로만 밀어내는**(재귀 변위 없는) 현 변위 설계와 정면충돌 | 진공 흡입, 밀폐 용기 파열, 기압에 따른 끓는점 변화, 진짜 펌프·사이펀, 폭발이 즉시전파가 아닌 파동이 됨 | 매우 큼 — 새 평면 + 매 틱 완화 패스 + 변위 전면 재작성 | [보류] 재미는 최상급이지만 **엔진의 뼈대를 바꾼다**. 축소판부터(아래) |
| **유체 속도장** | `Grid`에 자리를 비워 두라는 주석까지 있으나 미착수. 탄도 물질이 지금은 속도를 `temp`에 패킹 | 관성·운동량 보존, 물살·소용돌이, 폭발 후 유체가 실제로 흘러감, 탄도 파티클이 `temp` 해킹을 안 써도 됨 | 큼 — 셀당 2바이트(vx,vy) + 세이브 평면 2개 + 이동 프리미티브 전면 개편 | [보류] 압력과 사실상 한 세트. 따로 넣으면 두 번 고친다 |
| **농도 확산** | "확산"이라 부르는 것 3종이 전부 농도가 아니다 — id 지정 스왑·랜덤워크·열확산. 용해 농도는 **전역 스칼라 하나** | 진짜 용해도·과포화, 오염 확산 지도, 기체 혼합비, 삼투, 결정 석출이 격자 현상이 됨 | 큼 — `temp`와 같은 급의 신규 실수 레이어 | [부분채택] 전역이 아니라 **물질별 8비트 농도**로 좁히면 `aux` 안에서 시범 가능 |
| **열용량 / 잠열** | 셀당 온도 스칼라뿐. 질량 개념이 없다 | "쇳덩이는 천천히 식는다", 얼음이 녹는 동안 0°에 머무름, 열저장 장치, 증기 기관의 실감 | 중 — `Material.heatCapacity` 1개 + 확산 커널의 교환량에 계수 하나. **골든 테스트가 전부 깨진다** | [유력] **비용 대비 체감이 가장 큰 후보**. 아래 상세 |
| **응력 / 구조 붕괴** | 고체는 `phase` 기본 거동이 `undefined`라 **지지물이 사라져도 절대 안 무너진다**. 지지 그래프는 전역 패스가 필요한데 현 구조는 전부 국소 8이웃 | 아치 붕괴, 터널 낙반, 하중 초과 파괴, 지반 침하, 건축 놀이 | 큼 — 전역 연결 질의 + 매 틱 갱신 | [부분채택] 전역 그래프 대신 **"폭발 크레이터 주변만 국소 붕괴"** 축소판이 현실적 |
| **습도** | 기체에 농도 개념이 없다(Steam은 셀 자체가 수증기) | 구름·안개·이슬점 응결, 증발 속도가 환경에 반응, 사막/열대 대비 | 중 — 농도 확산의 하위 사례 | [기각] 농도 없이 단독으로는 못 만든다. 농도 채택 시 재검토 |
| **소리 전파** | Woofer의 "충격파"는 즉시 전파 플러드다. 프로젝트 전체가 **완전 무음**(오디오 API 0건) | 반향·차폐 퍼즐, 소리로 트리거되는 장치, 재질별 음속 | 중 — 전파 패스 + 오디오 자산(오프라인 PWA라 절차적 합성 필요) | [기각] 물리량으로서의 소리는 순위가 낮다. **효과음**은 별개 축이고 UI/UX 문서 소관 |
| **결정 성장** | 성장 프론트는 이미 식물·산호·크롤러가 **각자** 구현 중 — 공용화만 안 됐다. 석출은 전역 스칼라 정산 | 종유석·수정 동굴, 서리 패턴, 과포화 용액의 씨앗 결정, 얼음이 자라며 관을 막음 | 작음 — **새 물리량이 아니라 기존 3구현의 공용 헬퍼화** | [유력] 재미 대비 최저 비용. 아래 상세 |
| **pH / 산-염기** | Acid가 부식 확률 하나로 표현되고, 중화 산물을 데이터로 받을 태그가 없다 | 중화 반응, 지시약 색변화, 배수구 세정제 실연, 알칼리 계통 물질군 전체 | 중 — 스칼라 pH 필드 or 산물 태그 일반화 | [축소채택] 필드가 아니라 **`acidProduct` 태그 일반화**로 90%를 얻는다(§반응 표현력) |

### 상세 — 유력 후보 셋

G-A. 열용량 `Material.heatCapacity`
**지금 비어 있는 것** — 셀당 상태가 온도 스칼라뿐이라 질량이 없다. 지름 1칸 철가루와 100칸 철덩이가 **정확히 같은 속도로 식는다**. 용광로·냉각탑·열저장 어떤 빌드에서도 "덩치"가 아무 의미가 없다.

**재사용하는 인프라** — 열 확산 커널이 이미 `min(ci, cj)`로 교환을 게이트하고 있고, `Simulation` 생성자가 `thermal.conductivity`를 **id별 `Float32Array(256)`로 평탄화**한다. 열용량도 정확히 같은 방식으로 평탄화해 교환량을 나누면 된다 — 물질 쪽 작업은 태그 한 줄. WASM 커널도 같은 배열 하나를 더 받으면 된다.

**비용·리스크** — 열 골든 테스트가 **전부** 깨진다(그게 정상이고, 새 골든을 다시 굽는 게 작업의 절반이다). 에너지 보존이 대칭 교환에 의존하므로 계수를 잘못 넣으면 열이 새거나 늘어난다. 기본값을 1.0으로 두면 미선언 물질은 지금과 비트 동일이라 **점진 이주가 가능하다**.

[①재미] 용광로가 "달구는 데 시간이 걸리는 물건"이 된다 · [③고증] 비열은 초등 과학 수준의 직관과 일치


G-B. 성장 프론트 공용 헬퍼
**지금 비어 있는 것** — 식물의 L-시스템, 산호의 5방향 성장, 크롤러의 표면 이동이 **각자 자기 플러드와 자기 aux 레이아웃을** 갖고 있다. 새 성장체(종유석·서리·수정)를 넣으려면 네 번째 구현을 처음부터 쓴다.

**재사용하는 인프라** — `crawler.ts`가 이미 "새 벌레는 두 줄"이라는 공용화의 선례를 만들었다(`crawl()` + `eatAndReproduce()`). 성장 쪽도 같은 형태가 가능하다 — **방향 선택 정책 + 성장 조건 + 세대 예산**을 인자로 받는 `grow()` 하나.

**비용·리스크** — 새 물리량이 아니므로 세이브 포맷 무변경. 다만 식물·산호는 `aux` 비트를 이미 촘촘히 쓰고 있어(산호는 **16비트 완전 소진**) 공용 레이아웃으로 억지 이주하면 손해다. **기존 둘은 그대로 두고 새 성장체부터 헬퍼를 쓰는** 점진 도입이 옳다.

[①재미] 동굴 장식·서리·수정이 물질 파일 하나로 열림 · [②편의성] 네 번째 성장 구현을 안 씀


G-C. 압력의 축소판 — "밀폐 판정"만
**지금 비어 있는 것** — 전면 기압 필드는 비용이 엔진 뼈대급이다. 그런데 플레이어가 실제로 원하는 재미의 대부분은 **"막힌 통에 기체가 계속 나오면 터진다"** 한 문장이다.

**재사용하는 인프라** — 연결 덩어리 플러드가 이미 세 벌 있다(폭발 `surveyMass`, 장치 `floodDeviceBody`, 자석 측지 스윕). 그중 하나를 공용화해(§G-1) **"이 기체 셀이 속한 빈 공간 덩어리가 닫혀 있고 크기가 N 이하인가"** 만 물으면 된다. 터지는 쪽은 `detonate()` 재사용.

**비용·리스크** — 매 틱 전역 플러드는 비쌀 수 있어 **기체 생성 시점에만, 예산 상한을 걸고** 굴려야 한다(범위 스캔은 항상 상한을 건다는 이 프로젝트의 확립된 규율 — 상한 초과는 에러가 아니라 **조용한 undercount**). 오탐(터지면 안 되는 곳이 터짐)이 나면 재미가 아니라 짜증이 된다.

[①재미] 보일러·압력솥·수소 탱크가 성립 · [③고증] 정확한 기압은 아니지만 결과는 맞음


---

## 반응 표현력 개선

선언형 반응 테이블(`ReactionRule`)을 실제로 쓰는 물질은 **138종 중 6종**뿐이다(활성 알루미늄 · 알루미늄 가루 · 질산암모늄 · 과산화수소 · 액체 갈륨 · 비누). 나머지 상호작용은 **전부 각 물질 `update`의 하드코딩**이다. 이 비율 자체가 진단이다 — 테이블이 표현력이 모자라서 아무도 안 쓰는 것이다.

### 지금 표현되는 것

`with` · `produce` · `otherBecomes` · `probability` · `tempMin`/`tempMax` · `heat` · `byproduct` · `catalyst`/`catalystFactor` — 10개 필드로 **2체 접촉 치환 + 확률 + 자기 온도창 + 발열/흡열 + 촉매 가속 + 빈칸 부산물**까지.

### 못 하는 것 14가지


| # | 한계 | 지금의 우회법 |
|---|---|---|
| 1 | **촉매 필수 게이트** — `catalyst`가 확률 배수라 `0 × 4 = 0`. "촉매가 있어야만"을 못 만든다 | 중간 물질을 하나 더 만들어 우회(Activated Aluminum) |
| 2 | **산물 데이터화** — `acidHydrogen`이 표현하는 산물은 수소 하나로 **고정** | 석회암+산→CO₂를 못 넣고 있음 |
| 3 | **태그 상대 반응** — `with`는 구체적 id 하나. "모든 금속"·"모든 탄산염"이 없다 | 행을 물질 수만큼 복제 → 그게 `acidHydrogen` 태그가 태어난 이유 |
| 4 | **3체 이상 레시피** — `with`가 하나뿐 | 흑색화약(3원료)은 공용 헬퍼 파일로 이주 |
| 5 | **이웃 온도 조건** — 온도창은 **선언한 셀만** 본다 | 뜨거운 녹가루가 찬 알루미늄을 테르밋으로 끌어들이는 것을 "괜찮은 이유"로 변호 중 |
| 6 | **잠긴 셀 부산물** — 부산물은 **인접 빈칸에만** 스폰. 웅덩이 바닥이면 하나도 안 나옴 | `otherBecomes`로 파트너 자신을 기체로 바꿈(1:1 소모라 오히려 나았음) |
| 7 | **1:N 소모비** — 항상 1:1 | 3:1 테르밋을 1:1로 양보 |
| 8 | **확률의 의미가 "접촉당"** — 실패 시 다음 이웃으로 넘어가므로 8이웃이 전부 파트너면 실효 확률이 `1-(1-p)^8` | 튜닝으로 흡수 |
| 9 | **같은 틱 연쇄** — `hasMoved` 스킵이 의도적으로 막음 | A→B→C는 반드시 틱을 나눔 |
| 10 | **셀당 틱당 2회 이상** — 첫 매치에서 종료 | — |
| 11 | **양방향 선언** — 규칙은 **선언한 쪽 셀에서만** 발화 | 양쪽에 같은 규칙을 두 벌 |
| 12 | **겹침 유체와의 반응** — 1차 점유자만 보임 | 반응 물질이 `liquidOverlap: 0`/`overlapFluids`로 겹침을 막아 반응이 물을 보게 함 |
| 13 | **방향성·중력 의존** — 8이웃 고정 순서 첫 매치 | 부산물만 중력 반대쪽 우선 |
| 14 | **`heat` 외의 부수효과** — 스폰 위치 지정·`aux` 쓰기·오브젝트 상호작용·폭발 트리거 전부 불가 | `update` 행 |

### 비용 대비 효과 순위


| ID | 제안 | 지금 비어 있는 것 | 재사용 인프라 | 비용·리스크 |
|---|---|---|---|---|
| **R-1** | `ReactionRule.catalystRequired: true` | 한계 #1. 촉매 반응이라는 화학의 대표 문법이 통째로 막혀 있고, 이미 저장소 백로그에 명시돼 있다 | 판정 루프에 조건 한 줄. 확률 배수 경로는 그대로 | **매우 작음.** 기존 규칙은 필드 미선언이므로 무영향. 리스크는 사실상 0 |
| **R-2** | `acidHydrogen` → `acidProduct` 일반화 | 한계 #2. 산이 만나는 모든 것이 수소만 낸다. 탄산염 계열이 통째로 막혀 있다 | 산 부식 패스가 이미 접촉마다 태그를 굴린다. 구조 변경 없이 "무엇이 되나"를 데이터로 | **작음.** 기존 8종은 산물=수소로 자동 승계. **레지스트리 명단 검사가 이미 있어** 태그 이주를 방어한다 |
| **R-3** | 잠긴 셀 부산물 — 액체를 밀어내는 스폰 경로 | 한계 #6. 웅덩이 **바닥** 반응이 전부 무음이다. 물속 화학이 사실상 불가능 | 연막탄의 `displaceLiquidUp`이 **정확히 이 동작을 이미 한다**(액체를 위로 밀 뿐 삭제 안 함) — 그걸 부산물 스폰 경로로 승격 | **작음.** 검증된 코드의 재배치. 리스크는 밀어낸 액체가 어디로 가는가의 경계 처리 |
| **R-4** | `with` 대신 `withTag` — 태그 상대 반응 | 한계 #3. 새 금속을 하나 넣을 때마다 반응 행이 물질 수만큼 복제된다 | 태그를 `Simulation` 생성자에서 `Uint8Array(256)`로 평탄화하는 기존 패턴 그대로 | **중간.** 태그 집합 설계가 진짜 작업(무엇을 "금속"이라 부를지). 잘못 그으면 되돌리기 어렵다 |
| **R-5** | `partnerTempMin`/`partnerTempMax` | 한계 #5. "뜨거운 것이 찬 것에 옮겨붙는다"를 못 막는다 | 판정 루프에 `sim.getTemp(nx, ny)` 두 줄 | **작음.** 단 기존 테르밋 거동이 바뀔 수 있으므로 **미선언 = 지금 그대로**를 지켜야 한다 |
| **R-6** | `consume: n` — 1:N 소모비 | 한계 #7. 화학량론이 전부 1:1이라 "원료가 3배 든다"를 표현 못 함 | 부산물 스폰과 같은 이웃 스캔을 재사용 | **중간.** 소모 대상 n개를 어떻게 고르느냐가 스캔 순서 의존을 만든다. **활성 타일 정합 주의** |
| **R-7** | `perCell: true` — 확률을 셀당으로 | 한계 #8. 지금 튜닝값이 이웃 밀도에 따라 최대 8배까지 흔들린다 | 실패 시 `continue`를 `break`로 바꾸는 한 줄 + 옵트인 플래그 | **매우 작음.** 다만 **기존 6종의 체감 반응속도가 바뀌므로** 기본값은 반드시 현행 유지 |
| **R-8** | `mirror: true` — 양방향 자동 선언 | 한계 #11. 두 물질이 서로를 알아야 하는 반응은 규칙을 두 벌 쓴다 | 등록 시점에 상대 물질 규칙 배열에 역방향 규칙을 밀어 넣는 것으로 충분 | **작음.** 리스크는 등록 순서 의존(교차 참조는 런타임 함수 본문에만 두는 기존 규율 준수 필요) |
| **R-9** | `withAll: [id, id]` — 3체 레시피 | 한계 #4. 흑색화약·테르밋 같은 다원료 조합이 전부 헬퍼 파일로 샌다 | 이웃 스캔을 두 번 도는 것 | **중간.** 조합 폭발과 스캔 비용. **재미 대비 비용이 R-1~R-3보다 나쁘다** |
| **R-10** | `byproductAt` — 스폰 위치 지정 | 한계 #14의 일부. 부산물이 항상 중력 반대쪽 아무 데나 | 방향 벡터는 `gravityX/Y`+`perpX/Y`로 이미 표준화돼 있다 | **작음.** 중력 회전 시 자동으로 따라오게 하려면 화면 기준이 아니라 중력 기준으로 써야 한다 |

팁:  최소 노력 3종 세트
**R-1(촉매 필수) + R-2(산물 데이터화) + R-3(잠긴 셀 부산물)** 셋은 전부 저장소 백로그에 이미 적혀 있고, 셋 다 기존 코드의 재배치 수준이며, 셋이 합쳐지면 **"물속에서 촉매로 굴러가는 다단 공정"** 이라는 통째로 새로운 놀이 장르가 열린다. 화학 공정 놀이를 늘리려는 어떤 계획도 여기서 시작하는 게 맞다.


---

## 상변화의 데이터화

위험:  융점·비등점·승화점을 표현하는 **필드가 아예 없다**
`Material`의 74개 필드 중 상변화 온도를 담는 것은 `freeze` 하나뿐이고, 그마저 **물질을 바꾸지 않는** 제자리 동결이다(액체가 흐름만 멈춤). 나머지 모든 융해·비등·응고·승화는 **각 물질의 `update` 안에서 `getTemp` 문턱 비교를 손으로 반복**한다.

실측: `src/game/materials/` 중 **86개 파일이 `getTemp`를 읽고, 그중 60여 개가 직접 문턱 비교를 한다.** `*_TEMP` 형태의 온도 상수 선언은 **102개**다.


### 지금의 관용구 — 반드시 보존해야 하는 것

이주를 제안하기 전에, 기존 코드가 **왜** 그렇게 생겼는지를 못박는다. 선언형 필드가 이걸 깨면 이주는 실패한다.

| 관용구 | 무엇 | 왜 |
|---|---|---|
| **제자리 `set`은 온도 보존** | 융해는 `set(MOLTEN_X)` — 같은 자리를 덮어쓴다 | 뜨거운 온도를 그대로 물려받아야 **다음 틱에 즉시 재응고하지 않는다** |
| **`spawn`은 온도 리셋** | 미스캔 이웃에 쓸 때는 `spawn`(= set + markMoved) | 안 쓰면 갓 만든 셀이 같은 틱에 자기 턴을 또 받아 스캔 방향으로 폭주 |
| **히스테리시스는 상수 2개** | 갈륨 30/28, 알루미늄 660/560, 유리 1150/1050 | 융점과 응고점을 벌려 **경계에서 깜빡이는 것**을 막는다 |
| **예외 분기가 문턱보다 앞** | 물의 비등 앞에 용암 접촉·연소 석유층 분기가 있다 | 상황별 특례가 일반 규칙을 이긴다 |
| **문턱 2단** | 물: −12° 이하 Ice, 0° 이하 Snow | 하나의 물질이 온도대에 따라 다른 결과 |
| **문턱 + 확률** | 드라이아이스: −40° 초과 즉시 승화 + 저온에서도 낮은 확률 승화 | 정적인 상태에서도 조금씩 변한다는 연출 |

### 제안

P-1. `Material.melt` / `boil` / `sublime` 선언형 필드
```
melt?:    { temp: number; into: MatId; freezeAt?: number }
boil?:    { temp: number; into: MatId; condenseAt?: number }
sublime?: { temp: number; into: MatId; chance?: number }
```
**공백** — 60여 개 파일이 같은 3줄을 복사하고 있고, 새 물질에 융점을 주려면 반드시 `update`를 써야 한다. `reactions`의 `tempMin`으로 우회하려 해도 **상대 물질이 필요하므로 "혼자 녹는 것"은 원리적으로 표현 불가**다.

**재사용** — 엔진이 `Simulation` 생성자에서 id별 배열로 평탄화해 `updateCell`이 **`update` 앞에서** 구동하는 패턴이 이미 넷 있다(`thermal`, `life`, `magnetic`, `radiation`). 상변화도 같은 자리에 들어간다 — 그러면 **물질이 자기 `update`에서 호출을 빠뜨릴 수가 없다**.

**비용·리스크** — 구동 지점을 `update` 앞에 두면 예외 분기가 있는 물질(물·석유 계열)이 깨진다. 해법: **선언형 필드는 `update`가 `false`를 반환하거나 아예 없을 때만 발화**하도록 우선순위를 낮추고, 예외가 많은 물질은 이주하지 않는다. 히스테리시스 필드(`freezeAt`/`condenseAt`)를 처음부터 넣어야 기존 3종을 그대로 이주할 수 있다.

[②편의성] 새 물질의 상변화가 데이터 3줄 · [③고증] 온도 사다리가 한곳에 모여 충돌 검사가 가능해짐


P-2. 부분 이주 전략 — 전부 옮기지 않는다
**공백** — 60여 개를 전부 이주하려는 계획은 반드시 실패한다. 물의 비등 하나만 봐도 문턱 앞에 특례 분기가 둘이다.

**재사용** — `flammable` → `combustible` 이주가 이미 **부분 이주의 선례**다(11개 파일이 `flammable`을 읽지만 선언은 1종만 남았다 — 나머지는 새 태그로 옮겼고 옛 태그는 살아 있다).

**비용·리스크** — 이주 기준을 문서로 못박아야 한다. 제안: **"문턱 비교 한 줄 + `set` 한 줄"만 있는 물질은 이주, 앞에 분기가 붙은 물질은 유지.** 이주 대상을 세는 스크립트를 먼저 짜서 실제 규모를 재는 것이 첫 작업이다.

[②편의성] 위험을 나눠 진다


P-3. 온도 사다리 충돌 검사 하네스
**공백** — 새 반응의 온도창을 고를 때 기존 임계와 겹치면 플레이어가 두 반응을 분리할 수 없게 된다. 지금은 그 지도가 **사람 머릿속에만** 있다.

**재사용** — P-1로 상변화가 데이터가 되면 **레지스트리를 훑어 임계 목록을 뽑는 검사**를 붙일 수 있다 — 산 태그 명단 검사·방사선 커버리지 검사와 정확히 같은 형태다.

**비용·리스크** — P-1에 의존. 단독으로는 못 한다(지금은 상수가 파일마다 흩어져 있어 정적 스캔이 불안정하다).

[②편의성] 온도 충돌을 리뷰가 아니라 테스트가 잡음


---

## 태그 일반화가 막힌 지점

이 프로젝트에서 가장 잘 작동한 확장 패턴은 **"행 N개 → 태그 1개"** 다. `acidHydrogen`이 그 정본 사례다 — 알루미늄 3종이 같은 반응 행을 3벌 복사해 갖고 있던 것을 태그로 옮겼고, 태그 값(`chance`)이 곧 이온화 경향 서열이 되었다.

그런데 아직 태그가 안 된 자리가 넷 남아 있다.


| ID | 항목 | 현황 | 제안 | 비용·리스크 |
|---|---|---|---|---|
| **T-1** | **염소의 생명 명단** | `chlorine.ts`가 Plant·Virus·Yeast·Seed·Slime·Termite·Coral을 **하드코딩 id 목록**으로 들고 있다. `radiation.ts`가 이 이중 명단을 주석으로 경고하고 있고, **새 생명 물질을 추가하면 여기도 손대야 하는데 빠뜨려도 조용히 샌다** | `Material.chlorineKill`(또는 더 넓은 `living` 분류 태그) | **작음.** 방사선 태그 3종이 이미 "어느 쪽도 상대를 id로 모른다"는 모범을 만들어 뒀다. 리스크: 산호가 명단에서 일부러 빠져 있는 것 같은 **의도적 예외**를 태그로 옮길 때 잃지 않아야 함 |
| **T-2** | **`durability` 미사용** | 필드는 있는데 **어떤 물질도 명시 선언하지 않는다** — 전부 phase 기본값으로 떨어진다. 즉 "이 돌은 특별히 단단하다"를 표현할 수단이 이름만 있고 실질이 없다 | 방폭·구조 물질부터 실제 값을 선언, 또는 필드를 지우고 phase 기본값만 남긴다 | **작음.** 선언하기 시작하면 **폭발 밸런스가 전부 바뀐다** — 폭발 테스트 골든을 다시 굽는 것이 실질 비용 |
| **T-3** | **`flammable` 잔재 1종** | 11개 파일이 읽는데 선언은 **`virus.ts` 단 1종**. `combustible`로 사실상 이주 완료된 옛 태그 | 마지막 1종을 `combustible`로 옮기고 필드 제거, 또는 "표면 화염 전선 없이 즉발 발화"라는 의미를 문서로 확정 | **작음.** 리스크: Virus의 감염 대상 판정이 `flammable`\|`combustible`을 둘 다 보고 있어서 **감염 범위가 바뀔 수 있다** |
| **T-4** | **`magnetic` 4종뿐** | 철광석·금속가루·나노봇·녹가루만. **철과 녹은 일부러 태그하지 않았고** 그 판단이 주석에 남아 있다. 게다가 오브젝트 쪽 자성 판정은 태그가 아니라 `kind === 'drum' \|\| kind === 'smokebomb'` **하드코딩 분기**다 | 오브젝트에 `magneticBody` 플래그를 주어 셀과 같은 문법으로 통일 | **작음.** 셀 쪽 명단을 늘리는 것은 별개 재미 판정(고체 구조물이 자석에 끌리면 빌드가 무너진다는 것이 원 판단) |

성공:  이 프로젝트의 확립된 대응 — 레지스트리를 훑는 테스트
손으로 옮겨 적은 명단이 어긋나는 것은 **리뷰에서 세 번 반복된 실패**다. 그래서 대응이 관행으로 굳었다.

· `npm run test:acidmetal` — 레지스트리를 훑어 **`acidHydrogen` 태그 명단 자체를 비교**한다. 금속을 새로 태그하면 **이 검사가 먼저 깨지며 새 명단을 출력**하고, 그게 곧 주석·`docs/MATERIALS.md`·`MATERIAL-SYSTEMS.md`를 같이 갱신하라는 알림이다.

· `npm run test:radiation` — 방사능 전 물질이 선량을, 생명 전 물질이 사체나 자체 모델을 선언했는지 **커버리지**로 훑는다(예외 3종만 화이트리스트).

· `spark.ts` — 도체 명단과 손실표의 길이가 다르면 **로드 시 throw**하고, `wiring` 태그와 손실 0의 정합성까지 검사한다.

**따라서 위 T-1~T-4 어느 것을 하든, 태그를 만드는 작업과 그 명단을 훑는 검사를 만드는 작업은 한 세트다.** 검사 없는 태그화는 이 저장소에서 채택하지 않는다.


---

## 오브젝트 레이어

자유 강체 5종(고무공 · 드럼통 · 다이너마이트 · 연막탄 · 나무 상자)이 CA 스캔이 **끝난 뒤** 완전히 분리된 패스로 돈다.

위험:  최우선 부채 — 오브젝트는 세이브 어디에도 직렬화되지 않는다
`src/state/persistence.ts`에 `objects` 문자열이 **0건**이고, 스냅샷은 로드 시 오히려 `objects.length = 0`으로 **지운다**.

결과: 드럼통·나무 상자로 지은 구조물은 **새로고침 한 번에 사라지고, 스냅샷으로 공유할 수도 없다.** 오브젝트를 늘릴수록 "세이브 안 되는 장난감"이 하나씩 늘어난다.

**공백** — 저장 자체가 없다. **재사용** — `SimBody`는 평범한 POJO 유니온이라 JSON 직렬화 비용이 낮고, `.psbx.json` 스냅샷 파일 포맷이 이미 있다. **비용·리스크** — 작다. 리스크는 스냅샷 리샘플(축소) 시 오브젝트 좌표를 어떻게 스케일할 것인가와, 구버전 세이브 호환(필드 없으면 빈 배열)뿐이다.

[순위 1] **새 오브젝트를 하나라도 더 넣기 전에 이것부터.**


### 그 외 구조적 제약


| ID | 제안 | 지금 비어 있는 것 | 재사용 인프라 | 비용·리스크 |
|---|---|---|---|---|
| **O-1** | **세이브 직렬화** | 위 콜아웃 | `.psbx.json` 포맷, POJO 유니온 | 작음 [최우선] |
| **O-2** | 그리드 → 오브젝트 방향 개통 | **CA도 `SimContext`도 오브젝트 목록을 절대 읽지 않는다.** 그래서 모래는 고무공을 그대로 **통과해 떨어지고**, 물은 상자에 막혀 고이지 않으며, 셀 물질이 오브젝트를 밀어내지 못한다 | 오브젝트 쪽 `deepestContact`가 이미 원-셀사각형 최근접점과 법선을 계산한다 — 반대 방향에서 같은 판정을 재사용 | **중간~큼.** 순환 import 위험이 실재한다(`objects.ts → spark.ts → woofer.ts` 사슬이 이미 있음) → **`SimContext`에 중립 지대 필드/큐를 두는 기존 패턴**을 따라야 한다. 성능은 "오브젝트가 점유한 타일만" 검사로 묶어야 함 |
| **O-3** | 액체 고임(부분 개통) | O-2의 축소판. **연막탄의 `displaceLiquidUp`이 이미 액체를 밀어 올리되 삭제하지 않는다** — 그 동작을 "떠 있는 바디 아래 액체가 못 지나감"으로 확장 | 같은 함수 | **작음.** 총량 보존은 이미 `npm run test:smokebomb`이 지킨다(물 총량 완전 보존을 검사) — 같은 하네스로 방어 가능 |
| **O-4** | 다각형 충돌 | 형상이 **원과 캡슐(stadium) 둘뿐**. SAT도 관성텐서도 없다. 나무 상자조차 `halfLength = 0`인 캡슐(=디스크)로 처리된다 | — | **큼.** 새 수학이고, 접촉 토크·부유율·매질 샘플링이 전부 원/캡슐 가정 위에 있다. [보류] 재미 증분이 비용에 안 맞는다 |
| **O-5** | 바디 간 조인트/구속 | 충돌만 있고 **구속이 없다**. 로프·경첩·축·피스톤 어느 것도 불가 | `resolvePair`의 임펄스 계산 | **큼.** 구속 솔버는 새 수학. [보류] 다만 **"두 바디를 고정 거리로 묶는 막대" 하나만** 이라면 축소 채택 여지가 있다(반복 없는 단일 제약) |
| **O-6** | 자성 대상 태그화 | `isMagneticBody`가 `kind` 하드코딩 분기. 새 강철 바디를 넣을 때마다 여기를 손대야 한다 | 셀 쪽 `Material.magnetic` 태그와 같은 문법 | **매우 작음.** `npm run test:magnetobjects`가 이미 "강철 바디만 끌리고 고무공·나무 상자·다이너마이트는 안 끌린다"를 검사하므로 **회귀 방어가 공짜** |
| **O-7** | 광선 상호작용 개통 | Nuclear Ray가 오브젝트를 **모른다** — 빔이 소모되지도 반사되지도 않고 그냥 통과한다(비대칭 상호작용으로 문서화됨) | `footprintHazards`가 이미 Blast/Nuclear Ray/Antimatter 직격을 포착한다 — 역방향만 없다 | **작음.** 리스크: 빔이 오브젝트에 막히면 기존 빌드의 사거리가 바뀐다 |

---

## 전기 계통

### 현재 모델 — "세기 = 남은 도달 거리"

전압·전류·저항이 **없다**. Spark는 1틱 수명 파티클이고, `aux` 하위 8비트에 도체 클래스, 상위 8비트에 세기를 싣는다. 세기는 매질별 손실표를 따라 깎이므로 **"세기"의 실제 의미는 남은 도달 거리**다 — 금속은 무한, 염수·산은 60여 칸, 물은 30여 칸.

그 위에 두 규약이 있다.


전기 세기 무관
펄스 강도는 "얼마나 멀리 가나"만 결정한다. **장치 쪽에서는 읽히지 않는다** — 약한 펄스도 장치를 완전히 켠다.

연결 부위 전역 즉시 활성화
`floodDeviceBody`가 4연결 몸체 **전체**를 그 틱에 한 번에 덮는다. 셀 상한 없음(옛 256 상한이 큰 빌드를 반쪽만 켜던 버그의 교훈).



이 두 규약 덕분에 **새 장치 추가가 이례적으로 싸다 — 3단계**다. ① `conductive`를 **안** 달고 ② `directPulse` 훅을 등록하고 ③ `floodDeviceBody`를 호출하며 `SimContext`에 몸체 플러드 필드를 하나 더한다. **발생원 쪽은 한 줄도 안 고친다.** 모든 펄스 소스(배터리 2종·터빈·솔라·와이어 릴레이·스파크 브러시)가 단 하나의 디스패치 지점을 공유하기 때문이다.

### 완전 공백 — 논리 · 센서 · 타이머

grep으로 확정한 부재다: 논리 게이트 0 · 센서/검출기 0 · 인월드 스위치/레버/압력판 0 · 타이머/플립플롭/카운터 0. **유일한 타이머는 다이너마이트 도화선이고 그건 오브젝트 전용이다.**

즉 전기 계통은 **"전원 → 배선 → 장치" 단방향뿐이며, 조건도 시간도 판단도 전혀 없다.** 플레이어가 만들 수 있는 것은 "버튼을 누르면 켜지는 것"까지고, **켜고 끄는 판단은 전부 사람이 브러시로 해야 한다.**

C-1 · C-2 · C-3 — 센서 / 인버터 / 딜레이 3종
**공백** — 위 문단 전체. 조건·부정·시간이라는 **논리의 최소 3원소**가 하나도 없다.

**재사용** — 이례적으로 높다.

· `directPulse` 훅 + `floodDeviceBody`가 **이미 장치 공용 규약**이고 새 장치는 3단계면 끝난다.

· `aux` 카운트다운을 쓰는 장치가 **이미 넷**이다(팬·레이저·펌프·전자석의 통전 카운트다운) — **딜레이는 그 카운트다운을 출력으로 되돌리는 것**뿐이다.

· 도체 클래스 슬롯은 8비트 = 255칸 중 12칸만 쓴다. **더 이상 병목이 아니다.**

· 센서의 판정 대상도 전부 이미 있다 — `getTemp`(온도), `get`(물질), `getWind`/`magnetFields`(필드).

**비용·리스크** — 장치당 물질 파일 하나 수준. 리스크는 **인버터의 자기발진**이다(무전원일 때 펄스를 내면 자기 출력이 자기 입력으로 돌아올 수 있다) → 불응 기간(`REFRACTORY_TICKS`)이 이미 역류를 막는 장치로 존재하므로 그 위에 얹으면 된다.

[①재미 최상급]


이 셋만 있으면 **유저 빌드로 성립하는 것**:


1. 자동 원자로 제어
온도 센서가 임계 온도를 감지 → 펌프·냉각재 밸브를 켠다. 지금은 우라늄이 임계에 들어가는 것을 사람이 보고 있다가 손으로 물을 부어야 한다.

2. 자동 소화 시스템
센서가 화재 온도를 감지 → 스프링클러(펌프+물)를 켠다. 인버터가 있으면 "불이 꺼지면 물도 잠근다"까지.

3. 순차 폭파
딜레이 체인으로 다이너마이트를 N틱 간격으로 터뜨린다. 지금 다이너마이트 도화선은 **랜덤 3~5초**라 순서 제어가 원리적으로 불가능하다.

4. 발진기 · 경보
인버터 자기 루프가 곧 클록이다. 클록 하나가 생기면 컨베이어 타이밍·주기적 방출·점멸 신호가 전부 따라온다.



### 더 나아간 후보

| ID | 장치 | 근거 요약 |
|---|---|---|
| **C-4** | **물질 검출기** | **공백** — 인월드 조건이 온도조차 없다. **재사용** — 센서(C-1)의 판정 대상만 `getTemp`에서 `get`으로 바꾼 것. **비용** — C-1이 되면 사실상 공짜 |
| **C-5** | **래치 / 토글** | **공백** — 상태 유지가 없어 모든 것이 순간이다. **재사용** — `aux` 하위 비트 1개면 충분(카운트다운 장치들이 이미 하는 것). **비용** — 작음. 리스크는 세이브 왕복(상위 비트부터 쓰는 기존 규율 준수) |
| **C-6** | **펄스 분주기 / 카운터** | **공백** — "N번째마다"가 없다. **재사용** — 배터리가 이미 주기 박자를 갖는다(`PULSE_PERIOD`). **비용** — 작음 |
| **C-7** | **AND / OR — 인접 배선으로** | **공백** — 게이트 0건. **재사용** — 전용 물질을 만들지 않고, **딜레이+인버터 조합으로 유저가 직접 짜게 하는 것**도 선택지다(NOR만 있으면 완비성). **비용·리스크** — 물질을 안 늘리는 대신 빌드 난이도가 올라간다. [①재미] 축에서는 **전용 게이트 물질 쪽이 낫다** |
| **C-8** | **압력판 / 접촉 스위치** | **공백** — 인월드 입력이 브러시뿐이다. **재사용** — 오브젝트 접촉 판정이 이미 있다(`deepestContact`). 다만 그리드→오브젝트가 단방향이라 **O-2에 의존**한다. **비용** — O-2 선행 필요 |
| **C-9** | **전압·저항 모델화** | **공백** — 없음. **재사용** — 없음(현 모델을 갈아엎는다). **비용·리스크** — 크고, **재미가 오히려 준다**. 배선 길이 계산이 놀이를 방해한다. [기각] — 현 "세기 = 도달 거리" 모델은 ①재미 기준에서 **올바른 단순화**다 |

---

## 엔진 차원의 기능 제안

물리량이나 반응이 아니라 **엔진의 뼈대 기능** 쪽 후보다.


| ID | 제안 | 지금 비어 있는 것 | 재사용 인프라 | 비용·리스크 |
|---|---|---|---|---|
| **G-1** | **반경 질의 / 연결 덩어리 질의 공용화** | `SimContext`의 공개 원시연산에 `neighborsWithin(r)`도 연결 덩어리 질의도 **없다**. 그래서 폭발의 `surveyMass`, 장치의 `floodDeviceBody`, 자석의 측지 스윕, 방사선의 측지 플러드가 **각자 자기 플러드를 따로 갖고 있다** — 네 벌이다 | 넷 중 방사선 플러드가 가장 깔끔하다(고정 사거리 + 로컬 visited 버퍼). 그걸 원형으로 삼는다 | **중간.** 넷을 억지로 하나로 합치면 각자의 최적화(자석의 "어디서 도달했는지" 기억, 폭발의 틱당 예산)를 잃는다 → **기존 넷은 두고 새 기능이 쓸 공용 계층을 하나 추가**하는 것이 옳다. 범위 스캔은 반드시 상한을 걸고, **초과는 에러가 아니라 조용한 undercount**로 설계 |
| **G-2** | **시드 RNG로 교체** | `SimContext.chance`/`randInt`가 이미 유일한 seam이지만 내부는 시드가 없다. 벤치 장면만 고정 PRNG를 쓴다 | **seam이 이미 존재한다는 것이 전부다** — 내부를 xorshift/PCG로 갈아도 **물질 코드는 한 줄도 안 바뀐다** | **작음.** 얻는 것이 크다: **재현 가능한 버그 리포트**(시드+입력으로 장면 재생), **리플레이**, 그리고 멀티플레이 lockstep의 전제. 활성 타일 테스트가 이미 "같은 시드 재실행 결정론"을 검증하고 있어 하네스도 준비돼 있다. 리스크: 모듈 레벨 스크래치를 sim-local로 유지하는 기존 규율을 깨면 두 시뮬레이션이 서로의 롤을 억누른다 |
| **G-3** | **중력 임의 각도** | 방향이 **상하좌우 4방향뿐**이다. 그리고 세기는 가속도가 아니라 확률이라 "비스듬한 중력"이 표현될 자리가 아예 없다 | 모든 이동 프리미티브가 이미 `gravityX/Y` + `perpX/Y` 벡터를 통과한다 — **단위벡터를 대각으로 주는 것 자체는 이미 표현 가능한 형태다** | **중간.** 진짜 비용은 정수 격자에서 45°가 아닌 각도가 **계단 아티팩트**를 만든다는 것. 축소 채택: **8방향(대각 4개 추가)** 이면 벡터가 여전히 격자 정합적이라 아티팩트가 없고 재미는 대부분 얻는다. [축소채택 권장] |
| **G-4** | **대각 열전도** | `diffuseHeat`가 **4방향 직교만** 교환한다. 대각은 직접 전도가 아예 없고, 500°↑ 셀의 복사 보조 패스가 그 구멍을 부분적으로 메우고 있을 뿐이다 → **500° 미만 물체의 대각 열전달은 0** | 확산 커널에 대각 항을 `1/√2` 가중으로 추가 | **중간.** 열 골든 테스트가 전부 깨지고, WASM 커널도 같이 고쳐야 한다. 그리고 **틱당 열 비용이 늘어난다**(4방향 → 8방향). 이득: 대각선 벽이 단열재처럼 굴던 부자연스러움이 사라짐. [③고증] 축이 주된 이유라 순위는 중간 |
| **G-5** | **겹침 슬롯 2개 이상** | 셀당 겹침 유체가 **정확히 1개**다. 젖은 모래에 기름을 부으면 하나가 밀려난다. 게다가 포화도가 아니라 **0/1**이라 "반쯤 젖음"이 없다 | `overlay` + `overlayAux` 평면이 이미 세이브 왕복까지 갖췄다 — 슬롯을 늘리는 것은 같은 평면을 한 벌 더 만드는 것 | **중간.** 셀당 3바이트 추가 + **세이브 평면 2개 추가**(구버전 호환은 "없으면 0"). 리스크: 겹침 판정 코드가 "슬롯 1개" 가정 위에 있어 광범위하게 손댄다. 대안: 슬롯을 늘리는 대신 **`overlayAux` 일부 비트를 포화도로 승격**하면 "반쯤 젖음"만 싸게 얻는다 [대안 권장] |
| **G-6** | **오브젝트 힘 수신의 일반화** | 새 전역 필드를 만들면 오브젝트는 **자동으로 못 받는다** — `stepObjects` 앞단에 수신 코드를 한 줄 넣어야 한다(지금 폭발·우퍼·바람·자석 넷이 각자) | 넷의 형태가 이미 동일하다(필드 읽기 → 임펄스) | **작음.** "필드 등록 → 오브젝트 자동 수신"으로 묶으면 새 힘의 비용이 절반이 된다 |
| **G-7** | **크로스 레이어 이벤트 큐 일반화** | 순환 import가 **실제로 발생**해서(`objects.ts → spark.ts → woofer.ts` 사슬 때문에 역방향이 불가능) 우퍼 넉백이 `SimContext`의 중립 지대 큐로 우회하고 있다. 이게 **일회성 우회로 남아 있다** | 그 우회 자체가 이미 검증된 패턴이다 | **작음.** 이름 있는 계층으로 승격하고 문서화하면, 앞으로의 모든 크로스 레이어 상호작용(O-2 포함)이 같은 길을 쓴다 |
| **G-8** | **`update` 반환값 규약** | `update`가 `void`라 "나는 처리했다/안 했다"를 엔진에 말할 수 없다. P-1(선언형 상변화)이 `update` 뒤에 안전하게 붙으려면 이게 필요하다 | — | **작음이지만 파급이 넓다.** 138종 시그니처가 바뀌는 것은 아니고(`void`는 `boolean`과 호환되게 옵셔널로), **P-1의 전제 조건**으로만 정당화된다 |
| **G-9** | **브러시가 반응을 트리거하는 경로** | 스파크 브러시가 모든 전원과 같은 규칙을 쓰는 것처럼, **가열·냉각 브러시도 상변화를 즉시 트리거하지 않는다**(다음 틱의 물질 `update`를 기다린다). 미세한 것이지만 "칠했는데 아무 일도 안 나는" 체감이 있다 | 스파크 브러시의 "발자국 전 셀에 `pulseCell`" 패턴 | **작음.** 리스크: 브러시가 시뮬레이션 규칙을 앞지르면 스캔 순서 규율이 흔들린다 |

---

## 알려진 미수정 사항

정보:  코드에는 백로그가 없다
`src/`, `wasm/`, `scripts/` 전체에 **`TODO`/`FIXME`/`HACK`/`XXX:` 주석이 0건**이다(실측). 대신 **"의도적으로 안 고친 것"이 산문 주석으로 박혀 있고, 그게 사실상의 개선 후보 목록이다.**

문서 쪽 백로그는 정확히 넷뿐이다 — `docs/MATERIAL-IDEAS.md`(엔진 한계 3건), `docs/PERFORMANCE.md`(활성 타일 2단계), `docs/WASM-ENGINE-PORTING.md`(값싼 JS 최적화 4건 + 열린 결정 3건), `docs/MATERIAL-ICONS.md`(남은 손그림 발주). **UI/UX 쪽 백로그는 저장소 어디에도 없다.**

따라서 이 문서를 포함한 구상 문서들은 **백로그의 다섯 번째 자리를 만드는 일**이기도 하다. 구현되면 여기서 빼고 `docs/`의 해당 분야 문서로 옮기는 수명 규칙을 승계한다.


산문 주석으로 남은 의도적 미수정 목록:

| 위치 | 미수정 사항 | 왜 안 고쳤나 | 제안 |
|---|---|---|---|
| `src/game/engine/behaviors.ts` | **무거운 액체 사이에 낀 가벼운 액체의 영구 톱니 층리.** 물 밑의 휘발유·등유·경유가 웅덩이 한복판에서 톱니 모양으로 굳는다. 가루 쪽 `moveSidewaysBuoyant`가 고친 것과 **정확히 같은 버그**인데 액체에서만 안 고쳤다 | 액체-액체 수정은 두 액체의 **실제 밀도 비교**가 필요하고(가루 쪽은 "뜨는 가루는 항상 더 가볍다"는 전제를 썼다), **플레이어가 본 사례가 없어서** | [채택 권장] 석유 계열이 늘어난 지금은 사례가 나올 확률이 올라갔다. 가루 쪽 수정이 선례이므로 **설계는 이미 있다** |
| `src/game/engine/deviceBody.ts` | 도체 비지정 결정 — 장치 몸체가 도체가 아닌 것 | 도체로 만들면 몸체 전체가 스파크를 릴레이해 예측 불가 | [유지] 현 규약이 옳다 |
| `src/game/engine/objects.ts` | Void가 오브젝트를 **부산물 없이** 삼키는 예외 | 무한 싱크의 의미론 일관성 | [유지] |
| `src/game/engine/objects.ts` | 오브젝트의 겹침 슬롯 회피 | 겹침 계층과 강체 계층의 접점을 열지 않기로 | [재검토] O-2/O-3와 함께 다시 봐야 함 |
| `src/game/engine/types.ts` | **Acid Slime이 무손실 도체인데 `wiring` 태그가 아니다** — 로스터 유일의 비금속 무손실 도체 | 배선재 판정("소스가 배선만 먹인다")에 슬라임이 끼면 이상하다 | [재검토] 무손실인데 배선이 아닌 상태가 **`spark.ts`의 정합성 검사와 긴장 관계**다 |
| `src/game/engine/types.ts` | **철·녹을 일부러 `magnetic`으로 태그하지 않음** | 고체 구조물이 자석에 끌리면 빌드가 무너진다 | [유지] ①재미 기준의 옳은 판단. T-4는 **오브젝트 쪽 하드코딩만** 고치는 것 |
| `src/game/engine/Grid.ts` | `wind`/`shockwaves`/`magnetFields`/`objects`는 **리사이즈 시 보존되지 않는다** | 절대 좌표라 재매핑이 어긋난다 | [부분채택] O-1(세이브)을 하면 **오브젝트만은 리사이즈 보존도 같이 풀어야** 일관된다 |
| `src/game/engine/Simulation.ts` | `radiateHeat`가 더블버퍼 없이 `temp`를 in-place로 바꾼다 → **틱 내 순서 의존성이 소량 존재** | 의도적 트레이드(버퍼 한 벌 아낌) | [유지] 다만 G-4(대각 전도)를 하면 이 결정을 다시 재야 한다 |

---

## 검증 인프라

새 엔진 기능을 제안할 때는 **어느 하네스가 그것을 지켜 주는지, 아니면 새 하네스가 필요한지**를 항목마다 명시해야 한다. 이 저장소의 확립된 관행이다.

### 지금의 구조

`npm test`가 **14개 스크립트를 직렬**로 돈다: `test:heat` · `active-tiles` · `electricity` · `snapshot-file` · `woodbox` · `fireworks` · `aluminum` · `plantcoral` · `radiation` · `plastic` · `magnetobjects` · `smokebomb` · `materialicons` · `acidmetal`.

각 러너(`test/run-*.mjs`)는 동일 패턴이다 — **esbuild로 TS 하네스를 Node용 ESM 번들로 굽고**, `.wasm?url` import를 문자열 스텁으로 갈아 끼운 뒤 동적 `import()`한다.


프레임워크가 없다
`check(name, ok, detail)` 자작 헬퍼와 종료코드가 전부다. **새 검사를 붙이는 비용이 거의 0**이고, 러너 하나를 복붙하면 새 계통 하네스가 생긴다. 이게 이 저장소가 검사를 많이 갖게 된 이유다.

열 커널은 빌드 산출물 의존
`test:heat`만 `wasm/` Rust 빌드 산출물을 요구한다. 그게 없는 환경에서는 `npm test`가 **첫 줄에서 죽는다** — 개별 스크립트로 돌려야 한다.

esbuild 단일 실패점
**`test/run-*.mjs` 러너 13개 전부가 `esbuild`를 import하는데 `package.json`에 선언돼 있지 않다**(열 골든 검사 `test:heat`만 예외로, 그쪽은 `wasm/test/*.mjs`라 esbuild를 쓰지 않는다). astro/vite의 전이 의존으로만 존재해 지금은 우연히 동작한다. 상위 의존성이 esbuild를 벗기거나 메이저를 올리면 **엔진 검증 전체가 한꺼번에 죽는다.**



V-1. `esbuild`를 devDependency로 명시 선언
**공백** — 검증 하네스 전체가 선언되지 않은 전이 의존 위에 서 있다. 엔진 개선·최적화는 "거동 불변 증명"이 작업의 절반인데, 그 증명 수단이 통째로 우연에 걸려 있다.

**재사용** — 없음. `package.json` 한 줄이다.

**비용·리스크** — 거의 0. **이 문서의 모든 제안 중 가장 싸고, 안 하면 나머지 전부의 전제가 무너진다.**

[선행 작업]


### 새 기능별 방어 하네스 배정

| 제안 | 방어 방식 | 새 하네스 필요? |
|---|---|---|
| **R-1~R-3** (반응 표현력) | 기존 `test:aluminum`(활성 알루미늄·수소 라인)·`test:acidmetal`이 이미 이 반응들을 검사한다. 새 필드는 **미선언 시 현행 비트 동일**이어야 하므로 기존 골든이 그대로 방어 | 새 필드 자체의 검사만 추가 |
| **R-4** (`withTag`) | **레지스트리를 훑는 명단 검사**가 필수 — `test:acidmetal`의 태그 명단 비교가 정본 형식 | 필요 |
| **P-1** (상변화 데이터화) | 이주한 물질마다 "이 온도에서 이것이 된다"를 재는 검사. **히스테리시스 깜빡임 없음**을 반드시 포함 | 필요 (`test:phasechange`) |
| **T-1~T-4** (태그 일반화) | 레지스트리 커버리지 검사. `test:radiation`의 커버리지 절이 정본 형식 | 각 계통 하네스에 절 추가 |
| **O-1** (오브젝트 세이브) | `test:snapshot-file`이 이미 세이브 왕복을 검사한다 — **오브젝트 왕복 절을 추가**하면 된다 | 기존 확장 |
| **O-2/O-3** (그리드↔오브젝트) | `test:smokebomb`이 이미 "액체 총량 완전 보존"을 검사한다. 같은 불변식이 O-3의 핵심 방어선 | 기존 확장 + 신규 |
| **O-6** (자성 태그화) | `test:magnetobjects`가 이미 전부 검사한다 | 불필요 [공짜] |
| **C-1~C-8** (전기 장치) | `test:electricity`가 **"`directPulse`를 가진 전기 카테고리 물질 전부가 하네스에 등록됐는지" 먼저 확인**한다 → 새 장치는 **자동으로 걸린다** | 장치별 절 추가 |
| **G-2** (시드 RNG) | `test:active-tiles`가 이미 **같은 시드 재실행 결정론**을 검증한다 | 기존 확장 |
| **G-4** (대각 열전도) | 열 골든이 전부 깨진다 → **새 골든을 굽는 것 자체가 작업** | 골든 재생성 |
| **G-1** (질의 공용화) | 폭발·자석·방사선 하네스가 각각 거동을 이미 지킨다 → **공용화 전후 비트 동일**이 증명 목표 | 불필요(기존이 골든) |

팁:
이 저장소에서 가장 강한 검증 형태는 셋이다. ① **비트 동일 골든**(활성 타일이 전면 스캔과 매 틱 일치하는지 비교하는 방식이 원형) ② **레지스트리 전수 커버리지**(방사선) ③ **명단 비교 후 새 명단 출력**(산+금속). 새 엔진 기능은 이 셋 중 하나에 매핑되어야 한다.


---

## 우선순위 정리


| 순위 | ID | 제안 | 주 근거 축 | 비용 |
|---|---|---|---|---|
| 0 | **V-1** | `esbuild` devDependency 명시 | ②편의성 (전제 조건) | 거의 0 |
| 1 | **O-1** | **오브젝트 세이브 직렬화** | ②편의성 (부채 상환) | 작음 |
| 2 | **C-1·C-2·C-3** | **온도 센서 / 인버터 / 딜레이** | ①재미 최상급 | 작음 |
| 3 | **R-1** | `catalystRequired` 촉매 필수 게이트 | ①재미 ③고증 | 매우 작음 |
| 4 | **R-2** | `acidProduct` 산물 데이터화 | ①재미 ③고증 | 작음 |
| 5 | **R-3** | 잠긴 셀 부산물 (액체 밀어내기 스폰) | ①재미 | 작음 |
| 6 | **G-2** | 시드 RNG 교체 | ②편의성 (+ 리플레이·lockstep 전제) | 작음 |
| 7 | **P-1** | `melt`/`boil`/`sublime` 선언형 상변화 | ②편의성 ③고증 | 중간 |
| 8 | **G-A** | `heatCapacity` 열용량 | ①재미 ③고증 | 중간 |
| 9 | **T-1** | 염소 생명 명단 태그화 | ②편의성 (조용한 실패 제거) | 작음 |
| 10 | **O-6** | 오브젝트 자성 태그화 | ②편의성 | 매우 작음 |
| 11 | **G-B** | 성장 프론트 공용 헬퍼 | ①재미 ②편의성 | 작음 |
| 12 | **O-3** | 액체 고임 (그리드↔오브젝트 부분 개통) | ①재미 | 작음 |
| 13 | **G-1** | 반경/연결 덩어리 질의 공용화 | ②편의성 (다른 제안의 기반) | 중간 |
| 14 | **R-5·R-7·R-10** | 이웃 온도 조건 · 셀당 확률 · 부산물 위치 | ③고증 ②편의성 | 작음 |
| 15 | **G-3** | 중력 8방향 (임의 각도의 축소판) | ①재미 | 중간 |
| 16 | **T-2·T-3·T-4** | `durability` 선언 · `flammable` 정리 · `magnetic` 정합 | ②편의성 | 작음 |
| 17 | **G-C** | 밀폐 판정 (압력의 축소판) | ①재미 | 중간 |
| 18 | **C-4~C-8** | 검출기 · 래치 · 분주기 · 게이트 · 압력판 | ①재미 | 작음~중간 |
| 19 | **O-2** | 그리드→오브젝트 완전 개통 | ①재미 | 중간~큼 |
| 20 | **G-5** | 겹침 포화도 (슬롯 증설의 대안) | ③고증 | 중간 |
| 21 | **R-4·R-6·R-9** | 태그 상대 반응 · 소모비 · 3체 레시피 | ②편의성 ③고증 | 중간 |
| 22 | **G-4** | 대각 열전도 | ③고증 | 중간 |
| — | **O-4·O-5** | 다각형 충돌 · 바디 조인트 | ①재미 | 큼 [보류] |
| — | 압력장 · 속도장 · 농도장 | 전면 필드 3종 | ①재미 | 매우 큼 [보류] |
| — | **C-9** | 전압·저항 모델화 | ③고증 | 큼 [기각 — 재미가 준다] |
| — | 습도 · 소리 전파 | | ③고증 | [기각] |

성공:  첫 세 걸음
**V-1 → O-1 → C-1·C-2·C-3.** 하나는 전제 조건이고, 하나는 부채 상환이며, 하나는 이 문서에서 재미 대비 비용이 가장 좋은 항목이다. 셋 다 새 물리량을 하나도 추가하지 않고, 셋 다 기존 규약(`directPulse` 3단계 · POJO 직렬화 · 자작 `check()` 하네스)만 재사용한다.


---

## 관련 문서

- [[Web sandbox 프로젝트/구상]] — 구상 인덱스(상위)
- [[Web sandbox 프로젝트/구상/새 물질 후보]] — 이 문서의 엔진 기능이 열어 주는 물질들
- [[Web sandbox 프로젝트/구상/새 화학반응 공정 후보]] — §반응 표현력 개선의 소비자
- [[Web sandbox 프로젝트/구상/엔진 최적화 방안]] — 성능·비용 축(이 문서에서 의도적으로 제외한 영역)
- [[Web sandbox 프로젝트/구상/UI UX 개선안]] — 브러시·도구·접근성 축
- [[Web sandbox 프로젝트/구상/시작 화면과 도감 페이지]] — 페이지·온보딩 축
- [[Web sandbox 프로젝트]] — 프로젝트 소개문서
- [[Web sandbox 프로젝트/가이드/물질]] — 물질 도감 가이드