Four real run cycles, rolled one-call-one-row through a new _runroll.py
The old run frames were generated one at a time and it showed: 91/110/92/98 px of body-width drift, duplicated poses drawn differently, no racket. Each character (player m/f 6 frames, opponent m/f 8) now comes from ONE 4K call anchored on a composed identity reference of its settled frames — the broken run art stayed out of the reference on purpose. Stride-gated, cut with _fine, judged by eye per roll. drawChar probes run8/run6/run4/run2. Side effect: the male opponent's old run art faced LEFT against the art-faces-RIGHT convention — he had been moonwalking right since install. Fixed by drawing the new rows to the convention. Pipeline lessons recorded in PROJECT.md (the model negotiates the count; a back view has no direction; the racket clause goes early and loud). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No files matched your search
@@ -1805,6 +1805,33 @@ The gates, and the numbers they were calibrated on:
|
||||
**SUPERSEDED 2026-08-12 — see the next section.** Those 31 frames were not art: `--verify` found
|
||||
27 of them dead. The brute is now live (`ZFAMS` has `'br'`, the KEEPER wears it) off ONE sheet.
|
||||
|
||||
## THE RUN CYCLES — one call, one row, one character (2026-08-13)
|
||||
|
||||
The p_run/o_run frames were generated ONE AT A TIME in the pre-pipeline era and it showed: the male
|
||||
player's four frames measured 91/110/92/98 px wide, frames 1/3 and 2/4 were near-duplicates drawn
|
||||
differently, and the racket was missing outright. Oliver: *"really awkward... I think we need more
|
||||
frames to sell this as good running"* — the frame COUNT was never the fault, the drift was.
|
||||
|
||||
`assets/_runroll.py` re-rolled all four characters (player m/f, opponent m/f) on the family-pipeline
|
||||
discipline: identity from a composed reference of SETTLED frames (`_pose_ref_run_<k>.png`, tracked,
|
||||
no API — the broken run frames stay OUT of it), poses from skeleton language in the prompt, one 4K
|
||||
21:9 call per character, `_fine.py` cut, stride gate, judged by eye per roll. What it added to the
|
||||
pipeline's knowledge:
|
||||
|
||||
- **The model negotiates the COUNT.** Asked for 8 it returned 7, then 6, then 8 — and the row it
|
||||
draws at its own count is the coherent one. The cutter accepts 6..8 and `drawChar` probes
|
||||
run8/run6/run4/run2, so a good 6 beats a fought-for 8. State the count you can SEE with `--n`
|
||||
when the figures touch (a low racket bridges every blob gap).
|
||||
- **A run cycle is the one pose set whose generator cliché is RIGHT.** No pose reference needed —
|
||||
pin only the frame count, the facing, the racket ("a figure with an empty hand is WRONG", early
|
||||
and loud, after roll 1 dropped the racket from six figures of seven).
|
||||
- **A back view has no direction.** Roll 1's "seen from behind, sprinting right" came back running
|
||||
straight away up-court. The working language: back THREE-QUARTER view, body angled toward the
|
||||
right, "NOT a straight-away back view: he moves ACROSS the frame".
|
||||
- **The old male opponent art faced LEFT and had been MOONWALKING right since install** (char art
|
||||
faces RIGHT by convention — the gameplay.js note at the swing aim). The new rows are all drawn
|
||||
facing right, which quietly fixed it.
|
||||
|
||||
## THE ZOMBIE FAMILY PIPELINE — one sheet, one character, 31 sprites (2026-08-12)
|
||||
|
||||
**This replaces the nine-batch flow above for a whole opponent family.** `_regen.py` still exists and
|
||||
|
||||
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 188 KiB |
@@ -0,0 +1,203 @@
|
||||
# _runroll.py — roll ONE character's 8-frame RUN CYCLE: one call, one row, one character.
|
||||
#
|
||||
# uv run --with pillow --with numpy --with requests python assets/_runroll.py p # compose ref + roll
|
||||
# uv run --with pillow --with numpy --with requests python assets/_runroll.py p --cut # cut + gate -> review dir
|
||||
# uv run --with pillow --with numpy --with requests python assets/_runroll.py p --install # cut + gate -> assets/
|
||||
# keys: p (player m), pf (player f), o (opponent m), of (opponent f)
|
||||
#
|
||||
# WHY. The p_run*/o_run* frames were generated ONE AT A TIME in the pre-pipeline era, and it shows,
|
||||
# measurably: the male player's four frames are 91/110/92/98 px wide, frames 1/3 and 2/4 are near
|
||||
# duplicates of each other drawn differently, and the racket is missing outright. That is the drift
|
||||
# the family pipeline exists to make impossible, so the run cycles go through the same discipline:
|
||||
#
|
||||
# IDENTITY COMES FROM A COMPOSED REFERENCE — settled frames already on disk, pasted locally at one
|
||||
# scale, NEAREST, no API round trip (assets/_pose_ref_run_<k>.png, tracked). The broken run frames
|
||||
# stay OUT of it: the model copies what it is shown, and they are the defect being replaced.
|
||||
# THE POSES COME FROM THE PROMPT'S SKELETON LANGUAGE, not from example art — a run cycle is the
|
||||
# one pose set the generator's own cliché is RIGHT about; we only pin the frame count, the order,
|
||||
# the facing and the racket.
|
||||
#
|
||||
# FACING: every char sprite is drawn facing RIGHT and render.js mirrors on facing < 0 (see the note
|
||||
# at gameplay.js's swing aim). The old o_run art faced LEFT — the male opponent has been moonwalking
|
||||
# to the right since it was installed — so the facing is pinned in the prompt and gated after the cut.
|
||||
import base64, os, sys
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SCRATCH = os.path.join(HERE, '_refcheck')
|
||||
MODEL = os.environ.get('SPLAT_MODEL', 'google/gemini-3-pro-image')
|
||||
N = 8 # frames per cycle — render.js probes run4/run8 and subdivides the same stride
|
||||
TARGET_H = 224 # SPRITE_H: the cut anchors its tallest frame here, like every char sheet
|
||||
|
||||
# key -> identity frames (settled art, the character), install (prefix, suffix), who (words that
|
||||
# stop the model averaging the reference into a generic figure), view (the camera + direction).
|
||||
CHARS = {
|
||||
'p': {'id': ['p_idle1', 'p_bounce', 'p_swing1', 'p_smash', 'p_toss'], 'install': ('p_run', ''),
|
||||
'who': 'the LIVING male tennis player of image 1: neat brown hair under a white headband, '
|
||||
'a white polo shirt, blue shorts, white socks and white sneakers, tanned skin, '
|
||||
'carrying a wooden tennis racket',
|
||||
'view': 'his BACK to the camera in three-quarter view, the body angled toward the right, '
|
||||
'running to the RIGHT along the baseline — the right edge of his face just visible '
|
||||
'past his shoulder. NOT a straight-away back view: he moves ACROSS the frame, '
|
||||
'rightward, every figure leaning into that rightward run'},
|
||||
'pf': {'id': ['p_idle1_f', 'p_bounce_f', 'p_swing1_f', 'p_smash_f', 'p_toss_f'], 'install': ('p_run', '_f'),
|
||||
'who': 'the LIVING female tennis player of image 1: a dark ponytail, a white polo shirt, '
|
||||
'a white pleated tennis skirt, white socks and sneakers, carrying a wooden tennis racket',
|
||||
'view': 'her BACK to the camera in three-quarter view, the body angled toward the right, '
|
||||
'running to the RIGHT along the baseline — the right edge of her face just visible '
|
||||
'past her shoulder. NOT a straight-away back view: she moves ACROSS the frame, '
|
||||
'rightward, every figure leaning into that rightward run'},
|
||||
'o': {'id': ['o_idle1', 'o_bounce', 'o_swing1', 'o_smash', 'o_toss'], 'install': ('o_run', ''),
|
||||
'who': 'the male tennis opponent of image 1: shaggy blond hair under a red headband, a red '
|
||||
'polo shirt with a dark collar, dark grey shorts, red socks, white sneakers, '
|
||||
'carrying a wooden tennis racket',
|
||||
'view': 'in three-quarter FRONT view facing the camera like image 1, sprinting toward the '
|
||||
'RIGHT edge of the frame'},
|
||||
'of': {'id': ['o_idle1_f', 'o_bounce_f', 'o_swing1_f', 'o_smash_f', 'o_toss_f'], 'install': ('o_run', '_f'),
|
||||
'who': 'the female tennis opponent of image 1: a light-brown ponytail, a red polo shirt, a '
|
||||
'dark grey pleated skirt, dark red shoes, carrying a wooden tennis racket',
|
||||
'view': 'in three-quarter FRONT view facing the camera like image 1, sprinting toward the '
|
||||
'RIGHT edge of the frame'},
|
||||
}
|
||||
|
||||
MAGENTA = (255, 0, 255)
|
||||
REF_SIZE = (6720, 2880) # 21:9 — the aspect a one-row-of-eight roll is asked at, so the reference
|
||||
MARGIN = 0.03 # is composed at the same enum value and nothing gets rearranged to fit.
|
||||
GAP = 0.22 # gap between identity figures, as a fraction of their height
|
||||
|
||||
|
||||
def ref_path(k): return os.path.join(HERE, f'_pose_ref_run_{k}.png')
|
||||
|
||||
|
||||
def compose(k):
|
||||
"""The identity reference: the character's SETTLED frames, one scale, NEAREST, no API."""
|
||||
frames = [Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') for n in CHARS[k]['id']]
|
||||
rh = max(f.height for f in frames)
|
||||
gap = round(rh * GAP)
|
||||
cw = sum(f.width for f in frames) + gap * (len(frames) - 1)
|
||||
s = min(REF_SIZE[0] * (1 - 2 * MARGIN) / cw, REF_SIZE[1] * (1 - 2 * MARGIN) / rh)
|
||||
sheet = Image.new('RGB', REF_SIZE, MAGENTA)
|
||||
x = round((REF_SIZE[0] - cw * s) / 2)
|
||||
ground = round((REF_SIZE[1] + rh * s) / 2) # one ground line, bottom-aligned
|
||||
for f in frames:
|
||||
r = f.resize((round(f.width * s), round(f.height * s)), Image.NEAREST)
|
||||
sheet.paste(r, (x, ground - r.height), r)
|
||||
x += r.width + round(gap * s)
|
||||
sheet.save(ref_path(k))
|
||||
print(f'{ref_path(k)} {sheet.size} {len(frames)} figures at one scale {s:.2f}')
|
||||
|
||||
|
||||
def prompt(k):
|
||||
c = CHARS[k]
|
||||
return (
|
||||
f'Draw a pixel-art sprite sheet: ONE ROW of EXACTLY {N} figures on a flat magenta background. '
|
||||
f'COUNT THEM: {N} figures, no fewer, no more — a row of 7 or 9 is WRONG.\n\n'
|
||||
f'IMAGE 1 is the ONLY source for who the character is: {c["who"]}. Copy the build, the '
|
||||
'garments, the hair and every one of its colours exactly. The poses in image 1 exist only to '
|
||||
'show the character — do NOT copy the poses.\n\n'
|
||||
f'EVERY ONE OF THE {N} FIGURES GRIPS THE WOODEN TENNIS RACKET IN THE RIGHT HAND, carried low '
|
||||
'at the side mid-run. A figure with an empty hand is WRONG.\n\n'
|
||||
f'THE {N} FIGURES ARE ONE CONTINUOUS RUN CYCLE, {c["view"]}. Evenly spaced, every figure on '
|
||||
'one shared ground line, every figure at the same scale as its neighbours.\n\n'
|
||||
'The cycle, frame by frame: 1 the right foot planted under the body, left knee driving up and '
|
||||
'forward; 2 push-off, the right leg trailing straight behind, the left leg reaching forward; '
|
||||
'3 full flight, both feet off the ground, the stride at its widest; 4 the left foot landing '
|
||||
'ahead of the body. Frames 5 to 8 are the SAME four moments with the legs swapped — the left '
|
||||
'foot planted, the right knee driving. Across the row the gap between the feet opens, closes, '
|
||||
'opens and closes again. The arms counter-swing: the opposite arm to the driving knee comes '
|
||||
'forward. If two neighbouring figures have the same leg forward, the row is WRONG.\n\n'
|
||||
'MATCH THE DRAWING STYLE OF IMAGE 1 EXACTLY. Image 1 is hard-edged pixel art and your output '
|
||||
'must be indistinguishable from it in technique: the same square pixel grid at the same pixel '
|
||||
'size, the same hard dark outline around every figure, the same flat blocks of colour, the '
|
||||
'same stepped aliased edges, the same hand-placed dithering. No soft brushwork, no blur, no '
|
||||
'gradients, no anti-aliasing.\n\n'
|
||||
'The background is a solid, flat, uniform chroma key. Use EXACTLY hex color #FF00FF, one '
|
||||
'single flat tone across the whole canvas behind every figure, with no panels or boxes of any '
|
||||
'other colour. The inside of the racket head is that same EXACT #FF00FF. No text, no shadows, '
|
||||
'no ground lines.')
|
||||
|
||||
|
||||
def api_key():
|
||||
if os.environ.get('OPENROUTER_API_KEY'): return os.environ['OPENROUTER_API_KEY']
|
||||
for line in open(os.path.join(HERE, '..', '.env')):
|
||||
if line.startswith('OPENROUTER_API_KEY='):
|
||||
return line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||
sys.exit('no OPENROUTER_API_KEY')
|
||||
|
||||
|
||||
def u(p):
|
||||
return 'data:image/png;base64,' + base64.b64encode(open(p, 'rb').read()).decode()
|
||||
|
||||
|
||||
def roll(k):
|
||||
r = requests.post('https://openrouter.ai/api/v1/images',
|
||||
headers={'Authorization': f'Bearer {api_key()}', 'Content-Type': 'application/json'},
|
||||
# 4K and 21:9 spelled out: the endpoint defaults to 1K and snaps aspect to an enum — the
|
||||
# reference is composed at 21:9 for exactly this reason (see PROJECT.md, the API facts).
|
||||
json={'model': MODEL, 'prompt': prompt(k), 'resolution': '4K', 'aspect_ratio': '21:9',
|
||||
'input_references': [{'type': 'image_url', 'image_url': {'url': u(ref_path(k))}}]},
|
||||
timeout=1800)
|
||||
if r.status_code != 200:
|
||||
sys.exit(f'{k}: HTTP {r.status_code} {r.text[:300]}')
|
||||
d = r.json().get('data') or []
|
||||
if not d:
|
||||
sys.exit(f'{k}: no image returned')
|
||||
b = d[0].get('b64_json') or d[0].get('image_url', {}).get('url', '').split(',')[-1]
|
||||
os.makedirs(SCRATCH, exist_ok=True)
|
||||
out = os.path.join(SCRATCH, f'run_{k}.png')
|
||||
open(out, 'wb').write(base64.b64decode(b))
|
||||
print(f'{k}: {out} {Image.open(out).size}', flush=True)
|
||||
|
||||
|
||||
def cut(k, install=False, n=0):
|
||||
"""Cut the roll with _fine's machinery, gate the stride, and (on --install) place the frames.
|
||||
|
||||
THE COUNT IS WHAT THE MODEL DREW, 6..8 — asked for 8, it returns 6 or 7 as often as not, and a
|
||||
clean 6-frame cycle beats a fought-for 8 (the row it drew at its own count is the coherent one).
|
||||
render.js probes run8/run6/run4, so the game wires whichever count survives review.
|
||||
|
||||
`--n <count>` states the count when the figures TOUCH (a low-carried racket bridges every gap,
|
||||
so blob-counting reads one figure): the count is then a thing you judged by eye off the roll,
|
||||
which is the pipeline's own rule, and _fine's valley rescue does the cutting."""
|
||||
sys.path.insert(0, HERE)
|
||||
import _fine
|
||||
from _famref import legs, stride_diff
|
||||
from PIL import Image as I
|
||||
sheet = os.path.join(SCRATCH, f'run_{k}.png')
|
||||
if not os.path.exists(sheet): sys.exit(f'{sheet}: no roll — run `_runroll.py {k}` first')
|
||||
got = n or len(_fine.frames(_fine.key(I.open(sheet)), [])) # count what actually arrived
|
||||
if not 6 <= got <= 8:
|
||||
sys.exit(f'{k}: {got} figures on the sheet — outside 6..8. Re-roll, or state the count '
|
||||
'you can see with --n <count>')
|
||||
pre, suf = CHARS[k]['install']
|
||||
outdir = HERE if install else os.path.join(SCRATCH, f'runcut_{k}')
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
outs = [os.path.join(outdir, f'{pre}{i}{suf}.png') for i in range(1, got + 1)]
|
||||
_fine.main(sheet, outs, TARGET_H, [], -1)
|
||||
# THE STRIDE GATE, on what was actually cut: every neighbouring pair (wrapping — it loops) must
|
||||
# differ in the legs, or the cycle holds a duplicated pose. Threshold calibrated against
|
||||
# _famref's (30% = a real stride pair; 12 catches only true near-duplicates).
|
||||
L = [legs(Image.open(o)) for o in outs]
|
||||
bad = []
|
||||
for i in range(got):
|
||||
d = stride_diff(L[i], L[(i + 1) % got])
|
||||
print(f' stride {i + 1}->{(i + 1) % got + 1}: {d:5.1f}%')
|
||||
if d < 12: bad.append(f'{i + 1}->{(i + 1) % got + 1} at {d:.1f}%')
|
||||
if bad:
|
||||
sys.exit('GATE FAILED: near-identical neighbour frames: ' + ', '.join(bad))
|
||||
print(f'{k}: {got} frames -> {outdir}' + ('' if install else ' (review, then --install)'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argv = sys.argv[1:]
|
||||
n = 0
|
||||
if '--n' in argv: i = argv.index('--n'); n = int(argv[i + 1]); del argv[i:i + 2]
|
||||
args = [a for a in argv if not a.startswith('-')]
|
||||
if len(args) != 1 or args[0] not in CHARS:
|
||||
sys.exit(f'usage: _runroll.py <key> [--cut|--install] [--n <count>] keys: {" ".join(CHARS)}')
|
||||
k = args[0]
|
||||
if '--install' in argv: cut(k, install=True, n=n)
|
||||
elif '--cut' in argv: cut(k, n=n)
|
||||
else: compose(k); roll(k)
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=213"></script>
|
||||
<script type="module" src="src/main.js?v=214"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -221,15 +221,16 @@ function farB(z) {
|
||||
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
|
||||
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
|
||||
// many: the one that does not get bumped is the one that serves a cached 404.
|
||||
export const SPRV = 107;
|
||||
export const SPRV = 108;
|
||||
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
|
||||
// already requested at import — it just makes the wait visible and holds the menu back until the
|
||||
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
|
||||
export const LOAD = { done: 0, total: 0 };
|
||||
const SPR = {};
|
||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
|
||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_run5','p_run6','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
|
||||
'p_pull', // the serve wind-up: the racket dropping behind him while the pull is drawn
|
||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_swing1','o_swing2',
|
||||
'o_run1','o_run2','o_run3','o_run4','o_run5','o_run6','o_run7','o_run8', // the rolled 8-frame cycle (_runroll.py)
|
||||
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4',
|
||||
'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set
|
||||
'x_crowd_c','x_crowd_c2','x_ads_c', // TERRE ROUGE (clay)
|
||||
@@ -277,10 +278,8 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
'z_barrel1','z_barrel2', // the weapon barrel, two frames so it ROLLS
|
||||
// THE WOMEN'S TOUR. Same poses, same 224 px anchor, a different body — see SK().
|
||||
// Only the two players and the turned CPU have a twin; everything above is shared.
|
||||
// No o_run3_f/o_run4_f on purpose: runN reads the MEN'S table, where o_run4 is not
|
||||
// preloaded, so the opponent runs on two frames whichever tour you picked.
|
||||
'p_idle1_f','p_idle2_f','p_bounce_f','p_bounce2_f','p_toss_f','p_smash_f','p_pull_f',
|
||||
'p_run1_f','p_run2_f','p_run3_f','p_run4_f','p_swing1_f','p_swing2_f',
|
||||
'p_run1_f','p_run2_f','p_run3_f','p_run4_f','p_run5_f','p_run6_f','p_swing1_f','p_swing2_f',
|
||||
// The death is DRAWN for frames 1-3 and RECOLOURED for 4-5. The generator refuses to
|
||||
// draw a dead woman in any wording worth using, and frames 4-5 survive that fine —
|
||||
// she is flat and face down, then a heap of torn cloth — so those two are the men's
|
||||
@@ -288,7 +287,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
// could actually see was blue popping in after three frames of white.
|
||||
'p_die1_f','p_die2_f','p_die3_f','p_die4_f','p_die5_f',
|
||||
'o_idle1_f','o_idle2_f','o_bounce_f','o_bounce2_f','o_toss_f','o_smash_f',
|
||||
'o_run1_f','o_run2_f','o_swing1_f','o_swing2_f',
|
||||
'o_run1_f','o_run2_f','o_run3_f','o_run4_f','o_run5_f','o_run6_f','o_run7_f','o_run8_f','o_swing1_f','o_swing2_f',
|
||||
'z_c1_f','z_c2_f','z_c3_f','z_c4_f','z_cw1_f','z_cw2_f','z_cw1b_f','z_cw2b_f',
|
||||
'z_r1_f','z_r2_f','z_r3_f','z_r4_f',
|
||||
'z_rw1_f','z_rw1b_f','z_rw1s_f','z_rw2_f','z_rw2b_f','z_rw2s_f',
|
||||
@@ -1444,10 +1443,11 @@ function drawChar(ch) {
|
||||
// on a phone, at thumb speed, the middle stages are never read — the three states are the beat.
|
||||
if (G.aim && G.aim.mag > 0 && ch.side === 'P' && score.server === 'P' && ch.pose === 'idle')
|
||||
name = 'p_pull';
|
||||
// Four-frame run where the frames exist (the player), two where they don't (the opponent). Same
|
||||
// CYCLE rate either way — more frames subdivide the stride, they do not speed it up, exactly as
|
||||
// ZFRAMES does for the shamblers.
|
||||
const runN = SPR[pre + 'run4'] ? 4 : 2;
|
||||
// The run cycle at whatever count the character's sheet carries: the _runroll.py cycles are 6
|
||||
// (one call, one row, one character — see assets/_runroll.py for why the old per-frame art
|
||||
// flickered); un-rolled characters fall back to 4 or 2. Same CYCLE rate at every count — more
|
||||
// frames subdivide the stride, they do not speed it up, exactly as ZFRAMES does for the shamblers.
|
||||
const runN = SPR[pre + 'run8'] ? 8 : SPR[pre + 'run6'] ? 6 : SPR[pre + 'run4'] ? 4 : 2;
|
||||
if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * 4.5 * runN) % runN + 1);
|
||||
// (the receiver used to flip to run1/run2 here as a "split-step" — it read as a man sprinting on
|
||||
// the spot, because the run frames are a full leaning stride, not a ready-hop. He idles now.)
|
||||
|
||||