# AI 꾸러미 — 연쇄 번개 (튀는 번개) — Chain lightning
> 번개를 가장 가까운 대상 순서로 이어 튀게 하고 중간점을 흔드는 지그재그로 그려서, 맞을 때마다 번쩍 · 불티 · 바닥 빛이 따라오는 연쇄 번개를 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i184

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 연쇄 번개 (튀는 번개) — Chain lightning

#### 1. 목표
가까운 적 여럿 사이를 튀어 다니는 연쇄 번개를 만들어 줘 — 가장 가까운 대상 차례로, 지그재그 · 갈래, 맞을 때마다 번쩍 · 불티. 분위기는 푸른 전기 마법.

#### 2. 핵심 기술 용어
- **Chain lightning** — 연쇄 번개 — 대상에서 대상으로 튀는 번개
- **Midpoint displacement** — 중간점 흔들기 — 선을 반씩 나누며 비틀어 지그재그
- **Greedy nearest-neighbor chain** — 가장 가까운 다음 대상을 차례로 고르기
- **Camera-facing ribbon segments** — 화면을 보는 캡슐 띠 (가운데 하얀 심 + 색 빛)

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표

#### 4. 조건
- 튀는 순서는 가장 가까운 다음 대상(욕심쟁이) — 화면에서 지그재그로 왔다 갔다 하지 않게
- 번개는 중간점 흔들기(수직 방향, 단계마다 폭 0.55배)로, 0.045초마다 다시 만들어 지직거리게
- 선분은 굵은 옅은 빛 + 가는 밝은 심 두 겹, 더하기 섞기
- 맞을 때 번쩍 · 불티 · 점광 · 바닥 빛 · 작은 흔들림을 함께, 점광은 하나를 돌려 쓰기
- 튀는 횟수(1~6) · 갈래 켬/끔 · 세기 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 지팡이에서 나온 번개가 가장 가까운 적부터 차례로 튀고, 맞은 적마다 푸르게 번쩍이며 바닥이 빛난다
- 번개 선이 지직거리며 모양이 계속 바뀌고, 중간에 짧은 갈래가 뻗는다
- 「튀는 횟수」를 바꾸면 맞는 적 수가 바뀐다
- 폰에서도 60fps

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 순서 정하기: 지팡이 끝에서 가장 가까운 적 → 그 적에서 남은 것 중 가장 가까운 적 … (욕심쟁이 잇기).
- 번개 모양: 시작 · 끝 두 점에서 출발해 각 선분의 가운데를 진행 방향에 수직으로 흔든다. 5번 나누면 33점, 흔드는 폭은 길이 × 0.3 에서 단계마다 0.55배.
- 같은 모양을 0.045초마다 새 씨앗으로 다시 만들어 지직거린다. 선분마다 굵은 옅은 빛(3.2배, 0.22) + 가는 밝은 심 두 겹.
- i 번째 튐은 0.35 + 0.11i 초에 시작해 0.62초에 사라진다. 갈래 둘은 중간쯤 점에서 비스듬히 짧게(3번 나눔).
- 맞는 순간: 불티 18개 · 큰 번쩍 · 점광 한 번 · 적 색 물들이기 · 바닥 빛 1 → e^(−2.2t) · 화면 흔들림 0.05.

## 핵심 코드 — 중간점 흔들기 번개 + 욕심쟁이 순서
(발췌: demos/demosSkillB.ts boltPts() · drawBolt() · i184 make() 를 정리)
```ts
type V3 = [number, number, number];
// 선분 가운데를 진행 방향에 수직으로 흔들며 반씩 나누기
function boltPts(a: V3, b: V3, rnd: () => number, depth = 5, jag = 0.32): V3[] {
  let pts: V3[] = [a, b];
  let off = len3(sub3(b, a)) * jag;
  for (let d = 0; d < depth; d++) {
    const np: V3[] = [pts[0]];
    for (let i = 0; i < pts.length - 1; i++) {
      const p = pts[i], q = pts[i + 1];
      const dir = norm3(sub3(q, p));
      let r: V3 = [rnd() - 0.5, (rnd() - 0.5) * 0.8, rnd() - 0.5];
      r = sub3(r, sc3(dir, r[0] * dir[0] + r[1] * dir[1] + r[2] * dir[2])); // 수직 성분만
      np.push(add3(sc3(add3(p, q), 0.5), sc3(r, off)), q);
    }
    pts = np;
    off *= 0.55; // 잘게 나눌수록 작게 흔든다
  }
  return pts;
}
// 굵은 옅은 빛 + 가는 밝은 심 (streaks.seg = 화면을 보는 캡슐 띠 한 칸)
function drawBolt(pts: V3[], w: number, c: V3, a: number): void {
  for (let i = 0; i < pts.length - 1; i++) {
    streaks.seg(pts[i], pts[i + 1], w * 3.2, c, a * 0.22);
    streaks.seg(pts[i], pts[i + 1], w, sc3(c, 1.8), a);
  }
}
// 순서: 지팡이에서 가장 가까운 적 → 그다음 가까운 적 …
const order: number[] = [];
let cur = tip;
const left = new Set(mons.map((_, i) => i));
while (left.size) {
  let best = -1, bd = 1e9;
  for (const i of left) { const d = len3(sub3(mons[i].center(), cur)); if (d < bd) { bd = d; best = i; } }
  order.push(best); left.delete(best); cur = mons[best].center();
}
// 매 프레임: i 번째 튐은 0.35 + 0.11i 초부터 0.62초 — 씨앗을 0.045초마다 바꿔 지직
// const r = rng(Math.floor(lt / 0.045) * 31 + i * 977); drawBolt(boltPts(a, b, r, 5, 0.3), 0.045, BC, fade);
```

## 흔한 실수 · 확인 목록
- [ ] **흔들기를 무작위 방향으로 하면 번개가 앞뒤로 접혀 엉킨다** — 흔드는 벡터에서 진행 방향 성분을 빼 수직으로만 흔든다.
- [ ] **매 프레임 새 모양이면 너무 빨리 깜박여 눈이 아프다** — 씨앗을 0.045초 단위로만 바꾼다(그 사이엔 같은 모양).
- [ ] **순서를 배열 순서대로 하면 번개가 화면을 가로질러 왔다 갔다 한다** — 현재 자리에서 가장 가까운 남은 대상을 차례로 고른다.
- [ ] **맞을 때마다 점광을 새로 만들면 셰이더가 다시 컴파일된다** — 점광 하나를 미리 두고 자리 · 세기만 바꾼다(Flash.kick).

## 완성 기준 체크리스트
- [ ] 지팡이에서 나온 번개가 가장 가까운 적부터 차례로 튀고, 맞은 적마다 푸르게 번쩍이며 바닥이 빛난다
- [ ] 번개 선이 지직거리며 모양이 계속 바뀌고, 중간에 짧은 갈래가 뻗는다
- [ ] 「튀는 횟수」를 바꾸면 맞는 적 수가 바뀐다
- [ ] 폰에서도 60fps

## 이 기술 정보
- id: `i184` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 번개 선분 수십 개를 인스턴스 띠 하나로 그린다. 점광은 하나를 돌려 쓴다. 폰에서도 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i184
- 쓰면 좋을 때: 여러 대상을 차례로 맞히는 기술 · 연결 보여 주기 / 그래프에서 가까운 점끼리 잇는 과정을 보여 줄 때
- 쓰지 말 때: 대상이 하나뿐 — 그냥 빔(i172)이 낫다 / 번쩍임에 민감한 화면 — 0.045초 깜박임 대신 0.1초 이상으로 늦추고 세기를 낮춘다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i184 견본 항목 — `src/demos/demosSkillB.ts:1117`
```ts
  i184: {
    kind: '3d',
    caption: '번개가 가장 가까운 적에게 차례로 튀며 갈라진다 — 맞을 때마다 번쩍 · 불티 · 바닥 빛',
    make() {
      const A = arena();
      const mg = mage(A, 0x2b3a8a, [1.4, 2.2, 3.5]);
      mg.g.position.set(-3.4, 0, 1.0);
      mg.g.rotation.y = 0.5;
      A.scene.add(mg.g);
      const spots: V3[] = [[-1.5, 0, -0.5], [0.1, 0, 1.1], [1.2, 0, -1.4], [2.8, 0, 0.1], [1.9, 0, 2.0], [-0.3, 0, -2.4]];
      const mons = spots.map((p, i) => {
        const m = monster(A, i % 2 ? 0x5d4a78 : 0x4a5c6e, i);
        m.g.position.set(p[0], 0, p[2]);
        m.g.rotation.y = -0.25;
        A.scene.add(m.g);
        return m;
      });
      A.scene.updateMatrixWorld(true);
      // 사슬 순서: 지팡이에서 가장 가까운 적 → 그다음 가까운 적 (욕심쟁이 잇기)
      const order: number[] = [];
      {
        let cur = mg.tip();
        const left = new Set(mons.map((_, i) => i));
        while (left.size) {
          let best = -1, bd = 1e9;
          for (const i of left) {
            const d = len3(sub3(mons[i]!.center(), cur));
            if (d < bd) { bd = d; best = i; }
          }
          order.push(best);
          left.delete(best);
          cur = mons[best]!.center();
        }
      }
      const glows = mons.map((m) => {
        const d = decal(A.tx.glow(), 2.8, C(0x6ab0ff, 1.6), 0);
        d.position.set(m.g.position.x, 0.02, m.g.position.z);
        A.scene.add(d);
        return d;
      });
      const hitL = new Flash(0x8cc4ff, 60, 7);
      const tipL = new THREE.PointLight(0x8cc4ff, 0, 6, 2);
      A.scene.add(hitL.light, tipL);
      const loop = new Loop(3.2);
      let hops = 4, fork = true, gain = 1;
      const BC = C(0x7ab8ff, 1.6);
      return base3d(A, (t, dt) => {
        const lt = loop.tick(t);
        const tip = mg.tip();
        const charge = sstep(0, 0.35, lt) * (1 - sstep(0.4, 0.9, lt));
        orbGlow(A.fx, tip, BC, 0.18 + charge * 0.3 * gain, 0.6 + charge);
        tipL.position.set(tip[0], tip[1], tip[2]);
        tipL.intensity = (4 + charge * 30) * gain;
        if (charge > 0.2 && Math.random() < 0.5) {
          const r = rng(Math.floor(t * 30));
          const e = add3(tip, sc3(randDir(), 0.5));
          drawBolt(A.st, boltPts(tip, e, r, 3), 0.012, BC, 0.8 * charge);
        }
        for (let i = 0; i < hops; i++) {
          const st = 0.35 + i * 0.11;
          const age = lt - st;
          if (age < 0 || age > 0.62) continue;
          const mi = order[i]!;
          const a = i === 0 ? tip : mons[order[i - 1]!]!.center();
          const b = mons[mi]!.center();
          const fade = 1 - sstep(0.18, 0.62, age);
          const seed = Math.floor(lt / 0.045) * 31 + i * 977;
          const r = rng(seed);
          const pts = boltPts(a, b, r, 5, 0.3);
          drawBolt(A.st, pts, 0.045 * gain, BC, fade);
          if (fork) {
            for (let f = 0; f < 2; f++) {
              const at = pts[4 + Math.floor(r() * (pts.length - 8))]!;
              const dir = norm3(sub3(b, a));
              const end = add3(at, sc3(norm3(add3(dir, [r() - 0.5, r() * 0.4 - 0.3, r() - 0.5].map((x) => x * 2.2) as V3)), 0.5 + r() * 0.6));
              drawBolt(A.st, boltPts(at, end, r, 3, 0.35), 0.022 * gain, BC, fade * 0.8);
            }
          }
          orbGlow(A.fx, b, BC, 0.32 * gain, fade);
          if (loop.hit(st)) {
            sparks(A.st, b, { n: 18, c: C(0xbfe0ff, 3), c2: C(0x3a7bff, 1.2), speed: 6, life: 0.4, grav: 7, up: 0.5 });
            flashAt(A.fx, b, 2.6 * gain, C(0x9cc8ff, 2.2));
            hitL.kick(b, gain);
            mons[mi]!.hit(C(0x6aa8ff, 2.2), 1);
            glows[mi]!.material.opacity = 1;
            A.shake(0.05 * gain);
          }
        }
        for (const g of glows) g.material.opacity *= Math.exp(-dt * 2.2);
        for (const m of mons) m.tick(dt, t);
        hitL.tick(dt);
        A.fx.step(dt);
        A.st.step(dt);
        A.tickCam(dt);
      }, [
        { type: 'button', label: '다시 쏘기', on: () => loop.restart() },
        { type: 'range', label: '튀는 횟수', min: 1, max: 6, step: 1, value: hops, on: (v) => (hops = v) },
        { type: 'toggle', label: '갈래 번개', value: fork, on: (v) => (fork = v) },
        { type: 'range', label: '세기', min: 0.5, max: 2, step: 0.1, value: gain, on: (v) => (gain = v) },
      ]);
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [번개 (중간점 변위)](https://ai-techstudio.web.app/ai/t/i63.md) `i63` · [플라즈마 구 (손 대면 따라오는 빛 줄기)](https://ai-techstudio.web.app/ai/t/i505.md) `i505`
- 먼저 알면 좋은 기술: [층 쌓기 (핵 · 빛무리 · 불티 · 연기)](https://ai-techstudio.web.app/ai/t/i171.md) `i171`
- 다음에 해 볼 기술: [비전 미사일 (유도 탄)](https://ai-techstudio.web.app/ai/t/i185.md) `i185`
