@pleasantmachines/kento-arena
v0.1.0
Published
A strategy map's ground as data: a square grid of levels joined by ramps, authored once for seat 0 and copied for the others by the map's symmetry, rasterised to the masks a game walks and builds by, and checked for the things that make a map unplayable o
Readme
@kento/arena
A strategy map's ground as data: a square grid of levels joined by ramps, authored once for seat 0 and copied for the
others by the map's symmetry, rasterised to the masks a game walks and builds by, and checked for the things that make
a map unplayable or unfair. Pure and deterministic (integers and arithmetic; its only import is
@kento/navigation's flow fields). src/index.ts is the only public entry; symmetryCopies and the transform* copies live in src/symmetry.ts, apart from the
raster, so a first script that only asks how many seats a symmetry holds does not carry the rasteriser. No game nouns: what a start, a resource
field or an objective is stays the game's.
Contract
- Laying out (
ArenaLayout,src/layout.ts): a generator writes places in authored coordinates,new ArenaLayout(size, symmetry, swap, edge)holds each clear in every copy (free(rect, margin),take(rect),copies,flip,toMap,cellToMap), witharenaRect,growRectandrectsOverlapfor the rectangles. - Source (
ArenaSource):size(cells a side),levels,symmetry,areas(levels painted in order over level 0),ramps(a rectangle, its foot level and the way it climbs),blocks(nothing walks, builds or sees through),covers(walked on, never built on) andbreakables(blockers a match may take away). Rectangles are cells, inclusive. - Symmetry (
ArenaSymmetry):none,rotate2(half turn, 1v1),rotate4(quarter turns, four seats),mirror-x,mirror-z.transformPoint,transformRect,transformCell(a footprint's first cell; quarter turns swap its sides),transformClimbandexpandBySymmetrygive copyk; copy 0 is the identity. - Raster (
rasterArena):level,ramp(index intorampsor -1),cliff(a cell over a lower one that is no ramp, the border ring, a ramp's flanks across its climb: a ramp is entered only at its ends),block(blockers and standing breakables),cover,breakable(the standing breakable a cell belongs to, or -1) andbreakables(every breakable's cells,sourceandcopy). It throws on a bad source;sourceProblemsnames the problems first. - Masks (
arenaMasks(raster, footprints)):walk(no cliff, blocker or solid footprint) andbuild(walkable, no cover, no ramp).footprintFitsadds the one-level rule.arenaHeightAt(raster, frame, x, z)is the ground's height in the game's units, blended along a ramp. - Connectors (
connect.ts: a siege tower's ramp, a lift, rubble over a wall: ramps made at runtime across a cliff edge, standing for a while):connectArena(base, connectors)is the authored raster with each connector (anArenaRamp) rasterised as a ramp over the cliff cells it covers (source-1,copyits index) and the cliffs worked out again, soarenaMaskswalks it,arenaHeightAtclimbs it and a vision map readinglevelsees from it as from its foot, exactly as an authored ramp;connectArena(base, [])isbasecell for cell, so taking a connector away is connecting the rest again.connectorProblems(raster, c, footprints?)is the runtime check (off-arena,not-an-edge: every cell a cliff one level over its foot, no blocker, ramp or other connector;footandhead: open ground past either end at its level once it stands;shut), andfindConnectors(raster, at, reach, footprints?)lists the one-cell connectors whose foot is withinreachof a cell, nearest first. The authored map's checks (checkArena: fairness, symmetry, ramp widths) are for the raster as authored: a connected raster is deliberately not its own copy. Pathing over a changed ground is the game's rebuild (its masks into new@kento/navigationfields or guides, or a region grid'sset). - Breakables (
breakables.ts: a rubble plug sealing a tunnel, a barricade, a fallen wall): authored like blockers and copied by the symmetry, each copy its own breakable, and rasterised intoblock, so while they stand every mask, path and check takes them as rock.breakArena(base, broken)isbasewith those breakables' cells open again (out ofblockandbreakable; levels, ramps, cliffs and coversbase's own arrays), soarenaMaskswalks and builds them, flow fields over its walk mask go through, and a vision map whose blockers are itsblocksees through at its next update (@kento/vision reads its blockers each update);breakArena(base, [])isbasecell for cell, and it composes withconnectArena. A breakable overlaps no blocker and no other breakable or copy (sourceProblems), so breaking one opens all its cells.standingBreakables(raster)lists the ones standing;breakableCopy(raster, k, t)is breakablek's counterpart under copyt(a seat's state withkbroken is fair against its counterpart seat's with that one broken), andbreakableStates(raster, most?)every set that may stand broken (2^n, the empty set first), what a map's checks walk to cover every state.checkArenachecks the raster it is given: a state with every copy of a breakable broken is its own copy, one with some broken is not. What breaks one (its health, who may strike it) is the game's. - Painting (an editor's brush):
levelGridis the areas' levels per cell,paintLevels(grid, src, cells, brush)raises, lowers, sets or flattens cells with all their copies (cellCopies), andareasOf(grid, size, symmetry)folds a painted grid back into the fewest rectangles over the authored part (each orbit's least-index cell), so the edited source rasterises to exactly the painted levels. - Objects (
AuthoredObject): the game's kinds as footprints:owned(copy k goes to team k) or neutral (copies landing on the same cells are one),solid,start,slot,ground(build,walkorany) and agroupnaming counterparts.expandObjectsmakes the copies. Teams and seats: each copy of the authored part is a team; every start authored in it is one of the team's seats, itsslot(default 0) saying which, and copy k's start of slot s is seatk + copies * s(ArenaObject.seat;teamis the copy). One start a copy is a free-for-all ofcopiesseats (seats are teams); two starts under a half turn is a 2v2 (seats 0 and 2 against 1 and 3). No symmetry, seats by hand (none: an asymmetric map, a campaign's): nothing is copied, and an owned object'sseat(default itsslot, else 0) andteam(default its seat) are the author's, so a seat holds any number of objects, a start or none, and two seats may share a team. A symmetry ignoresseatandteam. - Validator (
checkArena(raster, objects, { seats, tolerance, fair })): the report (each seat's octile path cost from its start to every object, a fairness row per group and relation (its team's own, neutral, another team's) with its spread over the seats of one slot (a team's two seats stand differently and are not each other's counterparts; each team's first seats are), each ramp's width) and problems codedunreachable,unfair,ramp-blocked(shut cells, or no open ground at its foot or head),overlap,placement,asymmetric(the raster is not its own copy) andseats. Path costs are integers (10 a straight cell, 14 a diagonal), so a symmetric map is exactly fair andtolerancedefaults to 0.fair(default: unless the symmetry isnone) says whether a spread is a problem; an asymmetric map's rows are reported only. - Regions (
ArenaRegion,createRegions(regions, frame),regionProblems(regions, size)): named places a script asks about, a rectangle of cells or a circle round a cell's centrercells wide, authored in cells and read in the game's units through the frame:contains(name, x, z)(a rectangle is its cells' area, its far edge the next cell's; a circle its disc),at(x, z)(the names holding a point),inside(name, things)(the{x, z}things in it, in order),distance(name, x, z)(0 inside, else to its nearest edge),centre(name)andcells(name, size). A name the index lacks throws (the author's bug);regionProblemsnames an empty or repeated name and a shape off the arena. Regions are places, not ground: no mask, path or copy reads them, and a symmetric map's are authored whole. - Paths (
arenaDistances(size, walk, goals),arenaCorridor(size, walk, from, to, slack)): path costs to goal cells, and the cells on a shortest path between two places (or one at mostslacklonger): a map's main paths, for a game's check of what stands on them.
Relief
createArenaRelief(raster, frame, options) gives the flat tiers a shape for the eye: rolling land and shallow gullies,
falling to zero at cliffs, ramps, any change of level, the border and cells the game marks flat. A pure function of the
raster and the seed. It changes no level, cell or path: a game adds at(x, z) to arenaHeightAt where it draws.
Ground mesh
@kento/arena/ground (src/ground.ts, its own subpath so a page that only rasterises a map does not carry it, and free of
any renderer) draws the raster as one static mesh in typed arrays. arenaGroundMesh(raster, frame, { relief, lip, strip,
curb, skirt, shade }) returns unindexed triangles: position, color, uv, normal, each shade.channels value,
facing (-1 turned from shade.light to 1 toward it, on land that rolls) and topVerts, the split between the tops,
ramp curbs and skirt and what follows them, the faces and lit lip strips, for two draws. What it draws:
- Tops: a flat cell is one quad; a lip, a ramp and a cell sharing an edge with a ramp stay 2×2 so the midpoint is shared.
Colour comes from
shade.colourAt(cx, cz, x, z), per vertex, read at once so it may return one scratch object. - Lips:
arenaLipShiftandarenaLipPointset each lip point back onto its high cell (ArenaLip: a set-back, a seeded wander and bay, outer corners rounded in, capped past the first cellstandablesays a body can stand on). A point a half cell in carries half the mean shift, so a deep lip never folds the mesh. Nothing touching a ramp moves, and the low side of an edge stays put, so a face's foot is on the raster's edge. Props on a lip read the same functions. - Faces: split at the edge's midpoint through a mid row, a ledge here and there, with strata between
shade.rockandshade.rock2. A lit stripstripmetres deep behind each lip reads a step by value, never a black line. - Ramps: a lit curb
curbmetres wide on the ramp's surface along each side the ramp climbs past. - Skirt (optional,
ArenaSkirt): land past the arena's edge, level at the edge's height, then falling and darkening. - Relief: every vertex rides
relief(a flat ground iscreateArenaReliefat height 0), smooth-lit where it rolls.
shade.channels names per-vertex values the caller reads from a mesh point; the faces hold 0 in each. A same-input call
gives the same bytes. Also here: arenaEdgeDistance (cells to a plateau's edge), arenaCliffDistance (1 or 2 cells to
another level), arenaReached (a four-sided flood), arenaCliffSlabs (where rock slabs stand along cliffs and ramp sides,
from a caller's stream; the caller's meshes draw them) and arenaHash and arenaNoise, the seeded presentation noise the
lips use, for a game dressing the same ground. Colours, materials, what paves or wears the ground stay the game's.
Users
A game's map kind can be an arena source plus its own features (halls, iron, vents, a gate, watch points, props); its
content check runs checkArena on every map, and its grid rasterises the played map with it.
Known gaps
Connectors are rectangles climbing one level along an axis, like authored ramps; one crossing a corner or two levels is refused.
Levels are flat tiers; a smooth heightfield is a different ground. Ramps are axis-aligned rectangles climbing one level.
Sight blockers are the game's; the arena has no line-of-sight model of its own.
rotate4with a non-square authored part relies on the author keeping to one quarter; overlaps between copies are reported, not prevented.
