Fixing minor bugs, some renaming

This commit is contained in:
Vladislav Khorev committed 2026-11-10 21:06:27 +06:00
1 parent f3cafd2bf7
commit 63bf981e21
12 files changed
+1486 -240

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Hostile NPC Behavior
## Authority and update order
Hostile NPC behavior is simulated by `LocationGameState` and
`CharacterState`. Rendering objects do not select targets and do not change
NPC combat state.
During each authoritative simulation tick:
1. The location gathers the enabled players and NPCs visible to the
simulation.
2. Each hostile NPC selects or retains a player target.
3. Every character advances using the same tick-start observations.
4. Movement reservations and attack damage are resolved deterministically.
5. Navigation recovery events are applied by `InternalGameServer`.
The resulting state and recovery decisions are replicated through the existing
WebSocket lockstep stream. NPC target changes are simulation consequences, not
client commands.
## Eligible players and nearest selection
An eligible target must:
- exist in the campaign player map;
- be enabled;
- have positive health; and
- be within the NPC's `visibleRange` when a new target is selected.
Distance is measured in the X/Z plane. If two players are equally close,
their stable player IDs are compared lexicographically. This gives every
lockstep replica the same result regardless of hash-map iteration order.
## Ordinary target locking
Once a hostile NPC has a valid target, it keeps that target during ordinary
ticks. The target remains locked even if it moves outside `visibleRange`.
The lock ends when the player dies or becomes disabled.
When a lock ends, the NPC selects the nearest eligible player in range. If no
player qualifies, it clears the attack target, leaves combat state, and
requests a route to its `homePosition`.
## Movement blockage and target reconsideration
NPC movement uses a retry schedule after a character blocks its path. The
blocked NPC first stops and waits for its deterministic navigation retry
cooldown. When the retry event emits `forceNavigationReplan`, the authoritative
location state performs a fresh target selection across all eligible players.
This blocked-replan selection differs from ordinary target locking:
- the current target is not privileged;
- the nearest eligible player may remain the target;
- a nearer eligible player replaces the current target;
- the route is rebuilt from the NPC's current position toward the selected
player's current position.
If no eligible player is in range during blocked recovery, the NPC clears its
stale chase target and returns toward its home position instead. The blocker is
still passed to path planning so the immediate replan does not route through
the character that caused the recovery event.
## Death, disconnect, and attack safety
Dead or disabled players are excluded from target acquisition. A target that
dies or disconnects is therefore invalidated on the next authoritative
selection pass, and the NPC either switches to another player or returns home.
Attack impacts are also validated at application time. Damage is ignored if
the recorded target no longer exists, is disabled, or has already reached
zero health. This prevents a queued animation impact from damaging a stale
target.
## Regression expectations
The state and pathfinding tests should preserve these guarantees:
- nearest in-range player selection is deterministic;
- a valid ordinary lock is retained;
- blocked NPC recovery can replace the target and rebuild the chase route;
- no-target blocked recovery clears combat and routes home;
- dead and disabled players cannot be selected or damaged;
- retry cooldowns and blocker identity remain deterministic.
+24 -10
View File
@@ -2,7 +2,10 @@
"campaigns": [ "campaigns": [
{ {
"id": "main", "id": "main",
"title": { "ru": "Основная игра", "en": "Main Game" }, "title": {
"ru": "Основная игра",
"en": "Main Game"
},
"locations": [ "locations": [
"resources/config/locations/location_dorm.json", "resources/config/locations/location_dorm.json",
"resources/config/locations/uni_exterior.json", "resources/config/locations/uni_exterior.json",
@@ -17,12 +20,15 @@
}, },
{ {
"id": "battle_zone", "id": "battle_zone",
"title": { "ru": "Зона боя (одиночная)", "en": "Battle Zone (Single Player)" }, "title": {
"ru": "Зона боя (одиночная)",
"en": "Battle Zone (Single Player)"
},
"locations": [ "locations": [
"resources/config/locations/battle_zone_uni_exterior.json" "resources/config/locations/battle_zone_korea.json"
], ],
"startLocation": "uni_exterior", "startLocation": "park_korea",
"startMusic": "music/bishkek univer day.ogg", "startMusic": "music/bishkek fight calm.ogg",
"startState": "resources/config/start_state_battle_zone_singleplayer.json", "startState": "resources/config/start_state_battle_zone_singleplayer.json",
"quests": "resources/config/quests_battle_zone.json", "quests": "resources/config/quests_battle_zone.json",
"tutorial": false, "tutorial": false,
@@ -30,16 +36,24 @@
}, },
{ {
"id": "battle_zone_multiplayer", "id": "battle_zone_multiplayer",
"title": { "ru": "Зона боя (сетевая)", "en": "Battle Zone (Multiplayer)" }, "title": {
"ru": "Зона боя (сетевая)",
"en": "Battle Zone (Multiplayer)"
},
"locations": [ "locations": [
"resources/config/locations/battle_zone_uni_exterior.json" "resources/config/locations/battle_zone_korea.json"
], ],
"startLocation": "uni_exterior", "startLocation": "park_korea",
"startMusic": "music/bishkek univer day.ogg", "startMusic": "music/bishkek fight calm.ogg",
"startState": "resources/config/start_state_battle_zone.json", "startState": "resources/config/start_state_battle_zone.json",
"quests": "resources/config/quests_battle_zone.json", "quests": "resources/config/quests_battle_zone.json",
"tutorial": false, "tutorial": false,
"networkPlayerSlots": ["player", "player2"], "networkPlayerSlots": [
"player1",
"player2",
"player3",
"player4"
],
"networkCompatibility": "battle-zone-lockstep-v5", "networkCompatibility": "battle-zone-lockstep-v5",
"loadingScreen": "resources/w/ui/screen_loading.json" "loadingScreen": "resources/w/ui/screen_loading.json"
} }
@@ -1,5 +1,5 @@
{ {
"id": "uni_exterior", "id": "park_korea",
"gameObjects": "resources/w/shelter/game_objects.json", "gameObjects": "resources/w/shelter/game_objects.json",
"interactiveObjects": "resources/w/shelter/interactive_objects.json", "interactiveObjects": "resources/w/shelter/interactive_objects.json",
"npcs": "resources/config/npcs_battle_zone.json", "npcs": "resources/config/npcs_battle_zone.json",
+162
View File
@@ -161,6 +161,168 @@
"hp": 35, "hp": 35,
"visibleRange" : 6.5, "visibleRange" : 6.5,
"canAttack": true "canAttack": true
},
{
"id": "ghost_07x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -14.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_08x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -14.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_09x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -14.5,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_10x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -14.5,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_11x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -5.6,
"positionY": 0.0,
"positionZ": -15.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
},
{
"id": "ghost_12x",
"name": "Тень",
"meshTextures": {
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
},
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
"positionX": -6.4,
"positionY": 0.0,
"positionZ": -15.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 1.0,
"modelCorrectionRotX": -90.0,
"modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0,
"hp": 35,
"visibleRange" : 6.5,
"canAttack": true
} }
] ]
} }
+36 -8
View File
@@ -1,4 +1,13 @@
{ {
"localState": {
"locations": {
"location_dorm": {
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002
}
}
},
"networkState": {
"campaignId": "main", "campaignId": "main",
"chatHistory": [ "chatHistory": [
[], [],
@@ -16,8 +25,7 @@
true true
], ],
"currentLocationId": "", "currentLocationId": "",
"globalFloats": { "globalFloats": {},
},
"globalInts": {}, "globalInts": {},
"inventory": { "inventory": {
"items": [] "items": []
@@ -494,6 +502,22 @@
-15.038200378417969 -15.038200378417969
], ],
"hp": 100.0, "hp": 100.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
10,
4,
5,
8,
11,
9,
6,
3,
2,
7
],
"navigationRetryTicks": 0,
"pathWaypoints": [ "pathWaypoints": [
[ [
-6.296899795532227, -6.296899795532227,
@@ -530,8 +554,6 @@
}, },
"state": { "state": {
"activeNavigationIndex": 0, "activeNavigationIndex": 0,
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002,
"targetInteractNpcId": "", "targetInteractNpcId": "",
"targetInteractiveObjectId": "", "targetInteractiveObjectId": "",
"targetTeleportZoneId": "", "targetTeleportZoneId": "",
@@ -574,10 +596,9 @@
} }
}, },
"money": 5500, "money": 5500,
"playerHp": 200.0,
"playerMaxHp": 200.0, "playerMaxHp": 200.0,
"players": { "players": {
"player": { "player1": {
"animationFrame": 0.0, "animationFrame": 0.0,
"attackTargetId": "", "attackTargetId": "",
"attack_cooldown": 0.0, "attack_cooldown": 0.0,
@@ -594,6 +615,11 @@
0.0 0.0
], ],
"hp": 200.0, "hp": 200.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [],
"navigationRetryTicks": 0,
"pathWaypoints": [ "pathWaypoints": [
[ [
6.8031005859375, 6.8031005859375,
@@ -764,12 +790,14 @@
} }
] ]
}, },
"savedAt": "2026-10-30 14:48", "savedAt": "2026-11-10 20:38",
"schemaVersion": 1, "schemaVersion": 5,
"taxiIsCalled": false, "taxiIsCalled": false,
"tutorialJournalScreenOpened": false, "tutorialJournalScreenOpened": false,
"tutorialNeedOpenTaxiScreen": false, "tutorialNeedOpenTaxiScreen": false,
"tutorialPhoneChatScreenOpened": false, "tutorialPhoneChatScreenOpened": false,
"tutorialStep": 0, "tutorialStep": 0,
"tutorialTaxiHintDone": false "tutorialTaxiHintDone": false
},
"persistenceVersion": 1
} }
+523 -95
View File
@@ -1,7 +1,7 @@
{ {
"localState": { "localState": {
"locations": { "locations": {
"uni_exterior": { "park_korea": {
"cameraAzimuth": -2.3499999046325684, "cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002 "cameraInclination": 1.103600025177002
} }
@@ -10,9 +10,12 @@
"networkState": { "networkState": {
"campaignId": "battle_zone", "campaignId": "battle_zone",
"chatHistory": [ "chatHistory": [
[], [
[], ],
[] [
],
[
]
], ],
"chatPreviewMsg": [ "chatPreviewMsg": [
"", "",
@@ -27,15 +30,17 @@
"currentLocationId": "", "currentLocationId": "",
"globalFloats": { "globalFloats": {
}, },
"globalInts": {}, "globalInts": {
},
"inventory": { "inventory": {
"items": [] "items": [
]
}, },
"isDarklands": true, "isDarklands": true,
"isDawn": false, "isDawn": false,
"isNight": true, "isNight": true,
"locations": { "locations": {
"uni_exterior": { "park_korea": {
"dialogue": { "dialogue": {
"active": false "active": false
}, },
@@ -44,7 +49,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.6000000238418579, "positionX": 0.60000002384185791,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -54,7 +59,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 1.399999976158142, "positionX": 1.3999999761581421,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -64,7 +69,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 2.200000047683716, "positionX": 2.2000000476837158,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -85,7 +90,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.0, "positionX": 0.0,
"positionY": 3.200000047683716, "positionY": 3.2000000476837158,
"positionZ": 0.0, "positionZ": 0.0,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -95,7 +100,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": -1.0, "positionX": -1.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -105,7 +110,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.0, "positionX": 0.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -115,7 +120,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 1.0, "positionX": 1.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -134,7 +139,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -155,14 +160,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -170,7 +176,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
] ]
@@ -187,7 +193,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -208,14 +214,15 @@
6 6
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -223,7 +230,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
] ]
@@ -240,7 +247,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -261,14 +268,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -276,7 +284,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
] ]
@@ -293,7 +301,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -314,14 +322,15 @@
6 6
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -329,7 +338,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
] ]
@@ -346,7 +355,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -367,14 +376,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -382,7 +392,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
] ]
@@ -399,7 +409,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -420,14 +430,15 @@
2 2
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -435,13 +446,338 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
] ]
},
"ghost_07x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-14.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
3,
10,
5,
6,
8,
7,
9,
4,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-14.0
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-14.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-14.0
]
},
"ghost_08x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-14.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
11,
3,
10,
2,
9,
5,
4,
7,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-14.0
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-14.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-14.0
]
},
"ghost_09x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-14.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
4,
8,
3,
6,
10,
9,
7,
5,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-14.5
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-14.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-14.5
]
},
"ghost_10x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-14.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
3,
4,
7,
10,
5,
11,
9,
2,
8,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-14.5
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-14.5
]
},
"ghost_11x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-15.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
7,
2,
9,
4,
10,
6,
5,
3,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-15.0
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-15.0
]
},
"ghost_12x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-15.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
7,
11,
5,
4,
3,
10,
9,
8,
6,
2
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-15.0
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-15.0
]
} }
}, },
"scriptGlobals": {}, "scriptGlobals": {
},
"state": { "state": {
"activeNavigationIndex": 0, "activeNavigationIndex": 0,
"targetInteractNpcId": "", "targetInteractNpcId": "",
@@ -460,61 +796,23 @@
"rotation": 0.0, "rotation": 0.0,
"steeringAngle": 0.0, "steeringAngle": 0.0,
"velocity": 0.0, "velocity": 0.0,
"waypoints": [], "waypoints": [
],
"wheelAngle": 0.0 "wheelAngle": 0.0
} }
} }
}, },
"teleportZones": {}, "teleportZones": {
"triggerZones": {} },
"triggerZones": {
}
} }
}, },
"money": 5500, "money": 5500,
"playerHp": 200.0, "playerHp": 200.0,
"playerMaxHp": 200.0, "playerMaxHp": 200.0,
"players": { "players": {
"player2": { "player1": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [[
-6.4,
0.0,
13.0
]],
"position": [
-6.4,
0.0,
13.0
],
"requestedWalkTarget": [
-6.4,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.4,
0.0,
13.0
]
},
"player": {
"animationFrame": 0.0, "animationFrame": 0.0,
"attackTargetId": "", "attackTargetId": "",
"attack_cooldown": 0.0, "attack_cooldown": 0.0,
@@ -533,18 +831,18 @@
"hp": 200.0, "hp": 200.0,
"pathWaypoints": [ "pathWaypoints": [
[ [
-5.8, -5.4,
0.0, 0.0,
12.8 12.8
] ]
], ],
"position": [ "position": [
-5.8, -5.4,
0.0, 0.0,
12.8 12.8
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.8, -5.4,
0.0, 0.0,
12.8 12.8
], ],
@@ -552,14 +850,144 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.0, "visibleRange": 6.0,
"walkTarget": [ "walkTarget": [
-5.8, -5.4,
0.0, 0.0,
12.8 12.8
] ]
},
"player2": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-6.8,
0.0,
13.0
]
],
"position": [
-6.8,
0.0,
13.0
],
"requestedWalkTarget": [
-6.8,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
13.0
]
},
"player3": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-5.4,
0.0,
14.8
]
],
"position": [
-5.4,
0.0,
14.8
],
"requestedWalkTarget": [
-5.4,
0.0,
14.8
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-5.4,
0.0,
14.8
]
},
"player4": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-6.8,
0.0,
15.0
]
],
"position": [
-6.8,
0.0,
15.0
],
"requestedWalkTarget": [
-6.8,
0.0,
15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
15.0
]
} }
}, },
"questJournal": { "questJournal": {
"quests": [] "quests": [
]
}, },
"savedAt": "2026-11-05 17:43", "savedAt": "2026-11-05 17:43",
"schemaVersion": 5, "schemaVersion": 5,
@@ -1,7 +1,7 @@
{ {
"localState": { "localState": {
"locations": { "locations": {
"uni_exterior": { "park_korea": {
"cameraAzimuth": -2.3499999046325684, "cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002 "cameraInclination": 1.103600025177002
} }
@@ -10,9 +10,12 @@
"networkState": { "networkState": {
"campaignId": "battle_zone", "campaignId": "battle_zone",
"chatHistory": [ "chatHistory": [
[], [
[], ],
[] [
],
[
]
], ],
"chatPreviewMsg": [ "chatPreviewMsg": [
"", "",
@@ -27,15 +30,17 @@
"currentLocationId": "", "currentLocationId": "",
"globalFloats": { "globalFloats": {
}, },
"globalInts": {}, "globalInts": {
},
"inventory": { "inventory": {
"items": [] "items": [
]
}, },
"isDarklands": true, "isDarklands": true,
"isDawn": false, "isDawn": false,
"isNight": true, "isNight": true,
"locations": { "locations": {
"uni_exterior": { "park_korea": {
"dialogue": { "dialogue": {
"active": false "active": false
}, },
@@ -44,7 +49,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.6000000238418579, "positionX": 0.60000002384185791,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -54,7 +59,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 1.399999976158142, "positionX": 1.3999999761581421,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -64,7 +69,7 @@
"alpha": 1.0, "alpha": 1.0,
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 2.200000047683716, "positionX": 2.2000000476837158,
"positionY": 0.45736101269721985, "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684, "positionZ": 3.8499999046325684,
"rotationY": 0.0, "rotationY": 0.0,
@@ -85,7 +90,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.0, "positionX": 0.0,
"positionY": 3.200000047683716, "positionY": 3.2000000476837158,
"positionZ": 0.0, "positionZ": 0.0,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -95,7 +100,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": -1.0, "positionX": -1.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -105,7 +110,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 0.0, "positionX": 0.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -115,7 +120,7 @@
"isActive": true, "isActive": true,
"isAnimating": false, "isAnimating": false,
"positionX": 1.0, "positionX": 1.0,
"positionY": 0.9150000214576721, "positionY": 0.91500002145767212,
"positionZ": -3.9000000953674316, "positionZ": -3.9000000953674316,
"rotationY": 0.0, "rotationY": 0.0,
"scale": 1.0 "scale": 1.0
@@ -134,7 +139,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -155,14 +160,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -170,7 +176,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.0 -12.0
] ]
@@ -187,7 +193,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -208,14 +214,15 @@
6 6
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
], ],
@@ -223,7 +230,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.0 -12.0
] ]
@@ -240,7 +247,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -261,14 +268,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -276,7 +284,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-12.5 -12.5
] ]
@@ -293,7 +301,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -314,14 +322,15 @@
6 6
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
], ],
@@ -329,7 +338,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-12.5 -12.5
] ]
@@ -346,7 +355,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -367,14 +376,15 @@
11 11
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -382,7 +392,7 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-5.599999904632568, -5.5999999046325684,
0.0, 0.0,
-13.0 -13.0
] ]
@@ -399,7 +409,7 @@
"facingAngle": 0.0, "facingAngle": 0.0,
"homeDriftCheckTimer": 5.0, "homeDriftCheckTimer": 5.0,
"homePosition": [ "homePosition": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -420,14 +430,15 @@
2 2
], ],
"navigationRetryTicks": 0, "navigationRetryTicks": 0,
"pathWaypoints": [], "pathWaypoints": [
],
"position": [ "position": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
"requestedWalkTarget": [ "requestedWalkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
], ],
@@ -435,13 +446,338 @@
"targetFacingAngle": 0.0, "targetFacingAngle": 0.0,
"visibleRange": 6.5, "visibleRange": 6.5,
"walkTarget": [ "walkTarget": [
-6.400000095367432, -6.4000000953674316,
0.0, 0.0,
-13.0 -13.0
] ]
},
"ghost_07x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-14.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
3,
10,
5,
6,
8,
7,
9,
4,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-14.0
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-14.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-14.0
]
},
"ghost_08x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-14.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
11,
3,
10,
2,
9,
5,
4,
7,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-14.0
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-14.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-14.0
]
},
"ghost_09x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-14.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
2,
4,
8,
3,
6,
10,
9,
7,
5,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-14.5
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-14.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-14.5
]
},
"ghost_10x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-14.5
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
3,
4,
7,
10,
5,
11,
9,
2,
8,
6
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-14.5
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-12.5
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-14.5
]
},
"ghost_11x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-5.5999999046325684,
0.0,
-15.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
8,
7,
2,
9,
4,
10,
6,
5,
3,
11
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-5.5999999046325684,
0.0,
-15.0
],
"requestedWalkTarget": [
-5.5999999046325684,
0.0,
-15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-5.5999999046325684,
0.0,
-15.0
]
},
"ghost_12x": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
-6.4000000953674316,
0.0,
-15.0
],
"hp": 35.0,
"movedLastTick": false,
"movementBlockedTicks": 0,
"navigationRetryCooldownIndex": 0,
"navigationRetryCooldownTicks": [
7,
11,
5,
4,
3,
10,
9,
8,
6,
2
],
"navigationRetryTicks": 0,
"pathWaypoints": [
],
"position": [
-6.4000000953674316,
0.0,
-15.0
],
"requestedWalkTarget": [
-6.4000000953674316,
0.0,
-15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.5,
"walkTarget": [
-6.4000000953674316,
0.0,
-15.0
]
} }
}, },
"scriptGlobals": {}, "scriptGlobals": {
},
"state": { "state": {
"activeNavigationIndex": 0, "activeNavigationIndex": 0,
"targetInteractNpcId": "", "targetInteractNpcId": "",
@@ -460,61 +796,23 @@
"rotation": 0.0, "rotation": 0.0,
"steeringAngle": 0.0, "steeringAngle": 0.0,
"velocity": 0.0, "velocity": 0.0,
"waypoints": [], "waypoints": [
],
"wheelAngle": 0.0 "wheelAngle": 0.0
} }
} }
}, },
"teleportZones": {}, "teleportZones": {
"triggerZones": {} },
"triggerZones": {
}
} }
}, },
"money": 5500, "money": 5500,
"playerHp": 200.0, "playerHp": 200.0,
"playerMaxHp": 200.0, "playerMaxHp": 200.0,
"players": { "players": {
"player2": { "player1": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [[
-6.8,
0.0,
13.0
]],
"position": [
-6.8,
0.0,
13.0
],
"requestedWalkTarget": [
-6.8,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
13.0
]
},
"player": {
"animationFrame": 0.0, "animationFrame": 0.0,
"attackTargetId": "", "attackTargetId": "",
"attack_cooldown": 0.0, "attack_cooldown": 0.0,
@@ -556,10 +854,140 @@
0.0, 0.0,
12.8 12.8
] ]
},
"player2": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-6.8,
0.0,
13.0
]
],
"position": [
-6.8,
0.0,
13.0
],
"requestedWalkTarget": [
-6.8,
0.0,
13.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
13.0
]
},
"player3": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-5.4,
0.0,
14.8
]
],
"position": [
-5.4,
0.0,
14.8
],
"requestedWalkTarget": [
-5.4,
0.0,
14.8
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-5.4,
0.0,
14.8
]
},
"player4": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": false,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-6.8,
0.0,
15.0
]
],
"position": [
-6.8,
0.0,
15.0
],
"requestedWalkTarget": [
-6.8,
0.0,
15.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.8,
0.0,
15.0
]
} }
}, },
"questJournal": { "questJournal": {
"quests": [] "quests": [
]
}, },
"savedAt": "2026-11-05 17:43", "savedAt": "2026-11-05 17:43",
"schemaVersion": 5, "schemaVersion": 5,
+53
View File
@@ -194,6 +194,59 @@ int main()
return fail("Hostile NPC did not return home after losing its target"); return fail("Hostile NPC did not return home after losing its target");
} }
auto blocker = std::make_shared<FRG::CharacterState>();
blocker->enabled = true;
blocker->hp = 50.f;
blocker->position = {0.8f, 0.f, 0.f};
state.npcs["blocker"] = blocker;
hostileNpc.position = Eigen::Vector3f::Zero();
hostileNpc.homePosition = {5.f, 0.f, 0.f};
hostileNpc.attackTargetId = "player";
hostileNpc.requestedWalkTarget = playerOne->position;
hostileNpc.walkTarget = playerOne->position;
hostileNpc.pathWaypoints = {playerOne->position};
hostileNpc.currentWaypointIndex = 0;
hostileNpc.movementBlockedTicks = 0;
playerOne->enabled = true;
playerOne->hp = 75.f;
playerOne->position = {4.f, 0.f, 0.f};
playerTwo->enabled = true;
playerTwo->hp = 75.f;
playerTwo->position = {2.f, 0.f, 0.f};
bool blockedRetryRequested = false;
for (int tick = 0; tick < 10 && !blockedRetryRequested; ++tick) {
const FRG::LocationSimulationEvents events =
state.update(100, combatPlayers, false);
for (const auto& event : events.characters) {
if (event.characterId == "npc" &&
event.events.forceNavigationReplan) {
blockedRetryRequested = true;
if (!state.replanCharacter(
"npc", combatPlayers,
event.events.blockingCharacterId)) {
return fail("Blocked NPC replan was rejected");
}
}
}
}
if (!blockedRetryRequested ||
hostileNpc.attackTargetId != "player2" ||
(hostileNpc.requestedWalkTarget - playerTwo->position).norm() >
FRG::CharacterState::kWalkThreshold) {
return fail("Blocked NPC did not switch to the nearest player");
}
playerOne->enabled = false;
playerTwo->enabled = false;
if (!state.replanCharacter("npc", combatPlayers, "blocker") ||
!hostileNpc.attackTargetId.empty() ||
hostileNpc.battle_state != 0 ||
(hostileNpc.requestedWalkTarget - hostileNpc.homePosition).norm() >
FRG::CharacterState::kWalkThreshold) {
return fail("Blocked NPC did not return home without an eligible target");
}
FRG::CampaignConfig battleZone; FRG::CampaignConfig battleZone;
battleZone.id = "battle_zone"; battleZone.id = "battle_zone";
battleZone.questsPath = "resources/config/quests_battle_zone.json"; battleZone.questsPath = "resources/config/quests_battle_zone.json";
+77 -33
View File
@@ -173,11 +173,84 @@ bool LocationGameState::replanCharacter(
{ {
CharacterState* character = findCharacter(id, players); CharacterState* character = findCharacter(id, players);
if (!character || !location) return false; if (!character || !location) return false;
if (!forcedObstacleId.empty() && !character->isPlayer &&
character->canAttack && character->hp > 0.f) {
const std::string selectedTarget =
selectNpcTarget(*character, players, false);
if (selectedTarget != character->attackTargetId) {
character->stopInPlace();
character->attackTargetId = selectedTarget;
character->battle_state = 0;
character->attack = 0;
}
if (!selectedTarget.empty()) {
const CharacterState* target = findCharacter(selectedTarget, players);
if (target) {
character->setTarget(
target->position, {}, *location, id,
collectPathObstacles(id, players, forcedObstacleId));
return true;
}
}
character->attackTargetId.clear();
character->battle_state = 0;
character->attack = 0;
const Eigen::Vector3f homeTarget(
character->homePosition.x(), 0.f, character->homePosition.z());
if ((homeTarget - character->position).norm() >
CharacterState::kWalkThreshold) {
character->setTarget(
homeTarget, {}, *location, id,
collectPathObstacles(id, players, forcedObstacleId));
}
return true;
}
character->forceReplan( character->forceReplan(
*location, id, collectPathObstacles(id, players, forcedObstacleId)); *location, id, collectPathObstacles(id, players, forcedObstacleId));
return true; return true;
} }
std::string LocationGameState::selectNpcTarget(
const CharacterState& npc,
const PlayerStateMap& players,
bool preserveValidLock) const
{
if (preserveValidLock) {
const auto locked = players.find(npc.attackTargetId);
if (locked != players.end() && locked->second &&
locked->second->enabled && locked->second->hp > 0.f)
return npc.attackTargetId;
}
std::vector<std::string> playerIds;
playerIds.reserve(players.size());
for (const auto& [id, player] : players)
if (player && player->enabled && player->hp > 0.f)
playerIds.push_back(id);
std::sort(playerIds.begin(), playerIds.end());
const float visibleRangeSquared = npc.visibleRange * npc.visibleRange;
std::string selectedTarget;
float nearestDistanceSquared = visibleRangeSquared;
for (const std::string& playerId : playerIds) {
const Eigen::Vector3f delta =
players.at(playerId)->position - npc.position;
const float distanceSquared =
delta.x() * delta.x() + delta.z() * delta.z();
if (distanceSquared <= nearestDistanceSquared &&
(selectedTarget.empty() ||
distanceSquared < nearestDistanceSquared ||
(distanceSquared == nearestDistanceSquared &&
playerId < selectedTarget))) {
selectedTarget = playerId;
nearestDistanceSquared = distanceSquared;
}
}
return selectedTarget;
}
void LocationGameState::startTaxi() void LocationGameState::startTaxi()
{ {
location->taxiCar.waypoints = location->taxiCarWaypoints; location->taxiCar.waypoints = location->taxiCarWaypoints;
@@ -321,13 +394,6 @@ LocationSimulationEvents LocationGameState::update(
if (player) playerIds.push_back(id); if (player) playerIds.push_back(id);
std::sort(playerIds.begin(), playerIds.end()); std::sort(playerIds.begin(), playerIds.end());
std::vector<std::string> eligiblePlayerIds;
for (const std::string& id : playerIds) {
const auto& player = players.at(id);
if (player->enabled && player->hp > 0.f)
eligiblePlayerIds.push_back(id);
}
std::vector<std::string> npcIds; std::vector<std::string> npcIds;
npcIds.reserve(npcs.size()); npcIds.reserve(npcs.size());
for (const auto& [id, npc] : npcs) for (const auto& [id, npc] : npcs)
@@ -343,30 +409,8 @@ LocationSimulationEvents LocationGameState::update(
continue; continue;
const std::string previousTarget = npc.attackTargetId; const std::string previousTarget = npc.attackTargetId;
const auto locked = players.find(previousTarget); const std::string selectedTarget =
const bool lockValid = selectNpcTarget(npc, players, true);
locked != players.end() && locked->second &&
locked->second->enabled && locked->second->hp > 0.f;
std::string selectedTarget = lockValid ? previousTarget : std::string();
if (!lockValid) {
float nearestDistanceSquared = npc.visibleRange * npc.visibleRange;
for (const std::string& playerId : eligiblePlayerIds) {
const Eigen::Vector3f delta =
players.at(playerId)->position - npc.position;
const float distanceSquared =
delta.x() * delta.x() + delta.z() * delta.z();
if (distanceSquared <= nearestDistanceSquared) {
if (selectedTarget.empty() ||
distanceSquared < nearestDistanceSquared ||
(distanceSquared == nearestDistanceSquared &&
playerId < selectedTarget)) {
selectedTarget = playerId;
nearestDistanceSquared = distanceSquared;
}
}
}
}
if (selectedTarget != previousTarget) { if (selectedTarget != previousTarget) {
npc.stopInPlace(); npc.stopInPlace();
@@ -474,7 +518,7 @@ LocationSimulationEvents LocationGameState::update(
source.events.attackDirection}); source.events.attackDirection});
} }
auto player = players.find("player"); auto player = players.find("player1");
if (!interactiveObjectsLocked && if (!interactiveObjectsLocked &&
!location->targetInteractiveObjectId.empty() && !location->targetInteractiveObjectId.empty() &&
player != players.end() && player->second && player->second->enabled) { player != players.end() && player->second && player->second->enabled) {
@@ -502,7 +546,7 @@ LocationSimulationEvents LocationGameState::update(
(player->second->position - npc->position).norm(); (player->second->position - npc->position).norm();
if (distance <= NPC_TALK_DISTANCE) { if (distance <= NPC_TALK_DISTANCE) {
player->second->stopInPlace(); player->second->stopInPlace();
npc->faceTargetId = "player"; npc->faceTargetId = "player1";
result.npcInteractionId = location->targetInteractNpcId; result.npcInteractionId = location->targetInteractNpcId;
location->targetInteractNpcId.clear(); location->targetInteractNpcId.clear();
} }
+4
View File
@@ -86,6 +86,10 @@ struct LocationGameState {
void load(const nlohmann::json& in); void load(const nlohmann::json& in);
private: private:
std::string selectNpcTarget(
const CharacterState& npc,
const PlayerStateMap& players,
bool preserveValidLock) const;
bool updateNpcCar(std::int64_t deltaMs); bool updateNpcCar(std::int64_t deltaMs);
}; };
+1 -1
View File
@@ -172,7 +172,7 @@ namespace FRG {
<< npcId << "'\n"; << npcId << "'\n";
return; return;
} }
loc->faceCharacter(*npc, "player"); loc->faceCharacter(*npc, "player1");
}); });
api.set_function("player_walk_to", api.set_function("player_walk_to",
+1 -1
View File
@@ -51,7 +51,7 @@ private:
GameSnapshot buildSnapshot() const; GameSnapshot buildSnapshot() const;
GameState state_; GameState state_;
std::string localPlayerId_ = "player"; std::string localPlayerId_ = "player1";
std::string resourceZipFile_; std::string resourceZipFile_;
std::vector<std::string> networkPlayerSlots_; std::vector<std::string> networkPlayerSlots_;
std::int64_t battleZoneTestBotElapsedMs_ = 0; std::int64_t battleZoneTestBotElapsedMs_ = 0;