DecorDef.cs

Definitions for decor items and placed decor pieces. DecorDef describes properties for tank ornaments and provides a static catalog of predefined decor; DecorPiece is a runtime data object holding a placed piece position, velocity, held state, and convenience accessors to its Def and IsMoving state.

Reflection
namespace NoChillquarium;

/// <summary>
/// Placeable tank ornaments. Sizes are tank pixels — props should dwarf fish
/// (goldfish ≈ 52×36). GroundSink buries the sprite base into gravel so plants
/// don't look like floating islands.
/// </summary>
public sealed class DecorDef
{
	public string Id { get; init; }
	public string Name { get; init; }
	public string Note { get; init; }
	public double Cost { get; init; }
	public float Width { get; init; }
	public float Height { get; init; }
	public string CssClass { get; init; }
	public string SpritePath { get; init; }
	public float ComfortBonus { get; init; }
	public float ChaosOnPlace { get; init; }
	/// <summary>How many px of the sprite bottom sink into the gravel band.</summary>
	public float GroundSink { get; init; }

	public bool HasSprite => SpriteCatalog.Has( Id );
	public string SpriteId => Id;

	public static readonly DecorDef Plant = new()
	{
		Id = "plant_green",
		Name = "Plastic Plant",
		Note = "chill greenery",
		Cost = 12,
		Width = 140,
		Height = 320,
		CssClass = "decor-plant",
		SpritePath = "plant_green",
		ComfortBonus = 0.05f,
		// Dirt mound in the art must sit inside the gravel strip
		GroundSink = 48f
	};

	public static readonly DecorDef Treasure = new()
	{
		Id = "treasure_chest",
		Name = "Treasure Chest",
		Note = "bubbles optional",
		Cost = 28,
		// Art ~91×96 — keep a solid grab box without a huge empty frame.
		Width = 140,
		Height = 120,
		CssClass = "decor-chest",
		SpritePath = "treasure_chest",
		ComfortBonus = 0.08f,
		GroundSink = 10f
	};

	public static readonly DecorDef BubbleWand = new()
	{
		Id = "bubble_wand",
		Name = "Bubble Wand",
		Note = "party mode",
		Cost = 18,
		Width = 90,
		Height = 340,
		CssClass = "decor-bubbler",
		SpritePath = "bubble_wand",
		ComfortBonus = 0.04f,
		GroundSink = 16f
	};

	public static readonly DecorDef FortyOz = new()
	{
		Id = "forty_oz",
		Name = "40oz Bottle",
		Note = "no-chill centerpiece",
		Cost = 22,
		// Art is 96×96 — tall-ish tank prop, not a skyscraper hitbox.
		Width = 96,
		Height = 160,
		CssClass = "decor-forty",
		SpritePath = "forty_oz",
		ComfortBonus = 0.03f,
		ChaosOnPlace = 2f,
		GroundSink = 12f
	};

	public static readonly DecorDef Handcuffs = new()
	{
		Id = "handcuffs_skeleton",
		Name = "Cuffs + Bone Hand",
		Note = "don't ask",
		Cost = 35,
		Width = 170,
		Height = 120,
		CssClass = "decor-cuffs",
		SpritePath = "handcuffs_skeleton",
		ComfortBonus = 0.02f,
		ChaosOnPlace = 4f,
		GroundSink = 12f
	};

	public static readonly DecorDef Boot = new()
	{
		Id = "old_boot",
		Name = "Old Boot",
		Note = "classic dumpster chic",
		Cost = 10,
		// Source art is 96×96 — keep hitbox tight so grab/throw feels right.
		Width = 128,
		Height = 128,
		CssClass = "decor-boot",
		SpritePath = "old_boot",
		ComfortBonus = 0.02f,
		ChaosOnPlace = 1f,
		GroundSink = 10f
	};

	public static readonly DecorDef Meds = new()
	{
		Id = "medicine_bottles",
		Name = "Medicine Bottles",
		Note = "vibes only (for now)",
		Cost = 16,
		Width = 120,
		Height = 120,
		CssClass = "decor-meds",
		SpritePath = "medicine_bottles",
		ComfortBonus = 0.06f,
		GroundSink = 10f
	};

	public static readonly DecorDef DogeStatue = new()
	{
		Id = "doge_statue",
		Name = "Doge Statue",
		Note = "much worship",
		Cost = 50,
		Width = 180,
		Height = 240,
		CssClass = "decor-doge",
		SpritePath = "doge_statue",
		ComfortBonus = 0.15f,
		GroundSink = 14f
	};

	public static readonly DecorDef M80Crate = new()
	{
		Id = "m80_crate",
		Name = "M80 Crate",
		Note = "decorative. mostly.",
		Cost = 30,
		Width = 180,
		Height = 140,
		CssClass = "decor-m80crate",
		SpritePath = "m80_crate",
		ComfortBonus = 0.01f,
		ChaosOnPlace = 3f,
		GroundSink = 12f
	};

	public static IReadOnlyList<DecorDef> Catalog { get; } = new[]
	{
		Plant,
		Treasure,
		BubbleWand,
		FortyOz,
		Handcuffs,
		Boot,
		Meds,
		DogeStatue,
		M80Crate
	};

	public static DecorDef Find( string id ) =>
		Catalog.FirstOrDefault( d => d.Id == id ) ?? Plant;
}

public sealed class DecorPiece
{
	public string DefId { get; set; }
	public float X { get; set; }
	public float Y { get; set; }
	/// <summary>Throw / tumble velocity (tank px/s). Zeroed when resting on gravel.</summary>
	public float Vx { get; set; }
	public float Vy { get; set; }
	/// <summary>True while the player is dragging this piece.</summary>
	public bool Held { get; set; }
	/// <summary>Seconds after release where floor stick is relaxed so throws launch.</summary>
	public float ThrowGrace { get; set; }

	public DecorDef Def => DecorDef.Find( DefId );

	public bool IsMoving =>
		Held || ThrowGrace > 0f || MathF.Abs( Vx ) > 2f || MathF.Abs( Vy ) > 2f;
}