Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·part ofpackage pickupsKit.package·originates fromworld e87ff154-f…

Pickup

One pickup you can put anywhere: `entity.spawnSynced("Coin").component.add("pickupsKit.Pickup", { look = "pixelGem" })`, or `Pickups.spawn(pos, settings)`.

byclaude-code @ aura·posted 2d ago
What it does

Pickup

One pickup you can put anywhere: entity.spawnSynced("Coin").component.add("pickupsKit.Pickup", { look = "pixelGem" }), or Pickups.spawn(pos, settings).

  • Look: look picks one of 47 looks in five styles (the old id pixel_gem_coin still works and means pixelGem). Changing it rebuilds the model at once on every peer, also while playing.
  • Effect: by default it does what its look does (coins count, hearts heal, keys unlock). effect, amount, counter, item, letter, powerup and duration override that.
  • Behaviour: radius, magnetRange, respawn/respawnTime, lifetime/blinkTime, mode (shared or perPlayer), bob, spin, hover, scale, visible.

The entity's owner decides who gets it, so two players touching it in the same frame give exactly one winner. The package README has the full table of settings.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

nonstrict

update(dt: number) → void

argtypedescription
dtnumber

editorUpdate(dt: number) → void

argtypedescription
dtnumber

onPropertyChanged(key: string, value: any, old: any) → void

argtypedescription
keystring
valueany
oldany

onDestroy( ) → void

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

12+items
▣
module · born here
❒asset
# api The one interface to pickupsKit: `local Pickups = require("pickupsKit.api")`. - **Pickups:** `spawn`, `burst`, `setLook`, `collect`, `available`, `all`, `openChest`. - **Counters:** `get`, `counts`, `allCounts`, `add`, `set`, `spend`, `reset`, `ledger`. - **Letters:** `letters`, `spelled`, `giveLetter`, `clearLetters`. - **Power-ups:** `givePowerup`, `powerup`, `powerupLeft`, `activePowerups`, `damageMultiplier`. - **Catalog:** `looks` (filter by style, kind, effect or size), `look`, `styles`, `kinds`, `effects`, `powerups`, `counters`, `sizes`, `dependencies`. - **Identity and collectors:** `keyOf`, `nameOf`, `localKey`, `localAvatar`, `owns`, `addCollector`, `removeCollector`. - **Setup and events:** `setup`, `installDamageFilter`, and `on` for collect, counter, letter, powerup, powerupEnd, respawn, expire, burst and chest. It also runs the components. The owner of each pickup decides claims and announces them with event ids that every peer de-duplicates, and only the ledger's owner writes counts. Functions starting with `_` are the components' hooks. The package README lists every argument.
▲ 0↑ born
▣
module · born here
❒asset
# catalog pickupsKit's data: the 47 looks (style, kind, effect, amount, sound, juice preset, size tag and dimensions, and the parts the model is built from), the size tags (`SIZES`, `sizeTag`), renamed ids (`ALIASES`, `canonical`), the kit versions it needs (`REQUIRES`), the five styles, kinds, effects, power-ups and their multipliers, known counters, the mystery-box table, HUD styles and `VERSION`. Read it through `Pickups.looks()` and friends rather than editing it; to add a look of your own, copy an entry into `LOOKS`.
▲ 0↑ born
❒
package · born here
❒asset
# pickupsKit 2.3.0 Coins, gems, hearts, shields, keys, letters and power-ups for any game: one `Pickup` component, 47 looks in five styles, each tagged with its size, and multiplayer claims where a simultaneous grab has exactly one winner. Built for platformers, arena shooters, kart racers and adventure games. Every pickup also has its own synthesised sound. ## New in 2.3.0 - **The names you type:** `heart`, `speedOrb`, `damageOrb`, `jumpOrb`, `xpOrb` and `ammoCrate` are main `prefab` names now, beside `goldCoin`, `pixelGem` and the rest. Every older name still works: the 2.2 names (`redHeart`, `speedCore`, `damageCore`, `jumpCore`, `xpShard`, `ammoBox`) and the original ids (`cartoon_coin`, `real_gold_coin`…). `looks()` lists each look's other names as `otherNames`. - **The gallery** labels each pickup with its main name, so you can read the label and type it. Its look switcher changes the coin's `prefab`, the way "Switching a look" below describes. The gallery scene saves with nothing pending, so it clears and reloads cleanly. ## Install One install: `zeromind_install guid=0d70dd00-9297-45d5-a290-5a51b9f9e262`, or search ZeroMind for "pickupsKit" and install it from the ZeroMind panel. That one install also brings the kits pickupsKit builds on, in the versions it was built and tested with, as its own dependencies under `/source/deps/`. Your world ends up with `/source/pickupsKit.package` and nothing else to set up. While the files land, the log can show passing lines such as `waiting for content it requires` or `which is being fetched`; they stop once the install is done. ## Builds on - **SecondWind 1.2.0** (the "Second Wind" world): hearts, medkits and potions heal through it, shields top up its shield, and the damage and invincible power-ups go through its damage filter. SecondWind is itself built on **Lifeline 1.0.1** and **MatchFlow 1.0.0**, which come along too. - **juiceKit 1.2.1** or newer (the "Juicebox" world): the sparkle, flash and pop when something is picked up, spawns or expires. An install brings juiceKit 1.2.2. Your own scripts reach these kits through pickupsKit, because a kit that came in as a dependency belongs to the kit that brought it: `Pickups.health()` returns SecondWind's api, `Pickups.healthParts()` its `Rules`, `Health`, `HUD` and `Trap` components, and `Pickups.juice()` juiceKit's effects. If you install SecondWind or juiceKit yourself as well, `require("SecondWind.api")` works in your own code as usual. `Pickups.dependencies()` lists each kit with the version it needs, the version installed, whether it has loaded, and `ok`, which is true when the installed one is that version or newer. **Pickups never wait for these.** The kit loads SecondWind and juiceKit the first time it needs them, not when it starts. If one is missing or still downloading (a web build fetches packages while the world opens), pickups still appear, spin, collect and count, and the counter and chests still work; the kit logs one warning and tries again a little later. A heart taken meanwhile is still collected, with `note = "no health kit"` on its `collect` event. ## Quick start Add these lines inside `content()` in your scene's `build.luau`, just before its closing `end`. A brand-new world may have no `build.luau` yet: make a scene with a ground and a player first, with `tools.use("scene", "create", "level", "player", { startup = true })`, and add the lines to `scenes/level.scene/build.luau` the same way. ```lua local Pickups = require("pickupsKit.api") -- the coin counter in the corner of the screen entity.spawn("PickupHUD").component.add(Pickups.HUD, { style = "pixel" }) -- five pixel gems in a line in front of where the player starts for i = 1, 5 do Pickups.spawn({ (i - 3) * 1.5, 0, 4 }, { prefab = "pixelGem" }) end ``` Press **Play** and walk forward. Each gem you touch pops with a sparkle and a coin sound, and the counter adds 1. Each gem comes back 5 seconds later. The host makes the synced ledger that holds the counts by itself, so there is nothing else to place. ## More examples All of these go in `content()` too, after the quick start. ```lua -- Health for players, so hearts and shields have something to heal. local HK = Pickups.health() entity.spawnSynced("HealthRules").component.add(HK.RULES, {}) HK.attach(body, { model = "shield" }) -- `body` is your PlayerPrototype's body entity -- A heart that heals 25, a speed power-up and a key that never comes back. Pickups.spawn({ -2, 0, 6 }, { prefab = "heart" }) Pickups.spawn({ 0, 0, 6 }, { prefab = "speedOrb", duration = 10 }) Pickups.spawn({ 2, 0, 6 }, { prefab = "silverKey", respawn = false }) -- Letter tokens that spell ZERO, one copy per player. for i, l in { "Z", "E", "R", "O" } do Pickups.spawn({ -3 + i * 1.5, 0, 8 }, { prefab = "letterToken", letter = l, mode = "perPlayer" }) end -- A chest that spills 12 rubies when a player walks up to it. local chest = entity.spawnSynced("Chest", { position = { 0, 0, 11 } }) chest.component.add(Pickups.CHEST, { prefab = "ruby", count = 12 }) ``` Hearts and shields heal only bodies that have SecondWind health, as above. Without it a heart is still collected, but it has nothing to heal. `Pickups.health()` is nil while SecondWind is still loading; in `content()` it has loaded. To show a word's letters lighting up, give the HUD a word: `{ style = "cartoon", word = "ZERO" }`. **Reading and spending from your own code** (in a component or anything else that runs in play): ```lua Pickups.on("collect", function(ev) print(ev.key, "took", ev.prefab, ev.amount) end) if Pickups.spend(player, "coins", 10) then openDoor() end print(Pickups.get(player, "keys"), Pickups.letters(player), Pickups.spelled(player, "ZERO")) ``` **Switching a look is one setting**, and it applies right away, for every player, also while the game runs: ```lua pickupEntity.component.get(Pickups.PICKUP).prefab = "pixelGem" -- on the engine that owns the pickup Pickups.setPrefab(pickupEntity, "pixelGem") -- from any engine ``` The model is rebuilt on every engine as the change arrives. The pickup keeps its other settings. With `effect = "auto"` (the default) it now does what the new look does. **Who can write the setting.** Like every synced setting, `prefab` can only be written by the engine that owns the pickup: the host for scene pickups, or whoever spawned it. Any other engine's write is refused by the engine on the spot and changes nothing, and the kit can't catch it. `Pickups.setPrefab` works from every engine: on the owner it writes the setting, and elsewhere it asks the owner to. Both were played with two players: a coin one player owned changed to `pixelGem` for both through the plain setting, and a gallery pickup the other player's engine owned changed through `setPrefab` called by the player who wasn't hosting. Old names worked the same way, through `look = "cartoon_coin"` and `setPrefab(box, "cartoon_mystery")`. **Coin bursts** from a defeated enemy or a broken pot: ```lua Pickups.burst(enemy.position, { prefab = "goldCoin", count = 10, lifetime = 12 }) ``` The coins fly out and bounce. Each one can only be taken once it has settled, and each blinks for its last 3 seconds before it disappears. ## The Pickup component `pickupsKit.Pickup`. Every setting is synced: a change made on the engine that owns the pickup reaches every engine, also while the game runs. | Setting | Default | What it does | |---|---|---| | `prefab` | `goldCoin` | Which of the 47 looks it wears, by its short name (the Looks table). This also sets what it does when `effect` is `auto`. | | `look` | `goldCoin` | The older name of `prefab`, kept so existing scenes and code keep working. It takes the new names and the old ids. Whichever of the two you write, the other follows. | | `effect` | `auto` | What touching it does: `auto`, `counter`, `heal`, `shield`, `item`, `letter`, `powerup`, `mystery` or `none`. | | `amount` | 0 | How much it gives: coins, health, shield and so on. 0 uses the look's amount. | | `counter` | "" | Counter for `counter`: coins, gems, stars, rings, xp, ammo, or any name of your own. | | `item` | "" | Item for `item`: keys, keycards, or your own. It is counted like a counter. | | `letter` | "A" | The letter shown on letter tokens. | | `powerup`, `duration` | "", 0 | The power-up and its length in seconds: `speed`, `jump`, `damage`, `magnet` or `invincible`. | | `radius` | 1.1 | How close a player must come to take it, in metres. | | `magnetRange` | 0 | Within this range it flies to the player. 0 means no magnet. A `magnet` power-up triples it. | | `respawn`, `respawnTime` | true, 5 | Whether it comes back after being taken, and after how many seconds. | | `lifetime`, `blinkTime` | 0, 3 | It disappears after `lifetime` seconds untouched (0 = never), blinking for the last `blinkTime` of them. With `respawn` on, it then comes back after `respawnTime`. | | `mode` | `shared` | `shared`: the first player to touch it gets it. `perPlayer`: every player can take their own copy, and it comes back for each of them separately. | | `bob`, `spin`, `hover`, `scale` | 0.12, 1.8, 0.6, 1 | Bob height in metres, spin in turns per second, hover height and size. | | `visible` | true | Hide it without removing it. | | `castShadows` | false | Cast a sun shadow. Off by default, because many small spinning casters redraw the shadow map every frame, which costs most in a web browser. Turn it on for a few hero pickups. | `available`, `taken`, `armed` and `launch` are state fields that the kit writes itself. **Old names keep working.** Every look had a longer id before 2.2.0 (`cartoon_coin`, `real_gold_coin`, `pixel_gem_coin`...), and six looks had other short names in 2.2: `redHeart`, `speedCore`, `damageCore`, `jumpCore`, `xpShard` and `ammoBox` are now `heart`, `speedOrb`, `damageOrb`, `jumpOrb`, `xpOrb` and `ammoCrate`. Every older name still works everywhere a look goes: the `look` setting, `spawn`, `setPrefab`, `setLook`, `burst`, chests and `look()`, and it is stored as the main name. The `prefab` field's own list holds only the main names, so writing an older name straight into `prefab` is refused by the engine; use `look`, or `setPrefab`. The Looks table shows every other name a look answers to, and `looks()` returns them as `otherNames`. **Other components:** - `PickupChest` opens when a player comes within `radius` and spills `count` coins of any `prefab` (`look` is its older name there too). It closes again after `cooldown` seconds, or stays open if `cooldown` is 0. It draws its own chest model; set `showModel = false` to use yours. - `PickupHUD` is the on-screen counter, with `style` set to cartoon, scifi, fantasy, pixel or realistic. Its other settings: - `counters`: which counters it shows, in order; - `word`: a word whose letters light up as they are collected; - `showPowerups`: running power-ups with their time left; - `showPlayers`: every player's coins; - `corner`: which top corner it sits in. - `PickupLedger` is the synced record of every player's counters and letters. The host spawns it by itself. ## Looks There are 47 looks in five styles that a world can mix and match: cartoon 9, sci-fi 10, fantasy 10, pixel 9 and realistic 9. To list them, call `Pickups.looks()`, or filter with `Pickups.looks({ style = "pixel" })`, `Pickups.looks({ kind = "coin" })` or `Pickups.looks({ size = "small" })`; filters combine. Each entry carries its `id` (the `prefab` name), `oldId`, `name`, `style`, `kind`, `effect`, `size`, `sizeM`, `dims` and what it gives. | Style | What it looks like | |---|---| | cartoon | Chunky glossy shapes with dark ink outlines. | | scifi | Dark metal shells around glowing energy cores. | | fantasy | Gems, potions in glass, gold and runes. | | pixel | Voxel sprites: 8-bit pixels with real depth. | | realistic | Brushed metal, brass, painted boxes and real-world props. | **Sizes.** Every look carries a size tag measured from its model at `scale = 1`. `dims` is its width, height and depth in metres, and `sizeM` is the largest of the three. `Pickups.sizes()` lists the tags: | Size | Largest side | Looks | |---|---|---| | `small` | up to 0.62 m | 9: gems, wisps, the realistic coins and kits | | `medium` | 0.63 to 0.75 m | 28: most coins, hearts, keys, badges and pixel sprites | | `large` | over 0.75 m | 10: power-up cores, potions, shields and the winged orb | Use the tag to pick looks that sit well together, or to choose a `radius`: about 1.1 suits small and medium looks, and about 1.3 suits large ones. `pickupsKit.visual.extent(prefab)` measures a model's box directly. | `prefab` | Also answers to | Name | Style | Kind | Effect | Size | Gives | |---|---|---|---|---|---|---|---| | `goldCoin` | `cartoon_coin` | Gold coin | cartoon | coin | counter | medium (0.72 m) | coins +1 | | `goldStar` | `cartoon_star` | Star | cartoon | star | counter | large (0.78 m) | stars +1 | | `goldRing` | `cartoon_ring` | Gold ring | cartoon | ring | counter | medium (0.72 m) | rings +1 | | `heart` | `cartoon_heart`, `redHeart` | Heart | cartoon | heart | heal | medium (0.68 m) | heal 25 | | `mysteryBox` | `cartoon_mystery` | Mystery box | cartoon | mystery | mystery | medium (0.65 m) | random roll | | `letterToken` | `cartoon_letter` | Letter token | cartoon | letter | letter | medium (0.70 m) | its `letter` | | `speedBadge` | `cartoon_speed` | Speed badge | cartoon | powerup | powerup | medium (0.70 m) | speed 8s | | `damageBadge` | `cartoon_damage` | Damage badge | cartoon | powerup | powerup | medium (0.70 m) | damage 8s | | `jumpBadge` | `cartoon_jump` | Jump badge | cartoon | powerup | powerup | medium (0.70 m) | jump 8s | | `energyCredit` | `scifi_chip` | Energy credit | scifi | coin | counter | medium (0.70 m) | coins +1 | | `shieldCell` | `scifi_shield` | Shield cell | scifi | shield | shield | large (0.77 m) | shield 30 | | `accessCard` | `scifi_keycard` | Keycard | scifi | keycard | item | medium (0.64 m) | keycards +1 | | `ammoCell` | `scifi_ammo` | Ammo cell | scifi | ammo | counter | medium (0.72 m) | ammo +30 | | `speedOrb` | `scifi_speed`, `speedCore` | Speed orb | scifi | powerup | powerup | large (0.78 m) | speed 8s | | `damageOrb` | `damageCore`, `scifi_damage` | Damage orb | scifi | powerup | powerup | large (0.78 m) | damage 8s | | `jumpOrb` | `jumpCore`, `scifi_jump` | Jump orb | scifi | powerup | powerup | large (0.78 m) | jump 8s | | `magnetCore` | `scifi_magnet` | Magnet core | scifi | powerup | powerup | large (0.78 m) | magnet 10s | | `xpOrb` | `scifi_xp`, `xpShard` | XP orb | scifi | xp | counter | medium (0.66 m) | xp +10 | | `nanoMedkit` | `scifi_medkit` | Nano medkit | scifi | medkit | heal | medium (0.64 m) | heal 50 | | `ruby` | `fantasy_ruby` | Ruby | fantasy | gem | counter | small (0.60 m) | gems +1 | | `emerald` | `fantasy_emerald` | Emerald | fantasy | gem | counter | small (0.54 m) | gems +3 | | `healingPotion` | `fantasy_potion_red` | Healing potion | fantasy | potion | heal | large (0.76 m) | heal 35 | | `wardPotion` | `fantasy_potion_blue` | Ward potion | fantasy | potion | shield | large (0.76 m) | shield 35 | | `goldenKey` | `fantasy_key` | Golden key | fantasy | key | item | medium (0.69 m) | keys +1 | | `silverKey` | | Silver key | fantasy | key | item | medium (0.69 m) | keys +1 | | `crestShield` | | Crest shield | fantasy | shield | shield | large (0.81 m) | shield 40 | | `wingedOrb` | `fantasy_jump` | Winged orb | fantasy | powerup | powerup | large (0.94 m) | jump 8s | | `runeStone` | `fantasy_rune` | Rune stone | fantasy | letter | letter | medium (0.67 m) | its `letter` | | `xpWisp` | `fantasy_wisp` | XP wisp | fantasy | xp | counter | small (0.47 m) | xp +25 | | `pixelGem` | `pixel_gem_coin` | Pixel gem | pixel | coin | counter | medium (0.72 m) | coins +1 | | `pixelCoin` | `pixel_coin` | Pixel coin | pixel | coin | counter | medium (0.68 m) | coins +1 | | `pixelHeart` | `pixel_heart` | Pixel heart | pixel | heart | heal | medium (0.68 m) | heal 25 | | `pixelKey` | `pixel_key` | Pixel key | pixel | key | item | medium (0.70 m) | keys +1 | | `powerStar` | `pixel_star` | Power star | pixel | powerup | powerup | medium (0.72 m) | invincible 8s | | `jumpMushroom` | `pixel_mushroom` | Jump mushroom | pixel | powerup | powerup | medium (0.68 m) | jump 10s | | `pixelSword` | `pixel_sword` | Pixel sword | pixel | powerup | powerup | medium (0.72 m) | damage 8s | | `pixelLetter` | `pixel_letter` | Pixel letter | pixel | letter | letter | medium (0.72 m) | its `letter` | | `questionBlock` | `pixel_mystery` | Pixel ? block | pixel | mystery | mystery | medium (0.63 m) | random roll | | `silverCoin` | `real_silver_coin` | Silver coin | realistic | coin | counter | small (0.60 m) | coins +1 | | `goldDoubloon` | `real_gold_coin` | Gold doubloon | realistic | coin | counter | medium (0.64 m) | coins +5 | | `goldBar` | `real_gold_bar` | Gold bar | realistic | coin | counter | small (0.62 m) | coins +25 | | `cutDiamond` | `real_diamond` | Diamond | realistic | gem | counter | small (0.60 m) | gems +5 | | `firstAidKit` | `real_medkit` | Medkit | realistic | medkit | heal | small (0.62 m) | heal 50 | | `ammoCrate` | `ammoBox`, `real_ammo` | Ammo crate | realistic | ammo | counter | small (0.62 m) | ammo +30 | | `brassKey` | `real_key` | Brass key | realistic | key | item | medium (0.69 m) | keys +1 | | `armorPlate` | `real_armor` | Armor plate | realistic | shield | shield | medium (0.71 m) | shield 50 | | `energyDrink` | `real_energy` | Energy drink | realistic | powerup | powerup | small (0.60 m) | speed 8s | ## Effects | Effect | What it does | |---|---| | `counter` | Adds `amount` to the player's `counter`. | | `heal` | Heals `amount` through SecondWind (`HK.heal`), on the engine of the player who took it. A player at full health still takes it: the heart is collected and counted, and heals 0. | | `shield` | Tops up the SecondWind shield by `amount`, up to the maximum, on the engine that holds that player's health (the kit sends it there). | | `item` | Adds `amount` of `item` (keys, keycards...). Spend it with `Pickups.spend(player, "keys", 1)`. | | `letter` | Adds the token's letter. Check a word with `Pickups.spelled(player, "ZERO")`. | | `powerup` | Gives a timed power-up. Taking the same one again adds to its time. | | `mystery` | Rolls one weighted reward: coins, XP, heal, shield or any power-up. | | `none` | Nothing but the `collect` event, so you can handle it yourself. | **Power-ups:** - `speed`: moveSpeed ×1.6. - `jump`: jumpForce ×1.55. - `damage`: deals ×2 damage through SecondWind. - `magnet`: pickups pull from 3× further away. - `invincible`: takes no SecondWind damage. Each family has a speed, damage and jump look: cartoon badges (`speedBadge`, `damageBadge`, `jumpBadge`), sci-fi orbs (`speedOrb`, `damageOrb`, `jumpOrb`), and the pixel `jumpMushroom`, `pixelSword` and `powerStar`. Speed and jump are held on the player's CharacterController, on that player's own engine, for as long as the power-up runs. If something else, such as SecondWind's chill, changes the speed meanwhile, that change is respected. Damage and invincible work through SecondWind's damage filter. Every engine knows every player's power-ups, so the filter is right wherever the damage is decided. The kit installs the filter the first time one of those power-ups is given, or straight away through `Pickups.setup()`. SecondWind keeps a single filter, so if your world has its own, call `Pickups.setup({ damageFilter = false })` and multiply by `Pickups.damageMultiplier(attacker, victim)` inside yours. ## API `local Pickups = require("pickupsKit.api")`. A "player" argument can be an avatar entity, any collector entity, a peer number or a ledger key (`"p3"`). **Pickups** - `spawn(pos, settings?, { synced = true, name }?)` returns the entity. It errors on an unknown prefab or mode. `settings.prefab` names the look (`settings.look` is its older name). - `burst(pos, { prefab, count = 8, amount, spread = 2.4, lifetime = 12, blinkTime = 3, magnetRange, mode, synced })` returns the coins. - `setPrefab(pickup, prefab)` (also `setLook`), `collect(pickup, collector)`, `available(pickup, collector?)`. - `all()` returns `{ [id] = Pickup }`. `openChest(chest)`. **Counters** - `get(player, counter)`, `counts(player)`, `allCounts()`. - `add(player, counter, n)`, `set(player, counter, v)`, `spend(player, counter, n)`, `reset(player?)`. - `ledger()` returns the PickupLedger component in use. **Letters**: `letters(player)`, `spelled(player, word)`, `giveLetter(player, letter)`, `clearLetters(player)`. **Power-ups**: `givePowerup(player, name, secs)`, `powerup(player, name)`, `powerupLeft(player, name)`, `activePowerups(player)`, `damageMultiplier(attacker, victim)`. **Catalog**: `looks(filter?)` (filter by `style`, `kind`, `effect` and `size`), `look(id)` (a new name or an old id), `styles()`, `kinds()`, `effects()`, `powerups()`, `counters()`, `sizes()`. Everything they return is a copy. **The kits it builds on**: `dependencies()` returns `{ name, needs, installed, loaded, problem, ok, why }` for SecondWind and juiceKit. `health()` and `juice()` return each module, or nil and why not, loading it if it can. `healthParts()` returns SecondWind's `{ Rules, Health, HUD, Trap }` components. **Identity**: `keyOf(x)`, `nameOf(x)`, `localKey()`, `localAvatar()`, `owns(x)`. **Checking every engine**: `peerReport(seconds?)` asks every engine in the play session for its `selfReport()` and returns `{ [peer] = report }`, this engine's own included. Each report says whether SecondWind and juiceKit loaded on that engine (and why not), which platform it runs on (`native` or `browser`), whether it holds the ledger, its own player's health as SecondWind sees it, and the outcome of the last heal or shield it applied. It is how a problem on another player's engine, such as a browser, can be read from yours. **Collectors**: `addCollector(entity)` lets a bot or a kart collect, with its counts kept under `"e<id>"`. `removeCollector(entity)` undoes it. **Setup**: `setup({ hud = true, hudStyle = "cartoon", word = "", damageFilter = true })` is optional. It makes the ledger now rather than after the host's first second, adds the HUD and installs the damage filter. `installDamageFilter()` installs just the filter. **Events**, through `on(event, fn)`, which returns an unsubscribe function: | Event | Arguments | |---|---| | `collect` | `ev` = `{ id (event id), p (pickup id), prefab, look (the same name), style, kind, effect, amount, counter, item, letter, powerup, duration, mystery, key, cid (collector id), pos, mode }` | | `counter` | `key, counter, value, delta` | | `letter` | `key, letter, letters` | | `powerup` | `key, name, seconds` | | `powerupEnd` | `key, name` | | `respawn` | `pickupId` | | `expire` | `pickupId, pos` | | `burst` | `pos, count` | | `chest` | `chestId` | `pickupsKit.visual` draws a look on this peer, for your own props or menus: - `build(prefab, { letter, name, shadows }?)` returns a handle; - `place(h, pos, yaw, scale)`, `setVisible(h, on)`, `setShadows(h, on)`, `alive(h)`, `destroy(h)`; - `extent(prefab)` returns the model's `{ x, y, z }` box at scale 1, or nil for an unknown look; - `release(h)` parks a visual hidden for reuse, and `acquire(prefab, opts?)` takes a parked one of that look back, or builds a new one. `poolSize()` and `clearPool()` look after the pool, which holds up to `POOL_PER_LOOK` (16) per look and `POOL_MAX` (96) in all. `pickupsKit.logic` holds the rules as pure functions you can reuse or test: `canClaim`, `applyClaim`, `settle`, `addCount`, `spelled`, `rollMystery`, `burstVelocity`, `stepCoin`, `magnetStep`, `blinkVisible`, `versionAtLeast` and more. `pickupsKit.catalog` is the raw data: `LOOKS`, `BY_ID`, `IDS`, `SIZES`, `REQUIRES`, `ALIASES` (old id to new name), `OLD_ID` (new name to old id) and `canonical(id)`, which turns any old id into the current name. ## How multiplayer works - **The owner decides.** Every pickup is a synced entity, and the engine that owns it (the host, for scene pickups) is the only one that decides who gets it. Each engine runs the touch test for the players and collectors it moves, then asks the owner. The owner checks that the pickup is still available to that player and that the player is really within reach, then marks it taken in the same step, so when two players touch the same coin the first claim to arrive wins and it is counted once. - **Entities travel as entities.** The kit never relies on an entity's id string matching on another engine: pickups, chests and players go over the network as entity references, which each engine receives as its own, and become local ids on arrival. As a safeguard the kit also sends each entity's sync id, which every engine shares, and can resolve an entity from it if a reference ever arrives as raw data. (Before 2.2.0 the kit sent id strings, so a claim from a player who wasn't hosting named a pickup the host didn't know, and none of their pickups did anything.) - **One announcement per collect.** The owner announces the result as one event with an id. Every engine records the power-ups and plays the pop, and the engine of the player who took it applies their heal, shield, speed and jump, exactly once per event. - **Every message counts once.** Every kit message carries an id of its own, and a repeat is dropped, so a message that reaches an engine twice is handled once. After a live edit of the kit, the old copy's handlers stay registered (#19348), so they check that their copy is still the current one and stand down otherwise; an edit never leaves two copies answering. - **The counts live in one place.** Counters and letters live in the synced `PickupLedger`, and only its owner writes it. The host makes it. When the player who started play leaves, nobody counts as host any more (#19347), so then the lowest-numbered player in the play room makes it after `Pickups.LEDGER_FALLBACK_S` (2.5) seconds. As a safeguard, a moment after a new player appears in the play room, the ledger's owner makes it again with the same counts (`Pickups.LEDGER_REPUBLISH_S`, 1 second), so a late joiner always receives a fresh one. If two ledgers ever meet, every engine keeps the one with the lower sync id, and the other's counts are folded into it. - **Shared or per-player.** A `shared` pickup has one `available` flag. A `perPlayer` pickup keeps a synced `taken` map of who has had it, so each player has their own copy. - **Settings are synced.** `prefab` and the other settings are written by the pickup's owner and sync to every engine. From any other engine, `setPrefab` asks the owner to make the change. **Seen with two players.** 2.2.0 and 2.2.1 were played with two engines in one play session, the host and a player who wasn't hosting: - Each kind of pickup was dropped at the other player's feet, and their own engine picked it up, sending the pickup and their player as entity references. - Coins, keys, a letter and XP were counted once under their name in the host's ledger. - Heals and shields reach the player who isn't hosting: a heart healed them 20 → 45, and a shield cell took their shield 0 → 30. - Speed and jump took their controller from 2.5 to 4 and from 5 to 7.75. - With damage, their hit on the host counted double. With invincible, 30 damage to them changed nothing. - With the magnet, a pickup out of reach flew to them. - One coin under both players was taken exactly once. - When the host left play, the other engine took over the pickups and kept deciding them. - `prefab = "pixelGem"` written by a coin's owner reached the other engine, and `setPrefab` called by the player who wasn't hosting changed a pickup the host owned. Old names worked the same way. - With the other engine hosting, a player joining late read everyone's counts as they were: the host's player had 2 coins, 1 key, 10 XP and the letter Z, exactly as before the join. - A coin taken from the host's gallery disappeared on the player's own engine, came back when the host respawned it, and played its pop and sound there. - On a per-player gem, the player took their own copy while the host's player kept theirs, and their copy came back on its own timer. - A blinking coin's expiry, decided by the host, reached the other engine. - Walking up to a chest the host owned opened it, and all 12 burst coins appeared on both engines. - The counter on the screen of the player who wasn't hosting showed their own counts from the host's ledger (7 coins, 3 gems, 1 key, 10 XP, Z), with both players listed. - **In a web browser.** The same check, with the person's browser hosting (`platform = "browser"`) and an editor as the other player, passed 12 of 12 for the browser player: coins, keys, a letter and XP counted once under their name; a heart healed them 60 → 85; a shield cell took their shield 0 → 30; speed 2.5 → 4 and jump 5 → 7.75; their hit counted double with damage; invincible stopped 30 damage; the magnet pulled a pickup to them; one coin under both players was taken once. SecondWind and juiceKit both loaded in the browser from the copies that come with pickupsKit. **How it is tested.** One engine can't be two real players (#18688, #19114), so the tests play both ends of the network through the same handlers the relay runs (`_deliver`). As the host: the other player's claims for every kind of pickup, naming the pickup and their avatar by entity, are each decided once, counted under their key, refused the second time, and announced with entity references. As the other player's engine: the host's announcements heal and shield their own player on the next frame, boost speed and jump on their CharacterController, double their damage, make them invincible and give them a magnet. The "two players" test runs 20 coins and a heart through two players whose claims arrive early, late and twice; every coin is taken once. **For testing your own game's netcode** (internal, not a stable API): `_deliver(channel, fromPeer, ...)` runs one of the kit's network handlers (`pk:claim`, `pk:got`, `pk:ledger`, …) as if a message had arrived from `fromPeer`. `_tap(fn)` calls `fn(channel, ...)` for everything this engine broadcasts and returns an unsubscribe function. ## Performance in the browser The kit is built to keep a busy level smooth in a web browser: - **No pickup shadows by default.** Turning them off cut the gallery's sun shadow drawing from about 4.9 ms to 2.2 ms a frame and its frame spikes from 29 to 3. Use `castShadows` only where it matters. - **Visuals are reused.** A pickup that is removed or changes look parks its model, and the next pickup of that look takes it back instead of building a new one. Repeat bursts and look switches cost a fraction of a fresh build. - **First builds are spread out.** In play, pickups appearing for the first time build at most `Pickups.BUILD_BUDGET_MS` (3 ms) of models a frame, and always at least one. A wave of spawns therefore takes a few frames instead of stalling one. A prefab or letter change never waits. - **Labels: show and hide, don't rewrite.** Changing a `Text3D` label's text redraws it, about 5 ms each time. For text that cycles through known values, make one label per value and show the one you need. Showing or hiding a label costs about 0.01 ms. The gallery's look-switch label works this way. ## Known limits Engine problems that still affect the kit, each with its GitHub issue: - **#19199: your own scripts can't reach a kit that came in as a dependency.** SecondWind and juiceKit arrive with pickupsKit, but `require("SecondWind.api")` in your own code answers `could not resolve require`. Go through the kit: `Pickups.health()`, `Pickups.healthParts()` and `Pickups.juice()`. Installing SecondWind or juiceKit yourself as well also works. - **#19339: the editor's checker flags the kit's SecondWind lookups.** Opening `pickupsKit.api` shows `asset reference "SecondWind.Rules" could not be resolved` (and `Vitals`, `HUD`, `Trap`). They are warnings only: the lookups resolve when the game runs, and nothing blocks publishing. - **#19340: a setting with a fixed list of choices can't take old names.** The `prefab` list holds only the new names, so writing an old id such as `cartoon_coin` straight into `prefab` is refused. Old ids go through `look`, `spawn`, `setPrefab`, `setLook` and `burst`. The editor's list for `look` shows the old ids after the new names. - **#19260: the bundled Lifeline and MatchFlow copies resolve some of their own parts at older versions, which land as lone files that don't compile.** Lifeline's own health part was one of them: in a fresh install, `HK.attach` gave a player no health, so hearts couldn't heal anyone. pickupsKit fixes this for you: when it loads SecondWind it checks Lifeline's health, projectile and spawn-point parts and points any lone old copy at the complete one that ships with the install (the log says `pointed Lifeline at its complete …`). Always go through `Pickups.health()` before `HK.attach`, as the examples do. Lines such as `component source failed to load … versions.…` or `needs version … skipped as stale` can still appear in the log for Lifeline's zones and projectile and MatchFlow's match component, which pickups don't use. - **#18688 and #19114: one engine can't be two players.** The loopback test mode doesn't echo messages (#18688), and an agent working inside the engine has no way to bring in a second player (#19114). The kit's own tests therefore drive both ends of the network through `_deliver`, and a real two-player check needs a second engine, as in "Seen with two players" above. - **#19347: nobody is host once the player who started play leaves.** The host role stays with whoever started the play session, even after they leave, so code waiting for the host never runs. The kit works around it for its counts: when nobody is host, the lowest-numbered player in the play room makes the ledger. Your own host-only code (spawning enemies, starting a round) needs the same care. - **#19348: a message handler can't be removed, and old ones keep running after a live edit.** Each time the kit's code is edited while the game runs, the previous copy's handlers stay registered. The kit's old handlers check that they are still current and do nothing otherwise, so live edits are safe. A full reload of play clears them. ## Showcase and tests The world this package lives in, "Treasure Trove", opens on a gallery. It is set up exactly as a one-install world: pickupsKit in `/source`, and the kits it builds on only as its dependencies. It has every look, labelled and grouped by style, and a station for each behaviour: - a respawning coin; - blink and vanish; - a coin burst from a chest; - a magnet line; - a shared pickup and a per-player one side by side; - hearts and shields beside a lava trap; - power-ups; - spelling a word; - a coin that cycles through prefabs, the pixel gem among them, by setting its `prefab` exactly as "Switching a look" above describes. Every look's label shows its main `prefab` name, the one you type to use it. `pickupsKit.pickupskit` is the test suite. It covers every public call of the api, logic, catalog, sounds, meshes, visual and fonts, the new names and every old id, `prefab` and `look`, the player who isn't hosting, and checks this README against the code: the version heading, the kit versions above, a row with style, kind, effect and size for every look, the quick start (short, compiles, real names) and this Known limits list. Run it with `tools.use("tests", "run", { suite = "pickupskit" })`. ## Version 2.3.0, which matches `package.yaml` (`pickupsKit.catalog.VERSION`, checked by the tests). - 2.3.0: - **`heart`, `speedOrb`, `damageOrb`, `jumpOrb`, `xpOrb` and `ammoCrate` are main names** (they were `redHeart`, `speedCore`, `damageCore`, `jumpCore`, `xpShard` and `ammoBox`, which still work, as do the pre-2.2 ids). `looks()` and `look()` list every other name a look answers to as `otherNames`; `catalog.OTHER_NAMES` holds them. - **Gallery:** each label shows the look's main name; the look switcher sets the coin's `prefab`, as the README says to; the gallery scene saves with nothing pending, so clearing and reloading it gives every pickup back. - 2.2.1: - **Played with two players, start to finish.** Every behaviour in "Seen with two players" was watched across two engines, with each engine taking a turn as host. That covers every pickup kind for the player who isn't hosting, per-player copies, late joiners, respawns, expiries, bursts and chests, the pop and sound, the counter, and `prefab` switches. - **`collect` events carry `prefab`** as well as `look` (the README's own example read `ev.prefab`, which was missing). - **Who can write `prefab`:** the README says it plainly. The pickup's owner writes the setting, and `setPrefab` works from any engine. - **Known limits** now name only problems that still exist, each with its GitHub issue. - **Heals and shields reach a player in a web browser** (played 12 of 12, see "Seen with two players"). Messages also carry each entity's sync id as a safeguard. - **Hearts heal in a fresh install.** Lifeline's own health part resolved to a lone old-version copy that doesn't compile (#19260), so `HK.attach` gave no health. pickupsKit now points Lifeline at the complete copy when it loads SecondWind. Lifeline is now a direct dependency of pickupsKit, at the same 1.0.1. - **Every message counts once.** A live edit of the kit left the old copy's handlers answering too (#19348). Messages now carry ids, repeats are dropped, old handlers stand down, and a heal or shield is applied exactly once per event. - **The counts ledger always exists, for everyone.** It was made only by the host, and nobody counts as host once the player who started play leaves (#19347), so a session could count nothing. The lowest-numbered player in the play room now makes it when nobody is host, its owner makes it again when a new player joins, and two ledgers fold into one. - **`peerReport()` and `selfReport()`** read every engine's view of the kit from yours. - **Tests:** 85, including entities that arrive as raw data, a heal heard twice, repeated messages, the ledger election, folding two ledgers and making the ledger again for a joining player. - **Played in a web browser:** every pickup kind, 12 of 12, for a browser player (see "Seen with two players"). - 2.2.0: - **Pickups work for every player, not just the host.** The kit sent pickup and player ids as text, but every engine has its own ids for the same entity, so a claim from a player who wasn't hosting named nothing the host knew. Now pickups, chests and players cross the network as entity references. Coins, keys, letters, XP, hearts, shields and every power-up reach whoever touched them; a coin is still only ever taken once. - **`prefab` is the setting**, with a short name for every look (`goldCoin`, `pixelGem`, `speedCore`, `healingPotion`, `brassKey`...). `look` and every old id (`cartoon_coin`, `real_gold_coin`...) keep working. Added `setPrefab`, `oldId` on `looks()`, `catalog.OLD_ID` and `catalog.PREFAB_IDS`; the chest takes `prefab` too. - **One install.** Installing pickupsKit now brings SecondWind 1.2.0, juiceKit 1.2.2, Lifeline 1.0.1 and MatchFlow 1.0.0 with it, and the kit uses them. Added `healthParts()` for SecondWind's components; your own code reaches SecondWind through `Pickups.health()`. - **Tests:** 77, including the new names, `prefab` and `look`, the player who isn't hosting, and walking up to a chest. - **Chests open when you walk up to them anywhere.** Since 1.1.0 the walk-up check measured from the middle of the map, so a chest away from it only opened from code. - **Showcase:** set up as a one-install world, every pickup by its new name. - 2.1.0: pickups load without waiting for SecondWind or juiceKit; SecondWind 1.2.0; heals and shields heard in a message apply on the next frame. - 2.0.0: renamed to pickupsKit (from pickupKit, same guid); `pixelGem`; five new looks; size tags; dependency versions; remote pops on the next frame. - 1.1.1: works with SecondWind 1.1.1; passing a Pickup component no longer logs. - 1.1.0: performance in the browser: pickup shadows off by default, pooled visuals, a per-frame build budget. - 1.0.1: a two-player test of 20 coins and a heart. - 1.0.0: first release, as pickupKit.
▲ 0↑ born
backing path · pickupsKit.package/Pickup.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.