From 76f2be284bf57bb076b353bdc790e2f4cd7a8628 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 16:37:40 +0200 Subject: [PATCH] feat(vf): extend Block with BossBar and BossState --- core/include/vf/Block.h | 10 ++- core/include/vf/Boss.h | 160 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 core/include/vf/Boss.h diff --git a/core/include/vf/Block.h b/core/include/vf/Block.h index 60a09e6..7cc1d6f 100644 --- a/core/include/vf/Block.h +++ b/core/include/vf/Block.h @@ -490,6 +490,13 @@ inline int blockLightEmission(Block b) { case Block::FurnaceLit: return 13; case Block::RedstoneTorch: return 7; case Block::RedstoneOre: return 0; // lit only on touch, handled by gameplay + case Block::EndPortal: return 15; + case Block::SeaLantern: return 15; + case Block::Lantern: return 15; + case Block::NetherPortal: return 11; + case Block::CryingObsidian: return 10; + case Block::EnchantingTable: return 7; + case Block::Magma: return 3; default: return 0; } } @@ -499,7 +506,8 @@ inline bool occludesLight(Block b) { return isOpaque(b); } // Meshed in the transparent pass (depth write off, back-to-front): fluids + ice. inline bool isTransparent(Block b) { - return b == Block::Water || b == Block::Ice; + return b == Block::Water || b == Block::Ice || b == Block::Glass || + b == Block::NetherPortal || b == Block::EndPortal; } } // namespace vf diff --git a/core/include/vf/Boss.h b/core/include/vf/Boss.h new file mode 100644 index 0000000..0e8d12f --- /dev/null +++ b/core/include/vf/Boss.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include + +#include "Behavior.h" +#include "Block.h" +#include "EndGen.h" +#include "Math.h" +#include "World.h" + +namespace vf { + +// Two staged boss fights driven by the behaviour trees from Behavior.h. Both +// are pure tick machines: feed the same seed and the same sequence of player +// actions and the fight replays move for move. +enum class BossKind : uint8_t { + EnderDragon = 0, + Wither = 1, + Count, +}; + +const char* bossKindName(BossKind kind); + +enum class BossPhase : uint8_t { + Dormant = 0, + Circling, + Charging, + Breathing, + Perching, + Summoning, + Explosive, + Ranged, + Armored, + Dying, + Defeated, + Count, +}; + +const char* bossPhaseName(BossPhase phase); + +enum class BossBarColor : uint8_t { Pink = 0, Blue, Red, Green, Yellow, Purple, White, Count }; +enum class BossBarStyle : uint8_t { Solid = 0, Notched6, Notched10, Notched12, Notched20, Count }; + +struct BossBar { + std::string title; + float progress = 1.0f; + BossBarColor color = BossBarColor::Purple; + BossBarStyle style = BossBarStyle::Notched10; + bool visible = false; + + int notchCount() const; + // Whole segments currently lit; a partially drained segment reads as empty. + int filledNotches() const; + int percent() const; + void setFromHealth(float health, float maxHealth); +}; + +struct BossState { + BossKind kind = BossKind::EnderDragon; + BossPhase phase = BossPhase::Dormant; + float health = 200.0f; + float maxHealth = 200.0f; + Vec3 position; + Vec3 heading{1.0f, 0.0f, 0.0f}; + float orbitAngle = 0.0f; + int targetIndex = -1; + long long tick = 0; + int phaseTicks = 0; + int phaseChanges = 0; + int attacksLaunched = 0; + int minionsSummoned = 0; + float healingReceived = 0.0f; + float damageTaken = 0.0f; + bool alive = true; + uint32_t rng = 0x51ed270bu; + BossBar bar; + + void enterPhase(BossPhase next); + float healthFraction() const; +}; + +// One end crystal on top of an obsidian pillar from EndGen.h. The taller +// pillars carry a cage that has to come down before the crystal can be shot. +struct CrystalNode { + Vec3i pillarBase; + Vec3i position; + int pillarHeight = 0; + bool caged = false; + bool cageIntact = false; + bool alive = true; + float health = 5.0f; + long long destroyedTick = -1; +}; + +std::vector crystalsFromPillars(const std::vector& pillars); + +struct BossTickReport { + BossPhase phase = BossPhase::Dormant; + BossPhase previousPhase = BossPhase::Dormant; + bool phaseChanged = false; + float bossHealth = 0.0f; + float healedThisTick = 0.0f; + float damageToTargets = 0.0f; + int crystalsDestroyed = 0; + int minionsSpawned = 0; + bool defeated = false; +}; + +struct DragonFightConfig { + float maxHealth = 200.0f; + float orbitRadius = 44.0f; + float orbitHeight = 26.0f; + float orbitStep = 0.06f; + float chargeTrigger = 48.0f; + float chargeSpeed = 1.4f; + float chargeContactRange = 3.0f; + float chargeDamage = 10.0f; + int chargeCooldownTicks = 60; + int chargeWindowTicks = 40; + float breathRange = 14.0f; + float breathDamage = 3.0f; + int breathCooldownTicks = 40; + int swipeCooldownTicks = 25; + float swipeDamage = 6.0f; + float swipeRange = 8.0f; + float crystalHealPerTick = 0.4f; + float crystalHealRange = 128.0f; + float crystalBlastRange = 12.0f; + float crystalBlastDamage = 6.0f; + float perchHeight = 3.0f; + float perchDescentStep = 0.9f; + float meleeReach = 7.0f; + float flyingDamageFactor = 0.25f; + int deathTicks = 40; +}; + +// What the player did on one tick. `shootCrystal` indexes crystals(); a caged +// crystal needs `breakCage` on an earlier tick before a shot lands. +struct DragonPlayerInput { + Vec3 position; + bool present = true; + int shootCrystal = -1; + bool breakCage = false; + float meleeDamage = 0.0f; +}; + +// Crystals heal the dragon while they stand, so the fight cannot be won by +// damage alone. Killing the last one drops it onto the portal, where melee +// lands at full strength; the kill opens the exit portal and leaves the egg. +class DragonFight { +public: + DragonFight(const EndGenerator& end, uint32_t seed, const DragonFightConfig& config = {}); + + DragonFight(const DragonFight&) = delete; + DragonFight& operator=(const DragonFight&) = delete; + DragonFight(DragonFight&&) = delete; + DragonFight& operator=(DragonFight&&) = delete; +