diff --git a/core/src/Boss.cpp b/core/src/Boss.cpp index 84bc50e..6790e7d 100644 --- a/core/src/Boss.cpp +++ b/core/src/Boss.cpp @@ -154,3 +154,163 @@ DragonFight::DragonFight(const EndGenerator& end, uint32_t seed, const DragonFig portal = {0.5f, static_cast(surface), 0.5f}; eggCell = {0, surface + 1, 0}; + boss.position = portal + Vec3{cfg.orbitRadius, cfg.orbitHeight, 0.0f}; + ctx.rng = boss.rng; + buildTree(); +} + +int DragonFight::aliveCrystalCount() const { + int total = 0; + for (const CrystalNode& crystal : pillarCrystals) { + if (crystal.alive) ++total; + } + return total; +} + +void DragonFight::buildTree() { + auto death = seq("deathThroes"); + death->add(cond("healthGone", [this](BehaviorContext&) { return boss.health <= 0.0f; })); + death->add(act("collapse", [this](BehaviorContext&) { + if (boss.phase != BossPhase::Dying) { + boss.enterPhase(BossPhase::Dying); + return BehaviorStatus::Running; + } + if (boss.phaseTicks < cfg.deathTicks) return BehaviorStatus::Running; + boss.enterPhase(BossPhase::Defeated); + boss.alive = false; + boss.bar.visible = false; + boss.bar.setFromHealth(0.0f, boss.maxHealth); + portalOpen = true; + eggDropped = true; + return BehaviorStatus::Success; + })); + + auto volley = std::unique_ptr(new ParallelNode("perchAssault", 1, 2)); + volley->add(gated("breathCooldown", + act("breatheAcid", + [this](BehaviorContext& context) { + if (!context.playerPresent) return BehaviorStatus::Failure; + if (distanceBetween(boss.position, context.playerPosition) > + cfg.breathRange) { + return BehaviorStatus::Failure; + } + damageThisTick += cfg.breathDamage; + ++boss.attacksLaunched; + return BehaviorStatus::Success; + }), + cfg.breathCooldownTicks)); + volley->add(gated("swipeCooldown", + act("tailSwipe", + [this](BehaviorContext& context) { + if (!context.playerPresent) return BehaviorStatus::Failure; + if (distanceBetween(boss.position, context.playerPosition) > + cfg.swipeRange) { + return BehaviorStatus::Failure; + } + damageThisTick += cfg.swipeDamage; + ++boss.attacksLaunched; + return BehaviorStatus::Success; + }), + cfg.swipeCooldownTicks)); + + auto perchAction = sel("perchAction"); + perchAction->add(std::move(volley)); + perchAction->add(act("holdPerch", [](BehaviorContext&) { return BehaviorStatus::Success; })); + + auto perch = seq("perchAtPortal"); + perch->add(cond("allCrystalsDown", + [this](BehaviorContext&) { return aliveCrystalCount() == 0; })); + perch->add(act("descendToPortal", [this](BehaviorContext&) { + boss.enterPhase(BossPhase::Perching); + Vec3 landing = portal + Vec3{0.0f, cfg.perchHeight, 0.0f}; + Vec3 delta = landing - boss.position; + float span = length(delta); + if (span <= cfg.perchDescentStep) { + boss.position = landing; + return BehaviorStatus::Success; + } + boss.position += delta * (cfg.perchDescentStep / span); + return BehaviorStatus::Running; + })); + perch->add(std::move(perchAction)); + + auto charge = seq("chargePlayer"); + charge->add(cond("playerWorthCharging", [this](BehaviorContext& context) { + return context.playerPresent && + distanceBetween(boss.position, context.playerPosition) <= cfg.chargeTrigger; + })); + charge->add(gated("chargeCooldown", + bounded("chargeWindow", + act("charge", + [this](BehaviorContext& context) { + if (!context.playerPresent) return BehaviorStatus::Failure; + boss.enterPhase(BossPhase::Charging); + Vec3 delta = context.playerPosition - boss.position; + float span = length(delta); + if (span <= cfg.chargeContactRange) { + damageThisTick += cfg.chargeDamage; + ++boss.attacksLaunched; + return BehaviorStatus::Success; + } + boss.position += delta * (cfg.chargeSpeed / span); + return BehaviorStatus::Running; + }), + cfg.chargeWindowTicks), + cfg.chargeCooldownTicks)); + + auto breath = seq("acidBreath"); + breath->add(cond("playerNearPortal", [this](BehaviorContext& context) { + return context.playerPresent && + distanceBetween(context.playerPosition, portal) <= cfg.breathRange; + })); + breath->add(gated("flightBreathCooldown", + act("breatheFromAbove", + [this](BehaviorContext&) { + boss.enterPhase(BossPhase::Breathing); + damageThisTick += cfg.breathDamage; + ++boss.attacksLaunched; + return BehaviorStatus::Success; + }), + cfg.breathCooldownTicks)); + + auto root = sel("dragonRoot"); + root->add(std::move(death)); + root->add(std::move(perch)); + root->add(std::move(charge)); + root->add(std::move(breath)); + root->add(act("orbit", [this](BehaviorContext&) { + boss.enterPhase(BossPhase::Circling); + boss.orbitAngle += cfg.orbitStep; + if (boss.orbitAngle >= fullTurn) boss.orbitAngle -= fullTurn; + boss.position = portal + Vec3{std::cos(boss.orbitAngle) * cfg.orbitRadius, cfg.orbitHeight, + std::sin(boss.orbitAngle) * cfg.orbitRadius}; + boss.heading = normalize(Vec3{-std::sin(boss.orbitAngle), 0.0f, std::cos(boss.orbitAngle)}); + return BehaviorStatus::Success; + })); + + tree.setRoot(std::move(root)); +} + +int DragonFight::resolvePlayerShot(const DragonPlayerInput& input) { + if (input.shootCrystal < 0) return 0; + if (input.shootCrystal >= static_cast(pillarCrystals.size())) return 0; + + CrystalNode& crystal = pillarCrystals[static_cast(input.shootCrystal)]; + if (!crystal.alive) return 0; + if (crystal.cageIntact) { + if (input.breakCage) crystal.cageIntact = false; + return 0; + } + + crystal.alive = false; + crystal.health = 0.0f; + crystal.destroyedTick = boss.tick; + + if (distanceBetween(crystalCenter(crystal.position), boss.position) <= cfg.crystalBlastRange) { + float dealt = std::min(cfg.crystalBlastDamage, boss.health); + boss.health -= dealt; + boss.damageTaken += dealt; + } + return 1; +} +