// 카드 kind ↔ 효과 정합성 정적 검사 (협업자/codex가 카드 추가 후 실행). // 배경(2026-06-30): kind가 효과와 안 맞으면 카드가 사용불가/死카드가 된다. // - ResolveCardDrop 라우팅: Attack=몬스터 위 드롭(FindMonsterAtTouch>0 필요) / Skill·Power=위로 스윕 / Status=unplayable. // → block·유틸만 있고 데미지 없는 카드를 Attack으로 두면 위로 스윕으로 못 쓴다(아이언 바디 사고). // - PlayCard의 Power 분기는 PlayerPowers 등록만 하고 damage/aoe를 무시한다. // → Power인데 powerEffect도 power필드도 없으면 재생 시 아무 효과 없는 死카드(분노 사고). // 사용: node tools/verify/cardkinds.mjs (이상 0 → exit 0, 있으면 목록 + exit 1) import { readFileSync } from 'node:fs'; const cards = JSON.parse(readFileSync('data/cards.json', 'utf8')).cards; // Power 카드를 실제로 기능하게 하는 필드(powerEffect 지속효과 + 온플레이/지속 power 필드). // damage/aoe/block 같은 Attack/Skill 전용 필드는 Power 분기서 무시되므로 제외. const POWER_FIELDS = [ 'powerEffect', 'strength', 'dex', 'thorns', 'intangible', 'turnStartShiv', 'turnStartDraw', 'turnStartDiscard', 'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe', 'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier', 'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage', 'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage', 'skillSlyOnPlay', 'endTurnDexLoss', ]; const VALID_KINDS = ['Attack', 'Skill', 'Power', 'Status']; const issues = []; for (const [id, c] of Object.entries(cards)) { if (!VALID_KINDS.includes(c.kind)) { issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`); continue; } if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null) { issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`); } if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) { issues.push(`${id}(${c.name}): kind=Power인데 power효과 없음(死카드) → damage/aoe는 Power 분기서 무시, kind 재검토`); } } console.log(`카드 ${Object.keys(cards).length}장 kind↔효과 정합성: 이상 ${issues.length}`); for (const i of issues) console.log(' ⚠️ ' + i); console.log(issues.length ? 'RESULT: 정합성 위반 (위 카드 kind 수정 필요)' : 'RESULT: 모든 카드 kind↔효과 일치 ✓'); process.exit(issues.length ? 1 : 0);