Repository navigation
01 Prototype Tech 05 Item Effects
Per-item effect definitions, level tables, and cost/scaling for the prototype item set. Designs live in ../design/items.md (the causality and stat items) and ../design/starter-items.md (the starter items).
Cost formula: cost = base_cost * scaling ^ current_level. Level 1 cost is base_cost. Starter-item cost and scaling are TBD.
| Level | Extra balls (cap) | Frenzy trigger |
|---|---|---|
| 1 | 1 ball (cap 2) | On personal best |
| 2 | 2 balls (cap 3) | On personal best |
| 3 | 3 balls (cap 4) | On personal best or streak milestone (tuning target) |
Effect 1
trigger: on_miss
condition: game_state_is_not("frenzy")
outcome: spawn_ball [capped per level]
Effect 2
trigger: on_personal_best [levels 1-2] / on_personal_best or on_streak_milestone(n) [level 3]
outcome: set_game_state("frenzy")
outcome: multiply_stat_temporary(ball_speed_min, 2.0, until_state_exits("frenzy"))
Effect 3
trigger: on_miss
condition: game_state_is("frenzy")
outcome: clear_extra_balls
outcome: set_game_state(null)
Base cost: 60 friendship | Scaling: 1.7
| Level | Fires every | Colours |
|---|---|---|
| 1 | 20 hits | Yellow (x1), Red (x2), Green (x3) |
| 2 | 15 hits | Yellow (x1), Red (x2), Green (x3), Blue (x5) |
| 3 | 10 hits | Yellow (x1), Red (x2), Green (x3), Blue (x5), Gold (x8) |
Effect 1
trigger: on_streak_multiple(n) [n scales with level: 20/15/10]
outcome: multiply_stat_temporary(soul_per_hit, random_colour_tier, until_next_trigger)
outcome: deflect_ball
Base cost: 80 friendship | Scaling: 1.5
| Level | Gravity | Friendship bonus | Rescue pull |
|---|---|---|---|
| 1 | Mild pull, fixed position | Friendship scales with ball speed at hit | No |
| 2 | Stronger pull, point drifts | Better friendship scaling | No |
| 3 | Stronger pull, point drifts | Better friendship scaling | Intense temporary pull when ball passes behind paddle |
Effect 1
trigger: always
outcome: spawn_gravity_well(strength and drift scale with level)
Effect 2
trigger: on_hit
outcome: award_soul(scale_by: ball_speed)
tuning: on_hit fires every hit, so friendship generation rate climbs sharply at high ball speeds. Cap or diminishing returns may be needed to keep friendship economy balanced.
Effect 3 (level 3 only)
trigger: on_ball_behind_paddle
outcome: intensify_gravity_well(multiplier: tuning_target, duration: tuning_target)
Base cost: 100 friendship | Scaling: 1.8
No levels. Single purchase. Court role.
Effect 1
trigger: always
outcome: expand_kit_slots(1)
Base cost: 150 friendship
The expand_kit_slots outcome is deprecated under the role model in 06-roles.md; kit-slot enforcement for Spare is tracked in Linear (Kit slot enforcement).
| Level | Roll table | Delay range |
|---|---|---|
| 1 | Payout, False Start | 5-30s |
| 2 | + Photo Finish, Dead Heat | 5-25s |
| 3 | + Long Shot Pays | 5-20s |
Effect 1
trigger: on_streak_start
condition: delay_random(min, max) [scales with level]
outcome: roll_table([outcomes per level, equal weight])
Base cost: 70 friendship | Scaling: 1.6
| Level | Repair | Multiplier range (0-100 cracks) | Broken debuff |
|---|---|---|---|
| 1 | Fresh | x1.0 to x1.5 (tuning target) | Slight friendship reduction |
| 2 | Full repair | x1.0 to x2.0 | Same |
| 3 | Full repair | x1.0 to x3.0 | Same |
Effect 1
trigger: always
condition: degradation_below(100)
outcome: multiply_stat(soul_per_hit, scale_by: crack_count)
Effect 2
trigger: on_miss
outcome: increment_degradation(1) [hidden]
Effect 3 (broken state)
trigger: always
condition: degradation_at(100)
outcome: stat(soul_per_hit, -debuff)
Base cost: 90 friendship | Scaling: 1.5
Passive stat modifiers. Max level 10.
| Level | Effect |
|---|---|
| 1-10 | +50 paddle speed per level |
Effect 1
trigger: always
outcome: stat(paddle_speed, +50 per level)
Base cost: 30 friendship | Scaling: 1.5
| Level | Effect |
|---|---|
| 1-10 | +140% paddle size per level |
Effect 1
trigger: always
outcome: percentage(paddle_size, +140% per level)
Base cost: 30 friendship | Scaling: 1.5
| Level | Effect |
|---|---|
| 1-10 | +30 ball speed min per level |
Effect 1
trigger: always
outcome: stat(ball_speed_min, +30 per level)
Base cost: 40 friendship | Scaling: 1.6
| Level | Effect |
|---|---|
| 1-10 | +50 ball speed max range per level |
Effect 1
trigger: always
outcome: stat(ball_speed_max_range, +50 per level)
Base cost: 40 friendship | Scaling: 1.6
Cursed item.
| Level | Effect |
|---|---|
| 1-10 | +8 ball speed increment per level, -140% paddle size per level [cursed] |
Effect 1
trigger: always
outcome: stat(ball_speed_increment, +8 per level)
Effect 2
trigger: always
outcome: percentage(paddle_size, -140% per level) [cursed]
Base cost: 35 friendship | Scaling: 1.5
Effect 1 (all levels)
trigger: on_consolidation
outcome: split_ball(level 1: to two; levels 2-3: one more each consolidation)
Effect 2 (levels 1-2)
trigger: on_balls_close
outcome: merge_balls (any pair)
outcome: soul_burst(scale_by count merged)
Effect 3 (level 3)
trigger: on_balls_close
condition: one of the pair is the original (source) ball
outcome: merge_balls (only the original absorbs; husk pairs do not merge)
outcome: soul_burst(scale_by count merged)
Effect 4 (level 3)
trigger: on_consolidation
condition: the court was folded back to the single original before this consolidation
outcome: split_bonus(+1 to this split) [snowballs while the player keeps fully merging in time]
Split fires on soul consolidation; no dependency on the milestone system. Split balls are ephemeral: is_temporary = true (skipped by BallReconciler on adoption) with a source_key back-reference to the parent. They attach to BallTracker so effects and magnetism reach them but never enter BallReconciler._balls_by_key. Dragged off-court a split ball is destroyed, not racked (item_drag_controller.gd branches on is_temporary). Merge keeps the survivor's source_key and takes the max speed and tier of the pair; on_balls_close is a per-frame pairwise distance test over BallTracker.get_balls(), not a RigidBody contact. Level 3 gates merging to pairs containing the original (the only ball carrying live soul) and tracks a per-cycle fully-merged flag to award the next split's bonus.
grants: header (reworked racquet contact, struck with the head)
Effect 1 (levels 2-3)
trigger: on_header
outcome: add_speed(steps scale with header impact velocity; a normal hit adds one step)
Effect 2 (level 3)
trigger: on_header
outcome: increment_header_streak
outcome: soul_reward(scale_by header_streak)
Effect 3 (level 3)
trigger: on_racquet_hit or on_miss
outcome: reset_header_streak
The header reworks the existing racquet collision rather than adding a head collider or a second input path. Level 1 grants the contact with no further effect. Level 2 reads the contact's impact velocity (how hard the player rams) and adds speed steps on top of the normal per-hit increment. Level 3 rewards consecutive headers with a growing soul reward; any racquet hit or miss resets the streak. The earlier apex-burst is dropped.
Effect 1 (all levels)
trigger: on_hit
outcome: spawn_soul_beads(n; level 2 doubles n)
Effect 2 (all levels)
trigger: on_bead_gathered
outcome: bank_soul
Effect 3 (level 3)
trigger: on_bead_gathered
outcome: add_ball_speed(off-consolidation: speed that does not count toward the consolidation cap)
Beads auto-gather to the player only; uncollected beads expire at rally end. Ball-through-bead re-collection reuses the merge distance test, not physics contact. Level 3 adds off-consolidation speed per bead.
Effect 1 (all levels)
trigger: always
outcome: stat(ball_magnetism, +strength) [pull toward where the player reaches]
Effect 2 (levels 2-3)
trigger: on_would_miss
condition: the cycle's save is unspent
outcome: rescue_ball(semicircle the ball back into play); spend the cycle's save
Effect 3 (level 3)
trigger: on_would_miss (player or partner side)
note: the single per-consolidation save covers either side, first to need it
ball_magnetism and the per-frame pull toward a paddle are not implemented; level 1 needs both, targeting the player specifically. Levels 2-3 add a once-per-consolidation rescue: an on_would_miss trigger, a per-cycle save counter reset on consolidation, and a semicircle return path. Level 3 lets the partner's near-miss consume the same single save.
| Level | Behaviour |
|---|---|
| 1 | Ball speed rises and falls in a steady rhythm |
| 2 | The speed cap lifts so the ball climbs past max instead of consolidating there; the player consolidates on demand by blowing the whistle, and the longer they wait the bigger the step up |
| 3 | The rhythm turns uneven, its fast and slow stretches changing length so the fast moment is no longer predictable |
Effect 1 (all levels)
trigger: always
outcome: oscillate_stat(ball_speed_offset, scales with level)
level 3: randomise the oscillation period (the fast and slow stretch lengths), not the peak speed, so the rhythm reads irregular
Effect 2 (levels 2-3)
trigger: always
outcome: lift_speed_cap (ball climbs past ball_speed_max without auto-consolidating at the limit)
Effect 3 (levels 2-3)
trigger: on_whistle (player input)
outcome: consolidate_now (consolidate at current speed; the floor raise scales with how far past the old cap)
Base cost: 85 friendship | Scaling: 1.5
grants: pick (the cursor grabs a ball in play; held balls enter OUT_HELD, frozen and miss-suppressed)
Effect 1 (level 1)
outcome: hold one ball; release rejoins the rally at its held speed
Effect 2 (level 2)
outcome: raise the held-ball limit above one
Effect 3 (level 3)
outcome: launch a held ball on release, speed set by the release-flick velocity
Picking already exists in the engine for any in-play ball (grab_area.gd to grab_live_ball, the OUT_HELD state, release velocity from _compute_release_velocity). Pluck's work is gating that ability behind owning the glove, raising the single-held-ball limit (the drag controller refuses a second grab today), and reading the flick to set launch speed at level 3. Cursor gear is a proposed fourth role (SH-441).
The effect system as built carries five outcome types (stat, stat_until_miss, oscillate_stat, halve_streak, game_action) and process_event returns a payload-free Array[StringName]. Every parameterised outcome below is a dedicated Outcome subclass with its own apply(context), not a key on the game-action channel:
-
split_ball,merge_balls, theon_consolidationtrigger, theon_balls_closedistance check, the level-3 original-only merge gate, and the fully-merged-in-time split bonus (split). -
soul_burstandbank_soul, calculated grants throughItemManager(split, bracelet);soul_rewardscaling with a header streak (helmet). -
add_speedscaling with header impact velocity, header-streak tracking, andon_header/on_racquet_hittriggers (helmet). -
spawn_soul_beads, bead entities,on_bead_gathered, and off-consolidationadd_ball_speed(bracelet). -
rescue_ball, anon_would_misstrigger, and a per-consolidation save counter, with the level-3 save shared with the partner (magnetism). - a held-ball limit above one, a release-flick launch speed, and gating picking behind glove ownership (Pluck).
-
lift_speed_cap, theon_whistleplayer-input trigger, andconsolidate_now(Cadence).
Magnetism's pull needs no new primitive; it is a stat effect on the existing ball_magnetism, retargeted to the player.
Contributing
The game
The Cast
Process
Reference