diff --git a/core/src/Behavior.cpp b/core/src/Behavior.cpp index 0969728..bb54a27 100644 --- a/core/src/Behavior.cpp +++ b/core/src/Behavior.cpp @@ -467,3 +467,153 @@ bool isBreedingFood(Species species, ItemId held) { return wanted != ItemId::None && held == wanted; } +bool stepTowardPoint(BehaviorAgent& agent, const Vec3& goal, float arriveEpsilon) { + Vec3 delta = goal - agent.position; + float distance = length(delta); + if (distance <= arriveEpsilon) { + agent.position = goal; + return true; + } + Vec3 direction = delta * (1.0f / distance); + Vec3 flat{direction.x, 0.0f, direction.z}; + if (length(flat) > 1e-4f) agent.heading = normalize(flat); + if (agent.moveStep >= distance) { + agent.position = goal; + return true; + } + agent.position += direction * agent.moveStep; + return false; +} + +bool planRouteTo(BehaviorContext& ctx, const Vec3i& goal, bool force) { + BehaviorAgent& agent = *ctx.agent; + bool sameGoal = agent.routeGoal == goal; + bool fresh = (ctx.tick - agent.routePlannedTick) < ctx.tuning.repathIntervalTicks; + if (!force && sameGoal && fresh && agent.route.size() >= 2) return true; + + agent.route = findPath(*ctx.world, agent.cell(), goal, ctx.path); + agent.routeCursor = agent.route.empty() ? 0 : 1; + agent.routeGoal = goal; + agent.routePlannedTick = ctx.tick; + return agent.route.size() >= 2; +} + +BehaviorStatus followRouteStep(BehaviorContext& ctx) { + BehaviorAgent& agent = *ctx.agent; + if (agent.routeFinished()) return BehaviorStatus::Success; + + Vec3i next = agent.route[static_cast(agent.routeCursor)]; + if (stepTowardPoint(agent, behaviorCellCenter(next), ctx.tuning.arriveEpsilon)) { + ++agent.routeCursor; + } + return agent.routeFinished() ? BehaviorStatus::Success : BehaviorStatus::Running; +} + +bool findShadedCell(const World& world, const Vec3i& from, int radius, const PathConfig& config, + Vec3i& outCell) { + int best = -1; + Vec3i bestCell; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + for (int dy = -2; dy <= 2; ++dy) { + Vec3i probe{from.x + dx, from.y + dy, from.z + dz}; + if (!cellIsShaded(world, probe)) continue; + if (!isStandable(world, probe, config)) continue; + int cost = dx * dx + dz * dz + dy * dy * 4; + if (best >= 0 && cost >= best) continue; + best = cost; + bestCell = probe; + } + } + } + if (best < 0) return false; + outCell = bestCell; + return true; +} + +BehaviorTree makeHostileHunterTree(const BehaviorTuning& tuning) { + auto strike = std::unique_ptr(new SequenceNode("strike")); + strike->add(condition("withinAttackReach", [](BehaviorContext& ctx) { + float reach = statsOf(ctx).attackRange; + return behaviorHorizontalDistance(ctx.agent->position, ctx.playerPosition) <= reach; + })); + strike->add(std::unique_ptr(new CooldownNode( + "attackCooldown", + action("attack", + [](BehaviorContext& ctx) { + ++ctx.agent->attacksLanded; + ctx.blackboard.setNumber("lastAttackDamage", statsOf(ctx).attackDamage); + return BehaviorStatus::Success; + }), + tuning.attackCooldownTicks))); + + auto approach = std::unique_ptr(new SequenceNode("approach")); + approach->add(action("planRouteToTarget", [](BehaviorContext& ctx) { + Vec3i goal = ctx.blackboard.cell("targetCell", behaviorCellOf(ctx.playerPosition)); + return planRouteTo(ctx, goal, false) ? BehaviorStatus::Success : BehaviorStatus::Failure; + })); + approach->add(action("followRoute", followRouteAction)); + + auto closeIn = std::unique_ptr(new SelectorNode("strikeOrApproach")); + closeIn->add(std::move(strike)); + closeIn->add(std::move(approach)); + + auto engage = std::unique_ptr(new SequenceNode("engageTarget")); + engage->add(condition("targetInRange", [](BehaviorContext& ctx) { + if (!ctx.playerPresent) { + ctx.blackboard.setFlag("engaged", false); + return false; + } + const SpeciesStats& stats = statsOf(ctx); + bool engaged = ctx.blackboard.flag("engaged"); + float radius = engaged ? stats.disengageRadius : stats.alertRadius; + float distance = behaviorHorizontalDistance(ctx.agent->position, ctx.playerPosition); + bool inRange = distance <= radius; + ctx.blackboard.setFlag("engaged", inRange); + if (inRange) { + ctx.blackboard.setPoint("lastSeen", ctx.playerPosition); + ctx.blackboard.setCell("targetCell", behaviorCellOf(ctx.playerPosition)); + } + return inRange; + })); + engage->add(std::move(closeIn)); + + auto goHome = std::unique_ptr(new SequenceNode("returnHome")); + goHome->add(condition("awayFromHome", [](BehaviorContext& ctx) { + if (ctx.blackboard.flag("engaged")) return false; + return behaviorHorizontalDistance(ctx.agent->position, ctx.agent->homePosition) > + ctx.tuning.homeReturnRadius; + })); + goHome->add(action("planRouteHome", [](BehaviorContext& ctx) { + Vec3i goal = behaviorCellOf(ctx.agent->homePosition); + return planRouteTo(ctx, goal, false) ? BehaviorStatus::Success : BehaviorStatus::Failure; + })); + goHome->add(action("followRoute", followRouteAction)); + + auto root = std::unique_ptr(new SelectorNode("hostileRoot")); + root->add(std::move(engage)); + root->add(std::move(goHome)); + root->add(action("idle", [](BehaviorContext&) { return BehaviorStatus::Success; })); + + return BehaviorTree(std::move(root)); +} + +BehaviorTree makeGrazingAnimalTree(const BehaviorTuning& tuning) { + auto flee = std::unique_ptr(new SequenceNode("flee")); + flee->add(condition("threatened", [](BehaviorContext& ctx) { + if (ctx.agent->health < ctx.agent->maxHealth) return true; + if (!ctx.playerPresent) return false; + if (isBreedingFood(ctx.agent->species, ctx.playerHeldItem)) return false; + return behaviorHorizontalDistance(ctx.agent->position, ctx.playerPosition) < + ctx.tuning.fleeRadius; + })); + flee->add(action("fleeStep", [](BehaviorContext& ctx) { + BehaviorAgent& agent = *ctx.agent; + Vec3 away{agent.position.x - ctx.playerPosition.x, 0.0f, + agent.position.z - ctx.playerPosition.z}; + if (length(away) < 1e-3f) away = horizontalHeading(agent.rng); + away = normalize(away); + stepTowardPoint(agent, agent.position + away * 4.0f, ctx.tuning.arriveEpsilon); + return BehaviorStatus::Running; + })); +