Wind the serve up with the pull, and stop the frames flickering
Four new frames (p_pull1..4) turn the slingshot drag into the serve's wind-up: racket above the head, behind the head, behind the back, fully coiled, picked off pullFrac — the same 0..1 that sets the shot's power — so the gesture and the man are one movement and the release snaps to the smash. The two bounce frames are one drawing now. p_bounce2 is p_bounce with the free arm sheared up 12px and nothing else touched, so the swap cannot flicker, and it follows the ball instead of its rising phase; BOUNCE_IDLE was re-measured off the sprite so the ball tops out under the palm. p_toss had the tossed ball drawn into it — cut, canvas height kept, because the game scales him off img.height. The runner: hip centres sat -2.0/+7.1/-6.0/-0.1 px off the sprite centre, a 13px lateral jump at 12 frames a second, and Oliver's diagnosis was right that all four frames leaned the same way and lifted the same leg. Frames 3 and 4 mirrored, all four padded to one width with the hip on the centre line. assets/_derive.py holds the three derivations, since "the same drawing with one small difference" is the one thing the generator cannot draw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
@@ -5,7 +5,7 @@ everything they specified is shipped, and what still matters from them is record
|
|||||||
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
|
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
|
||||||
detail of any individual change, read the git log.
|
detail of any individual change, read the git log.
|
||||||
|
|
||||||
Last updated 2026-07-31. **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
Last updated 2026-07-31 (serve wind-up pass). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
||||||
shipping it"** (all three just above the CLOSED sections) — the first tells you how Oliver's feedback
|
shipping it"** (all three just above the CLOSED sections) — the first tells you how Oliver's feedback
|
||||||
should be handled, the second is everything known to be outstanding in the game, the third is the
|
should be handled, the second is everything known to be outstanding in the game, the third is the
|
||||||
demo/store/campaign plan, which is decided but entirely unbuilt. All four playtest rounds are closed
|
demo/store/campaign plan, which is decided but entirely unbuilt. All four playtest rounds are closed
|
||||||
@@ -384,6 +384,18 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
|
|||||||
survives as a rim on a green court.
|
survives as a rim on a green court.
|
||||||
12. **Two-row "style reference / redraw this" input does NOT work.** The model returns both rows and
|
12. **Two-row "style reference / redraw this" input does NOT work.** The model returns both rows and
|
||||||
invents a background. Describe the target detail level in words instead.
|
invents a background. Describe the target detail level in words instead.
|
||||||
|
13. **`assets/_derive.py` is where a frame that must MATCH another frame is made.** Three of them
|
||||||
|
live there now: `p_bounce2` (a per-column shear of `p_bounce` — the free arm bends, nothing else
|
||||||
|
moves), `p_toss` (the drawn-in ball cut out, canvas height KEPT so drawChar keeps his scale) and
|
||||||
|
the runner's hip registration. "Same drawing, one small difference" is the one request the
|
||||||
|
generator cannot fill: it returns a different man and the swap reads as a flicker, which is the
|
||||||
|
defect you were fixing. The bounce/toss steps are idempotent; the runner step is `--runner` and
|
||||||
|
must run exactly once per generated set, because it mirrors and it pads.
|
||||||
|
14. **A new pose needs a STANDING frame in the same sheet, as frame 0, thrown away after.** A sprite's
|
||||||
|
drawn size comes from its total height, so a sheet of nothing but arms-overhead poses has no
|
||||||
|
honest scale to anchor on and the man comes out short. `_regen.py`'s `ref` field does this, but
|
||||||
|
only when `ref_only` is unused — with `ref_only` set, list the throwaway as an extra sprite
|
||||||
|
(`p_pull0`) and delete the PNG afterwards.
|
||||||
|
|
||||||
Known generator quirks, all still true:
|
Known generator quirks, all still true:
|
||||||
- It sometimes ignores the magenta (harmless — the key uses the corner colour).
|
- It sometimes ignores the magenta (harmless — the key uses the corner colour).
|
||||||
@@ -397,6 +409,34 @@ Known generator quirks, all still true:
|
|||||||
- **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel
|
- **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel
|
||||||
linear map onto the original's mean and sd, then verify with a mean-difference number.
|
linear map onto the original's mean and sd, then verify with a mean-difference number.
|
||||||
|
|
||||||
|
## Session log: the serve is the gesture (2026-07-31, after the art pass)
|
||||||
|
|
||||||
|
Oliver's notes on the art pass: the zombies are good, the runner "flickers very hard", and the
|
||||||
|
player has three problems. All four fixed, all four measured first.
|
||||||
|
|
||||||
|
- **The pull IS the wind-up.** Four new frames (`p_pull1..4`, `assets/_regen_batches.json` batch
|
||||||
|
`player_pull`) — racket above the head, behind the head, behind the back, fully coiled — chosen in
|
||||||
|
`drawChar` off `pullFrac(G.aim.mag)`, the same 0..1 that sets the shot's power. Drag further, coil
|
||||||
|
further; release fires `p_smash`. Verified per stage with drawImage instrumentation:
|
||||||
|
mag 10/50/90/130 -> pull1/pull2/pull3/pull4.
|
||||||
|
- **The bounce pair is now ONE drawing.** `p_bounce2` is `p_bounce` with the free arm sheared up 12
|
||||||
|
px (0.10 m) — everything else is pixel-identical, so the swap cannot flicker. The swap follows the
|
||||||
|
BALL now (`|sin| > 0.5`) instead of its rising phase, and `BOUNCE_IDLE` was re-measured off the
|
||||||
|
sprite (`apex` 0.55 -> 0.6, `dx` 0.5 -> 0.43) so the ball tops out under the raised palm instead of
|
||||||
|
passing beside it.
|
||||||
|
- **`p_toss` had the tossed ball drawn into it.** Cut, without re-cropping the canvas: the game
|
||||||
|
scales him off `img.height`, so trimming the dead space would have shrunk him. Still 100x308.
|
||||||
|
- **The runner.** Two measured defects, both in the PNGs, no code change. Its four frames were
|
||||||
|
bbox-cropped and drawn centred on the body's x, so hip centres sat -2.0/+7.1/-6.0/-0.1 px off
|
||||||
|
centre — a 13 px lateral jump at 12 frames a second, which is the flicker. And Oliver's own
|
||||||
|
diagnosis was right: all four frames leaned the same way and lifted the SAME leg, so the cycle
|
||||||
|
never used his other side. Frames 3 and 4 are mirrored (he is front-on, so a mirror costs nothing)
|
||||||
|
and all four are padded to one width with the hip on the centre line.
|
||||||
|
|
||||||
|
Not done, and the honest reason: the runner's cadence is still time-driven (12 fps) rather than
|
||||||
|
stepped off distance covered like every other body. It did not read as wrong once the frames stopped
|
||||||
|
jumping, and it is one line if a playtest says otherwise.
|
||||||
|
|
||||||
## Session log: the art-resolution pass (2026-07-31)
|
## Session log: the art-resolution pass (2026-07-31)
|
||||||
|
|
||||||
**What changed.** The whole character roster was redrawn at real pixel-art resolution, plus a batch
|
**What changed.** The whole character roster was redrawn at real pixel-art resolution, plus a batch
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# _derive.py — the frame fixes that must NOT go through the generator.
|
||||||
|
#
|
||||||
|
# python3 assets/_derive.py
|
||||||
|
#
|
||||||
|
# Three sprites are derived from other sprites by arithmetic, because the thing that matters about
|
||||||
|
# them is that they stay pixel-identical to their neighbour. Ask the generator for "the same drawing
|
||||||
|
# with one small difference" and it returns a different man, and the swap reads as a flicker — which
|
||||||
|
# is exactly the bug each of these fixes. Re-run this after regenerating p_bounce or the runner;
|
||||||
|
# every step is idempotent, so running it twice changes nothing.
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
import os
|
||||||
|
|
||||||
|
A = os.path.dirname(os.path.abspath(__file__)) + '/'
|
||||||
|
load = lambda n: np.array(Image.open(A + n + '.png').convert('RGBA'))
|
||||||
|
save = lambda a, n: Image.fromarray(a, 'RGBA').save(A + n + '.png')
|
||||||
|
|
||||||
|
# 1. p_toss — the generator drew the tossed ball into the sprite; the game draws the real ball.
|
||||||
|
# The CANVAS keeps its height: drawChar scales the man off img.height, so re-cropping the dead
|
||||||
|
# space would shrink him.
|
||||||
|
a = load('p_toss')
|
||||||
|
rows = (a[..., 3] > 128).any(1)
|
||||||
|
cut = int(np.argmax(~rows)) # the ball floats clear above the raised hand
|
||||||
|
if cut and not rows[cut:cut + 8].any():
|
||||||
|
a[:cut] = 0; save(a, 'p_toss')
|
||||||
|
print(f'p_toss: ball removed (rows 0..{cut})')
|
||||||
|
else:
|
||||||
|
print('p_toss: already ball-free')
|
||||||
|
|
||||||
|
# 2. p_bounce2 — p_bounce with the free arm bent so the hand rises 12 px (0.10 m) to meet the ball
|
||||||
|
# at the top of its bounce. A per-column shear, pinned at the shoulder and rigid from the wrist out,
|
||||||
|
# so the forearm bends instead of the hand detaching.
|
||||||
|
X0, K, MAX = 96, 0.8, 12
|
||||||
|
b = load('p_bounce'); out = b.copy()
|
||||||
|
for x in range(X0, b.shape[1]):
|
||||||
|
s = min(MAX, round(K * (x - X0)))
|
||||||
|
if s: out[:-s, x] = b[s:, x]; out[-s:, x] = 0
|
||||||
|
save(out, 'p_bounce2')
|
||||||
|
print('p_bounce2: derived from p_bounce')
|
||||||
|
|
||||||
|
# 3. z_f1..4 — the runner, with `--runner`. This one is NOT idempotent (it mirrors, and it pads),
|
||||||
|
# so it is opt-in: run it exactly once on a freshly generated set of runner frames.
|
||||||
|
# Two defects, both measured. The generator drew all four frames leaning the same way and lifting
|
||||||
|
# the SAME leg, so the cycle never used his other side — mirroring frames 3 and 4 gives an
|
||||||
|
# alternating stride out of the poses that exist (he is front-on, so a mirror is free). And each
|
||||||
|
# frame is cropped to its own bbox and drawn centred on the body's x, so a swinging arm slid the
|
||||||
|
# whole man sideways: hip centres sat -2.0/+7.1/-6.0/-0.1 px off centre, a 13 px lateral jump at 12
|
||||||
|
# frames a second — that was the hard flicker. Pad every frame to one width with the HIP on the
|
||||||
|
# centre line. Padding is free: drawStatic derives the drawn width from img.width, so the figure
|
||||||
|
# inside keeps its scale and only its anchor moves.
|
||||||
|
import sys
|
||||||
|
if '--runner' not in sys.argv:
|
||||||
|
print('z_f1..4: skipped (pass --runner; it mirrors and pads, so it must run only once)')
|
||||||
|
raise SystemExit
|
||||||
|
NS = ['z_f1', 'z_f2', 'z_f3', 'z_f4']
|
||||||
|
for n in NS[2:]:
|
||||||
|
save(np.array(Image.open(A + n + '.png').convert('RGBA').transpose(Image.FLIP_LEFT_RIGHT)), n)
|
||||||
|
print(f'{n}: mirrored — the other leg leads')
|
||||||
|
ims = [load(n) for n in NS]
|
||||||
|
def hip(a): # midpoint of the hip band, not the mean: an arm
|
||||||
|
al = a[..., 3] > 128 # crossing the band would drag a mean sideways
|
||||||
|
xs = np.nonzero(al[int(al.shape[0] * .50):int(al.shape[0] * .62)].any(0))[0]
|
||||||
|
return (xs.min() + xs.max()) / 2
|
||||||
|
hips = [hip(a) for a in ims]
|
||||||
|
W = int(np.ceil(max(max(h, a.shape[1] - h) for h, a in zip(hips, ims)) * 2))
|
||||||
|
for n, a, h in zip(NS, ims, hips):
|
||||||
|
left = int(round(W / 2 - h))
|
||||||
|
o = np.zeros((a.shape[0], W, 4), np.uint8)
|
||||||
|
o[:, left:left + a.shape[1]] = a
|
||||||
|
save(o, n)
|
||||||
|
print(f'z_f1..4: hip-registered onto {W}px')
|
||||||
@@ -409,5 +409,22 @@
|
|||||||
"check": "loose",
|
"check": "loose",
|
||||||
"scale": 1.4,
|
"scale": 1.4,
|
||||||
"subject": "The reference is an enormous fat zombie brute \u2014 huge belly, stained shirt, brown trousers. Draw TWO frames of that SAME body at that SAME enormous size and bulk, standing, as two stages of damage: (1) both arms gone and the chest and belly torn wide open with the ribs showing through; (2) all of that AND no head at all \u2014 the neck ends in a ragged red stump at shoulder height, no skull, no face, nothing above the shoulders. Do not slim him down. Output exactly two figures in one row, far apart."
|
"subject": "The reference is an enormous fat zombie brute \u2014 huge belly, stained shirt, brown trousers. Draw TWO frames of that SAME body at that SAME enormous size and bulk, standing, as two stages of damage: (1) both arms gone and the chest and belly torn wide open with the ribs showing through; (2) all of that AND no head at all \u2014 the neck ends in a ragged red stump at shoulder height, no skull, no face, nothing above the shoulders. Do not slim him down. Output exactly two figures in one row, far apart."
|
||||||
|
},
|
||||||
|
"player_pull": {
|
||||||
|
"sprites": [
|
||||||
|
"p_pull0",
|
||||||
|
"p_pull1",
|
||||||
|
"p_pull2",
|
||||||
|
"p_pull3",
|
||||||
|
"p_pull4"
|
||||||
|
],
|
||||||
|
"ref_only": [
|
||||||
|
"p_idle1",
|
||||||
|
"p_toss",
|
||||||
|
"p_smash"
|
||||||
|
],
|
||||||
|
"anchor": 0,
|
||||||
|
"check": "same",
|
||||||
|
"subject": "The reference shows the SAME tennis player three times \u2014 white shirt, blue shorts, white headband, seen FROM BEHIND: standing, tossing, and smashing. Draw FIVE frames of that same player, same size, same style, same BRIGHT WHITE shirt and BRIGHT BLUE shorts \u2014 do not wash the colours out. Every figure is seen strictly FROM BEHIND, the back of his head towards you, NO face, NO eyes, NO nose. Frame 1 is him simply STANDING upright and relaxed, arms down, racket in his right hand \u2014 a plain standing frame. Frames 2 to 5 are four stages of ONE continuous serve wind-up and each must be clearly further into it than the one before. Through all four his LEFT arm stays thrown up high above him where he has just released the ball, and NO BALL is drawn anywhere \u2014 the game draws the ball. Frame 2: standing tall, the racket held UP ABOVE HIS HEAD in his right hand, body straight. Frame 3: the racket has dropped behind his head, his back beginning to arch backwards, knees slightly bent. Frame 4: the racket is down behind his back, his back clearly arched backwards, chest pushed up and out, knees bent. Frame 5: fully coiled \u2014 the racket hangs low behind his back near his waist, his back arched hard backwards, both knees deeply bent, weight back on his heels, ready to explode upwards. Output exactly five figures in one row, evenly spaced and far apart."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -333,7 +333,9 @@ export const POSE_TIME = 0.38;
|
|||||||
|
|
||||||
// the server's idle ball bounce: sideways offset to the free hand, apex height, tempo.
|
// the server's idle ball bounce: sideways offset to the free hand, apex height, tempo.
|
||||||
// tempo drives the ball, the hand sprite AND the pock sound — one value, always in sync.
|
// tempo drives the ball, the hand sprite AND the pock sound — one value, always in sync.
|
||||||
export const BOUNCE_IDLE = { dx: 0.5, apex: 0.55, tempo: 3.6 };
|
// Measured off the sprite, not guessed: in p_bounce2 the raised palm sits 0.63 m above his feet and
|
||||||
|
// 0.43 m to the side of his centre line, so the ball tops out just under the hand instead of beside it.
|
||||||
|
export const BOUNCE_IDLE = { dx: 0.43, apex: 0.6, tempo: 3.6 };
|
||||||
|
|
||||||
// generated sprite sheets: the idle pose is this many pixels tall; all scales derive from it
|
// generated sprite sheets: the idle pose is this many pixels tall; all scales derive from it
|
||||||
export const SPRITE_H = 224; // 2x remaster (was 112). RES is 3, so the player at the baseline
|
export const SPRITE_H = 224; // 2x remaster (was 112). RES is 3, so the player at the baseline
|
||||||
|
|||||||
@@ -97,6 +97,6 @@
|
|||||||
<p id="build"></p>
|
<p id="build"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=76"></script>
|
<script type="module" src="main.js?v=77"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { cv, ctx } from './dom.js';
|
import { cv, ctx } from './dom.js';
|
||||||
import { TUNE, UI, EATEN_GRAB, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, BOUNCE_IDLE, SPRITE_H } from './config.js';
|
import { TUNE, UI, EATEN_GRAB, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, BOUNCE_IDLE, SPRITE_H } from './config.js';
|
||||||
import { P, O, ball, score, crowd, G } from './state.js';
|
import { P, O, ball, score, crowd, G } from './state.js';
|
||||||
import { predictLanding, flickTarget } from './gameplay.js';
|
import { predictLanding, flickTarget, pullFrac } from './gameplay.js';
|
||||||
import { zombies, gore, piles, parts, pellets, extras, shots } from './zombies.js';
|
import { zombies, gore, piles, parts, pellets, extras, shots } from './zombies.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------- palette ----
|
// ---------------------------------------------------------------- palette ----
|
||||||
@@ -155,9 +155,10 @@ export function proj(x, y, z) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- sprites ----
|
// ---------------------------------------------------------------- sprites ----
|
||||||
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
||||||
const SPRV = 69; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
const SPRV = 70; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
||||||
const SPR = {};
|
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_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
|
||||||
|
'p_pull1','p_pull2','p_pull3','p_pull4', // the serve WIND-UP, one frame per quarter of the pull (see drawChar)
|
||||||
'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_run1','o_run2','o_swing1','o_swing2',
|
||||||
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4','x_crowd','x_crowd2','x_ads',
|
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4','x_crowd','x_crowd2','x_ads',
|
||||||
'x_boymop1','x_boymop2','x_boymop3','x_boymop4', // the push cycle, used only while he is stopped and working
|
'x_boymop1','x_boymop2','x_boymop3','x_boymop4', // the push cycle, used only while he is stopped and working
|
||||||
@@ -478,10 +479,19 @@ function drawChar(ch) {
|
|||||||
const pre = ch.side === 'P' ? 'p_' : 'o_';
|
const pre = ch.side === 'P' ? 'p_' : 'o_';
|
||||||
let name = pre + (Math.floor(G.idleT * 1.6) % 2 ? 'idle1' : 'idle2'); // idle weight shift
|
let name = pre + (Math.floor(G.idleT * 1.6) % 2 ? 'idle1' : 'idle2'); // idle weight shift
|
||||||
if (G.phase === 'pretoss' && ch.side === score.server && ch.pose === 'idle') {
|
if (G.phase === 'pretoss' && ch.side === score.server && ch.pose === 'idle') {
|
||||||
const w = G.idleT * BOUNCE_IDLE.tempo; // same phase as the bouncing ball
|
// The two bounce frames are ONE drawing whose free hand sits 0.10 m apart (p_bounce2 is sheared
|
||||||
const rising = Math.sin(w) * Math.cos(w) > 0;
|
// out of p_bounce, see assets/_bendarm.py) — generated as a pair they came back as two different
|
||||||
name = pre + (rising ? 'bounce2' : 'bounce'); // hand up while the ball rises, pat down exactly at the apex
|
// men and the swap read as a flicker. So the swap can follow the BALL: hand up when the ball is
|
||||||
|
// up at it, hand down when the ball is near the ground. BOUNCE_IDLE.apex is measured off the
|
||||||
|
// raised hand, so the ball meets the palm rather than passing it.
|
||||||
|
name = pre + (Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) > 0.5 ? 'bounce2' : 'bounce');
|
||||||
}
|
}
|
||||||
|
// THE PULL IS THE WIND-UP. While the slingshot is drawn, the frame is chosen by how far back the
|
||||||
|
// finger has come: racket above the head -> behind the head -> behind the back -> fully coiled.
|
||||||
|
// Four drawn stages over pullFrac (the same 0..1 that sets the shot's power), so the gesture and
|
||||||
|
// the man are the same movement, and the release snaps to p_smash.
|
||||||
|
if (G.aim && G.aim.mag > 0 && ch.side === 'P' && score.server === 'P' && ch.pose === 'idle')
|
||||||
|
name = 'p_pull' + (1 + Math.min(3, Math.floor(pullFrac(G.aim.mag) * 4)));
|
||||||
// Four-frame run where the frames exist (the player), two where they don't (the opponent). Same
|
// 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
|
// CYCLE rate either way — more frames subdivide the stride, they do not speed it up, exactly as
|
||||||
// ZFRAMES does for the shamblers.
|
// ZFRAMES does for the shamblers.
|
||||||
|
|||||||