Generated banner for Game Dev Combat Skill Polish
Generated logo for Game Dev Combat Skill Polish

Game Dev Combat Skill Polish

Meng ToCommunityCreated by Meng To

  • Skill
  • Review and audit
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Score a game's combat skills and spells out of 10 with blind judges, then raise the weakest to a bar, for timing and readability (does a skill read as itself, does the blow land when it lands) and for art (detail, light, trails, impact, art direction, judged against a reference such as Diablo IV). Covers filming casts on a stopped, stepped clock, contact strips and close-ups, anonymised two-judge rounds, the levers that moved scores (hero props made from a painted concept with image-to-3D, own schedules and keyed moves, painted additive textures, camera-facing ribbons, light outside the body), the traps that cost points, asset and load budgets, and shipping each skill as its own version. Use when the user asks to score, review, improve, polish or rebuild combat skills, spells or attacks; says a skill "doesn't make sense", "looks bad" or should look like another game; names a target score; or starts a "next skill" thread. Project specifics (review hooks, harness settings, where each skill's code lives, scores
— the description by Meng To

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured process for reviewing, scoring, and improving combat skills and spells in a game project. It utilizes a blind judging system where two subagents evaluate combat footage against specific readability and art rubrics. The workflow emphasizes objective measurement through headless frame-by-frame capture, contact strips, and performance metrics. It guides users through identifying weaknesses, implementing iterative improvements based on judge feedback, and finalizing assets for production.

What it does

  • Scores combat skills using blind, two-judge rounds based on readability and art rubrics.
  • Captures headless, frame-stepped footage of combat casts for objective analysis.
  • Generates contact strips and close-ups to visualize timing, impact, and visual fidelity.
  • Provides a framework for iterative refinement of animations, effects, and hero props.
  • Tracks progress through scorecard reporting and before/after comparisons.
  • Maintains project-specific memory of successful levers and common pitfalls.

When to use it

  • When you need to score or review the polish of combat skills, spells, or attacks.
  • When a skill is perceived as unclear, visually lacking, or inconsistent with a target reference.
  • When you want to improve a skill's timing, readability, or artistic detail.
  • When starting a new iteration or 'next skill' development thread.

Advantages

  • Reduces subjectivity by using blind judges and standardized rubrics.
  • Ensures timing and impact are measured against objective frame data.
  • Provides clear, actionable feedback for iterative development.
  • Helps identify and avoid common visual traps that degrade skill presentation.
  • Facilitates consistent quality across multiple combat assets.

Usage examples

“Score the current Fireball skill against the art rubric.”
The agent will initiate a blind judging round, capture the skill's contact strip, and provide a score out of 10 with specific feedback on art and readability.
“My Lightning Strike skill looks bad, how can I improve it?”
The agent will review the skill's current state, suggest improvements based on the readability and art rubrics, and outline a plan for the next round of polish.
“Start a new polish thread for the Whirlwind skill.”
The agent will set up the environment, check project references, and prepare the workflow to capture and evaluate the skill.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/mengto/game-dev-combat-skill-polish/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip game-dev-combat-skill-polish.zip -d .agents/skills

Documentation by Meng To

Game dev: combat skill polish

A skill is the cast (the caster's body), the effect (what it throws, raises or tears open), and the result (the target's reaction and what it leaves behind). Polish all three, judged from real footage by people who did not make it.

There are two bars. Know which one the user means:

BarAsksRubricTypical target
ReadabilityDoes it read as itself? Does the result land when and where the blow arrives? Is the pose its own?references/rubric-readability.md (reads, timing, body, effect, reaction)every skill 6+
ArtDetail and texture, light, motion and trails, impact, art direction, against a named referencereferences/rubric-art.md (Diablo IV paladin skills, described in words)7+ per skill

A skill can score 7 on readability and 3 on art. Never quote a score without naming its rubric.

1. Pin down the job

  • Which skills: all of them (a survey), or named ones (a rebuild). One skill per thread when several run at once.

  • The bar and the reference. If the user shows a reference image, save it into the project so judges can open it; otherwise describe it in words in the rubric.

  • Read the project reference first (references/<project>.md, if there is one): where each skill's code lives, the harness settings, the scorecard so far, and what other threads already learned.

  • Before designing, git fetch and check what other threads are changing; several skill threads edit the same effect and animation files.

2. Film the real thing

  • Work in your own git worktree off the main branch. Serve it on a free port; serve a clean main worktree on a second port for the "before".

  • Film headlessly on a stopped, stepped clock: freeze the game clock, step it 0.05 s per frame, wait for a rendered frame, shoot. A loaded machine then still gives evenly spaced frames. (scripts/film.mjs; staging per skill in a scenarios file like scripts/scenarios.example.mjs.) The game needs review-only hooks for this: stage a state, issue a command, aim the camera, freeze and step the clock (the contract is at the top of film.mjs; HOOK, PAGE, READY, SETTINGS_KEY and CHROME are set from the environment).

  • Stage each cast so the caster, the target, the hero object and the landing are in the crop and clear of the HUD (frame, span, points). A thing hidden behind a panel scores as "a speck".

  • Build a contact strip per cast (scripts/strips.py, N=16 and TAIL=3 to keep the aftermath) and a close-up of the key frames (scripts/close.py). Judges need both: strips show timing, close-ups show art.

  • Measure what can be measured: the frame the foe moves against the frame the blow lands (meta.json per-frame action and time), draw calls and triangles during the cast (scripts/perf.mjs), and download size.

  • Never edit served source files during a film; the dev server reloads mid-capture.

3. Judge blind, two at a time

  • Spawn two fresh general-purpose subagents per round. Give them the rubric, a list file and the image paths, and tell them to open no other files: no code, no earlier scores, no keys. scripts/judge-set.py anonymises strips under neutral ids and writes the list and a key; templates/judge-prompt.md has the two prompts.

  • Ask for JSON: part scores, a total, the biggest fault and the single change that would raise it most. Save every reply under judges/.

  • Keep the lower of the two. Never round up. Never score your own captures.

  • Judge spread is about ±0.5 to 1. Before calling a 7 solid, re-judge the same pictures or mix a calibration strip (an old version) into the set. One lucky round is not a pass.

  • The judges' "next" lines are the work list. When both name the same thing, do that first.

4. Raise it, round by round

Fix the criterion costing the most points. These moved scores before:

Timing and readability

  • Results must land on the beat: the target reacts, is pushed or gets its status when the blow, missile or wave arrives, never on the click. Check the scene layer that draws units at their rules position every frame; it will happily show a push before the blow (the first and biggest fix on the project this skill was built on).

  • A skill that borrows another's schedule (its duration and contact moment) cannot have its own body. Give it its own schedule and keyed move: a kneel then rise, a gather then throw, a bow then welcome. Then update every place that branched on the old schedule's name.

  • Travel time for waves and missiles: the hit waits for the arrival.

  • Hits across an arc are staggered along the measured blade path, not fired together.

  • The target's reaction: a recoil, buckle, daze or knock-back is half the read. Status labels alone score low.

Art

  • The hero object's modelling is the ceiling. Scripted primitives and hand-built meshes plateau at 5.5 to 6.5 ("plain prism", "toy"). A painted concept (gpt_image_2_5) turned into a textured model with image-to-3D (Higgsfield image_to_3d, Meshy, textured + PBR, about 12k triangles) took two skills past 7. Without any hero element, a skill tends to plateau around 6 (a wind skill with nothing modelled stopped at 6 after 20 rounds).

  • When the hero is a small world prop, keep the prop readable and make the effect painted wisps round it (a modelled "hand of force" never read from the tactical camera).

  • Painted textures on black for additive blending (craters, crescents, ribbons, shockwave rings, vortices) beat procedural rings and streaks. Gradient-map them to the skill's palette before shipping (lava red next to a red target marker reads wrong).

  • Ribbons and trails: camera-facing across the path, width growing with the path drawn, soft across their width, sampled back along the motion itself (not frame by frame). A thin uniform line reads as "a laser" or "a debug line"; a wide single band reads as "a flat card".

  • Metals need something to reflect. A tactical scene often has no environment map; give a model its own small equirect sky and a tight fresnel rim. A broad rim or warm tint washes gold to cream or copper.

  • Light: put it on the object or beside the body toward the caster, about a metre out. A light inside the body lights nothing; a light pool under a projectile reads as "a detached disc"; too bright bleaches the target.

  • Impact: a small hot core, debris flung and falling, dust low and dark, a mark left in the ground that cools or fades, and a strong recoil. A big white disc that hides the target always loses points.

Things judges always mark down: borrowed looks (another skill's ring, flash or pose); flat cream or white placeholder flashes; even radial streaks; pillars or lances of light over the target; additive dust rings that brighten the ground to cream; stretched white sparks ("white dashes"); long lingering trails ("laser", "lightning"); crack decals that read as electric; anything under the HUD.

Keep guardrails while climbing: frame budget, first-scene download budget (load new assets only when a fight that can use them starts), the readability score, the tests, and other skills' looks. Commit WIP locally each round; a restart then costs nothing.

5. Stop, lock in, ship

  • Stop when the bar is met on the lower judge (and a re-judge agrees), or when a skill plateaus for several rounds. Then say plainly what it would take (usually a modelled hero element).

  • Ship the version the judges scored, not an unjudged tweak of it.

  • Add a test that locks the skill's own schedule, model or recipes in place.

  • Write the scorecard: a round table (change, both scores, kept), what moved it, what is left, cost, and a before/after sheet (scripts/beforeafter.py).

  • Ship it as its own version with three pictures (start, key moment, result), following the project's ship routine (workflow-ship-change), with measured sizes.

  • Update the project memory with what moved scores and the new traps, so the next skill starts further on.

Report

Lead with the result per skill, on its named rubric: "Summon Hammer 4 → 7 (art, two blind judges, lower kept)". Then the round table, how it was judged and from what footage, where it still falls short in the judges' words, the cost, and the before/after pictures sent inline. Label captures as headless browser captures, not device tests.

Files

scripts/film.mjs

// Film skill casts headlessly on a stopped, stepped clock: one Chrome, one page, each scenario staged through the review
// fixture's hook load(), cast with act(), then frozen (freeze(true)) and stepped DT seconds a frame (step(dt)). The page must
// expose window[HOOK] with: state, load(json), holdAI(on), act(command), look(col,row), project(col,row), withWorld(fn),
// freeze(on), step(seconds) (resolving after the frame is drawn), pace(k). The staging block assumes a turn-based board of
// units ({id, col, row, team, hp, ap}) and a turnIndex; adapt it to your game.
// usage: node film.mjs <scenarios.mjs> [ids...]   env: BASE (dev server), OUT (frames dir), SPAN (camera span, 10), DT (.05),
//   SETTINGS (JSON merged into the review settings), HIDE (comma list of mesh-name prefixes to hide, e.g. the HUD's helpers).
// Per scenario: maxFrames (90), tail (idle frames kept after the cast, 8), span, frame [col,row], points [[col,row]] to keep in the crop.
import { spawn } from 'node:child_process';
import { writeFileSync, rmSync, mkdirSync } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
// Game-specific settings, all from the environment: HOOK (the page's review-hook global, default __review), PAGE (the
// review URL, {fixture} replaced, default /?review={fixture}), READY (an extra page expression that must be true before
// staging), SETTINGS_KEY (localStorage key for review settings), CHROME (a Chrome/Chromium binary).
const HOOK = process.env.HOOK || '__review', READY = process.env.READY || 'true';
const PAGE = fixture => (process.env.PAGE || '/?review={fixture}').replace('{fixture}', fixture);
const SETTINGS_KEY = process.env.SETTINGS_KEY || 'review-settings';
const CHROME = process.env.CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const BASE = process.env.BASE || 'http://127.0.0.1:5173';
const OUT = resolve(process.env.OUT || 'qa/captures/skill-motion/frames');
const PACE = Number(process.env.PACE || .5), SPAN = Number(process.env.SPAN || 10), REAL = Number(process.env.REAL || 9);
const [scenarioPath, ...only] = process.argv.slice(2);
const { default: SCENARIOS } = await import(pathToFileURL(resolve(scenarioPath)).href);
const list = only.length ? SCENARIOS.filter(s => only.includes(s.id)) : SCENARIOS;
const port = 9700 + Math.floor(Math.random() * 90), profile = `/tmp/skill-film-${port}`;
const chrome = spawn(CHROME, ['--headless=new', '--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist', '--disable-gpu-vsync', '--disable-frame-rate-limit', `--remote-debugging-port=${port}`, '--no-first-run', `--user-data-dir=${profile}`, '--window-size=1600,900', '--hide-scrollbars', '--mute-audio', 'about:blank'], { stdio: 'ignore' });
process.on('exit', () => { try { chrome.kill(); } catch {} try { rmSync(profile, { recursive: true, force: true }); } catch {} });
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => process.exit(1));
let version; for (let i = 0; i < 600 && !version; i++) { try { version = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json(); } catch { await sleep(100); } }
const ws = new WebSocket(version.webSocketDebuggerUrl); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
let seq = 0; const pending = new Map(); const logs = [];
ws.onmessage = e => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const { resolve, reject } = pending.get(m.id); pending.delete(m.id); m.error ? reject(new Error(m.error.message)) : resolve(m.result); } else if (m.method === 'Runtime.exceptionThrown') logs.push((m.params.exceptionDetails?.exception?.description || JSON.stringify(m.params)).slice(0, 600)); };
const send = (method, params = {}, sessionId) => new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params, sessionId })); });
const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
const cdp = (method, params) => send(method, params, sessionId);
await cdp('Page.enable'); await cdp('Runtime.enable');
await cdp('Emulation.setDeviceMetricsOverride', { width: 1600, height: 900, deviceScaleFactor: 1, mobile: false });
await cdp('Page.addScriptToEvaluateOnNewDocument', { source: `addEventListener('DOMContentLoaded',()=>{const st=document.createElement('style');st.textContent='#toast{display:none!important}';document.head.append(st);});` });
await cdp('Page.addScriptToEvaluateOnNewDocument', { source: `try{localStorage.setItem('${SETTINGS_KEY}',JSON.stringify({quality:'high',shadows:'high',reduced:false,sound:false,...${process.env.SETTINGS||'{}'}}));}catch{}` });
const evaluate = async expression => { const r = await cdp('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text); return r.result?.value; };
const ready = () => evaluate(`(()=>{try{return !!window.${HOOK}&&!!(${READY});}catch{return false;}})()`);
let loaded = null;
async function open(fixture) {
  if (loaded === fixture) return;
  await cdp('Page.navigate', { url: `${BASE}${PAGE(fixture)}` });
  for (let i = 0; i < 900; i++) { await sleep(200); if (await ready().catch(() => false)) break; }
  await sleep(4000); loaded = fixture;
}
async function shot(file) { const { data } = await cdp('Page.captureScreenshot', { format: 'jpeg', quality: 88 }); writeFileSync(file, Buffer.from(data, 'base64')); }
for (const s of list) {
  const t0 = Date.now();
  try {
    await open(s.fixture || 'combat');
    // Stage: positions, full AP, the caster's turn. Every scenario starts from the fixture's own state.
    const staged = await evaluate(`(()=>{const S=window.${HOOK};S.holdAI(true);S.pace(1);
      if(!window.__base)window.__base=JSON.stringify(S.state);
      const st=JSON.parse(window.__base),place=${JSON.stringify(s.place || [])},prep=${s.prep ? `(${s.prep})` : 'null'};
      for(const [id,col,row,extra] of place){const u=st.units.find(u=>u.id===id);if(!u)continue;Object.assign(u,{col,row,ap:6,hp:Math.max(u.hp,1)},extra||{});}
      for(const u of st.units)if(!place.some(p=>p[0]===u.id)&&${s.hideOthers ? 'true' : 'false'}&&u.team){u.hp=0;}
      if(prep)prep(st);
      let got=null;for(let i=0;i<st.units.length+4;i++){st.turnIndex=i;try{got=S.load(JSON.stringify(st));}catch(e){return 'load failed: '+e.message;}if(got===${s.caster})break;}
      S.holdAI(true);return got;})()`);
    if (staged !== s.caster) throw new Error('staging: active ' + staged);
    await sleep(2500);
    const [fc, fr] = s.frame || s.place[0].slice(1, 3);
    await evaluate(`(()=>{const S=window.${HOOK};S.look(${fc},${fr});S.withWorld(w=>{if(w.rig){w.rig.desiredSpan=${s.span || SPAN};w.rig.span=${s.span || SPAN};}});return true;})()`);
    // Park the pointer on the day plaque, off the board.
    await cdp('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 1440, y: 37 });
    await sleep(2500);
    const where = await evaluate(`(()=>{const S=window.${HOOK};const o={};for(const [id,col,row] of ${JSON.stringify(s.place)})o[id]=S.project(col,row);${JSON.stringify(s.points||[])}.forEach(([c,r],i)=>o['p'+i]=S.project(c,r));return o;})()`);
    const dir = `${OUT}/${s.id}`; rmSync(dir, { recursive: true, force: true }); mkdirSync(dir, { recursive: true });
    await shot(`${dir}/000-before.jpg`);
    if (process.env.HIDE) console.log('hidden', await evaluate(`(()=>{const names=${JSON.stringify((process.env.HIDE||'').split(','))};let n=[];window.${HOOK}.withWorld(w=>{const root=w.scene||w.world?.scene;(root||w).traverse?.(o=>{if(o.isMesh&&names.some(x=>o.name.startsWith(x))){o.material.visible=false;n.push(o.name);}});});return n.join('|');})()`));
    const DT = Number(s.dt || process.env.DT || .05);
    await evaluate(`window.${HOOK}.freeze(true)`);
    const cmd = s.cmd;
    const result = await evaluate(`JSON.stringify(window.${HOOK}.act(${JSON.stringify(cmd)}))`);
    const frames = [];let idle = 0, t = 0;
    const probe = `(()=>{let o=null;window.${HOOK}.withWorld(w=>{const m=w.nodes.get(${s.caster})?.model;o=[w.busy,m?.sample?.id||m?.currentAction||null,+(m?.sample?.elapsed||0).toFixed(3)];});return o;})()`;
    for (let i = 1; i <= (s.maxFrames || 90); i++) {
      await evaluate(`window.${HOOK}.step(${DT})`); t += DT;
      // Let the stepped moment reach the screen before it is shot (a loaded machine otherwise shoots the frame before).
      await evaluate(`new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(()=>r(true))))`);
      const f = `${dir}/${String(i).padStart(3, '0')}.jpg`; await shot(f);
      const [busy, action, elapsed] = await evaluate(probe).catch(() => [true, null, 0]); frames.push({ f, at: +t.toFixed(3), action, elapsed, busy });
      if (!busy && t > .6) { if (++idle >= (s.tail || 8)) break; } else idle = 0;
    }
    await evaluate(`window.${HOOK}.freeze(false)`);
    await evaluate(`window.${HOOK}.pace(1)`);
    writeFileSync(`${dir}/meta.json`, JSON.stringify({ id: s.id, cmd, result: JSON.parse(result), where, frames: frames.length, pace: 1, dt: DT, times: frames.map(f => f.at), actions: frames.map(f => [f.action, f.elapsed, f.busy]) }, null, 1));
    console.log(s.id, 'ok', result, frames.length, 'frames', ((Date.now() - t0) / 1000).toFixed(0) + 's');
  } catch (e) { console.log(s.id, 'FAILED', e.message.slice(0, 300)); }
}
if (logs.length) console.log('page errors:\n' + [...new Set(logs)].slice(0, 8).join('\n'));
ws.close(); chrome.kill(); process.exit(0);

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Meng To
Publisher
Meng To (Community)
Source
MengTo/Skills · agent-skills/game-combat/game-dev-combat-skill-polish · commit 5ea1661
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Meng To.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download game-dev-combat-skill-polish.zip