Skip to main content

delvewright_dsl/
firework.rs

1//! A firework's surface, and **every game fact it is built on, in one file**
2//! (spec-0068).
3//!
4//! # Why the constants are here and not spread through the emitter
5//!
6//! Each number below is read off a page of the Minecraft Wiki rather than
7//! measured on a server, and a cited number that is copied to its three call
8//! sites is three numbers a re-pin has to find. So the pages are named
9//! ([`WIKI_PAGES`]), each constant says which one it came from, and the
10//! emitter, the refusal and the skill page all read them from here: re-pinning
11//! the firework against a later game is one diff in this file.
12//!
13//! The rules built on the facts — the fixed `LifeTime`, the roof column, the
14//! five-block reach — are **authored**, and they live with the check that
15//! states them.
16
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20/// The wiki pages every constant in this module was read from (spec-0068
21/// *Ground*). Named so a re-pin knows what to re-read, and asserted by
22/// `crates/dsl/tests/v29_firework.rs` so the list cannot quietly shrink.
23pub const WIKI_PAGES: [&str; 3] = [
24    "Firework Rocket",
25    "Data component format/fireworks",
26    "Data component format/equippable",
27];
28
29/// The vanilla entity a firework effect summons.
30pub const ROCKET_ENTITY: &str = "minecraft:firework_rocket";
31
32/// The item id the summoned rocket's own item stack carries.
33pub const ROCKET_ITEM: &str = "minecraft:firework_rocket";
34
35/// The item component that holds the bursts [cited — *Data component
36/// format/fireworks*].
37pub const FIREWORKS_COMPONENT: &str = "minecraft:fireworks";
38
39/// **The entity's item field** [cited — *Firework Rocket*, entity data]. The
40/// summoned rocket reads its bursts from the stack under this key; a renamed
41/// key would ship a rocket that flies and shows nothing, so the emitted spelling
42/// is asserted against this constant by the emission test.
43pub const ITEM_FIELD: &str = "FireworksItem";
44
45/// The shortest flight the game crafts, and this verb's default.
46pub const MIN_FLIGHT: u8 = 1;
47
48/// The longest flight the game crafts [cited — *Firework Rocket*]. Durations
49/// beyond it are a non-goal: the wiki states a burst height for one, two and
50/// three, and for nothing else.
51pub const MAX_FLIGHT: u8 = 3;
52
53/// At least one burst: a rocket with none is a flare that glides along whatever
54/// it meets and shows nothing [cited — *Firework Rocket*].
55pub const MIN_EXPLOSIONS: usize = 1;
56
57/// At most seven bursts — **the game's own crafting cap**, and the largest count
58/// the page states a damage for [cited — *Firework Rocket*]. The component would
59/// take 256; a display of more is a `sequence` of rockets, not one rocket that
60/// could kill an unhurt player by itself.
61pub const MAX_EXPLOSIONS: usize = 7;
62
63/// Damage a one-star burst deals, in HP, to a body within [`BLAST_RADIUS`]
64/// blocks and not behind a solid block [cited — *Firework Rocket*].
65pub const DAMAGE_ONE_STAR_HP: u32 = 7;
66
67/// Additional HP per star beyond the first [cited — *Firework Rocket*].
68pub const DAMAGE_PER_EXTRA_STAR_HP: u32 = 2;
69
70/// How far a burst reaches, in blocks [cited — *Firework Rocket*].
71pub const BLAST_RADIUS: i32 = 5;
72
73/// The worst burst this verb can write, in HP: [`MAX_EXPLOSIONS`] stars.
74///
75/// Under a full body's twenty, which is the whole reason [`MAX_EXPLOSIONS`] is
76/// seven — no rocket this verb writes can kill an unhurt player by itself.
77#[must_use]
78pub fn worst_damage_hp() -> u32 {
79    DAMAGE_ONE_STAR_HP + DAMAGE_PER_EXTRA_STAR_HP * (MAX_EXPLOSIONS as u32 - 1)
80}
81
82/// The fixed term of the game's randomised `LifeTime`
83/// (`(flight + 1) × LIFETIME_STEP_TICKS + random(0..5) + random(0..6)` ticks)
84/// [cited — *Firework Rocket*, entity data].
85pub const LIFETIME_STEP_TICKS: u32 = 10;
86
87/// **The `LifeTime` the emitter writes**: the floor of the game's randomised
88/// range, `(flight + 1) × 10` ticks.
89///
90/// Authored, on the cited formula. Left unset the game rolls the two random
91/// terms at launch, so one datapack would burst at two heights and the reach
92/// proof would be about a number nobody chose. The floor is the conservative
93/// side of that proof: the burst is the lowest the game would ever put it.
94#[must_use]
95pub fn lifetime_ticks(flight: u8) -> u32 {
96    (u32::from(flight) + 1) * LIFETIME_STEP_TICKS
97}
98
99/// The burst height in blocks above the launch cell, per flight duration
100/// 1, 2, 3 — the **floor** of each range the wiki states (8–20, 18–34, 32–52),
101/// because [`lifetime_ticks`] fixes `LifeTime` at the range's floor [cited —
102/// *Firework Rocket*].
103pub const BURST_HEIGHTS: [i32; 3] = [8, 18, 32];
104
105/// The burst height for a flight duration, clamped to the crafted range.
106#[must_use]
107pub fn burst_height(flight: u8) -> i32 {
108    let i = flight.clamp(MIN_FLIGHT, MAX_FLIGHT) as usize - 1;
109    BURST_HEIGHTS[i]
110}
111
112/// One of the game's five explosion shapes [cited — *Data component
113/// format/fireworks*], spelled as the component spells it.
114#[derive(
115    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
116)]
117#[serde(rename_all = "snake_case")]
118pub enum FireworkShape {
119    /// A small ball — the plain firework star.
120    SmallBall,
121    /// A large ball.
122    LargeBall,
123    /// A star-shaped burst (a gold-nugget star).
124    Star,
125    /// A creeper-face burst (a creeper-head star).
126    Creeper,
127    /// A burst (a feather star).
128    Burst,
129}
130
131impl FireworkShape {
132    /// Every shape the game has, in the component's own order.
133    pub const ALL: [FireworkShape; 5] = [
134        FireworkShape::SmallBall,
135        FireworkShape::LargeBall,
136        FireworkShape::Star,
137        FireworkShape::Creeper,
138        FireworkShape::Burst,
139    ];
140
141    /// The token the `minecraft:fireworks` component's `shape` field carries —
142    /// the same string the DSL writes.
143    #[must_use]
144    pub fn token(self) -> &'static str {
145        match self {
146            FireworkShape::SmallBall => "small_ball",
147            FireworkShape::LargeBall => "large_ball",
148            FireworkShape::Star => "star",
149            FireworkShape::Creeper => "creeper",
150            FireworkShape::Burst => "burst",
151        }
152    }
153}
154
155/// One burst of a [`crate::Verb::Firework`] — one firework star.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
157#[serde(deny_unknown_fields)]
158pub struct FireworkExplosion {
159    /// Which of the game's five shapes this burst takes.
160    pub shape: FireworkShape,
161    /// The burst's colours, one or more `#rrggbb` literals — the spelling
162    /// [`crate::PotionContents::color`] uses, emitted as the packed integers the
163    /// component reads. At least one: a star with no colour is not a star.
164    #[schemars(length(min = 1), inner(pattern(r"^#[0-9a-fA-F]{6}$")))]
165    pub colors: Vec<String>,
166    /// Colours the burst fades to, same spelling. Empty = no fade.
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    #[schemars(inner(pattern(r"^#[0-9a-fA-F]{6}$")))]
169    pub fade_colors: Vec<String>,
170    /// Whether the burst trails (a diamond star). Default false.
171    #[serde(default, skip_serializing_if = "is_false")]
172    pub trail: bool,
173    /// Whether the burst twinkles/crackles (a glowstone-dust star). Default
174    /// false.
175    #[serde(default, skip_serializing_if = "is_false")]
176    pub twinkle: bool,
177}
178
179/// serde `skip_serializing_if` helper: skip a `false` flag.
180fn is_false(b: &bool) -> bool {
181    !*b
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn the_worst_burst_cannot_kill_an_unhurt_player() {
190        assert_eq!(worst_damage_hp(), 19);
191        assert!(worst_damage_hp() < 20);
192    }
193
194    #[test]
195    fn lifetime_is_the_floor_of_the_games_range() {
196        assert_eq!(lifetime_ticks(1), 20);
197        assert_eq!(lifetime_ticks(2), 30);
198        assert_eq!(lifetime_ticks(3), 40);
199    }
200
201    #[test]
202    fn a_height_per_crafted_flight() {
203        assert_eq!(burst_height(1), 8);
204        assert_eq!(burst_height(2), 18);
205        assert_eq!(burst_height(3), 32);
206    }
207}