feat: add witherBaseBlock and witherHeadBlock to Boss
This commit is contained in:
1 parent
663b0cae3a
commit
78b986bdb1
1 file changed
+158
@@ -314,3 +314,161 @@ int DragonFight::resolvePlayerShot(const DragonPlayerInput& input) {
|
|||||||
return 1;
|
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<int>(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;
|
||||||
|
}));
|
||||||
|
|
||||||
Reference in new issue
Block a user