# AI 꾸러미 — 타격 반짝 (hit spark)
> 맞는 순간 별 모양 빛살 + 짧은 선 입자 사방으로 + 하얀 번쩍  
> 견본: https://ai-techstudio.web.app/#t/i193

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

## 주문서

### 만들어 줘: 타격 반짝 (hit spark)

#### 1. 목표
모든 명중에 「타격 반짝 (hit spark)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 맞는 순간 별 모양 빛살 + 짧은 선 입자 사방으로 + 하얀 번쩍

#### 2. 핵심 기술 용어
- **hit spark**

#### 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: `i193` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i193
- 쓰면 좋을 때: 쓰면 좋을 곳 — 모든 명중 · 정답 「딱!」

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i193 견본 항목 — `src/demos/demosSkillB.ts:2202`
```ts
  i193: {
    kind: '3d',
    caption: '맞는 순간 — 하얀 번쩍 · 별 모양 빛살 · 사방 줄 불티 · 퍼지는 고리, 그리고 아주 짧게 멈춤(히트스톱)',
    make() {
      const A = arena({ cam: [0, 5.0, 6.2], look: [0, 0.8, 0] });
      const foe = monster(A, 0x4a5c6e, 0);
      foe.g.scale.setScalar(1.4);
      A.scene.add(foe.g);
      const ringMat = new THREE.SpriteMaterial({ map: A.tx.ring(), color: new THREE.Color(2.5, 2.2, 1.6), transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, opacity: 0 });
      const ring = new THREE.Sprite(ringMat);
      ring.renderOrder = 8;
      A.scene.add(ring);
      const glow = decal(A.tx.glow(), 3, C(0xffc070, 1.5), 0);
      A.scene.add(glow);
      const hitL = new Flash(0xffd8a0, 90, 10);
      A.scene.add(hitL.light);
      const loop = new Loop(1.3);
      let rays = 6, stopOn = true;
      let freeze = 0, side = 1, local = 0;
      let hitP: V3 = [0, 1, 0.5];
      let rayAge = 9, ringAge = 9;
      let rayDirs: number[] = [];
      const right = new THREE.Vector3(), up = new THREE.Vector3();
      const BOLT = C(0xffd070, 2.5);
      return base3d(A, (t, dt) => {
        const lt = loop.tick(t);
        // 히트스톱: 그 사이 모든 것이 멈춘다
        let d = dt;
        if (freeze > 0) {
          freeze -= dt;
          d = 0;
        }
        local += d;
        // 날아오는 빛 주먹
        const T = 0.55;
        const k = lt / T;
        if (k < 1) {
          const from: V3 = [side * 4.5, 1.4, 1.5];
          const to: V3 = [side * 0.45, 0.85, 0.35];
          const p = mix3(from, to, k * k);
          const pp = mix3(from, to, Math.max(0, k - 0.12) * Math.max(0, k - 0.12));
          A.st.seg(pp, p, 0.08, BOLT, 0.8, 0);
          orbGlow(A.fx, p, BOLT, 0.2, 1);
        }
        if (loop.hit(T)) {
          hitP = [side * 0.45, 0.85, 0.35];
          side = -side;
          if (stopOn) freeze = 0.085;
          rayAge = 0;
          ringAge = 0;
          rayDirs = [];
          for (let i = 0; i < rays; i++) rayDirs.push((i / rays) * TAU + rr(-0.2, 0.2), rr(0.7, 1.5));
          flashAt(A.fx, hitP, 2.0, C(0xfff0d0, 2.5), 0.14);
          sparks(A.st, hitP, { n: 26, c: C(0xffffff, 3), c2: C(0xffa030, 1.2), speed: 9, life: 0.38, grav: 7, len: 0.035, w: 0.022 });
          for (let i = 0; i < 12; i++) A.fx.emit({ p: hitP, v: sc3(randDir(), rr(1, 3)), life: rr(0.2, 0.4), size: rr(0.1, 0.18), size2: 0.02, c: C(0xffd090, 3), a: 1, drag: 4, fadeIn: 0 });
          hitL.kick(hitP, 1);
          foe.hit([1.6, 1.3, 0.9], 0.8);
          glow.material.opacity = 1;
          glow.position.set(hitP[0], 0.02, hitP[2] - 0.2);
          A.shake(0.12);
        }
        rayAge += d;
        ringAge += d;
        // 별 빛살 (화면을 보는 평면 위)
        if (rayAge < 0.24) {
          right.setFromMatrixColumn(A.cam.matrixWorld, 0);
          up.setFromMatrixColumn(A.cam.matrixWorld, 1);
          const rk = rayAge / 0.24;
          for (let i = 0; i < rayDirs.length; i += 2) {
            const a = rayDirs[i]!, L = rayDirs[i + 1]! * (0.7 + easeOut(rk) * 1.1);
            const dir = right.clone().multiplyScalar(Math.cos(a)).addScaledVector(up, Math.sin(a));
            const end = add3(hitP, sc3(ofV(dir), L));
            A.st.seg(hitP, end, 0.1 * (1 - rk) + 0.01, C(0xffd890, 3), 1 - rk * rk, 0.6);
          }
          A.fx.put(hitP, 1.4 * (1 - rk) + 0.4, C(0xffffff, 3), 1 - rk);
        }
        ring.position.set(hitP[0], hitP[1], hitP[2]);
        ring.scale.setScalar(0.3 + easeOut(ringAge / 0.3) * 2.6);
        ringMat.opacity = Math.max(0, 1 - ringAge / 0.3);
        glow.material.opacity *= Math.exp(-dt * 4);
        foe.tick(d, local);
        hitL.tick(dt);
        A.fx.step(d);
        A.st.step(d);
        A.tickCam(dt);
      }, [
        { type: 'button', label: '때리기', on: () => loop.restart() },
        { type: 'range', label: '빛살 수', min: 3, max: 12, step: 1, value: rays, on: (v) => (rays = v) },
        { type: 'toggle', label: '히트스톱 (멈칫)', value: stopOn, on: (v) => (stopOn = v) },
      ]);
    },
  }
```

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