use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const WIKI_PAGES: [&str; 3] = [
"Firework Rocket",
"Data component format/fireworks",
"Data component format/equippable",
];
pub const ROCKET_ENTITY: &str = "minecraft:firework_rocket";
pub const ROCKET_ITEM: &str = "minecraft:firework_rocket";
pub const FIREWORKS_COMPONENT: &str = "minecraft:fireworks";
pub const ITEM_FIELD: &str = "FireworksItem";
pub const MIN_FLIGHT: u8 = 1;
pub const MAX_FLIGHT: u8 = 3;
pub const MIN_EXPLOSIONS: usize = 1;
pub const MAX_EXPLOSIONS: usize = 7;
pub const DAMAGE_ONE_STAR_HP: u32 = 7;
pub const DAMAGE_PER_EXTRA_STAR_HP: u32 = 2;
pub const BLAST_RADIUS: i32 = 5;
#[must_use]
pub fn worst_damage_hp() -> u32 {
DAMAGE_ONE_STAR_HP + DAMAGE_PER_EXTRA_STAR_HP * (MAX_EXPLOSIONS as u32 - 1)
}
pub const LIFETIME_STEP_TICKS: u32 = 10;
#[must_use]
pub fn lifetime_ticks(flight: u8) -> u32 {
(u32::from(flight) + 1) * LIFETIME_STEP_TICKS
}
pub const BURST_HEIGHTS: [i32; 3] = [8, 18, 32];
#[must_use]
pub fn burst_height(flight: u8) -> i32 {
let i = flight.clamp(MIN_FLIGHT, MAX_FLIGHT) as usize - 1;
BURST_HEIGHTS[i]
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum FireworkShape {
SmallBall,
LargeBall,
Star,
Creeper,
Burst,
}
impl FireworkShape {
pub const ALL: [FireworkShape; 5] = [
FireworkShape::SmallBall,
FireworkShape::LargeBall,
FireworkShape::Star,
FireworkShape::Creeper,
FireworkShape::Burst,
];
#[must_use]
pub fn token(self) -> &'static str {
match self {
FireworkShape::SmallBall => "small_ball",
FireworkShape::LargeBall => "large_ball",
FireworkShape::Star => "star",
FireworkShape::Creeper => "creeper",
FireworkShape::Burst => "burst",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FireworkExplosion {
pub shape: FireworkShape,
#[schemars(length(min = 1), inner(pattern(r"^#[0-9a-fA-F]{6}$")))]
pub colors: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(inner(pattern(r"^#[0-9a-fA-F]{6}$")))]
pub fade_colors: Vec<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub trail: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub twinkle: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
use crate::diagnostic::{Diagnostic, codes};
use crate::envelope::Campaign;
use crate::{QuestEffect, Verb};
pub(crate) fn firework_checks(c: &Campaign, d: &mut Vec<Diagnostic>) {
let mut found: Vec<(String, &'static str, String)> = Vec::new();
fn descend(
stage: &'static str,
path: String,
eff: &QuestEffect,
found: &mut Vec<(String, &'static str, String)>,
) {
firework_shape(stage, &path, eff, found);
for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
for (j, inner) in list.iter().enumerate() {
descend(stage, format!("{path}/{pseg}/{j}"), inner, found);
}
}
}
crate::effects::for_each_effect_root(c, &mut |site, effs| {
for (i, eff) in effs.iter().enumerate() {
descend(site.stage, format!("{}/{i}", site.path), eff, &mut found);
}
});
for (path, stage, message) in found {
d.push(Diagnostic::error(codes::SCHEMA, stage, path, message));
}
}
fn firework_shape(
stage: &'static str,
path: &str,
eff: &QuestEffect,
found: &mut Vec<(String, &'static str, String)>,
) {
use crate::firework;
let Verb::Firework {
flight, explosions, ..
} = &eff.verb
else {
return;
};
if let Some(f) = flight
&& !(firework::MIN_FLIGHT..=firework::MAX_FLIGHT).contains(f)
{
found.push((
format!("{path}/flight"),
stage,
format!(
"`firework` `flight` is {f}. A flight duration is one of the three the game \
crafts — {min}, {min2} or {max} — and the wiki states a burst height for \
those and for nothing else, so a fourth would put the burst at a height this \
engine cannot state. Write {min}, {min2} or {max}.",
min = firework::MIN_FLIGHT,
min2 = firework::MIN_FLIGHT + 1,
max = firework::MAX_FLIGHT,
),
));
}
if explosions.len() < firework::MIN_EXPLOSIONS {
found.push((
format!("{path}/explosions"),
stage,
format!(
"`firework` declares no explosion. A rocket with none is a flare: it glides \
along whatever it meets and shows nothing. Declare between {} and {} \
burst(s).",
firework::MIN_EXPLOSIONS,
firework::MAX_EXPLOSIONS,
),
));
}
if explosions.len() > firework::MAX_EXPLOSIONS {
found.push((
format!("{path}/explosions"),
stage,
format!(
"`firework` declares {n} explosions, and a rocket carries at most {max} — the \
game's own crafting cap, and the largest count the page states a damage for \
({worst} HP, under a full body's twenty). A display of more rockets is a \
`sequence` of `firework` effects, not one rocket that could kill an unhurt \
player by itself.",
n = explosions.len(),
max = firework::MAX_EXPLOSIONS,
worst = firework::worst_damage_hp(),
),
));
}
for (i, ex) in explosions.iter().enumerate() {
if ex.colors.is_empty() {
found.push((
format!("{path}/explosions/{i}/colors"),
stage,
"`firework` explosion declares no `colors`. A star with no colour is not a \
star — write at least one `#rrggbb` literal (e.g. `#ffd700`)."
.to_string(),
));
}
for (field, list) in [("colors", &ex.colors), ("fade_colors", &ex.fade_colors)] {
for (j, col) in list.iter().enumerate() {
if !crate::color::is_hex(col) {
found.push((
format!("{path}/explosions/{i}/{field}/{j}"),
stage,
format!(
"`firework` colour `{col}` is malformed — write a burst colour as \
`#rrggbb` (e.g. `#ffd700`), the spelling a potion's `color` \
uses. The schema's own pattern is `{pat}`.",
pat = crate::color::HEX_PATTERN,
),
));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_worst_burst_cannot_kill_an_unhurt_player() {
assert_eq!(worst_damage_hp(), 19);
assert!(worst_damage_hp() < 20);
}
#[test]
fn lifetime_is_the_floor_of_the_games_range() {
assert_eq!(lifetime_ticks(1), 20);
assert_eq!(lifetime_ticks(2), 30);
assert_eq!(lifetime_ticks(3), 40);
}
#[test]
fn a_height_per_crafted_flight() {
assert_eq!(burst_height(1), 8);
assert_eq!(burst_height(2), 18);
assert_eq!(burst_height(3), 32);
}
}