NAME
Physics::Terrain - a destructible field, bodies that walk and fall on it, shells that carve it
VERSION
Version 0.01
SYNOPSIS
use Physics::Terrain;
my $field = Physics::Terrain->new(seed => 7, teams => 2);
my $out = $field->run_turn(0,
[ [0, Physics::Terrain::RIGHT], [60, 0] ],
{ tick => 90, weapon => 0, angle => 512, power => 70 });
say $out->{events}[0][1]; # 'fire'
say $out->{end}{health}[4]; # what the shell left of Blue's first
# a tick at a time, for a live player or a bot
$field->start_turn(1);
my $phase = $field->advance(Physics::Terrain::LEFT);
$phase = $field->advance(0, { weapon => 1, angle => 1536, power => 40 });
$phase = $field->advance(0) while $phase ne 'done';
my $same = $field->outcome;
DESCRIPTION
A field of cells generated from a seed, with a ground, overhangs and caves; bodies of one size that stand on it, walk up and down three cells at a time, jump and fall; projectiles that fly under gravity and wind, bounce, split in the air or burst on contact or on a fuse; and explosions that carve a disc out of the ground, hurt everything in reach and throw it. A chunk cut free floats. The engine knows nothing about a game beyond a seat number on each body: it places a squad per seat, plays one turn from an input log, and reports what happened.
Everything is an integer, and the same inputs give the same integers on every platform. Positions and velocities are at 1/256 of a cell; a tick is 1/60 s; an angle is 0 to 4095 with 0 to the right and 1024 straight up; a power is 1 to 100; wind is -20 to 20. A turn is live for up to 1800 ticks of input and then settles for up to 1800 more, and running past that is reported as an error, never as a silent stop.
The recorded turns and the mask digests under t/fixtures come from the JavaScript prototype the engine was transliterated from, and the engine reproduces them bit for bit, which is what lets a browser simulate a turn live and a server replay the same input log to the same result.
CONSTANTS
The held-key bits an input log carries, and the modes a body reports.
- LEFT
-
1, walking left.
- RIGHT
-
2, walking right.
- JUMP
-
4, held once per jump.
- STANDING
-
Mode 0.
- WALKING
-
Mode 1.
- FALLING
-
Mode 2.
- FLYING
-
Mode 3: jumping, or thrown by a blast.
CLASS METHODS
new
my $field = Physics::Terrain->new(%options);
my $field = Physics::Terrain->new(\%options);
Builds a field and places the bodies. The options:
- seed
-
An unsigned 32-bit integer. The field, the landing spots and every turn's wind follow from it. Default 1.
- teams, per_team
-
How many seats and how many bodies each, 1 to 4 and 1 to 8, at most 32 bodies in all. Default two teams of four. Seat
t's bodies are indicest x per_teamonward, in placement order. - place
-
teams(the default) places every team on generated landing spots;explicitplaces thebodiesgiven;noneplaces nobody. - bodies
-
For
explicit:[[seat, x, y_from, hp], ...]. Each body is dropped onto the first ground at or belowy_fromin columnx;hpis optional and defaults to 100. Givingbodiesimpliesexplicit. - gen
-
A hash of generator settings.
profileisnoise(the default),flat(solid from rowfloordown) orempty.WandHsize the field, default 1280 by 640. The noise settingscoarse,fine,cave,threshold,biasScale,biasOffset,caveLo,caveHi,caveTop,caveBottom,platformandheadroomtake the prototype's defaults when absent. - sculpt
-
Ops applied after generation, in order:
['fill', x0, y0, x1, y1],['clear', x0, y0, x1, y1],['slope', x0, y0, x1, y1](every cell on or below the segment),['disc', x, y, r]and['platform', x, ground_row, half_width, headroom]. - wind
-
Fixes every turn's wind; otherwise each turn draws one from the seed.
- live_cap, settle_cap
-
How many ticks of input a turn takes before it expires, and how many ticks it may take to settle after the shot before that is reported as the
tick caperror. Default 1800 each.
launch
my ($vx, $vy) = Physics::Terrain->launch($weapon, $angle, $power);
The velocity a weapon leaves the muzzle with, in units a tick, without firing.
weapons
my $list = Physics::Terrain->weapons;
The five weapons as hashes: id, name, kind, speedMax, wind, fuse, radius, damage, knock, and where they apply bounce, friction, range, count, spread and pop. A shot names a weapon by its index here: 0 the bazooka, 1 the grenade, 2 the shotgun, 3 the cluster, 4 the dynamite.
abi_version
The version of the C table include/pt_abi.h publishes.
THE FIELD
width, height, seed, teams, per_team, tick
The field's size in cells, its seed, how it was populated, and the ticks simulated so far over every turn.
solid
my $ground = $field->solid($x, $y);
Whether a cell is ground. Every cell outside the field is air.
swept
my ($hit, $x, $y, $px, $py) = $field->swept($x0, $y0, $x1, $y1);
The first solid cell on the segment between two cells, both ends included, and the last free cell before it. A wall one cell thick is never crossed, even at a corner.
carve
my $cleared = $field->carve($x, $y, $r);
Clears every cell within r of the centre, compared squared, and returns how many were ground. The ring just outside is marked scorched.
sculpt
$field->sculpt([ ['fill', 0, 400, 1279, 639] ]);
Applies sculpt ops, as new does.
surface_at
my $row = $field->surface_at($x, $y_from);
The first ground row at or below y_from in a column, or -1.
count
How many cells are ground.
mask
my $digest = Digest::SHA::sha256_hex($field->mask);
The field as packed bytes, row-major, eight cells a byte, the leftmost cell in the low bit. The fixtures record the SHA-256 of this.
craters, graves
Every crater carved so far as [x, y, r], in order, and every body that died in place as [seat, x, y].
THE BODIES
add_body
my $index = $field->add_body($seat, $x, $ground_row);
Stands a body on a ground row. Returns undef over the cap of 32, and error then says bodies.
body, bodies, body_count
my $b = $field->body(3);
A body as a hash: index, seat, k (its number within its seat), x, y (the feet, in units), vx, vy, mode, hp, alive, facing. bodies is all of them.
alive
my $n = $field->alive; # every seat
my $n = $field->alive($seat);
How many bodies are alive.
A TURN
run_turn
my $out = $field->run_turn($active, \@inputs, $shot);
Plays a whole turn for one body from a recorded input log and returns the outcome. @inputs is [[tick, bits], ...], one entry per change of the held keys; $shot is {tick, weapon, angle, power} or undef for a turn that ends without one. The outcome hash carries engine, seed, active, wind, inputs (the log as recorded, canonical), shot, events as [tick, kind, a, b] in order (the kinds are jump, fire, ray, hit, explode, bounce, split, lost, hurt, die, out, land, blast, expire and error), craters as [x, y, r] in order, trace with per-body and per-projectile position lists, hashes (the state hash every 64 ticks), end (the tick, every body, every body's health and the crater count), ticks, settledAt and error: undef, or tick cap, weapon, projectiles, box, memory or state.
start_turn
my $wind = $field->start_turn($active);
Begins a turn for a body and returns its wind, or undef for a body that does not exist.
advance
my $phase = $field->advance($bits, $shot);
One tick. $bits is what is held this tick; $shot is given on the one tick it fires. Returns live, settle or done.
phase, turn_tick, error
Where the turn is, how many ticks it has run, and the current error name or undef.
outcome
The outcome of the turn so far, in the shape run_turn returns.
hash
my ($h1, $h2) = $field->hash;
The state hash as two unsigned 32-bit halves, over the tick, the wind, the craters and every body and projectile.
snapshot, restore
my $snap = $field->snapshot;
$field->restore($snap);
Everything a turn changes, so a turn can be replayed from the same start. A snapshot is a Physics::Terrain::Snapshot and is only good for the field it came from.
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)