Skip to content

Commit 106a119

Browse files
committed
feat: implement brandish scare mechanic for hostiles targeting players
1 parent 4ac8bd1 commit 106a119

6 files changed

Lines changed: 162 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ Feature (F) and decision (D) numbers refer to [FEATURES.md](FEATURES.md).
1313
- F01: hold right-click with no block targeted to brandish a lit main-hand
1414
torch (collectible behavior), with third- and first-person raise animations.
1515
Block-targeted right-clicks keep vanilla placement, relighting, and ignition.
16+
- F02a: brandishing has a periodic chance to make nearby hostiles that are
17+
targeting the player flee (vanilla AI).
1618
- Initial design baseline for torch, fire, and wildlife interactions.
1719
- Local build and packaging script.
1820
- GitHub Actions build and draft-release workflow.
1921
- Starter project, design, release, and testing documentation.
2022

2123
### Known limitations
2224

23-
- Animals do not react to the brandished torch yet.
25+
- Only brandish scare so far - ground fires and richer reactions are later.
2426
- Aiming at an entity with a lit main-hand torch brandishes instead of
2527
running right-click entity interactions such as boarding a boat.
2628
- Runtime configuration does not exist yet.
27-
- Entity targeting, fire-source integration, and animal reaction rules remain
28-
planned work.
29+
- Dedicated-server brandish->scare sync still needs a playtest confirmation.

FEATURES.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ history belongs in [CHANGELOG.md](CHANGELOG.md).
99
| ID | Status | Feature | Config key | Default | Authority |
1010
|---|---|---|---|---|---|
1111
| F01 | Implemented | Hold right-click to brandish a lit main-hand torch | - | - | Universal |
12-
| F02 | Planned | Fire sources, including a brandished torch, influence nearby animal behavior | - | - | Server |
12+
| F02 | In progress | Fire sources influence nearby animals — F02a brandish scare done; ground fires later | - | range 12 / chance 0.25 / 1s | Server |
1313
| F03 | Planned | Animals can flee, disengage, investigate, or ignore fire | - | - | Server |
1414
| F04 | Planned | Configure fire sources, ranges, probabilities, and reactions | - | - | Server |
1515

@@ -46,16 +46,39 @@ is in the main hand. Scoping the stance to actual threats is F02/F03 work.
4646
| D02 | Active | Release metadata is owned by `resources/modinfo.json`. |
4747
| D03 | Active | The server is authoritative for animal reactions and probability checks. |
4848
| D04 | Active | The first scope is hostile animals targeting or aggressive toward the player. |
49-
| D05 | Planned | A scare probability is evaluated on controlled intervals, not every game tick. |
49+
| D05 | Active | Scare checks run on a server tick interval (~1s), not every frame. |
5050
| D06 | Planned | Fire reactions are configurable per fire source and animal behavior profile. |
5151
| D07 | Active | Brandishing uses the interact (right-click) channel and only begins with no block targeted, because vanilla assigns every block-targeted right-click of a lit torch: plain right-click places it and `CanIgnite` owns the shift ignite gesture. Block-targeted input reaches the mod untouched. |
5252
| D08 | Active | F01 is a `CollectibleBehavior` prepended onto lit torches in `AssetsFinalize` (both sides), not Harmony. Must run before `CanIgnite` and use `PreventSubsequent` while raised so aim-crossing a block cannot start fires. Code attach (not a JSON patch) so we can skip extinct / non-torch `BlockTorch` assets. |
53+
| D09 | Active | F02 ships in slices. F02a: brandished torch only — no ground fires, no config. Later: placed fire sources (F02b), richer reactions (F03). |
54+
| D10 | Active | F02a calls vanilla `AiTaskFleeEntity` / `AiTaskFleeEntityR`.InstaFleeFrom when an active aggressive targetable task is targeting the brandishing player. No custom AI task. |
5355

5456
## Extension inventory
5557

5658
| Kind | Type | Side | Feature | Notes |
5759
|---|---|---|---|---|
5860
| CollectibleBehavior | `BrandishTorch` | Universal | F01 | Prepended in `AssetsFinalize` on lit `BlockTorch`. Stance is in-memory, not `Attributes`. |
59-
erialized settings only
60-
when a feature uses them, define ownership and synchronization here, and
61-
document user-facing values in [README.md](README.md).
61+
| Server tick | `BrandishScareService` | Server | F02a | 1s interval; range 12; flee chance 0.25. |
62+
63+
## F02a (done)
64+
65+
Brandishing players: nearby hostiles actively targeting them get a periodic chance to flee via `InstaFleeFrom`. No network channel — relies on use sync for server brandish state (confirm on dedicated server).
66+
67+
**Later:** F02b ground fires, F03 reaction profiles, F04 config.
68+
## Design notes
69+
70+
Fire is treated as a signal, not a universal fear effect. A torch held up by
71+
the player may be more immediate and threatening than a distant firepit. Some
72+
predatory animals may investigate an isolated fire, creating a configurable
73+
counter-effect rather than a guaranteed safety zone.
74+
75+
The first release should distinguish between:
76+
77+
- fleeing from the player;
78+
- abandoning an active pursuit without fleeing;
79+
- investigating a fire source; and
80+
- remaining unaffected.
81+
82+
## Configuration contract
83+
84+
No runtime config yet. F04 adds it when reactions need knobs.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Pyrophobia is a universal code Vintage Story mod about the relationship between
44
wildlife: raise a torch to deter hostile animals, while torches and fires in
55
the world influence how nearby animals behave.
66

7-
The mod currently implements the brandish stance: holding right-click with a
8-
lit torch in the main hand raises it defensively, in first and third person.
9-
Animal reactions to the raised torch are the next implementation step.
7+
The mod currently implements the brandish stance and a first animal reaction:
8+
holding right-click with a lit torch raises it, and nearby hostiles that are
9+
targeting you may flee while it is raised.
1010

1111
## Compatibility
1212

src/Behaviors/CollectibleBehaviorBrandishTorch.cs

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@ public CollectibleBehaviorBrandishTorch(CollectibleObject collObj)
2525

2626
internal static void ResetStanceState() => BrandishingByEntityId.Clear();
2727

28-
/// <summary>
29-
/// PreventDefault marks the use as handled so the caller sets
30-
/// <c>Controls.HandUse</c>. With a block targeted or Shift held we
31-
/// PassThrough, which keeps placement, relighting, and CanIgnite vanilla.
32-
/// </summary>
28+
/// <summary>Query for F02 scare checks (populated on both sides via use sync).</summary>
29+
internal static bool IsBrandishing(EntityAgent byEntity) =>
30+
BrandishingByEntityId.ContainsKey(byEntity.EntityId);
31+
3332
public override void OnHeldInteractStart(
3433
ItemSlot slot,
3534
EntityAgent byEntity,
@@ -55,10 +54,6 @@ public override void OnHeldInteractStart(
5554
handling = EnumHandling.PreventDefault;
5655
}
5756

58-
/// <summary>
59-
/// Keep HandUse active for the hold duration, even when aim later crosses
60-
/// a block. PreventSubsequent stops CanIgnite from treating that as ignite.
61-
/// </summary>
6257
public override bool OnHeldInteractStep(
6358
float secondsUsed,
6459
ItemSlot slot,
@@ -76,9 +71,6 @@ public override bool OnHeldInteractStep(
7671
return true;
7772
}
7873

79-
/// <summary>
80-
/// Accept cancellation (release / swap) and ease the stance out.
81-
/// </summary>
8274
public override bool OnHeldInteractCancel(
8375
float secondsUsed,
8476
ItemSlot slot,
@@ -98,11 +90,6 @@ public override bool OnHeldInteractCancel(
9890
return true;
9991
}
10092

101-
/// <summary>
102-
/// Brandishing produces no effect on stop. PreventSubsequent keeps
103-
/// CanIgnite's stop handler from starting fires after a long hold that
104-
/// began as a brandish and later aimed at a block.
105-
/// </summary>
10693
public override void OnHeldInteractStop(
10794
float secondsUsed,
10895
ItemSlot slot,
@@ -120,10 +107,6 @@ public override void OnHeldInteractStop(
120107
handling = EnumHandling.PreventSubsequent;
121108
}
122109

123-
/// <summary>
124-
/// Only active brandishes get the stance animation; vanilla uses such as
125-
/// the ignite gesture keep their own animation.
126-
/// </summary>
127110
public override string GetHeldTpUseAnimation(
128111
ItemSlot activeHotbarSlot,
129112
Entity forEntity,
@@ -140,10 +123,6 @@ public override string GetHeldTpUseAnimation(
140123
return null!;
141124
}
142125

143-
/// <summary>
144-
/// Lit main-hand torch only. Slot identity keeps mirrored calls for other
145-
/// slots or entities on their vanilla path.
146-
/// </summary>
147126
private bool IsBrandishableTorch(ItemSlot slot, Entity byEntity)
148127
{
149128
if (collObj is not BlockTorch torch ||
@@ -159,9 +138,6 @@ byEntity is not EntityPlayer entityPlayer ||
159138
return true;
160139
}
161140

162-
private static bool IsBrandishing(EntityAgent byEntity) =>
163-
BrandishingByEntityId.ContainsKey(byEntity.EntityId);
164-
165141
private static void SetBrandishing(EntityAgent byEntity, bool value)
166142
{
167143
if (value)

src/PyrophobiaModSystem.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Pyrophobia.Behaviors;
3+
using Pyrophobia.Scare;
34
using Vintagestory.API.Client;
45
using Vintagestory.API.Common;
56
using Vintagestory.API.Server;
@@ -9,11 +10,16 @@ namespace Pyrophobia;
910

1011
/// <summary>
1112
/// Pyrophobia entry point. F01 attaches in <see cref="AssetsFinalize"/> (D08).
13+
/// F02a scare tick is server-only (D03).
1214
/// </summary>
1315
public class PyrophobiaModSystem : ModSystem
1416
{
1517
public const string ModId = "pyrophobia";
1618

19+
private ICoreServerAPI? sapi;
20+
private BrandishScareService? scare;
21+
private long scareListenerId;
22+
1723
public override void Start(ICoreAPI api)
1824
{
1925
base.Start(api);
@@ -77,11 +83,24 @@ public override void StartClientSide(ICoreClientAPI api)
7783
public override void StartServerSide(ICoreServerAPI api)
7884
{
7985
base.StartServerSide(api);
80-
api.Logger.Notification("[{0}] server side loaded.", ModId);
86+
sapi = api;
87+
scare = new BrandishScareService(api);
88+
scareListenerId=api.Event.RegisterGameTickListener(scare.OnTick,
89+
e => api.Logger.Error("[{0}] brandish scare tick failed: {1}", ModId, e),
90+
BrandishScareService.IntervalMs);
91+
api.Logger.Notification("[{0}] server side loaded (F02a scare every {1}ms).", ModId, BrandishScareService.IntervalMs);
8192
}
8293

8394
public override void Dispose()
8495
{
96+
if (sapi != null && scareListenerId != 0)
97+
{
98+
sapi.Event.UnregisterGameTickListener(scareListenerId);
99+
scareListenerId = 0;
100+
}
101+
102+
scare = null;
103+
sapi = null;
85104
CollectibleBehaviorBrandishTorch.ResetStanceState();
86105
base.Dispose();
87106
}

src/Scare/BrandishScareService.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Pyrophobia.Behaviors;
2+
using Vintagestory.API.Common;
3+
using Vintagestory.API.Common.Entities;
4+
using Vintagestory.API.Server;
5+
using Vintagestory.GameContent;
6+
7+
namespace Pyrophobia.Scare;
8+
9+
/// <summary>
10+
/// F02a / D03–D05 / D09–D10: server tick - brandishing players periodically
11+
/// scare hostiles that are actively targeting them via vanilla InstaFleeFrom.
12+
/// (Main thread. Hardcoded knobs until F04)
13+
/// </summary>
14+
internal sealed class BrandishScareService
15+
{
16+
internal const float Range = 12f;
17+
internal const float FleeChance = 0.25f;
18+
internal const int IntervalMs = 1000;
19+
20+
private readonly ICoreServerAPI sapi;
21+
22+
public BrandishScareService(ICoreServerAPI sapi)
23+
{
24+
this.sapi = sapi;
25+
}
26+
27+
public void OnTick(float dt)
28+
{
29+
foreach (IPlayer player in sapi.World.AllOnlinePlayers)
30+
{
31+
if (player.Entity is not EntityPlayer playerEntity || !playerEntity.Alive)
32+
{
33+
continue;
34+
}
35+
36+
if (!CollectibleBehaviorBrandishTorch.IsBrandishing(playerEntity))
37+
{
38+
continue;
39+
}
40+
41+
ScareAround(playerEntity);
42+
}
43+
}
44+
45+
private void ScareAround(EntityPlayer playerEntity)
46+
{
47+
long playerId = playerEntity.EntityId;
48+
Entity[] nearby = sapi.World.GetEntitiesAround(
49+
playerEntity.Pos.XYZ,
50+
Range,
51+
Range,
52+
e => e is EntityAgent agent && agent.Alive && agent.EntityId != playerId);
53+
54+
foreach (Entity entity in nearby)
55+
{
56+
EntityBehaviorTaskAI? taskAi = entity.GetBehavior<EntityBehaviorTaskAI>();
57+
if (taskAi == null)
58+
{
59+
continue;
60+
}
61+
62+
if (!IsAggressivelyTargeting(taskAi.TaskManager, playerId))
63+
{
64+
continue;
65+
}
66+
67+
if (sapi.World.Rand.NextDouble() >= FleeChance)
68+
{
69+
continue;
70+
}
71+
72+
TryInstaFlee(taskAi.TaskManager, playerEntity);
73+
}
74+
}
75+
76+
private static bool IsAggressivelyTargeting(AiTaskManager tasks, long playerEntityId)
77+
{
78+
foreach (IAiTask? task in tasks.ActiveTasksBySlot)
79+
{
80+
if (task is AiTaskBaseTargetable targetable &&
81+
targetable.AggressiveTargeting &&
82+
targetable.TargetEntity?.EntityId == playerEntityId)
83+
{
84+
return true;
85+
}
86+
87+
if (task is AiTaskBaseTargetableR targetableR &&
88+
targetableR.AggressiveTargeting &&
89+
targetableR.TargetEntity?.EntityId == playerEntityId)
90+
{
91+
return true;
92+
}
93+
}
94+
95+
return false;
96+
}
97+
98+
private static void TryInstaFlee(AiTaskManager tasks, Entity playerEntity)
99+
{
100+
tasks.GetTask<AiTaskFleeEntity>()?.InstaFleeFrom(playerEntity);
101+
tasks.GetTask<AiTaskFleeEntityR>()?.InstaFleeFrom(playerEntity);
102+
}
103+
}

0 commit comments

Comments
 (0)