# SecondWind 1.2.0
Health, damage, death and respawn for any multiplayer game, in one component. Give players, enemies and breakable props health with one call, hurt them with another, and choose everything else with a setting: six health displays (a plain HP bar, a regenerating shield, armour, Zelda-style hearts, lives, or downed-then-revive), eight damage types with flames, sparks and numbers every player sees, eight death styles, teams and friendly fire, a kill feed in five looks, traps, and genre presets from shooter to party game, all labelled in a catalog you can filter by genre and visual style. Built on Lifeline (health, bars, numbers) and MatchFlow (countdowns, rounds), and multiplayer-safe.
## What's new in 1.2.0
The package is now called **SecondWind**, like this world, and it sits beside Lifeline and MatchFlow. It used to be called healthKit. It is still the same package with the same guid (`5da0a53c-5953-4adc-80fb-f5afb02f6568`), and every call, setting, event, preset and network channel works exactly as in 1.1.4.
- **New require line:** `local HK = require("SecondWind.api")`, in place of `require("healthKit.api")`.
- **New asset names:** the components are `SecondWind.Vitals`, `SecondWind.Rules`, `SecondWind.HUD` and `SecondWind.Trap`, the modules are `SecondWind.catalog`, `SecondWind.logic`, `SecondWind.feed` and `SecondWind.fx`, and the test suite is `secondwind`.
- **Upgrading from 1.1.x:** after you update, change `healthKit.` to `SecondWind.` in your `require` and `asset.resolve` lines. An installed copy stays at the version it was installed at, so a world on 1.1.4 keeps working until it updates.
- **Kept on purpose:** the network channels keep their names (`healthKit:evts`, `healthKit:state` and the rest), and the HUD's screen id stays `healthkit_hud`, so nothing a game sends, listens for or looks up changes.
- **A store listing:** the package description now says what the kit gives a creator, its six health displays and eight death styles, and how to install it with Lifeline and MatchFlow.
## What's new in 1.1.4
Every call, setting, event and preset works exactly as in 1.1.1. This version fixes the first example and the descriptions.
- **The first example works as pasted.** Its dummy stood outside the lava and above it, and nothing in the kit moves a dummy, so it couldn't be pushed in and never burned. It now stands in the edge of the lava, on the ground, and an orange slab shows where the lava is. Checked in a new world holding only the two-call install: the dummy went from 60 to 40 to 20 to dead in about 4 s, came back with full hearts where it stood 4 s later, and did it again; a player standing in the lava burned from 150 to 95 in 3 s.
- **Second Wind opens in the gallery again.** 1.1.3 saved a small test scene as the world's main scene by mistake, so the editor opened in it.
- **Known limits names the public engine issue #19188** for the install problem, instead of two internal ticket ids.
- **The package description says eight death styles**, as the catalog and this README do. It said six and left out `remove` and `stay`, and it called the component `Health` instead of `Vitals`.
## What's new in 1.1.3
Every call, setting, event and preset works exactly as in 1.1.1. This version corrects step 1 again.
- **Install in two calls.** The one-call install that 1.1.2 gave doesn't work. When SecondWind, Lifeline and MatchFlow go in one call, the last two only update the copies SecondWind brought under `/source/deps/` and nothing is installed in `/source`, so SecondWind still can't reach them. Step 1 now installs SecondWind first, then Lifeline and MatchFlow in a second call with `at=/source`. Checked with the published kit in a new world: after the one call, `module 'Lifeline.api' not found` and five publish errors remained. After the second call, the scene built, the player got 150 HP, the lava burned, the hearts dummy died and came back, and `zm push` published the world. If you followed 1.1.2's step 1, run the second call now; it fixes the world in place.
## What's new in 1.1.2
Every call, setting, event and preset works exactly as in 1.1.1. This version fixes the install instructions.
- **The first example works in a brand-new world.** Step 1 used to say Lifeline and MatchFlow come along with SecondWind. In a new world they do arrive, but SecondWind can't reach them: its components fail with `module 'Lifeline.api' not found` and the world can't be published (see Known limits). Step 1 now installs Lifeline and MatchFlow as well. Checked in a new world holding only those three installs: the scene builds, the player gets 150 HP from the `heroShooter` preset, the lava burns, the hearts dummy dies and comes back, the health bar shows, and `zm push` publishes the world. Earlier versions were only checked in the world that authors the kit, which has both installed.
## What's new in 1.1.1
Every 1.0.0 and 1.1.0 call, setting, event and preset works as before. New in this version:
- **Fewer messages per hit.** Events for other peers wait up to 0.05 s and leave together as one `healthKit:evts` message, in order, so a burst (a chain, a blast, a kill and its death) is one message instead of one per event. Listeners on the peer that raised them (`HK.on`) still hear each event at once, and a health snapshot never overtakes the events queued before it. 1.1.1 still receives the one-per-event `healthKit:evt` messages that 1.1.0 peers send, so mixed sessions work.
- **`HK.syncReport().totals`** also counts `evtSent` (event batches sent) and `evtBatched` (the events inside them).
- **A lighter showcase gallery.** Its dummies and damage stations only work while a player is within 18 m (`wakeRadius` on the Gallery component). The dummies drain just as fast with half as many hits, the kill-feed boards rebuild only when the feed changes, signs and pads refresh a few times a second instead of every frame, and name labels are looked up by name instead of by scanning every entity. With a player inside, the gallery's host now sends about 12 to 14 messages a second instead of about 46, a damage request through a browser host comes back in 0.2 to 0.4 s instead of 25 s or more, and on a peer that doesn't host it the Gallery script costs 0.07 ms a frame.
- **Gallery labels stay hidden after a scene rebuild.** Right after a rebuild the engine can briefly refuse to switch a `{PlayerName}` label off; the gallery now retries until it holds.
## What's new in 1.1.0
Every 1.0.0 call, setting, event and preset works as before. New in this version:
- **A labelled catalog.** Every health model, damage type, death style, kill feed look and preset now says its `kind`, its `family`, the `genres` it suits, whether it has a look of its own (`hasLook`) and, if so, its `style`. `HK.find({ kind = "preset", genre = "coop" })` picks by label, and `HK.vocabulary()` lists the words. See [Picking from the catalog](#picking-from-the-catalog).
- **Hit effects for everyone.** Flames, sparks, bubbles and frost now show on every player's screen, not just for the player who landed the hit. Each hit is also written into the target's synced `hitSeq`, `hitType` and `hitAmount`, so a peer that misses the hit message still draws the effect.
- **One health on every screen.** On every death, respawn, revive, life lost and knock-down, the owner sends a full state snapshot (`healthKit:state`) at once, again after 0.5 s and again after 2 s. It also sends one after any change and every 5 s regardless. A peer whose view stays off for more than half a second asks for a fresh one (`healthKit:resync`). `HK.state` gives the latest agreed snapshot, and `HK.syncReport()` shows how sync is doing.
- **Fall damage fixed around death and respawn.** Fall tracking now starts fresh on death, on respawn and on any teleport, with one second of grace after a respawn. A player who died from a fall no longer takes a second fall hit when they reappear, and their own screen and everyone else's agree.
- **A clean gallery and a new cover.** The gallery dummies no longer show a `{PlayerName}` placeholder, in play or in the editor.
## Install and use (five minutes)
1. **Install.** Install three packages from ZeroMind, in two calls:
- first **SecondWind**: `zeromind_install guid=5da0a53c-5953-4adc-80fb-f5afb02f6568`
- then the two it is built on, **Lifeline** (`7b85ce05-3903-4374-b654-2d4ffea295a5`) and **MatchFlow** (`31cf13b4-df7d-4888-85e2-7947a7301614`), into `/source`: `zeromind_install guid=7b85ce05-3903-4374-b654-2d4ffea295a5,31cf13b4-df7d-4888-85e2-7947a7301614 at=/source`
Don't skip the second call, don't merge the two, and keep `at=/source`. SecondWind brings its own copies of Lifeline and MatchFlow but the engine doesn't connect it to them yet, and in one call, or without `at`, the second install only updates those copies (see Known limits). While Lifeline installs, the log may say `'~Lifeline.api' is ambiguous` for a moment; that clears when the install finishes.
2. **Set it up in your scene's `build.luau`.** A new world's main scene (or one made with the `player` scene template) builds its player in `build.luau` with a line like `local body = entity.spawn("body", { parent = prototype })`. Add these lines at the end of `content()`, after that player setup:
```lua
local HK = require("SecondWind.api")
-- World rules + on-screen HUD (kill feed, death screen, bars). Rules must be synced.
local rules = entity.spawnSynced("HealthRules")
rules.component.add(HK.RULES, { preset = "heroShooter" }) -- or leave preset empty
entity.spawn("HealthHUD").component.add(asset.resolve("SecondWind.HUD", "component"), {})
-- Players: give the PlayerPrototype's body health. Every joining player gets it.
HK.attach(body, { team = "blue" }) -- `body` from the default player setup
-- A trap: lava that burns anyone standing in it (a 3 x 1 x 3 m box on the ground).
local lava = entity.spawnSynced("Lava", { position = { 0, 0.5, 4 } })
lava.component.add(asset.resolve("SecondWind.Trap", "component"),
{ damageType = "fire", label = "Lava", amount = 15, sizeX = 3, sizeZ = 3 })
-- An orange slab on the ground, so you can see where the lava is.
local glow = entity.spawn("LavaGlow", { position = { 0, 0.05, 4 } })
glow.component.add("Model", { model = "cube", material = asset.resolve("@builtin::materials.plastic_white", "material"),
tintR = 1, tintG = 0.35, tintB = 0.05, tintBlend = 1 })
glow.localScale = { 3, 0.1, 3 }
-- A dummy standing in the edge of the lava: it burns, dies, and comes back where it stood.
local dummy = entity.spawnSynced("Dummy", { position = { 1, 0.5, 4 } })
dummy.component.add("Model", { model = "cube" })
HK.attach(dummy, { model = "hearts", deathStyle = "timed", displayName = "Dummy" })
```
3. **Press Play.** The lava is the orange slab about 4 m in front of where you start, and the dummy stands in its edge. The dummy catches fire, loses its hearts, dies, and comes back where it stood 4 seconds later, then burns again. Walk onto the slab and you catch fire too: flames, damage numbers, BURNING on screen and your bar dropping from 150. Stay in and you die and come back at the spawn point after the death screen's countdown.
Hurting something from code is one call:
```lua
HK.damage(target, 20, { by = attacker, type = "fire", team = "red" })
```
Switching anything is one setting, even while the game runs:
```lua
HK.setModel(target, "shield") -- or hearts, lives, downed, armor, hitPoints
HK.setDeathStyle(target, "spectate") -- or instant, timed, wave, ragdoll, dissolve, remove, stay
HK.applyPreset("battleRoyale") -- the whole world at once
```
`HK.catalog()` lists everything below, with descriptions and labels.
## Picking from the catalog
Every entry (model, damage type, death style, feed look, preset) carries these labels:
| label | values |
|---|---|
| `kind` | `healthModel`, `damageType`, `deathStyle`, `feedLook`, `preset` |
| `family` | health models: `pool`, `counted`, `secondChance`. Damage types: `direct`, `elemental`, `world`. Death styles: `respawn`, `deathAnimation`, `outOfPlay`. Feed looks: `killFeed`. Presets: `competitive`, `adventure`, `cooperative`, `casual` |
| `genres` | a list from `shooter`, `heroShooter`, `battleRoyale`, `adventure`, `roguelike`, `rpg`, `coop`, `survival`, `platformer`, `party` |
| `hasLook` | `true` when the entry draws something of its own |
| `style` | for entries with a look: `minimal`, `scifi`, `fantasy`, `cartoon`, `pixel` (a preset takes the style of its kill feed look) |
```lua
local HK = require("SecondWind.api")
HK.find({ kind = "preset", genre = "coop" }) -- { coopRevive entry }
HK.find({ kind = "feedLook", style = { "pixel", "cartoon" } }) -- any of the listed values
HK.find({ kind = "healthModel", family = "counted" }) -- hearts, lives
HK.find({ genre = "roguelike" }) -- everything that suits a roguelike
for _, p in HK.find({ kind = "preset", genre = "battleRoyale" }) do
print(p.id, p.family, p.style, table.concat(p.genres, ", "))
end
HK.vocabulary() -- { kinds, families, genres, styles }, each word with a label and description
```
Every key is optional and takes one value or a list (any of them matches). An unknown key or kind is an error that names the valid ones, so a typo never just returns nothing. `SecondWind.catalog` also offers `C.filter(kind, query)` for one kind (a kind id like `"preset"` or a list name like `"presets"`).
## What it offers
### Health models (`model`)
| id | what it does |
|---|---|
| `hitPoints` | One health bar. |
| `shield` | A shield soaks damage first and recharges after `shieldRegenDelay` seconds without a hit. Electric damage does double to shields. |
| `armor` | An armour pool absorbs `armorAbsorb` of each physical, fire, ice, electric or explosion hit and wears down. Poison, fall and environment damage go around it. |
| `hearts` | Zelda-style hearts with half hearts (one half heart = 10 HP). Every hit costs at least half a heart and rounds up to whole half hearts. |
| `lives` | `lives` lives, each with a pool of `lifeHealth`. Emptying a pool spends a life and refills it. Losing the last life is death. |
| `downed` | A lethal hit knocks you down instead. You crawl at reduced speed and bleed out over `bleedOutTime`. A living teammate who stands within `reviveRadius` for `reviveTime` picks you up. |
### Damage types (`type`)
| id | behaviour | looks like |
|---|---|---|
| `physical` | A plain hit. | white sparks |
| `fire` | Keeps burning for a few seconds after the hit. Ice puts it out. | rising flames; burning targets trail embers |
| `poison` | Stacks up to 5. Each stack ticks every second, and a new hit refreshes them. Goes around armour. | poison bubbles; poisoned targets trail bubbles |
| `ice` | Slows the target to half speed for a few seconds. Puts out fire. | ice shards; chilled targets trail frost |
| `electric` | Chains to up to 3 nearby targets at 60% damage. Double damage to shields. | electric sparks and arcs to each chained target |
| `explosion` | Falls off with distance and knocks targets back (`HK.explode`). | an expanding explosion shell |
| `fall` | Taken automatically when landing faster than `Rules.fallSafeSpeed`. Goes around armour. | landing dust |
| `environment` | Lava, spikes, acid: the world itself (`SecondWind.Trap`). Goes around armour. | red spikes |
### Death styles (`deathStyle`)
| id | what happens |
|---|---|
| `instant` | Back at a spawn point straight away. |
| `timed` | Back at a spawn point after `respawnDelay` seconds, with a MatchFlow countdown on screen. |
| `wave` | Everyone who died comes back together on the next wave, every `Rules.waveInterval` seconds. |
| `ragdoll` | The body is flung and tumbles limp, then respawns after `respawnDelay`. |
| `dissolve` | The body sinks and dissolves into sparks, then respawns after `respawnDelay`. |
| `spectate` | Stays dead and watches a living player until the round ends (`HK.endRound()`, a MatchFlow round ending, or `Rules.autoEndRound`). |
| `remove` | Gone for good: for breakable props and one-shot enemies. |
| `stay` | Stays dead until a script calls `HK.revive`. |
### Presets (`preset`)
| id | model | death style | feed | friendly fire |
|---|---|---|---|---|
| `arcadeShooter` | hitPoints | instant | pixel | off |
| `heroShooter` | shield (150 HP + 75 shield) | timed (5 s) | scifi | off |
| `adventureHearts` | hearts (5) | dissolve | fantasy | off |
| `roguelikeLives` | lives (3 × 40 HP) | ragdoll | pixel | off |
| `coopRevive` | downed (20 s bleed-out) | wave | minimal | off |
| `battleRoyale` | armor | spectate | minimal | on |
| `partyBrawl` | hitPoints, huge knockback | ragdoll | cartoon | on |
### Kill feed looks (`feedLook`)
`minimal`, `scifi`, `fantasy`, `cartoon`, `pixel`. Rows read like "Nova burned Kai" or "Kai was killed by Lava".
## Reference
### How settings resolve
For each entity, each value is taken from the first of these that sets it:
1. The entity's own `Vitals` fields. A `model` or `deathStyle` of `auto` means "not set".
2. The entity's own `preset`.
3. For player bodies only: the world `Rules` (`playerModel`, `playerDeathStyle`, and the `preset`'s tuning).
4. For everything else: `hitPoints` and `Rules.otherDeathStyle`.
`HK.config(target)` returns the resolved result. A model switch keeps the current health fraction.
### `SecondWind.api` (`local HK = require("SecondWind.api")`)
Every target argument accepts an entity, a component or an entity id.
| call | what it does |
|---|---|
| `HK.damage(target, amount, opts?) -> ok, reason` | Hurt a target. opts: `by`, `type`, `team`, `origin {x,y,z}`, `knockback` (m/s), `crit`, `dot`, `label` ("Lava"), `ignoreInvulnerability`, `status` (false = no burn/poison/chill). |
| `HK.heal(target, amount, opts?) -> ok, reason` | Restore health. Hearts round up to half hearts. opts: `by`. |
| `HK.kill(target, opts?) -> ok, reason` | Kill outright, skipping lives and downed. opts: `by`, `type`. |
| `HK.revive(target, opts?) -> ok, reason` | Pick a downed target up, or respawn a dead one now. |
| `HK.explode(pos, opts?) -> count` | Explosion with falloff and knockback. opts: `radius` (5), `damage` (60), `by`, `team`, `knockback` (10), `minFalloff` (0.2). |
| `HK.attach(entity, settings?) -> Vitals` | Add Lifeline.Health and SecondWind.Vitals with these settings. Use it in a build or on a freshly spawned entity. |
| `HK.configure(target, settings) -> ok, reason` | Change any Vitals settings at runtime. |
| `HK.setModel(target, model)` / `HK.setDeathStyle(target, style)` | Switch one setting. Pass `"auto"` to follow the preset or the rules again. |
| `HK.applyPreset(name, target?)` | With no target, sets the world preset. With a target, sets that entity's own preset. |
| `HK.setRules(t) -> ok, reason` | Change world rules, e.g. `{ friendlyFire = true }`. |
| `HK.endRound()` | End the round: spectators come back. |
| `HK.state(target)` | Snapshot in the terms of its model: `health, maxHealth, fraction, shield, maxShield, armor, maxArmor, hearts, heartsLeft, lives, livesLeft, downed, bleedLeft, reviveProgress, dead, deaths, respawnIn, dyingStyle, burning, poisonStacks, chilled, model, deathStyle, preset, team, name`. |
| `HK.isAlive(target)` / `HK.isDowned(target)` | Booleans. A downed target counts as alive. |
| `HK.config(target)` | The resolved settings (model, deathStyle, preset, all tuning). |
| `HK.get(target)` / `HK.lifeline(target)` / `HK.all()` | The Vitals, the Lifeline.Health underneath, and every Vitals keyed by id. |
| `HK.rules()` / `HK.ruleValues()` | The Rules component, and its values as a table (defaults when there is no Rules entity). |
| `HK.nameOf(target)` / `HK.isPlayer(id)` / `HK.idOf(x)` | Display name, player-body test, and id normaliser. |
| `HK.on(event, fn) -> conn` | Listen to an event on this peer (`"*"` = all). `conn:Disconnect()` stops it. |
| `HK.killFeed()` | Recent kill feed entries on this peer. |
| `HK.setDamageFilter(fn?)` | `fn(vitals, amount, info) -> amount`, run on the owner after team checks. Return 0 to cancel. |
| `HK.catalog()` | Every model, damage type, death style, preset, feed look and event, plus `version` and `vocabulary`. Every entry carries its labels. |
| `HK.find(query?) -> { entry }` | Catalog entries matching labels: `kind`, `family`, `genre`, `style`, `hasLook`, `id`. Each takes one value or a list. |
| `HK.vocabulary()` | The words the labels use: kinds, families per kind, genres and styles. |
| `HK.syncReport()` | How health sync is doing on this peer: `totals` (`sent`, `asked`, `served`, `resyncs`, `snapshots`, `evtSent`, `evtBatched`), the entities this peer `owned` and the ones it `watched` (in sync, seconds off, latest snapshot). |
| `HK.VERSION`, `HK.HEALTH`, `HK.RULES`, `HK.logic` | The version string, the Vitals and Rules component assets, and the pure rules maths. |
### Events (`HK.on`)
`damaged`, `healed`, `blocked` (reason: immune, friendly, self, armor, dead), `lifeLost`, `downed`, `revived`, `died`, `killed` (a kill feed entry: `killer, killerName, victim, victimName, type, text`), `respawned`, `statusApplied`, `chained`, `exploded`, `wave`, `roundEnded`. They fire on every peer.
### `SecondWind.Vitals` component
Settings: `model`, `deathStyle`, `preset`, `team`, `displayName`, `barStyle`, `startFull`, `invulnerable`, `fallDamage`, `canRevive`.
Tuning: `maxHealth` 100, `regenRate` 0, `maxShield` 50, `shieldRegenRate` 20, `shieldRegenDelay` 3, `maxArmor` 50, `armorAbsorb` 0.66, `hearts` 3, `lives` 3, `lifeHealth` 30, `bleedOutTime` 15, `reviveTime` 3, `reviveRadius` 2, `reviveHealth` 0.3, `respawnDelay` 4, `invulnerability` 0.5 (i-frames after a hit), `knockbackScale` 1.
Read-only synced state: `armor`, `livesLeft`, `downed`, `bleedLeft`, `reviveProgress`, `reviver`, `respawnIn`, `dyingStyle`, `burning`, `poisonStacks`, `chilled`, `activeModel`, `activeStyle`, `activePreset`, `hitSeq`, `hitType`, `hitAmount` (the last hit, for its effect), `changeSeq` (bumped on every health change).
Methods: `damage(amount, opts)`, `heal(amount, opts)`, `kill(opts)`, `revive(opts)`.
It needs `Lifeline.Health` on the same entity; `awake` adds it when missing.
### `SecondWind.Rules` component (one per scene, on a synced entity)
`preset`, `friendlyFire` (off), `selfDamage` (on), `playerModel` (hitPoints), `playerDeathStyle` (timed), `otherDeathStyle` (timed), `feedLook` (minimal), `waveInterval` 10, `autoEndRound` (on), `matchEntity` ("Match", a MatchFlow entity whose round end ends the SecondWind round), `maxRemoteDamage` 1000, `fallSafeSpeed` 10, `fallDamagePerSpeed` 7, `allowClientConfig` (off). Read-only: `waveIn`, `waveSeq`, `roundSeq`.
### `SecondWind.HUD` component (per peer, local)
`feedLook` (`rules` follows Rules.feedLook), `showFeed`, `maxFeed` 5, `feedTime` 6, `showDeathScreen`, `showStatus`, `showDownedMarkers`, `countdownStyle` (any MatchFlow countdown style; default `bold`), `spectateSwitch` 8, `addLifelineHUD`, `autoBarStyle`. Method: `nextTarget()` switches the spectated player.
### `SecondWind.Trap` component
A box volume that hurts what enters it: `damageType`, `label`, `amount`, `mode` (`perSecond` | `onEnter`), `interval`, `sizeX/Y/Z`, `knockback`, `team`, `by`, `playersOnly`, `active`. It runs on the authority of each victim, so put it on any entity, synced or not.
### Other modules
- `SecondWind.catalog`: the data behind `HK.catalog()`.
- `SecondWind.logic`: pure maths (pools, armour and shield split, falloff, fall damage, team rule, feed text).
- `SecondWind.feed`: `render(look, entries, opts)` for your own UI.
- `SecondWind.fx`: `burst(type, pos, amount)` hit effects.
### Multiplayer
- **Authority:** each health is owned by the peer that owns its entity. A player's body belongs to that player, and synced world entities belong to the host. Only the owner changes health. All state is synced fields, so every peer sees the same health, deaths and respawns.
- **Remote calls:** `HK.damage` on a target you don't own sends a request to its owner. The owner checks that you own the attacker you named, applies team and friendly-fire rules, and caps the amount at `maxRemoteDamage`.
- **Admin calls:** `configure`, `kill`, `revive` and `endRound` from another peer are refused unless that peer owns the Rules entity or `allowClientConfig` is on.
- **Events:** effects and events run on every peer. Events for other peers leave in batches on `healthKit:evts`, at most one message every 0.05 s, in the order they were raised; `healthKit:evt` messages from 1.1.0 peers are still received. Hit effects are drawn on the next frame from a queue, and each hit is drawn once per peer however it arrives (event or synced `hitSeq`).
- **State snapshots:** the owner sends the full health state on the `healthKit:state` channel. It sends right away on every death, respawn, revive, life lost and knock-down, then again after 0.5 s and 2 s. It also sends after changes (at most once a second for players, every 3 s for others) and every 5 s regardless. Snapshots are batched, about 2 to 3 messages a second for a busy scene. A peer whose fields disagree with the last snapshot for over 0.5 s asks for a fresh one on `healthKit:resync`. All the kit's channels are registered when the kit loads; you do not call them yourself.
### Tests
The `SecondWind.secondwind` test suite has 87 tests covering every public call: catalog labels and filtering, hit effects reaching peers, fall damage around death and respawn, health sync, event batching, the README's install step and first example, the package description, and the 1.2.0 rename (new names, same guid, no old requires left, channels kept). Run it with `tools.use("tests", "run", { suite = "secondwind" })`.
## Known limits
- **#2612: a package cannot list Lifeline as a dependency.** `package.yaml` names no dependencies, so installing SecondWind doesn't install Lifeline and MatchFlow as packages of your world. Install them yourself, as step 1 says.
- **#19188: installing SecondWind doesn't connect it to Lifeline and MatchFlow.** The install logs errors saying a component type was not found and a version is still being fetched. It writes Lifeline and MatchFlow under `/source/deps/`, but SecondWind's `require("Lifeline.api")` doesn't find them there: its components fail with `module 'Lifeline.api' not found`, and `zm push` refuses with `could not resolve require("Lifeline.api")`. Installing Lifeline and MatchFlow in a call of their own with `at=/source`, as step 1 says, fixes both. In the same call as SecondWind, or without `at`, that install answers `updated` with a path under `/source/deps/` and puts nothing in `/source`. The copies under `/source/deps/` stay behind; the MatchFlow one logs `module ~.rules … has no root to resolve against` on each scene rebuild, which the first example plays through.
- **#19168: effects spawned from a message another player sent.** Worked around: the kit queues hit effects and draws them on the next frame instead of inside the message handler, and also draws them from the synced `hitSeq`/`hitType`/`hitAmount` fields when no message arrives.
- **#19201: the `{PlayerName}` label.** The engine's humanoid avatar shows a `{PlayerName}` placeholder on any body no player owns, and has no setting to turn it off. This only matters if you give dummies or enemies the humanoid avatar. The showcase gallery hides those labels in play and in the editor: it disables `PlayerNameLabel` on bodies with no `PlayerAvatar` above them.
- **Message rate.** Lifeline, which the kit builds on and does not change, sends one `lifeline:evt` message for every hit, so the owner of many bodies taking damage at once still sends at least one message per hit. A peer with a low frame rate, such as a browser tab at 10 to 15 fps, drains those messages slowly, and everything sent after them waits behind them. The kit's own events are batched (see 1.1.1). Keep the number of bodies taking damage at the same moment modest, or only run them near players, as the showcase gallery does.
- **Tower freeze, seen once in a browser.** During a two-player test, a player in a web browser walked off the gallery's JUMP OFF platform and stayed hanging in the air, unable to move, until knocked out. It was not reproduced on the native engine, where the same walk falls and takes fall damage, and it may have come from the browser's controls. Nothing in the kit was found to cause it: the kit only moves a player's body when it respawns.
- **Two-player checks.** Checked live with two players, one on the native engine and one in a web browser. In 1.1.0, hit effects, fall deaths and respawns matched on both screens. In 1.1.1, with the browser hosting the gallery, batched events arrived in order and damage requests came back in 0.2 to 0.4 s. The test suite also plays the remote peer's side (receiving, drifting, asking for a resync, catching up).
- **Web browser.** The kit uses only cross-platform engine APIs (no file, OS or native calls outside its tests). In 1.1.1 a browser tab hosted the showcase gallery for the checks above and ran at about 15 frames a second with the gallery awake around a player, against 80 to 200 ms a frame before. Big scenes run slower in a browser than on the native engine, which is why the message rate above matters most there. The first example works in a browser: in a brand-new world with SecondWind installed by the two-call step 1 (on 1.1.2's code, which 1.1.3 and 1.1.4 don't change), the player started with 150 health, the lava set them on fire with flames, damage numbers and BURNING on screen, the bar dropped to about 20 of 150, and the dummy took damage. It plays choppily in a browser tab, even in that small scene. On the native engine the same scene runs at about 19 ms a frame, and the kit's components take about 0.5 ms of it; most of the rest is UI drawing, the built-in character controller and rendering, so the kit is not the likely cause. The browser frame itself has not been profiled. 1.1.4's example only moves the dummy and adds a plain coloured cube, and 1.2.0 only renames the package; both were checked on the native engine.
# barsKit 1.3.0
Health and resource bars for any world: **one kit, lots of looks.**
You pick a look and say what the bar shows. To change the look later you change one word, live, and nothing else gets rewritten.
- **20 looks of its own**, plus Lifeline's 13 as `lifeline:<style>`, in **8 theme families**: sci-fi, fantasy, cartoon, pixel, minimal, realistic, horror and cozy. Each look says its **shape**, whether it is a **screen or world** bar, and its **size**, so you can pick one without trying them all.
- **Works with the SecondWind health kit (formerly healthKit) out of the box**: your health, shield, armour, hearts and lives, and the health of any enemy, NPC or prop.
- **Not tied to health**: it can show stamina, mana, energy, XP, a boss, or any number your game keeps. The `Resource` component gives you a pool that drains and refills if you don't have one yet.
- **Feels good**: the value eases to its new level, the lost chunk lingers as a ghost before draining, the bar pulses when low, numbers are optional, and each kind of resource has sensible colours.
- **Fits any screen**: the HUD scales with the screen and respects safe areas, so it stays readable on a phone as well as a desktop. Bars never leave the screen, and the gallery or a panel of yours and the HUD never cover each other.
- **Leaves nothing behind**: `BK.remove` takes a bar down along with the entity it was made on, and closing or removing the gallery takes its screens with it.
- **Multiplayer**: your HUD bars always show *your* body. Bars over other players, enemies and props read synced SecondWind health, so everyone sees the same value.
It depends on **SecondWind 1.2.0** (the health kit formerly called healthKit) and, through it, **Lifeline 1.0.1**. Both install with it.
## Install and use
Install the package into your world with `zeromind_install` (search ZeroMind for "barsKit"). SecondWind and Lifeline come with it.
### 1. Give your player health first
A health bar shows the health the health kit keeps, so **the player's body needs health before any health, shield, armour, hearts or lives bar can show anything.** Without it, the bar finds no health to read and is **hidden entirely**. You get no error, just no bar. The same goes for an enemy or prop you put a bar on.
The health kit installs with barsKit. It is **SecondWind** (it used to be called healthKit: same package, same guid), and it lands in `deps/7a497fbf-1b4e-4655-8abf-e588fa7caa5c/SecondWind.package`. A short name such as `SecondWind.api` or `healthKit.api` does not reach into `deps/`, so load it by its full name, as below.
Add these lines to your scene's `build.luau`, inside `content()`, after the default player setup (`body` is the player body it makes):
```lua
-- the health kit (SecondWind, formerly healthKit), by the name its install gives it
local VITALS = asset.resolve("deps.7a497fbf-1b4e-4655-8abf-e588fa7caa5c.SecondWind.Vitals", "component")
body.component.add(VITALS, { maxHealth = 100 }) -- every joining player gets health
-- or with a regenerating shield, the way this world's own scene does it:
-- body.component.add(VITALS, { model = "shield", maxHealth = 100, maxShield = 40 })
-- anything else that should have a health bar: here, an enemy to hit
local enemy = entity.spawnSynced("Grunt")
enemy.component.add("Asset", { source = asset.resolve("@builtin::avatars.humanoid", "avatar") })
enemy.localPosition = { 0, 0, 6 }
enemy.component.add(VITALS, { maxHealth = 250, displayName = "Grunt" })
```
Adding the `Vitals` component gives the entity SecondWind's health and Lifeline's `Health` with it. In `build.luau`, add the component as above rather than calling `HK.attach`: the scene rebuilds every time you edit the file, and a second `HK.attach` on the same body stops the rebuild with "already has a Vitals component". From game code that runs while you play, use the API by the same full name: `local HK = require("deps.7a497fbf-1b4e-4655-8abf-e588fa7caa5c.SecondWind.api")`, then `HK.attach(entity, { ... })`, `HK.damage(...)` and `HK.heal(...)`. See the SecondWind README for health models (`hitPoints`, `shield`, `armor`, `hearts`, `lives`), death styles and teams.
To check it worked: `BK.read("health", body).ok` is `true` once the body has health, and `false` before.
If your game keeps its own health instead of using SecondWind, skip this step and point the bar at your number (`show = "value"` with `bar:set`, or `BK.bind`; see *Pointing a bar at something*). Stamina and mana bars need a `Resource` instead (see *No stamina or mana yet?*).
### 2. Put bars on screen
```lua
local BK = require("barsKit.api")
-- your health in the top-left corner
local hp = BK.bar({ look = "ghost", show = "health" })
-- a little bar over an enemy's head (the enemy needs health too, step 1)
BK.bar(enemy, { look = "overhead" })
-- a boss plate across the top
BK.bar({ look = "boss", target = "The Iron Warden", anchor = "topCenter", phases = "0.66 0.33" })
-- switch the look: one word, live
BK.setLook(hp, "pixel")
BK.setLookAll("hologram") -- every bar at once
BK.setLookAll("ornate", { placement = "screen" }) -- just the HUD
```
In a scene, you can add the component directly: `e.component.add(asset.resolve("barsKit.Bar", "component"), { look = "orb", show = "mana", anchor = "bottomRight" })`. Every field below is also settable in the editor's inspector.
## Pointing a bar at something
`show` says what the bar shows, and `target` says whose it is. With no target, a screen bar reads the local player and a world bar reads the entity it sits on.
| `show` | reads |
|---|---|
| `health` (default) | SecondWind health (works on players, enemies and props) |
| `shield`, `armor`, `hearts`, `lives` | the matching SecondWind value |
| `stamina`, `mana`, `xp`, anything else | the `Resource` of that name on the target |
| `value` | a number you set with `bar:set(value, max)` |
You can also bind any function, or point at any component field:
```lua
BK.bind(bar, function() return myGame.energy, myGame.maxEnergy end)
BK.bind(bar, { component = "Turret", field = "heat", max = 100 }) -- or maxField = "maxHeat"
BK.bar({ look = "segmented", show = "value", kind = "ammo" }):set(12, 30)
```
### No stamina or mana yet? Make a resource
```lua
BK.resource(player, "stamina", { max = 100, regenRate = 25, regenDelay = 0.8, drainRate = 35, drainKey = "ShiftLeft" })
BK.resource(player, "mana", { max = 120, regenRate = 8 })
if BK.spend(player, "mana", 30) then castFireball() end -- false (and nothing spent) when there isn't enough
BK.gain(player, "mana", 10)
BK.bar({ look = "orb", show = "mana", anchor = "bottomRight" })
```
A `Resource` regenerates after `regenDelay` seconds without spending, and drains while `drainKey` is held or `draining` is true. `demo = true` makes it drain and refill on its own, for showcases. The owner of the entity is the authority; other peers' `spend`/`gain` calls are sent to it, and the value syncs to everyone.
Note: a *second* Resource on one entity is a named component instance. On player avatars, add it at runtime with `BK.resource(...)` (for example in `localPlayerReady`) rather than on the PlayerPrototype, because named second instances don't carry over when the prototype is cloned.
## The looks
**Where** is `screen` (HUD only), `world` (over an entity only) or `both`. **Size** is the HUD size in points at scale 1, and the size class is in brackets: `small`, `medium`, `wide` or `tall`.
| id | name | shape | where | size | families | suits |
|---|---|---|---|---|---|---|
| `line` | Thin line | line | both | 240×22 (medium) | minimal | health, stamina, xp, energy |
| `segmented` | Segmented | segmented | both | 264×36 (medium) | realistic, sci-fi | health, armor, energy, ammo |
| `ring` | Radial ring | ring | both | 88×88 (small) | sci-fi, minimal | health, shield, energy, cooldown |
| `hearts` | Heart containers (full, half and empty; blue shield hearts) | hearts | both | 180×52 (medium) | fantasy, cozy, pixel | health, lives, hearts |
| `boss` | Boss bar (name, phase marks, phase count) | bar | screen | 560×62 (wide) | fantasy, horror, realistic | boss |
| `overhead` | Over-head bar | bar | world | 180×34 (medium) | minimal, realistic | overhead, enemy, npc |
| `ghost` | Ghost bar (the lost chunk lingers, then drains) | bar | both | 280×30 (medium) | realistic, minimal | health, boss, shield |
| `stacked` | Shield on health | stacked | both | 272×44 (medium) | sci-fi, realistic | health, shield, armor |
| `arc` | Crosshair arc | arc | screen | 76×76 (small) | minimal, sci-fi | stamina, energy, ammo, crosshair |
| `orb` | Glass orb | orb | screen | 108×126 (small) | fantasy, realistic | mana, health, energy |
| `gauge` | Vertical gauge | vertical | screen | 58×176 (tall) | realistic, sci-fi | fuel, heat, oxygen, energy |
| `pips` | Pips / dots | pips | both | 194×32 (medium) | minimal, cozy, cartoon | lives, charges, ammo, combo |
| `pixel` | Pixel art | bar | both | 248×34 (medium) | pixel, cartoon | health, mana, stamina, xp |
| `hologram` | Sci-fi hologram | bar | both | 300×52 (medium) | sci-fi | shield, energy, oxygen |
| `ornate` | Ornate fantasy frame | bar | both | 312×50 (medium) | fantasy | health, mana, boss |
| `bubbly` | Bubbly cartoon | bar | both | 272×46 (medium) | cartoon, cozy | health, xp, stamina |
| `blood` | Blood vial (drips, flickers near death) | bar | both | 270×48 (medium) | horror | health, sanity, boss |
| `monitor` | Heart monitor (ECG that flatlines at zero) | trace | screen | 214×62 (medium) | realistic, horror, sci-fi | health, stress |
| `stitched` | Stitched felt | bar | both | 262×44 (medium) | cozy, cartoon | health, hunger, warmth |
| `megabar` | Tick column (retro platformer meter) | vertical | screen | 30×150 (tall) | pixel, cartoon | health, energy, weapon, boss |
The eleven shapes are `line`, `bar`, `segmented`, `stacked`, `ring`, `arc`, `hearts`, `pips`, `orb`, `vertical` and `trace`.
Lifeline's own styles are there too, each with the same labels: `lifeline:classic`, `segmented`, `ring`, `hearts`, `boss`, `overhead`, `minimal`, `stacked`, `scifi`, `fantasy`, `cartoon`, `pixel`, `orb`.
### Asking the kit
```lua
BK.looks() -- every look: { id, name, families, suits, description,
-- shape, placement, size = { w, h }, worldSize = { w, h }, sizeClass }
BK.looks({ family = "horror" }) -- blood, monitor, boss
BK.looks({ suits = "mana" }) -- orb, pixel, ornate
BK.looks({ family = "cozy", suits = "health" })
BK.looks({ placement = "world" }) -- looks that can float over an entity ("both" counts)
BK.looks({ size = "small" }) -- ring, arc, orb (or "medium", "wide", "tall")
BK.looks({ shape = { "ring", "arc" } }) -- one shape or several
BK.looks({ family = "pixel", shape = "bar" }) -- filters combine
BK.looks({ includeLifeline = true })
BK.families() -- { { id, name, looks } } for the 8 families
BK.shapes() -- { { id, name, looks } } for the 11 shapes
BK.suits() -- everything looks say they suit
BK.render("orb", { frac = 0.4, kind = "mana" }) -- draw a look inside your own UI tree
```
## Bar fields
| field | default | what it does |
|---|---|---|
| `look` | `ghost` | Which look. Change it any time. |
| `show` | `health` | What it shows (see above). |
| `target` | — | Whose value: an entity name or id. Empty means the local player (screen) or this entity (world). |
| `placement` | `screen` | `screen` = HUD; `world` = floats over the target. |
| `anchor` | `topLeft` | Screen corner or edge: `topLeft`, `topCenter`, `topRight`, `left`, `center`, `right`, `bottomLeft`, `bottomCenter`, `bottomRight`, `crosshair`. Bars on one anchor stack by `order`. |
| `label` | — | Caption. Defaults to the resource's name (or the boss's name). |
| `kind` / `color` | auto | Colour by resource kind (health, shield, armor, stamina, mana, energy, xp, boss, rage, oxygen, heat, ammo, lives, hunger, generic), or a hex override. |
| `numbers` | `auto` | `value` (75 / 100), `percent`, `none` or `auto`. |
| `lowWarning` | `0.25` | Below this fraction the bar pulses. 0 turns it off. |
| `size` | `1` | Scale on top of the screen-size scale. |
| `phases` | — | Boss phase marks as fractions, e.g. `"0.66 0.33"`. |
| `segments` | auto | Cell, pip or heart count, where the look uses one. |
| `worldHeight`, `range` | 2.2, 30 | World bars: height above the target, and how far from the camera they still show. |
| `hideWhenFull`, `hideOwn` | false, true | Hide while full; don't draw a world bar over your own body. |
| `offsetX`, `offsetY`, `visible` | | Fine placement and on/off. |
Methods on a Bar (call them with a colon): `bar:setLook(id)`, `bar:cycleLook(dir)`, `bar:bind(fn | spec)`, `bar:set(value, max)`, `bar:sample()`. On a Resource: `res:spend(n)`, `res:gain(n)`, `res:set(v)`, `res:fraction()`. The API mirrors them: `BK.setLook`, `BK.bind`, `BK.sample`, `BK.read(show, target)`, `BK.bars()`, `BK.barOf(entity)`, `BK.localPlayer()`.
## Multiplayer
- Bar settings are local (not synced). Each peer runs its own HUD, so your screen bars always read **your** avatar.
- World bars and boss bars read SecondWind's synced state, so every player sees the same value over the same enemy.
- `Resource` values are owned by the entity's owner and synced. `BK.spend`/`BK.gain` from another peer are sent to the owner.
- Damage and healing go through SecondWind (`HK.damage`, `HK.heal`), which already routes them to the authority.
## The gallery (`barsKit.Showcase`)
Add the `Showcase` component to any entity to get the gallery:
- Every look side by side, all following one value that drains and refills. Each card is labelled with the look's name, its id and families, and a gold line giving its shape, where it goes and its size.
- Filter chips for families, recolour chips for resource kinds, a numbers toggle, and Lifeline's looks.
- A switcher that flips your own health bar through every look. Use `[` / `]` or the ◀ ▶ buttons; `G` hides or shows the gallery.
The gallery and the HUD never cover each other, at any screen size:
- The panel takes the largest part of the screen the HUD leaves free (`BK.freeRect`), clear of every screen bar and the touch buttons.
- While the gallery is open the switcher sits inside it. When it is closed, the switcher stands alone and screen bars step clear of it.
- Bars that would land under the panel (the crosshair arc, world bars) are hidden until it closes.
- Cards are dealt into even rows. Rows differ by at most one card, and every row is stretched to fill the width, so there are no empty spots.
**On a desktop-sized window, every look and its label is on screen at once, with no scrolling.** The gallery tries every column count from 3 to 8, splits the panel's height evenly between the rows, and picks the grid that draws the looks largest without shrinking any one of them too far (`BK.layout.fitGrid`). A 1080p window comes out as 6 columns by 4 rows, and a shorter 1280×682 window as 7 by 3. The cards get compact there: the family line shortens or drops families ("+2") instead of being cut off. A panel too small to fit every card at a readable size scrolls instead, with the larger cards. That covers a phone in either orientation, where the touch buttons and the boss plate leave the gallery a small panel. On a phone the cards reflow to two columns, and a card's labels shorten rather than get cut off.
```lua
local g = entity.find("BarsGallery").component.get(asset.resolve("barsKit.Showcase", "component"))
g:gridInfo() -- the grid it drew last: { fits, scrolls, cols, rows, cellH, gridH, minScale, cards, panel }
g:layoutFor(1920, 1080) -- lay it out for another screen size without drawing: info, widget tree
```
`G` toggles the gallery, as does setting the component's `open` field. Removing the component or despawning its entity takes down both of its screens.
## Phones and touch screens
- Bars scale with the screen but never drop below 0.85 on a phone, so text stays legible.
- Corner bars are laid out first. A centred bar (a boss bar) that meets one first slides sideways, and only then steps a row down (or up, for a bottom bar), so nothing overlaps on a narrow screen.
- A bar never leaves the screen. On a tiny screen with no clear row left, it shrinks into the widest gap of a row it tried.
- While the player is on touch, screen bars step clear of the on-screen buttons (Jump, Sprint, …). Top stacks move down past them and bottom stacks move up.
- World bars (over heads) draw under screen bars, with the nearest on top.
## Your own panels and the HUD
All of these use UI points, the same space as `ui.screenSize()`. Pass `nil` as the rect to clear a key.
```lua
-- keep screen bars out of a rect (a minimap, a chat box): the bars move
BK.keepOut("minimap", { x = 1100, y = 10, w = 170, h = 170 })
-- a panel of yours covers a rect (an inventory): bars that would land under it are hidden until you clear it
BK.cover("inventory", rect)
-- or let the HUD tell you where there is room: the largest rect clear of every bar, touch button and keepOut
local sz = ui.screenSize()
local rect = BK.freeRect(sz.width, sz.height, { pad = 8, margin = 6, minW = 300, minH = 200 })
BK.screenRects() -- where the HUD drew its screen bars last frame: { { x, y, w, h, anchor, look, hidden } }
BK.layout -- the pure helpers the gallery uses: largestFree, evenRows, rowWidths, packLines, fitGrid, overlaps
```
## Removing bars
```lua
local hp = BK.bar({ look = "ghost", show = "health" }) -- makes an entity for the bar
BK.remove(hp) -- removes the bar and despawns that entity; true when it took a bar down
BK.remove(hp) -- false: the bar is already gone (calling it again is harmless)
local tag = BK.bar(enemy, { look = "overhead" })
BK.remove(tag) -- removes only the Bar component; the enemy stays
BK.unbind(bar) -- undo BK.bind: the bar goes back to its `show` field
BK.isBound(bar) -- is a function or field spec bound right now?
```
Removing a Bar component by hand, despawning its entity or removing a `Resource` cleans up the same way. No HUD entries, bindings, covers, keepOuts or screens stay behind; the tests check this.
## Known limits
Checked on 2026-10-18 in the desktop app: a fresh install into a brand-new, empty world, and two people in Gaugeworks at once. barsKit loaded in full and every bar in this README worked there. Each limit below happened.
- **#19201: a player's name tag reads "{PlayerName}".** Some avatars show the literal text `{PlayerName}` over their heads instead of a name. This is the engine's own name tag, not a barsKit bar. barsKit's gallery panel is opaque, so the tag never shows through a gallery label.
- **#19260: older copies of dependencies sit in `versions/` folders.** A fresh install leaves single-module copies of two of the kit's dependencies beside the full packages. The health kit's own folder (`deps/7a497fbf-1b4e-4655-8abf-e588fa7caa5c/SecondWind.package`) holds no old copies.
- `deps/45f92606-2e79-4b2b-8e03-b1fa2649e5c2/versions/<hash>/Lifeline.package/` holds older copies of the Lifeline health package's `api` module and its `Health` component.
- `deps/40bc9b7f-d32c-427f-b0ac-22d25071e2fa/versions/<hash>/MatchFlow.package/` holds an older copy of MatchFlow's `rules` module.
- The warning in the log comes from the copied Lifeline `Health` component, which can't find its `api` module: "component source failed to load … `deps.45f92606-…versions.<hash>.Lifeline.Health` … module `~.api` in the calling script has no root to resolve against".
- barsKit uses the full `Lifeline.package` and `MatchFlow.package` and never these copies. Bars, the gallery and `Bar` and `Showcase` components saved in a scene all load and survive a world reload. Nothing needs deleting.
- **#19449: a second `Resource` on one entity reaches other players mixed into the first.** The engine syncs one instance of a component per entity. If a body has both a stamina and a mana `Resource`, the other players' copy of that body has one `Resource`: it keeps the first pool's name and max, but its value follows the second pool. In the Gaugeworks test, another player read your "stamina" as 120 out of 100 while your mana was full. **Your own bars are right**, because they read your own copy. Bars that show *another* player's second pool are not. Until #19449 is fixed, keep pools that other players need to see on separate entities (for example a child entity per pool), or show only one synced pool per body.
- **#19169: a second session that joins a game someone is already playing doesn't fully get its player.** The first player in a game is not affected. Seen twice in Gaugeworks on 2026-10-18:
- **Web browser version:** the joining player got no player view (the camera stayed on the spawn and they couldn't move) and none of the world's screens, so no barsKit bars, gallery or switcher. barsKit's code did run in that session: its health took the demo's hits and its bars reached the other player. The engine just never finished handing that player their body. The browser console showed only the engine refusing to spawn the world's shared entities again (the dummy, the pot, the boss and the crate), nothing from barsKit.
- **Desktop app:** the joining player got their HUD and saw the right health, boss and dummy values. But bars over the bodies (barsKit's and the health kit's own) were drawn at ankle height instead of over the heads.
- **Web browser version, one player:** a fresh install of 1.3.0 works. All of these work: the gallery with every card, your health bar with numbers, a boss bar, an over-head bar, `G` and `[` / `]`. #19201 shows there too. Two people in the web version can't be checked until #19169 is fixed.
## Version
1.3.0. `require("barsKit.api").VERSION`. Tests: `barsKit.testSuite`, 68 tests (run with the `tests` toolbox).
- 1.3.0: the release where barsKit behaves differently: it runs on **SecondWind 1.2.0** (formerly healthKit) and draws a backing plate behind numbers over bars. Both came in 1.2.1; this version number says so. No call or setting changed. Known limits now name the platform they were checked on (the desktop app; the 1.2.1 notes wrongly said the web browser version) and add **#19449**: a second `Resource` on one entity reaches other players mixed into the first. A new test checks that the version agrees in `package.yaml`, `BK.VERSION`, the README's title and its Version line. Heart containers now draw real heart shapes at 180×52, with all five hearts inside the card, even in a session where Lifeline's heart renderer has not loaded yet. It used to fall back to 100×46 solid red squares with the fifth one cut off, seen in the phone gallery. barsKit now keeps trying to load Lifeline's renderer and switches to it once it loads. `BK.remove` now returns false for a bar that is already gone, as documented; it used to return true again. Known limits describe what #19260 really leaves on a fresh install: old Lifeline `api` and `Health` copies and an old MatchFlow `rules` copy.
- 1.2.1: tidy-up release; no call or setting changed. barsKit now uses the health kit by the name a fresh install gives it: **SecondWind 1.2.0** (formerly healthKit, same package and guid), loaded by its full name. Before this, barsKit and its README's first example failed to load in a new world. The first example now works pasted as-is into a new world with only barsKit installed, and keeps working when you edit the scene again. The gallery's sample value and screens run only while the gallery is placed on an entity and the game is playing, not when its script loads. The gallery panel and switcher are opaque, so nothing in the world shows through their labels. A number drawn on top of a bar (over-head, ghost, pixel art, ornate, bubbly) sits on a backing plate, so the pale ghost trail can't hide it. The heart monitor's readout lines no longer touch. Gallery components get `sample()` and `isRunning()`. Known limits now list only what still happens on a fresh install.
- 1.2.0: every look is on screen at once on desktop-sized windows, labelled and without scrolling: the gallery picks its column count and card size to fit the panel (`BK.layout.fitGrid`, `BK.layout.packLines`), and adds `gridInfo()` and `layoutFor()`. Phones scroll as before. The shared value, boss bar, overhead dummy, ghost bar and look switcher are unchanged. The README now says to give your player health first, and how, and lists the known limits.
- 1.1.0: every look says its shape, where it goes (screen, world or both) and its size, and `BK.looks` filters on all three (`BK.shapes()` lists the shapes). The gallery places itself in the room the HUD leaves free and hides bars under it, keeps the switcher inside it while open, and deals cards into even rows. A centred bar slides sideways before stepping down and never leaves the screen, and world bars draw under screen bars. New: `BK.remove`, `BK.unbind`, `BK.isBound`, `BK.cover`, `BK.screenRects`, `BK.freeRect` and `BK.layout`. Removing a bar, unbinding or closing the gallery leaves nothing behind.
- 1.0.2: lighter gallery: virtual scrolling (only the rows in view are laid out and painted), the look switcher is redrawn only when it changes, and the gallery rebuilds 20 times a second on phones (30 on desktop). On a phone the open gallery now costs about 3ms a frame instead of about 9.5ms.
- 1.0.1: phone layout: a legible minimum scale, no overlap between corner and centre bars, bars and the gallery keep clear of touch buttons, and `BK.keepOut`.
- 1.0.0: first release.