Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using Theriapolis.Core.Rules.Combat;
|
||||
using Theriapolis.Core.Util;
|
||||
|
||||
namespace Theriapolis.Core.Entities.Ai;
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="BrigandBehavior"/>, but flees when reduced below
|
||||
/// <c>FLEE_HP_FRACTION</c> of max HP — wild animals don't have honor.
|
||||
/// </summary>
|
||||
public sealed class WildAnimalBehavior : INpcBehavior
|
||||
{
|
||||
public const float FLEE_HP_FRACTION = 0.25f;
|
||||
|
||||
public void TakeTurn(Combatant self, AiContext ctx)
|
||||
{
|
||||
if (self.IsDown) return;
|
||||
var target = ctx.FindClosestHostile(self);
|
||||
if (target is null) return;
|
||||
|
||||
// Flee at low HP — move directly away, don't attack.
|
||||
if (self.CurrentHp <= self.MaxHp * FLEE_HP_FRACTION)
|
||||
{
|
||||
int tiles = ctx.Encounter.CurrentTurn.RemainingMovementFt / 5;
|
||||
for (int i = 0; i < tiles; i++)
|
||||
{
|
||||
var away = StepAwayFrom(self.Position, target.Position);
|
||||
if ((int)away.X == (int)self.Position.X && (int)away.Y == (int)self.Position.Y) break;
|
||||
self.Position = away;
|
||||
ctx.Encounter.AppendLog(CombatLogEntry.Kind.Move,
|
||||
$"{self.Name} flees to ({(int)away.X},{(int)away.Y}).");
|
||||
}
|
||||
ctx.Encounter.CurrentTurn.ConsumeMovement(tiles * 5);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, identical to Brigand: close + attack.
|
||||
var attack = self.AttackOptions[0];
|
||||
int tilesAvail = ctx.Encounter.CurrentTurn.RemainingMovementFt / 5;
|
||||
while (!ReachAndCover.IsInReach(self, target, attack) && tilesAvail > 0)
|
||||
{
|
||||
var next = ReachAndCover.StepToward(self.Position, target.Position);
|
||||
if ((int)next.X == (int)self.Position.X && (int)next.Y == (int)self.Position.Y) break;
|
||||
self.Position = next;
|
||||
ctx.Encounter.AppendLog(CombatLogEntry.Kind.Move,
|
||||
$"{self.Name} moves to ({(int)next.X},{(int)next.Y}).");
|
||||
tilesAvail--;
|
||||
}
|
||||
int consumedTiles = (ctx.Encounter.CurrentTurn.RemainingMovementFt / 5) - tilesAvail;
|
||||
ctx.Encounter.CurrentTurn.ConsumeMovement(consumedTiles * 5);
|
||||
|
||||
if (!ReachAndCover.IsInReach(self, target, attack)) return;
|
||||
Resolver.AttemptAttack(ctx.Encounter, self, target, attack);
|
||||
ctx.Encounter.CurrentTurn.ConsumeAction();
|
||||
}
|
||||
|
||||
private static Vec2 StepAwayFrom(Vec2 from, Vec2 menace)
|
||||
{
|
||||
// Reverse of StepToward — increment by sign of (from - menace) so we
|
||||
// move away from the menace.
|
||||
int dx = System.Math.Sign(from.X - menace.X);
|
||||
int dy = System.Math.Sign(from.Y - menace.Y);
|
||||
if (dx == 0 && dy == 0) dx = 1; // pick a direction if standing on top
|
||||
return new Vec2((int)from.X + dx, (int)from.Y + dy);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user