SecondWind
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, Zel…
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 ofrequire("healthKit.api"). - New asset names: the components are
SecondWind.Vitals,SecondWind.Rules,SecondWind.HUDandSecondWind.Trap, the modules areSecondWind.catalog,SecondWind.logic,SecondWind.feedandSecondWind.fx, and the test suite issecondwind. - Upgrading from 1.1.x: after you update, change
healthKit.toSecondWind.in yourrequireandasset.resolvelines. 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:stateand the rest), and the HUD's screen id stayshealthkit_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
removeandstay, and it called the componentHealthinstead ofVitals.
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 withat=/source. Checked with the published kit in a new world: after the one call,module 'Lifeline.api' not foundand 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, andzm pushpublished 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 foundand 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 theheroShooterpreset, the lava burns, the hearts dummy dies and comes back, the health bar shows, andzm pushpublishes 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:evtsmessage, 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-eventhealthKit:evtmessages that 1.1.0 peers send, so mixed sessions work. HK.syncReport().totalsalso countsevtSent(event batches sent) andevtBatched(the events inside them).- A lighter showcase gallery. Its dummies and damage stations only work while a player is within 18 m (
wakeRadiuson 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, itsfamily, thegenresit suits, whether it has a look of its own (hasLook) and, if so, itsstyle.HK.find({ kind = "preset", genre = "coop" })picks by label, andHK.vocabulary()lists the words. See 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,hitTypeandhitAmount, 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.stategives the latest agreed snapshot, andHK.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)
-
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 withoutat, the second install only updates those copies (see Known limits). While Lifeline installs, the log may say'~Lifeline.api' is ambiguousfor a moment; that clears when the install finishes. - first SecondWind:
-
Set it up in your scene's
build.luau. A new world's main scene (or one made with theplayerscene template) builds its player inbuild.luauwith a line likelocal body = entity.spawn("body", { parent = prototype }). Add these lines at the end ofcontent(), after that player setup:
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" })
- 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:
HK.damage(target, 20, { by = attacker, type = "fire", team = "red" })
Switching anything is one setting, even while the game runs:
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) |
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:
- The entity's own
Vitalsfields. AmodelordeathStyleofautomeans "not set". - The entity's own
preset. - For player bodies only: the world
Rules(playerModel,playerDeathStyle, and thepreset's tuning). - For everything else:
hitPointsandRules.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 behindHK.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.damageon 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 atmaxRemoteDamage. - Admin calls:
configure,kill,reviveandendRoundfrom another peer are refused unless that peer owns the Rules entity orallowClientConfigis 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:evtmessages 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 syncedhitSeq). - State snapshots: the owner sends the full health state on the
healthKit:statechannel. 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 onhealthKit: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.yamlnames 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'srequire("Lifeline.api")doesn't find them there: its components fail withmodule 'Lifeline.api' not found, andzm pushrefuses withcould not resolve require("Lifeline.api"). Installing Lifeline and MatchFlow in a call of their own withat=/source, as step 1 says, fixes both. In the same call as SecondWind, or withoutat, that install answersupdatedwith a path under/source/deps/and puts nothing in/source. The copies under/source/deps/stay behind; the MatchFlow one logsmodule ~.rules … has no root to resolve againston 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/hitAmountfields 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 disablesPlayerNameLabelon bodies with noPlayerAvatarabove them. - Message rate. Lifeline, which the kit builds on and does not change, sends one
lifeline:evtmessage 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.
Scoped to this part · feeds back into the world's score.