# A shrinking zone in your game (zone.shrink) ## What it is for `zone.shrink` manages a circular safe zone that closes in by phases of data: it holds still, then shrinks to a radius over some seconds, around a centre that may slide. Every entity that carries one of the `affects` tags (and none of the `ignore` tags) is placed against the edge, on foot or in a vehicle. Outside, it either takes damage every `damageEvery` seconds (the storm of a battle royale), is thrown out with an event (a sumo ring, a party round), or nothing happens beyond the events. The kit draws the wall, the edge on the ground and the next circle; deciding who is eliminated is left to other kits. ## Turn it on Install `zone.shrink` (it requires `game.engine@^1`) and give the project a `zone_phases` collection. Without phases the zone keeps its config radius, a fixed ring. With `autoStart: false` the zone waits for `start()`. ```json [ { "id": "first", "wait": 30, "shrink": 65, "radius": 140, "damage": 2 }, { "id": "second", "wait": 20, "shrink": 65, "radius": 86, "damage": 4 }, { "id": "last", "wait": 5, "shrink": 20, "radius": 0, "damage": 10 } ] ``` ## First example The fixture starts a zone of `radius: 8` with `damage: 1.5`, `damageEvery: 0.5` and `margin: 0.25`, and two phases: `first` (hold 1 s, shrink 1 s to radius 5, damage 3) then `second` (shrink 0.5 s to radius 2, centre sliding to `[1, 0.5]`). A phase whose `radius` is not a number is skipped. The player (tag `player`) and an NPC (tag `contestant`) are watched; a prop without the tags and a `contestant` tagged `dead` are not. The trace checks `state()`, `view()`, `outside()`, `isOutside`, `contains`, `distance`, and the events and damage over the following ticks. ## Settings and defaults | Setting | Type | Default | Meaning | |---|---|---|---| | `center` | `[x, z]` | `[0, 0]` | Centre of the zone when it starts. | | `radius` | number | `60` | Radius when it starts, before the first phase. | | `outside` | string | `damage` | `damage`, `out` or `none`. | | `damage` | number | `2` | Damage per second outside, until a phase says otherwise. | | `damageEvery` | number | `1` | Seconds between two damage ticks. | | `margin` | number | `0` | Metres past the edge before an entity counts as outside (negative: before the edge). | | `affects` | list | `player, contestant` | Tags of watched entities (any of them). | | `ignore` | list | `dead, eliminated, spectator` | Tags that take an entity out of the watch. | | `autoStart` | boolean | `true` | Start with the game; off: call `start()`. | | `wallHeight` | number | `40` | Height of the wall at the edge (0: ground circle only). | | `color` | string | `#a177f5` | Colour of the wall and the edge. | | `showNext` | boolean | `true` | Draw the circle the zone is closing in to. | | `wallOpacity` | number | `0.15` | Opacity of the wall. | | `rim` | boolean | `false` | A glowing rim along the edge (a sumo ring). | | `pylons` | integer | `0` | Lamp pylons around the starting circle (0 to 64). | | `halo` | boolean | `false` | A glowing halo far under the starting circle. | A phase `{ id, radius, wait?, shrink?, center?, damage? }`: `wait` seconds of hold, then a linear shrink over `shrink` seconds to `radius`, sliding to `center`; `damage` replaces the outside damage from that phase on. With `outside: damage`, a tick of `damage x damageEvery` goes through `combat.damage` when the entity has combat health, through `vehicle.damage` for a vehicle, else it removes hp from the entity's `health` component. `combat.core` is optional. ## Services, events, variables - `zone.storm` (service): `state()` (`started`, `stage`, `index`, `phase`, `center`, `radius`, `target`, `targetCenter`, `timer`, `damage`), `start()`, `stop()`, `contains(x, z, margin?)`, `distance(x, z)` (negative inside), `outside()` (entity ids), `isOutside(entity)`, `view()` (what the renderer draws). - Events: `zone.started`, `zone.phase_started`, `zone.shrinking`, `zone.closed`, `zone.finished`, `zone.stopped`, `zone.left` and `zone.returned` (`{ entity }`), `zone.expelled` (`{ entity, radius, distance }`), `zone.damaged` (`{ entity, amount, applied }`; `applied` is false when no health is known for the entity). - Variables: `zone.stage` (idle, wait, shrink, done), `zone.phase`, `zone.radius`, `zone.timer`, `zone.player_outside`. ## Hooks and modding | Hook | Payload | What a tap does | |---|---|---| | `zone.phase` | `{ phase, index, wait, shrink, radius, damage }` | `wait`, `shrink`, `radius`, `damage` are mutable; cancelled, the phase is skipped. | | `zone.expel` | `{ entity, radius, distance }` | With `outside: out`; cancelled, nothing happens until the entity returns and leaves again. | | `zone.damage` | `{ entity, amount, index }` | With `outside: damage`; `amount` is mutable; cancelled, no damage this tick. | ```json { "id": "shelter", "hook": "zone.damage", "match": { "entity": "e1" }, "mul": { "amount": 0.5 } } ``` A mod can override every config key above and change the `zone_phases` collection. ## Engines The logic is the same Rust implementation on the web and in the native player, which draw the wall and circles. The kit is delivered for the web and Rust targets. ## When it does not do what you expect - Nothing is watched: the entity lacks every `affects` tag, or has an `ignore` tag. - The zone never moves: `autoStart` is off (call `start()`), or there are no valid phases. - No damage with `outside: out`: that mode only emits `zone.expelled`; another kit takes the entity out of the game. - `zone.damaged` with `applied: false`: no health is known for that entity. ## Limits One zone per game, circular only. The kit does not eliminate anyone itself and does not keep score. ## Versions Kit 1.1.0, service `zone.storm` 1.0.0. The ring dressing (`wallOpacity`, `rim`, `pylons`, `halo`) arrived in 1.1.0.