feat: extend Boss
This commit is contained in:
1 parent
1748324d41
commit
663b0cae3a
1 file changed
+160
@@ -154,3 +154,163 @@ DragonFight::DragonFight(const EndGenerator& end, uint32_t seed, const DragonFig
|
|||||||
portal = {0.5f, static_cast<float>(surface), 0.5f};
|
portal = {0.5f, static_cast<float>(surface), 0.5f};
|
||||||
eggCell = {0, surface + 1, 0};
|
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<ParallelNode>(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<int>(pillarCrystals.size())) return 0;
|
||||||
|
|
||||||
|
CrystalNode& crystal = pillarCrystals[static_cast<std::size_t>(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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user