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.

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 indices t x per_team onward, in placement order.

place

teams (the default) places every team on generated landing spots; explicit places the bodies given; none places nobody.

bodies

For explicit: [[seat, x, y_from, hp], ...]. Each body is dropped onto the first ground at or below y_from in column x; hp is optional and defaults to 100. Giving bodies implies explicit.

gen

A hash of generator settings. profile is noise (the default), flat (solid from row floor down) or empty. W and H size the field, default 1280 by 640. The noise settings coarse, fine, cave, threshold, biasScale, biasOffset, caveLo, caveHi, caveTop, caveBottom, platform and headroom take 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 cap error. 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)