# AI 꾸러미 — 불덩이 (꼬리 · 불티 · 폭발)
> 빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림  
> 견본: https://ai-techstudio.web.app/#t/i182

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

## 주문서

### 만들어 줘: 불덩이 (꼬리 · 불티 · 폭발)

#### 1. 목표
숫자 공격에 「불덩이 (꼬리 · 불티 · 폭발)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림

#### 2. 핵심 기술 용어
- 불덩이 (꼬리 · 불티 · 폭발) — 빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「빛나는 핵 + 흔들리는 불꽃 껍질 + 연기 꼬리 + 날리는 불티 → 명중 폭발 · 충격 고리 · 화면 흔들림」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i182` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i182
- 쓰면 좋을 때: 쓰면 좋을 곳 — 숫자 공격 · 정답 불꽃

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i182 견본 항목 — `src/demos/demosSkillA.ts:2664`
```ts
  i182: {
    kind: '3d',
    caption: '빛나는 핵 + 일렁이는 불꽃 껍질 + 연기 꼬리 + 불티 → 명중 폭발 · 파편 · 충격 고리 · 그을음 · 번쩍',
    make(): Scene3D {
      const S = makeStage();
      const fx = new Fx(S, 2600);
      const lab = S.hud.label('tl');
      // 과녁 돌기둥
      const totem = pillar(1.8);
      totem.position.set(3.1, 0, 0.2);
      S.scene.add(totem);
      const shellMat = new THREE.ShaderMaterial({
        uniforms: { uTime: { value: 0 }, uA: { value: 1 } },
        vertexShader: /* glsl */ `uniform float uTime; varying vec3 vN; varying vec3 vV; varying vec3 vP;
          float h3(vec3 p){ return sin(p.x*4.1 + uTime*7.0) * sin(p.y*3.7 - uTime*9.0) * sin(p.z*4.3 + uTime*6.0); }
          void main(){ vec3 p = position; float d = h3(p*3.0)*0.5 + h3(p*6.0 + 2.0)*0.3; p += normal * d * 0.13; vP = position;
            vec4 mv = modelViewMatrix * vec4(p, 1.0); vN = normalize(normalMatrix * normal); vV = normalize(-mv.xyz);
            gl_Position = projectionMatrix * mv; }`,
        fragmentShader: /* glsl */ `uniform float uTime, uA; varying vec3 vN; varying vec3 vV; varying vec3 vP;
          void main(){ float f = abs(dot(vN, vV));
            float n = 0.5 + 0.5 * sin(vP.x*18.0 + uTime*20.0) * sin(vP.y*16.0 - uTime*25.0) * sin(vP.z*17.0 + uTime*15.0);
            vec3 hot = vec3(1.0, 0.95, 0.8) * 6.0; vec3 mid = vec3(1.0, 0.42, 0.06) * 3.0; vec3 edge = vec3(0.8, 0.1, 0.02) * 1.6;
            vec3 col = mix(edge, mid, smoothstep(0.1, 0.5, f)); col = mix(col, hot, smoothstep(0.6, 0.98, f));
            float a = (0.4 + 0.6 * n) * smoothstep(0.0, 0.4, f) * uA;
            gl_FragColor = vec4(col, 1.0); ${TONE}
            gl_FragColor.a = clamp(a, 0.0, 1.0); }`,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      const shell = new THREE.Mesh(new THREE.IcosahedronGeometry(0.34, 4), shellMat);
      S.scene.add(shell);
      const carry = new THREE.PointLight(0xff8a30, 0, 7, 1.5);
      S.scene.add(carry);
      const castC = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), runeMat(2, 0xff9040, 2.4));
      castC.rotation.x = -Math.PI / 2;
      castC.scale.setScalar(1.6);
      S.scene.add(castC);
      const A = new THREE.Vector3(-3.6, 1.5, -1.5);
      castC.position.set(A.x, 0.02, A.z);
      const B = new THREE.Vector3(2.4, 0.15, 0.9);
      const C = new THREE.Vector3(-0.6, 2.6, -0.5);
      const p = new THREE.Vector3();
      const q = new THREE.Vector3();
      let k = -1;
      let wait = 0.4;
      let slow = false;
      let shakeOn = true;
      let mult = 1;
      let phase = '';
      const CORE = flat(0xfff6e0, 9, 1);
      const HALO = flat(0xff7a20, 1.6, 0.6);
      const HALO2 = flat(0xff4a10, 0.6, 0.4);
      const controls: Control[] = [
        ctrlBtn('다시 쏘기', () => {
          k = 0;
        }),
        ctrlTog('느리게 보기', false, (v) => (slow = v)),
        ctrlTog('화면 흔들림', true, (v) => (shakeOn = v)),
        ctrlRange('입자 양 (배)', 0.3, 2, 0.1, 1, (v) => (mult = v)),
      ];
      const FLY = 0.9;
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05) * (slow ? 0.3 : 1);
          fx.orb.clear();
          shellMat.uniforms.uTime!.value = t;
          const cu = (castC.material as THREE.ShaderMaterial).uniforms;
          cu.uTime!.value = t;
          castC.rotation.z += dt * 1.5;
          if (k < 0) {
            wait -= dt;
            if (wait <= 0) k = 0;
            shell.visible = false;
            carry.intensity = 0;
            cu.uA!.value = clamp(1 - wait, 0, 1) * 0.8;
            cu.uRev!.value = clamp(1 - wait / 0.6, 0, 1);
            if (wait < 0.6) phase = '시전';
          } else {
            q.copy(bez(A, C, B, k, p));
            k += dt / FLY;
            cu.uA!.value = Math.max(0, 1 - k * 2);
            if (k >= 1) {
              k = -1;
              wait = 2.0;
              shell.visible = false;
              carry.intensity = 0;
              fx.boom(B.x, B.y, B.z, { k: mult, sc: 1.05, shake: shakeOn ? 0.16 : 0 });
              phase = '명중 — 폭발 · 파편 · 충격 고리 · 그을음';
            } else {
              bez(A, C, B, k, p);
              const vx = (p.x - q.x) / dt;
              const vy = (p.y - q.y) / dt;
              const vz = (p.z - q.z) / dt;
              shell.visible = true;
              shell.position.copy(p);
              shell.rotation.set(t * 3, t * 4, 0);
              carry.position.copy(p);
              carry.intensity = 22 + Math.sin(t * 40) * 4;
              fx.orb.spawn({ x: p.x, y: p.y, z: p.z, life: 1, s0: 0.42, ramp: CORE, shape: 0 });
              fx.orb.spawn({ x: p.x, y: p.y, z: p.z, life: 1, s0: 1.5, ramp: HALO, shape: 0 });
              fx.orb.spawn({ x: p.x, y: p.y, z: p.z, life: 1, s0: 3.0, ramp: HALO2, shape: 0 });
              const n = Math.max(1, Math.round(4 * mult * (slow ? 0.5 : 1)));
              for (let j = 0; j < n; j++) {
                const o = R(0, 1);
                fx.fire.spawn({ x: p.x - vx * dt * o + R(-0.08, 0.08), y: p.y - vy * dt * o + R(-0.08, 0.08), z: p.z - vz * dt * o + R(-0.08, 0.08), vx: vx * 0.08 + R(-0.4, 0.4), vy: R(0, 0.8), vz: vz * 0.08 + R(-0.4, 0.4), life: R(0.35, 0.6), s0: R(0.4, 0.55), s1: 0.08, ramp: RAMP.fire, shape: 1, rot: R(0, TAU), rv: R(-3, 3), drag: 2 });
              }
              if (Math.random() < mult * 0.9) fx.smoke.spawn({ x: p.x, y: p.y, z: p.z, vy: 0.5, life: R(1.0, 1.6), s0: 0.25, s1: 1.0, ramp: RAMP.smoke, shape: 1, rot: R(0, TAU), rv: R(-1, 1), drag: 1, curl: 0.3, delay: 0.12 });
              const ns = Math.round(3 * mult);
              for (let j = 0; j < ns; j++) {
                const d = sphereDir();
                const sp = R(0.8, 2.5);
                fx.fire.spawn({ x: p.x, y: p.y, z: p.z, vx: d.x * sp - vx * 0.15, vy: d.y * sp + 0.5, vz: d.z * sp - vz * 0.15, life: R(0.4, 0.9), s0: 0.06, s1: 0.02, ramp: RAMP.spark, st: 0.06, shape: 2, drag: 1.2, grav: 4, bnc: 0.3 });
              }
              phase = '날아감 — 핵 · 불꽃 껍질 · 연기 꼬리 · 불티';
            }
          }
          lab.set(phase || '시전', '#ffa050');
          fx.update(dt, t);
          S.tick(dt);
        },
        resize: (w, h) => S.resize(w, h),
        dispose: () => S.dispose(),
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [층 쌓기 (핵 · 빛무리 · 불티 · 연기)](https://ai-techstudio.web.app/ai/t/i171.md) `i171`
