diff --git a/core/src/Boss.cpp b/core/src/Boss.cpp index 6790e7d..4bc757d 100644 --- a/core/src/Boss.cpp +++ b/core/src/Boss.cpp @@ -314,3 +314,161 @@ int DragonFight::resolvePlayerShot(const DragonPlayerInput& input) { return 1; } +void DragonFight::applyPlayerMelee(const DragonPlayerInput& input) { + if (!input.present || input.meleeDamage <= 0.0f || !boss.alive) return; + if (distanceBetween(boss.position, input.position) > cfg.meleeReach) return; + + float factor = boss.phase == BossPhase::Perching ? 1.0f : cfg.flyingDamageFactor; + float dealt = std::min(input.meleeDamage * factor, boss.health); + boss.health -= dealt; + boss.damageTaken += dealt; +} + +float DragonFight::applyCrystalHealing() { + if (boss.health <= 0.0f) return 0.0f; + + float healed = 0.0f; + for (const CrystalNode& crystal : pillarCrystals) { + if (!crystal.alive) continue; + if (distanceBetween(crystalCenter(crystal.position), boss.position) > cfg.crystalHealRange) + continue; + float room = boss.maxHealth - boss.health - healed; + if (room <= 0.0f) break; + healed += std::min(cfg.crystalHealPerTick, room); + } + boss.health += healed; + boss.healingReceived += healed; + return healed; +} + +BossTickReport DragonFight::tick(const DragonPlayerInput& input) { + BossTickReport report; + report.previousPhase = boss.phase; + report.phase = boss.phase; + report.bossHealth = boss.health; + + if (boss.phase == BossPhase::Defeated) { + report.defeated = true; + return report; + } + + lastInput = input; + damageThisTick = 0.0f; + + int crystalsKilled = resolvePlayerShot(input); + applyPlayerMelee(input); + float healed = applyCrystalHealing(); + + bool lastCrystalDown = crystalsKilled > 0 && aliveCrystalCount() == 0; + bool lethalBlow = boss.health <= 0.0f && boss.phase != BossPhase::Dying; + if (lastCrystalDown || lethalBlow) tree.reset(); + + ctx.playerPresent = input.present; + ctx.playerPosition = input.position; + + ++boss.phaseTicks; + int changesBefore = boss.phaseChanges; + tree.tick(ctx); + boss.tick = ctx.tick; + if (boss.phase != BossPhase::Defeated) boss.bar.setFromHealth(boss.health, boss.maxHealth); + + report.phase = boss.phase; + report.phaseChanged = boss.phaseChanges != changesBefore; + report.bossHealth = boss.health; + report.healedThisTick = healed; + report.damageToTargets = damageThisTick; + report.crystalsDestroyed = crystalsKilled; + report.defeated = boss.phase == BossPhase::Defeated; + return report; +} + +bool DragonFight::placeVictoryStructures(World& world, const EndGenerator& end) const { + if (!portalOpen) return false; + end.placeExitPortal(world); + return true; +} + +Block witherBaseBlock() { return Block::SoulSand; } +Block witherHeadBlock() { return Block::Pumpkin; } + +bool witherPatternAt(const World& world, const Vec3i& baseCell, bool alongX) { + if (world.blockAt(baseCell.x, baseCell.y, baseCell.z) != witherBaseBlock()) return false; + for (int along = -1; along <= 1; ++along) { + Vec3i arm = witherOffset(baseCell, along, 1, alongX); + if (world.blockAt(arm.x, arm.y, arm.z) != witherBaseBlock()) return false; + Vec3i head = witherOffset(baseCell, along, 2, alongX); + if (world.blockAt(head.x, head.y, head.z) != witherHeadBlock()) return false; + } + return true; +} + +bool findWitherPattern(const World& world, const Vec3i& baseCell, bool& outAlongX) { + if (witherPatternAt(world, baseCell, true)) { + outAlongX = true; + return true; + } + if (witherPatternAt(world, baseCell, false)) { + outAlongX = false; + return true; + } + return false; +} + +void consumeWitherPattern(World& world, const Vec3i& baseCell, bool alongX) { + world.setBlock(baseCell.x, baseCell.y, baseCell.z, Block::Air); + for (int along = -1; along <= 1; ++along) { + Vec3i arm = witherOffset(baseCell, along, 1, alongX); + world.setBlock(arm.x, arm.y, arm.z, Block::Air); + Vec3i head = witherOffset(baseCell, along, 2, alongX); + world.setBlock(head.x, head.y, head.z, Block::Air); + } +} + +WitherFight::WitherFight(uint32_t seed, const WitherFightConfig& config) : cfg(config) { + boss.kind = BossKind::Wither; + boss.maxHealth = cfg.maxHealth; + boss.health = cfg.maxHealth; + boss.rng = seed != 0u ? seed : 0x2f6b3ca1u; + boss.phase = BossPhase::Dormant; + boss.bar.title = "Wither"; + boss.bar.color = BossBarColor::Purple; + boss.bar.style = BossBarStyle::Notched6; + boss.bar.visible = false; + ctx.rng = boss.rng; + buildTree(); +} + +int WitherFight::aliveMinionCount() const { + int total = 0; + for (const BossMinion& minion : summoned) { + if (minion.alive) ++total; + } + return total; +} + +int WitherFight::nearestAliveTarget() const { + if (activeTargets == nullptr) return -1; + int best = -1; + float bestSpan = 0.0f; + for (std::size_t i = 0; i < activeTargets->size(); ++i) { + const WitherTarget& candidate = (*activeTargets)[i]; + if (!candidate.alive) continue; + float span = distanceBetween(boss.position, candidate.position); + if (best >= 0 && span >= bestSpan) continue; + best = static_cast(i); + bestSpan = span; + } + return best; +} + +void WitherFight::buildTree() { + auto defeat = seq("defeat"); + defeat->add(cond("healthGone", [this](BehaviorContext&) { return boss.health <= 0.0f; })); + defeat->add(act("collapse", [this](BehaviorContext&) { + boss.enterPhase(BossPhase::Defeated); + boss.alive = false; + boss.bar.visible = false; + boss.bar.setFromHealth(0.0f, boss.maxHealth); + return BehaviorStatus::Success; + })); +