mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-03-04 04:30:21 +00:00
feat: Add bot player system for PlanetSide population
Initial implementation of server-side bots that: - Spawn as real Player entities with full equipment - Move and broadcast position updates (10 tick/sec) - Take damage and die with backpack drops - Respawn after death - Combat system with accuracy model (adjustment vs recoil) Includes project documentation in bot-docs/ and Claude agent helpers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9646b3f99e
commit
2e5b5e0dbd
17 changed files with 3813 additions and 0 deletions
309
bot-docs/ARCHITECTURE.md
Normal file
309
bot-docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# PlanetSide Bots - Technical Architecture
|
||||
|
||||
## PSF-LoginServer Codebase Analysis
|
||||
|
||||
### Technology Stack
|
||||
- **Language**: Scala (99.5%)
|
||||
- **Actor Framework**: Akka (classic actors)
|
||||
- **Database**: PostgreSQL
|
||||
- **Build**: sbt
|
||||
|
||||
### Key Components Discovered
|
||||
|
||||
#### Entity Hierarchy
|
||||
```
|
||||
PlanetSideServerObject (base)
|
||||
├── Player
|
||||
│ ├── Vitality (health, armor)
|
||||
│ ├── FactionAffinity (TR/NC/VS)
|
||||
│ ├── Container (inventory)
|
||||
│ ├── ZoneAware (continent awareness)
|
||||
│ └── MountableEntity (vehicle seats)
|
||||
├── Vehicle
|
||||
├── Deployable
|
||||
└── FacilityTurret
|
||||
```
|
||||
|
||||
#### Actor System
|
||||
```
|
||||
SessionActor (per-connection)
|
||||
├── Handles network packets from client
|
||||
├── Manages player state
|
||||
├── Routes to subsystem handlers
|
||||
└── Mode-based behavior (Normal, Spectator, CSR)
|
||||
|
||||
PlayerControl (per-player entity)
|
||||
├── Akka Actor controlling Player object
|
||||
├── Handles damage, healing, death
|
||||
├── Equipment management
|
||||
├── Containable behavior
|
||||
└── Environment interaction
|
||||
```
|
||||
|
||||
#### Relevant Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `objects/Player.scala` | Player entity class |
|
||||
| `objects/avatar/Avatar.scala` | Persistent player data (certs, loadouts) |
|
||||
| `objects/avatar/PlayerControl.scala` | Player behavior Actor |
|
||||
| `actors/session/SessionActor.scala` | Network session handler |
|
||||
| `objects/SpawnPoint.scala` | Spawn location trait |
|
||||
| `objects/serverobject/turret/auto/AutomatedTurretBehavior.scala` | **AI reference implementation** |
|
||||
|
||||
---
|
||||
|
||||
## Existing AI Pattern: AutomatedTurretBehavior
|
||||
|
||||
The codebase already has AI! `AutomatedTurretBehavior` is a trait that provides:
|
||||
|
||||
### Target Management
|
||||
- `Targets` - list of known potential targets
|
||||
- `Target` - current active target
|
||||
- `AddTarget()` / `RemoveTarget()` - target list management
|
||||
- `Detected()` - check if target is already known
|
||||
|
||||
### Detection & Engagement
|
||||
- `Alert(target)` - new target spotted
|
||||
- `Unalert(target)` - target lost
|
||||
- `ConfirmShot(target)` - hit confirmation
|
||||
- Range-based detection (`ranges.trigger`, `ranges.escape`, `ranges.detection`)
|
||||
- Periodic validation sweeps (`detectionSweepTime`)
|
||||
|
||||
### Combat Logic
|
||||
- `engageNewDetectedTarget()` - begin shooting
|
||||
- `noLongerEngageDetectedTarget()` - stop shooting
|
||||
- `trySelectNewTarget()` - target selection algorithm
|
||||
- Target decay checks (destroyed, out of range, MIA)
|
||||
- Retaliation behavior (respond to being attacked)
|
||||
|
||||
### Timing/Cooldowns
|
||||
- `cooldowns.missedShot` - timeout for unconfirmed hits
|
||||
- `cooldowns.targetSelect` - delay before selecting new target
|
||||
- `cooldowns.targetElimination` - delay after killing target
|
||||
- Self-reported refire timer for continuous fire
|
||||
|
||||
### Key Insight
|
||||
> The turret AI works by being an Akka Actor that receives messages (`Alert`, `ConfirmShot`, `PeriodicCheck`) and maintains internal state. **Bots can follow the same pattern.**
|
||||
|
||||
---
|
||||
|
||||
## Proposed Bot Architecture
|
||||
|
||||
### Option A: Server-Side Native Bots (Preferred)
|
||||
|
||||
```
|
||||
BotActor (extends Actor)
|
||||
├── BotBehavior trait (similar to AutomatedTurretBehavior)
|
||||
│ ├── Target detection (vision cone, partial/full spot)
|
||||
│ ├── Combat engagement
|
||||
│ ├── V-menu communication
|
||||
│ └── Attitude/Vengeance system
|
||||
├── BotMovement trait
|
||||
│ ├── Pathfinding
|
||||
│ ├── ADAD strafing
|
||||
│ └── Retreat behavior
|
||||
├── BotObjective trait
|
||||
│ ├── Follow orders (attack/defend)
|
||||
│ ├── Base capture
|
||||
│ └── Help responses (VNG, VNH)
|
||||
└── Controls a Player object (no SessionActor needed)
|
||||
```
|
||||
|
||||
#### How It Would Work
|
||||
1. **BotManager** actor manages bot lifecycle
|
||||
- Spawns bots when population is low
|
||||
- Removes bots when real players join
|
||||
- Assigns bots to factions
|
||||
|
||||
2. **Bot entity** is a `Player` object
|
||||
- Has `Avatar` with certs, loadouts (predefined by class)
|
||||
- Has `PlayerControl` actor for damage/death handling
|
||||
- Has **new** `BotActor` for AI decision-making
|
||||
|
||||
3. **Bot appears to clients** as normal player
|
||||
- Spawns at SpawnPoints
|
||||
- Sends PlayerStateMessage updates
|
||||
- Fires weapons, takes damage, dies normally
|
||||
|
||||
#### Integration Points
|
||||
- `Zone.LivePlayers` - bots appear here
|
||||
- `Zone.AvatarEvents` - bots send/receive events
|
||||
- `PlayerStateMessage` - bots broadcast position/orientation
|
||||
- `ChatMsg` - bots send V-menu voice commands
|
||||
|
||||
### Option B: Bot-as-Client (Fallback)
|
||||
|
||||
External process connects to server as fake client, mimics player packets.
|
||||
|
||||
**Pros**: No server code changes needed initially
|
||||
**Cons**: Network overhead, harder to scale, more fragile
|
||||
|
||||
---
|
||||
|
||||
## Bot Class Implementation
|
||||
|
||||
Each bot class needs:
|
||||
|
||||
### Data Definition
|
||||
```scala
|
||||
case class BotClass(
|
||||
name: String,
|
||||
certifications: Set[Certification],
|
||||
loadout: Loadout,
|
||||
experienceLevel: BotExperience, // Newbie, Vet, Ace
|
||||
primaryRole: BotRole // Driver, Support, Hacker, AV, MAX, Vet, Ace
|
||||
)
|
||||
```
|
||||
|
||||
### Behavior Weights
|
||||
```scala
|
||||
trait BotPersonality {
|
||||
def aggressionLevel: Float // 0.0 = passive, 1.0 = aggressive
|
||||
def accuracyBase: Float // Base accuracy modifier
|
||||
def movementStyle: MovementStyle // Newbie (straight), Vet (ADAD), etc.
|
||||
def retreatThreshold: Float // HP percentage to retreat
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Communication System
|
||||
|
||||
### V-Menu Integration
|
||||
```scala
|
||||
// Bot sends voice command
|
||||
def sendVoiceCommand(cmd: VoiceCommand): Unit = {
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.SendResponse(botGUID, ChatMsg(ChatMessageType.CMT_VOICE, cmd.text))
|
||||
)
|
||||
}
|
||||
|
||||
// Bot responds to nearby voice commands
|
||||
def handleVoiceCommand(sender: Player, cmd: VoiceCommand): Unit = cmd match {
|
||||
case VNG if canBeGunner => respondAndAssist(sender)
|
||||
case VNH if canHack => respondAndAssist(sender)
|
||||
case VVV => evaluateHelpRequest(sender)
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Celebration Coordination
|
||||
```scala
|
||||
object CelebrationCoordinator {
|
||||
def onBaseCapture(zone: Zone, faction: PlanetSideEmpire): Unit = {
|
||||
val eligibleBots = zone.LivePlayers
|
||||
.filter(_.isBot)
|
||||
.filter(_.Faction == faction)
|
||||
.filter(_.isAlive)
|
||||
|
||||
val responderCount = Random.nextInt(5) + 1 // 1-6
|
||||
val responders = Random.shuffle(eligibleBots).take(responderCount)
|
||||
|
||||
responders.zipWithIndex.foreach { case (bot, i) =>
|
||||
val delay = Random.nextFloat() * 1.5f // 0-1.5 seconds
|
||||
scheduler.scheduleOnce(delay.seconds) {
|
||||
bot.sendVoiceCommand(randomCelebration())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spawn/Despawn Logic
|
||||
|
||||
### Dynamic Population Management
|
||||
```scala
|
||||
class BotPopulationManager(zone: Zone) {
|
||||
val targetBotsPerFaction = 100
|
||||
val minRealPlayersBeforeScaling = 10
|
||||
|
||||
def tick(): Unit = {
|
||||
PlanetSideEmpire.values.foreach { faction =>
|
||||
val realPlayers = zone.LivePlayers.count(p => !p.isBot && p.Faction == faction)
|
||||
val currentBots = zone.LivePlayers.count(p => p.isBot && p.Faction == faction)
|
||||
val targetBots = math.max(0, targetBotsPerFaction - realPlayers)
|
||||
|
||||
if (currentBots < targetBots) spawnBots(faction, targetBots - currentBots)
|
||||
if (currentBots > targetBots) despawnBots(faction, currentBots - targetBots)
|
||||
}
|
||||
}
|
||||
|
||||
def despawnBots(faction: PlanetSideEmpire, count: Int): Unit = {
|
||||
// Remove non-Ace bots first, Ace is last to go
|
||||
val bots = zone.LivePlayers
|
||||
.filter(p => p.isBot && p.Faction == faction)
|
||||
.sortBy(b => if (b.botClass == Ace) Int.MaxValue else 0)
|
||||
.take(count)
|
||||
|
||||
bots.foreach(gracefulLogout)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Proof of Concept Milestones
|
||||
|
||||
### Milestone 1: Static Bot
|
||||
- [ ] Create `BotActor` skeleton
|
||||
- [ ] Spawn a `Player` entity without `SessionActor`
|
||||
- [ ] Bot appears in zone, visible to clients
|
||||
- [ ] Bot stands still (no AI)
|
||||
|
||||
### Milestone 2: Moving Bot
|
||||
- [ ] Implement basic movement
|
||||
- [ ] Bot walks in a pattern
|
||||
- [ ] `PlayerStateMessage` broadcasts correctly
|
||||
|
||||
### Milestone 3: Reactive Bot
|
||||
- [ ] Detect nearby enemies (vision cone)
|
||||
- [ ] Turn to face target
|
||||
- [ ] Basic shooting (ChangeFireStateMessage)
|
||||
|
||||
### Milestone 4: Smart Bot
|
||||
- [ ] Target selection logic
|
||||
- [ ] Retreat on low HP
|
||||
- [ ] V-menu help requests
|
||||
|
||||
### Milestone 5: Team Bot
|
||||
- [ ] Follow orders from Ace
|
||||
- [ ] Respond to V-menu requests
|
||||
- [ ] Coordinated behavior
|
||||
|
||||
---
|
||||
|
||||
## Questions for PSForever Devs
|
||||
|
||||
1. **Player without SessionActor**: Is this currently possible? What breaks?
|
||||
2. **Bot flag**: Should we add `isBot: Boolean` to `Player` class?
|
||||
3. **GUID allocation**: How do we get GUIDs for bot entities?
|
||||
4. **Zone registration**: What's the proper way to add a player to a zone without client connection?
|
||||
5. **Existing NPC code**: Is there any other AI code beyond `AutomatedTurretBehavior`?
|
||||
|
||||
---
|
||||
|
||||
## File Structure (Proposed)
|
||||
|
||||
```
|
||||
src/main/scala/net/psforever/
|
||||
├── objects/
|
||||
│ └── bot/
|
||||
│ ├── Bot.scala # Bot entity (extends Player?)
|
||||
│ ├── BotClass.scala # Class definitions (Driver, Support, etc.)
|
||||
│ ├── BotLoadouts.scala # Predefined loadouts per class
|
||||
│ └── BotPersonality.scala # Behavior weights
|
||||
├── actors/
|
||||
│ └── bot/
|
||||
│ ├── BotActor.scala # Main bot AI actor
|
||||
│ ├── BotBehavior.scala # Combat/detection trait
|
||||
│ ├── BotMovement.scala # Movement trait
|
||||
│ ├── BotObjective.scala # Objective handling trait
|
||||
│ └── BotManager.scala # Population management
|
||||
└── services/
|
||||
└── bot/
|
||||
├── BotService.scala # Global bot coordination
|
||||
└── BotVoiceCoordinator.scala # V-menu coordination
|
||||
```
|
||||
399
bot-docs/CODEBASE_MAP.md
Normal file
399
bot-docs/CODEBASE_MAP.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# PSF-LoginServer Codebase Map
|
||||
|
||||
> **Purpose**: Quick reference for understanding the PlanetSide server emulator codebase.
|
||||
> This document maps key files, line numbers, and relationships for bot implementation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Concept | File | Key Lines | Notes |
|
||||
|---------|------|-----------|-------|
|
||||
| Player Entity | `objects/Player.scala` | 29-150 | Main player class |
|
||||
| Player Behavior | `objects/avatar/PlayerControl.scala` | Full file | Akka actor for damage/death/equipment |
|
||||
| Avatar Data | `objects/avatar/Avatar.scala` | 130-205 | Persistent character data (certs, loadouts) |
|
||||
| Session Handling | `actors/session/SessionActor.scala` | 99-400 | Network packet processing (NOT needed for bots) |
|
||||
| Zone Population | `objects/zones/ZonePopulationActor.scala` | 30-85 | Join/Spawn/Leave/Release flow |
|
||||
| GUID Registration | `objects/guid/GUIDTask.scala` | 185-204 | `registerAvatar()` for full registration |
|
||||
| Position Broadcast | `services/avatar/AvatarServiceMessage.scala` | 83-97 | `AvatarAction.PlayerState` |
|
||||
| Turret AI Reference | `objects/serverobject/turret/auto/AutomatedTurretBehavior.scala` | Full file | Example of existing AI pattern |
|
||||
|
||||
---
|
||||
|
||||
## Core Entity System
|
||||
|
||||
### Player.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/Player.scala`
|
||||
|
||||
```
|
||||
Line 29-41: class Player extends PlanetSideServerObject with many traits
|
||||
Line 54-70: Private state (armor, capacitor, exosuit, holsters, inventory)
|
||||
Line 72-78: Movement state (facingYawUpper, crouching, jumping, cloaked, afk)
|
||||
Line 123-133: Spawn() - resurrects a dead player
|
||||
Line 135-139: Die() - kills the player
|
||||
Line 141-147: Revive() - revives without full reset
|
||||
```
|
||||
|
||||
**Key Traits Mixed In**:
|
||||
- `Vitality` - health/damage system
|
||||
- `FactionAffinity` - TR/NC/VS
|
||||
- `Container` - inventory
|
||||
- `ZoneAware` - knows which continent it's on
|
||||
- `MountableEntity` - can sit in vehicles
|
||||
|
||||
### Avatar.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/avatar/Avatar.scala`
|
||||
|
||||
```
|
||||
Line 85-87: Factory method Avatar(charId, name, faction, sex, head, voice)
|
||||
Line 130-151: case class Avatar with all fields:
|
||||
- id: Int (DB row ID - could use negative for bots)
|
||||
- basic: BasicCharacterData (name, faction, sex, head, voice)
|
||||
- bep/cep: Experience points
|
||||
- certifications: Set[Certification]
|
||||
- implants, shortcuts, locker, deployables
|
||||
- loadouts, cooldowns, scorecard
|
||||
Line 155-156: br/cr: Battle Rank and Command Rank derived from BEP/CEP
|
||||
```
|
||||
|
||||
**Important**: `Avatar.id` is described as "unique identifier corresponding to a database table row index". For bots, we can use negative IDs to avoid collision.
|
||||
|
||||
### PlayerControl.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/avatar/PlayerControl.scala`
|
||||
|
||||
This is an **Akka Actor** that handles player behavior.
|
||||
|
||||
```
|
||||
Key behaviors it handles:
|
||||
- Damage intake and death
|
||||
- Healing (from equipment, implants)
|
||||
- Equipment changes
|
||||
- Environment interaction (water, lava, radiation)
|
||||
- Jamming effects
|
||||
```
|
||||
|
||||
**Critical**: When `Zone.Population.Spawn()` is called, it creates a `PlayerControl` actor for the player:
|
||||
```scala
|
||||
// From ZonePopulationActor.scala line 60-63
|
||||
player.Actor = context.actorOf(
|
||||
Props(classOf[PlayerControl], player, avatarActor),
|
||||
name = GetPlayerControlName(player, None)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zone & Population System
|
||||
|
||||
### Zone.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/zones/Zone.scala`
|
||||
|
||||
Key properties:
|
||||
- `LivePlayers` - list of alive players in zone
|
||||
- `Corpses` - list of dead player backpacks
|
||||
- `Population` - ActorRef to ZonePopulationActor
|
||||
- `AvatarEvents` - ActorRef to AvatarService for broadcasting
|
||||
- `GUID` - UniqueNumberOps for GUID allocation
|
||||
|
||||
### ZonePopulationActor.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala`
|
||||
|
||||
**This is critical for bot spawning.**
|
||||
|
||||
```
|
||||
Line 31-34: Zone.Population.Join(avatar)
|
||||
- Adds avatar.id to playerMap with None value
|
||||
- Starts player management systems if first player
|
||||
|
||||
Line 36-50: Zone.Population.Leave(avatar)
|
||||
- Removes from playerMap
|
||||
- Cleans up player position, block map
|
||||
|
||||
Line 52-71: Zone.Population.Spawn(avatar, player, avatarActor)
|
||||
- Associates player with avatar in playerMap
|
||||
- Sets player.Zone = zone
|
||||
- CREATES PlayerControl actor (line 60-63)
|
||||
- Adds to block map
|
||||
|
||||
Line 73-84: Zone.Population.Release(avatar)
|
||||
- Disassociates player from avatar
|
||||
- Used when player dies/respawns
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GUID System
|
||||
|
||||
### GUIDTask.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/guid/GUIDTask.scala`
|
||||
|
||||
```
|
||||
Line 72-73: registerObject() - basic GUID registration
|
||||
Line 102-107: registerTool() - register weapon + ammo boxes
|
||||
Line 125-130: registerEquipment() - dispatches to appropriate registrar
|
||||
Line 200-204: registerPlayer() - registers player + holsters + inventory
|
||||
|
||||
Usage:
|
||||
TaskWorkflow.execute(GUIDTask.registerPlayer(zone.GUID, player))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Broadcasting System
|
||||
|
||||
### AvatarServiceMessage.scala
|
||||
**Path**: `src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala`
|
||||
|
||||
```
|
||||
Line 20: AvatarServiceMessage(forChannel: String, actionMessage: AvatarAction.Action)
|
||||
|
||||
Line 83-97: AvatarAction.PlayerState - position/orientation broadcast
|
||||
Fields: player_guid, pos, vel, facingYaw, facingPitch,
|
||||
facingYawUpper, timestamp, is_crouching, is_jumping,
|
||||
jump_thrust, is_cloaked, spectator, weaponInHand
|
||||
```
|
||||
|
||||
**To broadcast bot position**:
|
||||
```scala
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlayerState(
|
||||
player.GUID,
|
||||
player.Position,
|
||||
Some(velocity),
|
||||
facingYaw, facingPitch, facingYawUpper,
|
||||
timestamp,
|
||||
is_crouching, is_jumping, jump_thrust,
|
||||
is_cloaked, spectator = false, weaponInHand
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Other Important AvatarActions
|
||||
```
|
||||
AvatarAction.LoadPlayer - Creates player on clients (spawn)
|
||||
AvatarAction.ObjectDelete - Removes player from clients (despawn)
|
||||
AvatarAction.ChangeFireState_Start/Stop - Weapon firing
|
||||
AvatarAction.Killed - Death notification
|
||||
AvatarAction.HitHint - Damage indicator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Existing AI Pattern
|
||||
|
||||
### AutomatedTurretBehavior.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala`
|
||||
|
||||
**NOT the behavior we want** (turrets are mechanical), but useful as a **technical reference** for:
|
||||
- Akka actor message patterns
|
||||
- Target tracking state management
|
||||
- Periodic checks via scheduler
|
||||
- Damage/retaliation responses
|
||||
|
||||
```
|
||||
Line 26-27: trait AutomatedTurretBehavior { _: Actor with DamageableEntity =>
|
||||
Shows how to mix behavior into an actor
|
||||
|
||||
Line 51-54: Timer pattern for periodic checks:
|
||||
context.system.scheduler.scheduleWithFixedDelay(...)
|
||||
|
||||
Line 145-154: Target list management (AddTarget, RemoveTarget, Detected)
|
||||
Line 245-258: Engagement flow (engageNewDetectedTarget)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session System (Reference Only)
|
||||
|
||||
Bots don't need SessionActor, but understanding it helps:
|
||||
|
||||
### SessionActor.scala
|
||||
**Path**: `src/main/scala/net/psforever/actors/session/SessionActor.scala`
|
||||
|
||||
```
|
||||
Line 99: class SessionActor - receives from middlewareActor (network)
|
||||
Line 115: def receive = startup (initial state)
|
||||
Line 168-171: parse() - handles PlanetSideGamePacket from client
|
||||
Line 372+: handleGamePkt() - dispatches packets to handlers
|
||||
```
|
||||
|
||||
**Key insight**: SessionActor's job is handling network I/O. Bots generate actions internally, so we don't need this.
|
||||
|
||||
### AvatarActor.scala
|
||||
**Path**: `src/main/scala/net/psforever/actors/session/AvatarActor.scala`
|
||||
|
||||
Handles avatar persistence, certification management, loadouts, etc.
|
||||
|
||||
```
|
||||
Line 70-88: Factory and commands
|
||||
Line 81: apply() creates the actor with sessionActor reference
|
||||
```
|
||||
|
||||
**For bots**: We need a stub `BotAvatarActor` that accepts the messages `PlayerControl` might send but doesn't persist anything.
|
||||
|
||||
---
|
||||
|
||||
## SpawnPoint System
|
||||
|
||||
### SpawnPoint.scala
|
||||
**Path**: `src/main/scala/net/psforever/objects/SpawnPoint.scala`
|
||||
|
||||
```
|
||||
Line 11-70: trait SpawnPoint
|
||||
- GUID, Position, Orientation
|
||||
- SpecificPoint(target) -> (Vector3, Vector3) for spawn pos/orient
|
||||
|
||||
Line 72-160: object SpawnPoint
|
||||
- Default, Tube, AMS, Gate spawn point calculations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Locations Summary
|
||||
|
||||
```
|
||||
src/main/scala/net/psforever/
|
||||
├── actors/
|
||||
│ ├── session/
|
||||
│ │ ├── SessionActor.scala # Network session (NOT for bots)
|
||||
│ │ ├── AvatarActor.scala # Avatar persistence
|
||||
│ │ └── support/
|
||||
│ │ ├── SessionData.scala # Session state
|
||||
│ │ └── ZoningOperations.scala # Spawn flow reference
|
||||
│ └── zone/
|
||||
│ └── ZoneActor.scala # Zone management
|
||||
├── objects/
|
||||
│ ├── Player.scala # Player entity
|
||||
│ ├── SpawnPoint.scala # Spawn locations
|
||||
│ ├── avatar/
|
||||
│ │ ├── Avatar.scala # Character data
|
||||
│ │ └── PlayerControl.scala # Player behavior actor
|
||||
│ ├── guid/
|
||||
│ │ └── GUIDTask.scala # GUID registration
|
||||
│ ├── serverobject/turret/auto/
|
||||
│ │ └── AutomatedTurretBehavior.scala # AI reference
|
||||
│ └── zones/
|
||||
│ ├── Zone.scala # Zone class
|
||||
│ └── ZonePopulationActor.scala # Population management
|
||||
└── services/
|
||||
└── avatar/
|
||||
├── AvatarService.scala # Broadcasting service
|
||||
└── AvatarServiceMessage.scala # Message definitions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Patterns
|
||||
|
||||
### Spawn a Player (Derived from Real Flow)
|
||||
```scala
|
||||
// 1. Create avatar
|
||||
val avatar = Avatar(botId, name, faction, sex, head, voice)
|
||||
|
||||
// 2. Create player
|
||||
val player = new Player(avatar)
|
||||
player.Position = spawnPosition
|
||||
player.Spawn()
|
||||
|
||||
// 3. Register GUIDs
|
||||
TaskWorkflow.execute(GUIDTask.registerPlayer(zone.GUID, player))
|
||||
|
||||
// 4. Join zone
|
||||
zone.Population ! Zone.Population.Join(avatar)
|
||||
|
||||
// 5. Spawn (creates PlayerControl)
|
||||
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
|
||||
|
||||
// 6. Broadcast existence
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
|
||||
```
|
||||
|
||||
### Broadcast Position
|
||||
```scala
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlayerState(player.GUID, pos, vel, yaw, pitch, yawUpper,
|
||||
timestamp, crouch, jump, thrust, cloak, false, weaponOut)
|
||||
)
|
||||
```
|
||||
|
||||
### Despawn a Player
|
||||
```scala
|
||||
zone.Population ! Zone.Population.Leave(avatar)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(guid, guid))
|
||||
TaskWorkflow.execute(GUIDTask.unregisterAvatar(zone.GUID, player))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bot Implementation - Lessons Learned
|
||||
|
||||
### Critical: Use `registerAvatar` NOT `registerPlayer`
|
||||
`PlayerControl` creates a `LockerContainerControl` actor in its constructor (line 74-80) which calls `PlanetSideServerObject.UniqueActorName(locker)`. This requires `locker.GUID` to be assigned.
|
||||
|
||||
- `registerPlayer()` skips locker registration
|
||||
- `registerAvatar()` includes locker registration
|
||||
- **Always use `registerAvatar()` for bots**
|
||||
|
||||
### Critical: Avatar IDs Must Be Positive
|
||||
Packet encoding uses 32-bit unsigned integers. Negative IDs cause:
|
||||
```
|
||||
ERROR: basic_appearance/a/unk6: -1 is less than minimum value 0
|
||||
```
|
||||
**Solution**: Use high positive IDs (900000+) to avoid collision with real DB player IDs.
|
||||
|
||||
### Critical: GUID Registration is Async
|
||||
`TaskWorkflow.execute()` returns a `Future[Any]`. You MUST wait for completion before:
|
||||
- Accessing `player.GUID`
|
||||
- Calling `Zone.Population.Spawn()`
|
||||
- Broadcasting `LoadPlayer`
|
||||
|
||||
```scala
|
||||
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
|
||||
case Success(_) =>
|
||||
// NOW safe to use player.GUID and spawn
|
||||
self ! CompleteSpawn(...)
|
||||
case Failure(ex) =>
|
||||
log.error(s"GUID registration failed: ${ex.getMessage}")
|
||||
}
|
||||
```
|
||||
|
||||
### AvatarActor Must Be Typed
|
||||
`Zone.Population.Spawn` expects `ActorRef[AvatarActor.Command]` (typed), not classic `ActorRef`.
|
||||
Use Akka typed actor system:
|
||||
```scala
|
||||
val typedSystem = context.system.toTyped
|
||||
val botAvatarActor: ActorRef[AvatarActor.Command] =
|
||||
typedSystem.systemActorOf(BotAvatarActor(), s"bot-avatar-$botId")
|
||||
```
|
||||
|
||||
### Bot Files Created
|
||||
```
|
||||
src/main/scala/net/psforever/actors/bot/
|
||||
├── BotAvatarActor.scala # Stub typed actor - absorbs AvatarActor messages
|
||||
└── BotManager.scala # Spawns/manages bots, handles async GUID flow
|
||||
```
|
||||
|
||||
### Working Spawn Flow (Verified)
|
||||
```scala
|
||||
// 1. Create avatar with HIGH POSITIVE ID
|
||||
val avatar = Avatar(900000, BasicCharacterData(name, faction, sex, head, voice))
|
||||
|
||||
// 2. Create player
|
||||
val player = new Player(avatar)
|
||||
player.Position = position
|
||||
player.Spawn()
|
||||
|
||||
// 3. Create typed stub avatar actor
|
||||
val botAvatarActor = typedSystem.systemActorOf(BotAvatarActor(), name)
|
||||
|
||||
// 4. Register GUIDs (ASYNC!) - includes locker
|
||||
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
|
||||
case Success(_) =>
|
||||
// 5. Join zone
|
||||
zone.Population ! Zone.Population.Join(avatar)
|
||||
// 6. Spawn (creates PlayerControl)
|
||||
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
|
||||
// 7. Broadcast to clients
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
|
||||
}
|
||||
```
|
||||
123
bot-docs/DEV_SETUP.md
Normal file
123
bot-docs/DEV_SETUP.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Development Environment Setup
|
||||
|
||||
> Verified working on: Ubuntu 22.04 (Linux Mint 21.3 Virginia)
|
||||
|
||||
## Requirements
|
||||
|
||||
| Component | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Java | JDK 8 (1.8.0_xxx) | **Must be Java 8** - other versions fail |
|
||||
| sbt | 1.8.2+ | Scala build tool |
|
||||
| PostgreSQL | 14+ | Database |
|
||||
|
||||
## Installation (Ubuntu/Mint)
|
||||
|
||||
### 1. Java 8
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y openjdk-8-jdk
|
||||
|
||||
# Set as default
|
||||
sudo update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
|
||||
sudo update-alternatives --set javac /usr/lib/jvm/java-8-openjdk-amd64/bin/javac
|
||||
|
||||
# Verify
|
||||
java -version
|
||||
# Should show: openjdk version "1.8.0_xxx"
|
||||
```
|
||||
|
||||
### 2. sbt
|
||||
```bash
|
||||
echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list
|
||||
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x99E82A75642AC823" | sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install -y sbt
|
||||
```
|
||||
|
||||
### 3. PostgreSQL
|
||||
```bash
|
||||
sudo apt install -y postgresql postgresql-contrib
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
|
||||
# Create database and user
|
||||
sudo -u postgres psql -c "CREATE USER psforever WITH PASSWORD 'psforever';"
|
||||
sudo -u postgres psql -c "CREATE DATABASE psforever OWNER psforever;"
|
||||
```
|
||||
|
||||
## Building & Running
|
||||
|
||||
### First Compile
|
||||
```bash
|
||||
cd PSF-LoginServer
|
||||
sbt compile
|
||||
```
|
||||
- Takes ~2-3 minutes on first run (downloads dependencies)
|
||||
- Expect some warnings (unused imports, non-exhaustive matches) - these are fine
|
||||
|
||||
### Run Server
|
||||
```bash
|
||||
sbt "server/run"
|
||||
```
|
||||
|
||||
**Expected behavior:**
|
||||
- Terminal stays open with live debug output
|
||||
- Shows player connections, actions, etc. in real-time
|
||||
- Does NOT return to prompt - this is correct
|
||||
|
||||
**Expected startup output:**
|
||||
```
|
||||
PSForever Server - PSForever Project
|
||||
Login server is running on 127.0.0.1:51000
|
||||
```
|
||||
|
||||
### Known Startup Errors (IGNORE)
|
||||
```
|
||||
ERROR akka.actor.SupervisorStrategy - null
|
||||
java.lang.NullPointerException: null
|
||||
at net.psforever.objects.guid.GUIDTask$RegisterObjectTask.action(GUIDTask.scala:44)
|
||||
...
|
||||
at net.psforever.objects.serverobject.shuttle.OrbitalShuttlePadControl...
|
||||
```
|
||||
These are **HART shuttle (orbital pad) initialization errors** - known issue, doesn't affect gameplay or our bot work.
|
||||
|
||||
## Server Ports
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| 51000 | TCP | Login server |
|
||||
| 51001 | UDP | World server |
|
||||
|
||||
## Connecting a Client
|
||||
|
||||
1. Use PlanetSide client version 3.15.84.0
|
||||
2. Point client at server IP (127.0.0.1 for local)
|
||||
3. Create account on first login (auto-created)
|
||||
|
||||
## Running in Background
|
||||
|
||||
```bash
|
||||
# Start in background
|
||||
nohup sbt "server/run" > server.log 2>&1 &
|
||||
|
||||
# View logs
|
||||
tail -f server.log
|
||||
|
||||
# Find and kill
|
||||
ps aux | grep sbt
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "authentication method 10 not supported"
|
||||
PostgreSQL version too old or password encryption mismatch. Upgrade PostgreSQL or check `postgresql.conf`.
|
||||
|
||||
### Java version errors
|
||||
Make sure `java -version` shows 1.8.x. Use `update-alternatives` to switch if needed.
|
||||
|
||||
### Port already in use
|
||||
```bash
|
||||
sudo lsof -i :51000
|
||||
# Kill the process using the port
|
||||
```
|
||||
414
bot-docs/GAME_FEEL.md
Normal file
414
bot-docs/GAME_FEEL.md
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
# PlanetSide Bot Behavior & Game Feel
|
||||
|
||||
## Vision System
|
||||
|
||||
**Field of View**: 60 degrees horizontal and vertical
|
||||
- Adjustable variable, expected final range: 60-90 degrees
|
||||
- Bots cannot see beyond this cone - must turn to acquire targets
|
||||
- Creates realistic "blind spots" and flanking opportunities for players
|
||||
|
||||
### Spotting Mechanics
|
||||
|
||||
**Full Spot**: Clear visual on target, can identify and engage
|
||||
- Time to acquire scales with distance:
|
||||
- Close range: Very fast (nearly instant)
|
||||
- Long range: Takes longer to confirm target
|
||||
|
||||
**Partial Spot**: Know something is there but can't fully engage
|
||||
- Target went behind cover (saw them go there)
|
||||
- See tracers coming from a direction (investigate)
|
||||
- Heard gunfire/explosions from location
|
||||
- **Behavior**: Move towards OR around to get better angle
|
||||
|
||||
---
|
||||
|
||||
## Core Decision Logic
|
||||
|
||||
```
|
||||
SPOTTED TARGET?
|
||||
├── Partial spot?
|
||||
│ └── Move towards target OR move around target (reposition for better view)
|
||||
├── Full spot?
|
||||
│ └── ATTACK or COMPLETE OBJECTIVE
|
||||
│ (enter vehicle, unlock door, hack terminal, etc.)
|
||||
└── Cannot proceed?
|
||||
└── REQUEST HELP (V-menu)
|
||||
└── State what you need (VNG, VNH, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## V-Menu Communication System
|
||||
|
||||
Bots use the in-game quick voice system to communicate, creating authentic battlefield chatter.
|
||||
|
||||
### Voice Commands Used
|
||||
|
||||
**Requests & Responses**
|
||||
| Command | Meaning | Use Case |
|
||||
|---------|---------|----------|
|
||||
| VVV | "HELP!" | General distress, need assistance |
|
||||
| VNG | "Need Gunner" | Vehicle waiting for gunner before proceeding |
|
||||
| VNH | "Need Hacker" | Door locked, can't proceed, need hacker |
|
||||
| VVY | "Yes" | Acknowledging help request, on my way |
|
||||
| VVB | Taunt (variant B) | Vengeance kill, celebration |
|
||||
| VVZ | Taunt (variant Z) | Vengeance kill, celebration |
|
||||
|
||||
**Warnings (Intel Sharing)**
|
||||
| Command | Meaning | Use Case |
|
||||
|---------|---------|----------|
|
||||
| VWA | "Warning: Aircraft!" | Enemy air incoming, triggers AA MAX spawns |
|
||||
| VWV | "Warning: Vehicles!" | Enemy armor incoming, triggers AV MAX spawns |
|
||||
| VWT | "Warning: Troops!" | Enemy infantry push, triggers AI MAX spawns |
|
||||
|
||||
### Help Request Flow
|
||||
1. Bot encounters obstacle it cannot handle alone
|
||||
2. Bot broadcasts appropriate V-menu request
|
||||
3. Capable bots in range evaluate:
|
||||
- Can I help with this? (Do I have the cert/ability?)
|
||||
- Can I path to them? (Pull `/loc` of requester, check route)
|
||||
4. If yes to both:
|
||||
- Respond with `VVY`
|
||||
- Begin movement to assist
|
||||
5. **Result**: Organic teamwork feel, tactical advantage, immersion
|
||||
|
||||
---
|
||||
|
||||
## Bot Classes
|
||||
|
||||
PlanetSide 1 has no rigid classes, but bots need defined roles for sanity. Each class is a **pre-built loadout** with associated behavior logic.
|
||||
|
||||
### Class Definitions
|
||||
|
||||
| Class | Role | Key Abilities | Notes |
|
||||
|-------|------|---------------|-------|
|
||||
| **Driver** | Vehicle operation | Drive/pilot vehicles | Low priority on VNG (see below) |
|
||||
| **Support** | Heal & Repair | Medical applicator, BANK/Nano repair | LOVES gunning (can heal vehicle) |
|
||||
| **Hacker** | Infiltration & Capture | REK, door unlocking, base/tower/vehicle hacking | Critical for objective play |
|
||||
| **AV** | Anti-Vehicle | Decimator, Lancer, Phoenix, Striker? | Tank hunters |
|
||||
| **MAX** | Heavy Exosuit | Faction MAX suits (AI/AV/AA variants) | Walking tanks |
|
||||
| **Vet** | Versatile Veteran | HA + AV + Basic Hacking + Basic Support | Jack of all trades |
|
||||
| **Ace** | Empire Leader | Best at everything + Command authority | ONE per empire, special role |
|
||||
|
||||
### Vet Class Details
|
||||
- Can do *most* things but slower/weaker than specialists
|
||||
- Won't get stuck at locked doors (can hack, just slower)
|
||||
- Won't panic at incoming tank (has AV, can fight back)
|
||||
- Field-flexible, self-sufficient
|
||||
- Good baseline "competent soldier" behavior
|
||||
|
||||
### Ace Class Details
|
||||
- One per empire (TR Ace, NC Ace, VS Ace)
|
||||
- **Last bot to logout** as real players join
|
||||
- Uses **Command Chat** (CR3+ channel) for tactical orders
|
||||
- Makes strategic decisions for bot team:
|
||||
- Where to attack
|
||||
- Where to defend
|
||||
- Force distribution
|
||||
- **CRITICAL**: Defers to real players
|
||||
- Won't overstep human commanders
|
||||
- Offers advice if no player is commanding
|
||||
- Lets players make the calls when they want to
|
||||
|
||||
### MAX Class Details
|
||||
**Situational Loadout Switching** based on intel:
|
||||
- **Indoors** (last death location or teammate intel): AI MAX
|
||||
- **VWV received** (Warning: Vehicles): AV MAX
|
||||
- **VWA received** (Warning: Aircraft): AA MAX
|
||||
- **VWT received** (Warning: Troops) + outdoors: Context-dependent
|
||||
|
||||
This creates reactive, intelligent heavy support that adapts to battlefield needs.
|
||||
|
||||
### Driver & Gunner Dynamics
|
||||
- **Anyone** can be a gunner - it's not role-restricted
|
||||
- **Drivers** respond to VNG but low priority (they want to drive)
|
||||
- **Support** actively seeks gunner seats - can heal vehicle while gunning
|
||||
- **Drivers** can also heal their own vehicle but teamwork = faster repairs
|
||||
- Vehicles wait for gunner before proceeding (VNG call) for effectiveness
|
||||
|
||||
---
|
||||
|
||||
## Attitude & Vengeance System
|
||||
|
||||
Gives Vet+ bots personality and memorable behavior.
|
||||
|
||||
### Attitude Stat
|
||||
- Internal emotional state of the bot
|
||||
- Affects decision-making and aggression
|
||||
|
||||
### Vengeance Mechanic
|
||||
1. **Death Memory**: Vet remembers where they died and who killed them
|
||||
2. **Revenge Decision**: Based on attitude, may decide to hunt killer
|
||||
3. **Constraints**:
|
||||
- Must be reasonable range (same base spawn area)
|
||||
- No cross-continent adventures
|
||||
4. **On Successful Revenge**:
|
||||
- Taunt victim (VVB or VVZ)
|
||||
- Attitude decreases (calms down)
|
||||
5. **On Repeated Death (no revenge)**:
|
||||
- Attitude increases ("rage" building)
|
||||
- **Rage Effects**:
|
||||
- Increased accuracy
|
||||
- Increased aggression
|
||||
6. **Rage Reset Conditions**:
|
||||
- Achieve vengeance
|
||||
- Get a 3-kill streak
|
||||
- Participate in base capture
|
||||
|
||||
---
|
||||
|
||||
## Movement Patterns
|
||||
|
||||
Movement varies by experience level and empire. Critical for not looking like a bot.
|
||||
|
||||
### By Experience Level
|
||||
|
||||
**New Players / Basic Bots**
|
||||
- Run mostly straight at target
|
||||
- Held strafe to maintain firing angle (not ADAD, just constant drift)
|
||||
- Minimal evasion, focused on keeping crosshairs on target
|
||||
|
||||
**Vets & Aces**
|
||||
- Heavy ADAD strafing (left-right-left-right)
|
||||
- Combined with crouch spam during firefights
|
||||
- Much harder to hit, more "tryhard" movement
|
||||
|
||||
### Empire-Specific Movement
|
||||
- **NC**: Benefits from closing distance (shotguns) - more aggressive advance
|
||||
- **TR/VS**: TBD - circle back when adding faction identity
|
||||
|
||||
### What NOT To Do
|
||||
- **Almost no jumping** - PS players don't bunny hop
|
||||
- Exception: MAXes might jump to dodge AV rockets (ADVANCED - maybe skip for v1)
|
||||
- Robotic pathing, perfect angles, inhuman reaction times
|
||||
|
||||
### Tuning Note
|
||||
> "I don't know what [bot movement] would feel like yet, we've never had bots before. I will help dial this in with many many play tests."
|
||||
|
||||
Movement feel will require extensive playtesting iteration.
|
||||
|
||||
---
|
||||
|
||||
## Retreat & Self-Preservation
|
||||
|
||||
### When Bots Retreat
|
||||
|
||||
| Class | Retreat Trigger |
|
||||
|-------|-----------------|
|
||||
| **Vets** | Low HP |
|
||||
| **Drivers** | Low vehicle HP (save the vehicle!) |
|
||||
| **Everyone** | Out of ammo (not just reloading - actually empty) |
|
||||
|
||||
### Order Context Matters
|
||||
|
||||
**If orders = DEFEND:**
|
||||
- Stay at position no matter what (unless need supplies)
|
||||
- Quick runs to spawn room / nearest terminal for ammo/resupply
|
||||
- Return immediately to defensive position
|
||||
|
||||
**If orders = ATTACK:**
|
||||
- More flexible retreat for self-preservation
|
||||
- Regroup and push again
|
||||
|
||||
### What Retreat Looks Like
|
||||
- Not a panicked sprint
|
||||
- Backing away while facing threat when possible
|
||||
- Finding cover, then breaking line of sight
|
||||
|
||||
---
|
||||
|
||||
## Combat & Accuracy System
|
||||
|
||||
Bot shooting should feel natural, not robotic. Two opposing forces create a "sweet spot" in sustained fire.
|
||||
|
||||
### Target Recognition Time
|
||||
|
||||
Before engaging, bots must **recognize** their target. Time to recognize scales with distance:
|
||||
|
||||
| Distance | Recognition Time | Notes |
|
||||
|----------|-----------------|-------|
|
||||
| **Close** (< 15m) | Near instant | Light them up immediately |
|
||||
| **Medium** (15-50m) | 0.3-0.8 sec | Brief pause, then engage |
|
||||
| **Far** (50m+) | 1-2 sec | Takes time to confirm target |
|
||||
| **Obscured** | Investigate | Move to get better angle (defer to navmesh) |
|
||||
|
||||
### Accuracy System (Two Forces)
|
||||
|
||||
**Force 1: Adjustment (Accuracy improves)**
|
||||
- First shots are inaccurate (bot is "dialing in" aim)
|
||||
- Accuracy increases with each shot as bot finds the target
|
||||
- Close targets = higher base accuracy, faster adjustment
|
||||
- Far targets = lower base accuracy, slower adjustment
|
||||
|
||||
**Force 2: Recoil (Spread worsens)**
|
||||
- Each shot adds recoil
|
||||
- Recoil accumulates, making spread worse over time
|
||||
- Creates "diminishing returns" on sustained fire
|
||||
|
||||
### The Sweet Spot
|
||||
|
||||
```
|
||||
Accuracy over time during sustained fire:
|
||||
|
||||
↑ Accuracy
|
||||
│ ╭──────╮
|
||||
│ ╱ ╲
|
||||
│ ╱ ╲
|
||||
│ ╱ ╲ ← recoil takes over
|
||||
│ ╱
|
||||
│╱ ← adjusting aim
|
||||
└────────────────────────→ Time/Shots
|
||||
First Middle Late
|
||||
shots shots shots
|
||||
(miss) (HIT!) (spray)
|
||||
```
|
||||
|
||||
**Result by distance:**
|
||||
- **Close range**: High base accuracy + fast adjustment = nearly all shots hit
|
||||
- **Medium range**: Sweet spot matters - middle of burst is most dangerous
|
||||
- **Long range**: May only land a few hits early (lucky) or middle (adjusted)
|
||||
|
||||
### Class Modifiers (Future)
|
||||
|
||||
| Class | Accuracy Modifier | Notes |
|
||||
|-------|------------------|-------|
|
||||
| Basic/Newbie | 1.0x (baseline) | Average accuracy |
|
||||
| Vet | 1.3x accuracy, 0.8x recoil | Much better aim |
|
||||
| Ace | 1.5x accuracy, 0.7x recoil | Best of the best |
|
||||
| Support | 0.9x accuracy | Focused on healing |
|
||||
| MAX | 0.8x accuracy, 1.2x recoil | Big guns, more spray |
|
||||
|
||||
### Practical Implications
|
||||
|
||||
1. **Players should win fair fights** - bots aren't aimbots
|
||||
2. **Getting close is dangerous** - bots don't miss at point blank
|
||||
3. **Distance = safety** - but not immunity (lucky shots happen)
|
||||
4. **Burst fire beats spray** - short controlled bursts reset recoil
|
||||
5. **Flanking works** - recognition time gives advantage
|
||||
|
||||
---
|
||||
|
||||
## The Chaos Factor
|
||||
|
||||
> "Spam? Spam. Be it bullets, grenades, the voice menus it's all about the chaotic spam. It's a war unlike the world has ever recreated."
|
||||
|
||||
### What Makes PS Fights Feel Like PS
|
||||
- **Bullet spam**: Walls of tracers, suppressive fire is real
|
||||
- **Grenade spam**: Explosions everywhere
|
||||
- **Voice spam**: V-menu callouts constantly firing
|
||||
- **Scale**: Hundreds of participants, not 32v32
|
||||
|
||||
### Bot Contribution to Chaos
|
||||
- Bots should ADD to the chaos, not feel sterile
|
||||
- Coordinated V-menu chatter (see calibration below)
|
||||
- Miss shots (don't be laser accurate)
|
||||
- React to nearby explosions/deaths
|
||||
- Fire at enemies even when hit chance is low (suppression)
|
||||
|
||||
---
|
||||
|
||||
## Damage Reaction
|
||||
|
||||
How bots respond to taking damage depends on context.
|
||||
|
||||
### In Combat (Already Engaged)
|
||||
- **Direct bullet hit from new direction**: May turn toward new threat OR smart-switch targets
|
||||
- **Explosions/grenades**: Ignore direction (damage comes from everywhere) - stay on current target
|
||||
- **Decision**: Stick with current target vs switch based on threat assessment
|
||||
|
||||
### Out of Combat (Doing Task)
|
||||
Example: Repairing a vehicle, then gets shot
|
||||
|
||||
1. **PANIC** - immediate state change
|
||||
2. **Run to cover** - most likely the vehicle they were repairing
|
||||
3. **Swap to weapon** while running
|
||||
- Drivers: Have sidearm ready (fast)
|
||||
- Support: Must swap from repair tool (slower, panicked weapon swap animation)
|
||||
4. **Engage threat** once in cover with weapon out
|
||||
|
||||
### Key Feel
|
||||
- The "panic run while swapping weapons" is authentic PS behavior
|
||||
- Not a clean tactical response - messy, human reaction
|
||||
- Repair slot = weapon slot means class differences in response time
|
||||
|
||||
---
|
||||
|
||||
## V-Menu Calibration
|
||||
|
||||
Balancing authentic chatter vs annoying spam.
|
||||
|
||||
### Always Fire (Functional)
|
||||
| Type | When | Purpose |
|
||||
|------|------|---------|
|
||||
| **Needs** (VNG, VNH, VVV) | When actually needed | Gameplay function |
|
||||
| **Warnings** (VWA, VWV, VWT) | When threat spotted | Intel sharing |
|
||||
| **Acknowledgment** (VVY) | When responding to request | Coordination |
|
||||
|
||||
### Rare/Conditional
|
||||
| Type | When | Frequency |
|
||||
|------|------|-----------|
|
||||
| **Taunts** (VVB, VVZ) | Vengeance kills, domination | Low |
|
||||
| **Celebrations** (VVF, VVE) | Exceptional events only | Very controlled |
|
||||
|
||||
### Celebration Events
|
||||
Triggers for VVF ("Fantastic!") and VVE ("Excellent!"):
|
||||
- Pesky vehicle destroyed
|
||||
- Killed someone with 10+ kill streak
|
||||
- Base capture
|
||||
|
||||
### Coordinated Celebration System
|
||||
**Problem**: 50 bots all saying "VVE!" at once = annoying
|
||||
**Solution**: Backend coordination
|
||||
|
||||
```
|
||||
EVENT: Base captured
|
||||
|
||||
1. RNG determines responder count (1-6 bots)
|
||||
2. Eligible bots (alive, in range) "claim" slots via backend
|
||||
3. Each responder gets RNG delay timer
|
||||
4. Staggered, natural response
|
||||
|
||||
EXAMPLE TIMELINE:
|
||||
0.01s - Bot 1: "VVE" (Excellent!)
|
||||
0.30s - Bot 2: "VVF" (Fantastic!)
|
||||
1.20s - Bot 3: "VVF"
|
||||
1.30s - Bot 4 (Vet): "VVB VVF" ("You can't beat me." + "Outstanding!")
|
||||
```
|
||||
|
||||
### Vet/Ace Flavor
|
||||
- Vets and Aces can do **combo callouts** (taunt + celebration)
|
||||
- Adds personality, shows experience
|
||||
- E.g., "You can't beat me." followed by "Outstanding!"
|
||||
|
||||
---
|
||||
|
||||
## Open Questions / Needs Expansion
|
||||
|
||||
### Resolved
|
||||
- [x] "Partial spot" vs full spot - distance + time based, also tracers/intel
|
||||
- [x] Driver class VNG response - low priority, anyone can gun
|
||||
- [x] MAX variants - switches based on intel (VWA/VWV/VWT, indoor/outdoor)
|
||||
- [x] Movement patterns - ADAD for vets, straight-run for newbies
|
||||
- [x] Retreat behavior - HP, ammo, vehicle HP triggers
|
||||
|
||||
### Deferred (Future Phases)
|
||||
- [ ] **FACTION TACTICS**: TR vs NC vs VS combat style differences (post-v1)
|
||||
- [ ] **MAX jumping to dodge AV**: Advanced behavior, maybe skip
|
||||
|
||||
### Resolved (Round 2)
|
||||
- [x] **REACTION TO DAMAGE**: Context-dependent (see Damage Reaction section)
|
||||
- [x] **V-menu calibration**: Coordinated celebration system (see V-menu section)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Server Integration Questions
|
||||
- Can bots trigger V-menu voice commands through server?
|
||||
- Is Command Chat accessible for bot messages?
|
||||
- Is CR (Command Rank) system implemented in emulator?
|
||||
|
||||
### Technical Considerations
|
||||
- `/loc` command - server-side coordinate access for pathfinding
|
||||
- Death tracking - need to log killer + location per bot
|
||||
- Attitude persistence - per-session or reset on logout?
|
||||
313
bot-docs/HANDOFF.md
Normal file
313
bot-docs/HANDOFF.md
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# PlanetSide Bots - Handoff Document
|
||||
|
||||
> **Read this first** if you're a new Claude instance or agent picking up this project.
|
||||
|
||||
## What Is This Project?
|
||||
|
||||
We're adding **bot players** to PlanetSide 1 via the PSForever server emulator. The goal is to make the game world feel alive even with low real player population.
|
||||
|
||||
### The Vision
|
||||
- Hundreds of bots per server (scaling down as real players join)
|
||||
- Bots that feel like real players, not mechanical turrets
|
||||
- V-menu chatter, teamwork, attitude/personality systems
|
||||
- Infantry first, vehicles later
|
||||
|
||||
### Key Stakeholder
|
||||
The user is a **veteran PlanetSide player** (decade+ experience) and serves as the expert on "game feel". They have detailed notes on how bots should behave. Always defer to them on gameplay questions.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
PSF-LoginServer/
|
||||
├── bot-docs/ ← Bot project documentation
|
||||
│ ├── HANDOFF.md ← YOU ARE HERE - Start here
|
||||
│ ├── PROJECT.md ← Overview, goals, status
|
||||
│ ├── GAME_FEEL.md ← Behavioral spec (vision, movement, V-menu, classes, etc.)
|
||||
│ ├── ARCHITECTURE.md ← Technical design decisions
|
||||
│ ├── CODEBASE_MAP.md ← Key files with line numbers (CRITICAL REFERENCE)
|
||||
│ ├── POC_PLAN.md ← Phased implementation milestones
|
||||
│ ├── DEV_SETUP.md ← Development environment setup
|
||||
│ └── SKETCHES/ ← Conceptual code (not production)
|
||||
│ ├── BotActor_v1.scala
|
||||
│ └── BotSpawner_v1.scala
|
||||
├── src/main/scala/net/psforever/
|
||||
│ └── actors/bot/ ← Bot implementation code
|
||||
│ ├── BotManager.scala
|
||||
│ └── BotAvatarActor.scala
|
||||
└── ... ← Rest of PSForever server codebase
|
||||
```
|
||||
|
||||
### Read Order for New Instance
|
||||
1. **HANDOFF.md** (this file) - Context and decisions
|
||||
2. **CODEBASE_MAP.md** - Where things are in the code
|
||||
3. **GAME_FEEL.md** - How bots should behave
|
||||
4. **SKETCHES/** - Conceptual code to understand approach
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions Made
|
||||
|
||||
### 1. BotActor Approach (NOT SessionActor)
|
||||
We decided to create a new `BotActor` rather than emulating `SessionActor` because:
|
||||
- SessionActor's main job is handling network packets from clients
|
||||
- Bots have no client, so no incoming packets
|
||||
- BotActor can be optimized specifically for AI needs
|
||||
- Less overhead, better scalability
|
||||
|
||||
### 2. Server-Side Native Bots
|
||||
Bots are first-class server entities, not fake clients connecting from outside.
|
||||
- Bots use existing `Player` and `PlayerControl` code
|
||||
- No network overhead
|
||||
- Actions broadcast through same services as real players
|
||||
|
||||
### 3. Bot Identity
|
||||
Bot characters use:
|
||||
- **High positive IDs** (900000+) to avoid collision with real DB players
|
||||
- **No special characters in names** - use `xxBOTxxName` format (not `[BOT]Name`)
|
||||
- Predefined loadouts per class
|
||||
|
||||
### 4. Tick Rate
|
||||
- 10-20 FPS for bot AI decisions (not 60)
|
||||
- Can reduce further for bots far from real players
|
||||
- Server load monitoring will inform final values
|
||||
|
||||
---
|
||||
|
||||
## Bot Architecture Summary
|
||||
|
||||
```
|
||||
BotManager (per zone)
|
||||
├── Monitors real player population
|
||||
├── Spawns/despawns bots to maintain target count
|
||||
└── Manages one Ace per faction (last to logout)
|
||||
|
||||
BotActor (per bot)
|
||||
├── 10 FPS tick loop
|
||||
├── Vision cone detection (60-90 degrees)
|
||||
├── Decision making (attack, retreat, objective, help)
|
||||
├── Movement execution
|
||||
├── State broadcasting
|
||||
└── Controls a Player entity
|
||||
|
||||
BotAvatarActor (stub)
|
||||
└── Minimal - just accepts messages PlayerControl sends
|
||||
|
||||
Player + PlayerControl (existing code)
|
||||
├── Player entity with Vitality, inventory, etc.
|
||||
└── PlayerControl handles damage/death (already exists)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spawn Flow (Critical Path)
|
||||
|
||||
```scala
|
||||
// 1. Create avatar (HIGH POSITIVE ID - 900000+)
|
||||
val avatar = Avatar(900000 + botNum, BasicCharacterData(name, faction, sex, head, voice))
|
||||
|
||||
// 2. Create player entity
|
||||
val player = new Player(avatar)
|
||||
player.Position = spawnPosition
|
||||
player.Spawn()
|
||||
|
||||
// 3. Create typed stub avatar actor (MUST be typed ActorRef)
|
||||
val typedSystem = context.system.toTyped
|
||||
val botAvatarActor: ActorRef[AvatarActor.Command] =
|
||||
typedSystem.systemActorOf(BotAvatarActor(), s"bot-avatar-$botId")
|
||||
|
||||
// 4. Register GUIDs (ASYNC - use registerAvatar, NOT registerPlayer!)
|
||||
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
|
||||
case Success(_) =>
|
||||
// 5. Join zone population
|
||||
zone.Population ! Zone.Population.Join(avatar)
|
||||
// 6. Spawn player (creates PlayerControl actor)
|
||||
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
|
||||
// 7. Broadcast to clients
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
|
||||
case Failure(ex) =>
|
||||
log.error(s"Failed: ${ex.getMessage}")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Broadcasting (How Bots Appear to Real Players)
|
||||
|
||||
Bots broadcast their state via the same channels real players use:
|
||||
|
||||
```scala
|
||||
// Position updates (10-20 times per second)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlayerState(guid, pos, vel, facing, crouch, jump, ...)
|
||||
)
|
||||
|
||||
// Weapon fire
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.ChangeFireState_Start(playerGuid, weaponGuid)
|
||||
)
|
||||
|
||||
// V-menu voice commands
|
||||
// (Need to investigate ChatMsg format)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bot Classes (From User's Notes)
|
||||
|
||||
| Class | Role | Behavior |
|
||||
|-------|------|----------|
|
||||
| Driver | Vehicle operation | Low priority for gunning, wants to drive |
|
||||
| Support | Heal & Repair | LOVES gunning (can heal vehicle), prioritizes healing |
|
||||
| Hacker | Infiltration | Door unlocking, base capture |
|
||||
| AV | Anti-Vehicle | Tank hunters |
|
||||
| MAX | Heavy Exosuit | AI/AV/AA variants, switches based on intel |
|
||||
| Vet | Versatile | Jack of all trades, ADAD movement, vengeance system |
|
||||
| Ace | Empire Leader | ONE per empire, last to logout, uses Command Chat |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### COMPLETED (POC Working!)
|
||||
- [x] BotAvatarActor stub (typed actor, absorbs messages)
|
||||
- [x] BotManager (spawns bots, handles async GUID flow)
|
||||
- [x] `!bot` chat command for testing
|
||||
- [x] Bot spawns visible to players
|
||||
- [x] Bot takes damage from players (PlayerControl works!)
|
||||
- [x] Bot movement (10 tick/sec, random wandering, PlayerState broadcasts)
|
||||
- [x] Multiple bots work independently
|
||||
|
||||
### KNOWN ISSUES (Expected for POC)
|
||||
- No backpack on death (likely because bot has no items/loadout)
|
||||
- Walks through walls (no collision detection)
|
||||
- No Z-height adjustment (melts into stairs, terrain)
|
||||
- Walk speed slightly fast for animation (4 units/sec, try 3)
|
||||
|
||||
### NOT YET IMPLEMENTED
|
||||
- [ ] Bot loadout/equipment
|
||||
- [ ] Bot shooting (weapon fire broadcasts)
|
||||
- [ ] Bot death/respawn cycle
|
||||
- [ ] Terrain following (Z height from map data)
|
||||
- [ ] Collision avoidance
|
||||
- [ ] Pathfinding
|
||||
- [ ] V-menu voice command sending
|
||||
- [ ] Celebration coordination system
|
||||
- [ ] Vengeance/attitude system
|
||||
- [ ] Population scaling (spawn/despawn based on real players)
|
||||
|
||||
---
|
||||
|
||||
## Resolved Questions (Through Trial & Error)
|
||||
|
||||
1. **Avatar IDs**: Must be POSITIVE. Negative IDs break packet encoding (32-bit unsigned). Use 900000+.
|
||||
2. **PlayerControl + stub AvatarActor**: WORKS! BotAvatarActor just absorbs messages, PlayerControl functions normally.
|
||||
3. **GUID Registration**: Must use `registerAvatar()` not `registerPlayer()` - locker needs GUID for PlayerControl init.
|
||||
|
||||
## Open Questions for PSForever Devs
|
||||
|
||||
1. **Bot identification**: Should we add `isBot: Boolean` to Player?
|
||||
2. **Spawn point access**: Best way to find valid faction spawn locations?
|
||||
|
||||
---
|
||||
|
||||
## Spawn Location Logic (For Future Implementation)
|
||||
|
||||
When a player dies, they choose from up to 3 spawn options (all must be friendly-owned):
|
||||
|
||||
1. **Nearest Tower** - Guard towers scattered across continents
|
||||
2. **Nearest AMS** - Advanced Mobile Station (deployable spawn vehicle)
|
||||
3. **Nearest Base/Facility** - The main base spawn tubes
|
||||
|
||||
**Special Cases:**
|
||||
- **Sanctuary** (home2, home1, home3): Only one spawn option (the sanctuary itself)
|
||||
- **Warpgates**: Can spawn at warpgates your faction owns
|
||||
|
||||
**For Bots:**
|
||||
- Currently: Respawn at original spawn position (simple)
|
||||
- Future: Query zone for valid faction spawn points, pick nearest or random
|
||||
- Need to find: `SpawnPoint` instances, check faction ownership, calculate distance
|
||||
|
||||
**Code References to Investigate:**
|
||||
- `SpawnPoint.scala` - Spawn point trait and calculations
|
||||
- `Zone.spawnGroups` - Map of building -> spawn points
|
||||
- `Building.Faction` - Check ownership for spawn eligibility
|
||||
|
||||
---
|
||||
|
||||
## Important Behavioral Notes
|
||||
|
||||
### From GAME_FEEL.md (Key Points)
|
||||
- **Vision**: 60-90 degree FOV, distance-based spotting time
|
||||
- **Movement**: Newbies run straight, Vets do ADAD + crouch spam
|
||||
- **Retreat**: Vets retreat at low HP, Drivers save vehicles, everyone retreats when out of ammo
|
||||
- **V-menu**: "Needs" always fire, celebrations are coordinated (1-6 responders, staggered timing)
|
||||
- **Vengeance**: Vets remember who killed them, may seek revenge, taunt on success
|
||||
- **Panic**: Non-combat bots (repairing) panic when shot, run to cover while swapping weapons
|
||||
- **Chaos**: The goal is authentic battlefield chaos - spam bullets, grenades, voice commands
|
||||
|
||||
### What NOT to Do
|
||||
- No bunny hopping (almost no one jumps except MAX dodging AV)
|
||||
- No laser accuracy (bots should miss)
|
||||
- No instant reactions (bots need reaction time)
|
||||
- No perfect coordination (spread out, don't stand on same pixel)
|
||||
- No mechanical behavior (turrets are terrible, we want human-like chaos)
|
||||
|
||||
---
|
||||
|
||||
## Development Environment
|
||||
|
||||
```bash
|
||||
# Server codebase
|
||||
cd PSF-LoginServer
|
||||
sbt compile # Compile
|
||||
sbt server/run # Run server
|
||||
|
||||
# Requirements
|
||||
- Java 8 JDK
|
||||
- sbt (Scala Build Tool)
|
||||
- PostgreSQL 10+
|
||||
- PlanetSide client (version 3.15.84.0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context for Future Agents
|
||||
|
||||
If you're a **coding agent** tasked with implementation:
|
||||
1. Read `CODEBASE_MAP.md` for file locations
|
||||
2. Read `SKETCHES/` for conceptual starting points
|
||||
3. Start with Milestone 1: spawn a static bot that appears in-game
|
||||
4. Test empirically - compile, run, connect with client, observe
|
||||
|
||||
If you're a **research agent** investigating something:
|
||||
1. The PSF-LoginServer codebase is in `./PSF-LoginServer/`
|
||||
2. All server code is under `src/main/scala/net/psforever/`
|
||||
3. Tests are under `src/test/scala/`
|
||||
4. Use grep/glob to find patterns
|
||||
|
||||
If the user says **"update the docs"**:
|
||||
1. Update the relevant .md files
|
||||
2. Keep `CODEBASE_MAP.md` current with any new discoveries
|
||||
3. Add line numbers when referencing code
|
||||
|
||||
---
|
||||
|
||||
## Last Session Summary
|
||||
|
||||
**Date**: 2024 (context creation date)
|
||||
|
||||
**Accomplished**:
|
||||
- Analyzed PSF-LoginServer codebase architecture
|
||||
- Mapped spawn/broadcast/GUID flows
|
||||
- Created behavioral spec from user's notes
|
||||
- Sketched BotActor and BotSpawner concepts
|
||||
- Documented key files with line numbers
|
||||
|
||||
**Next Steps**:
|
||||
- Set up dev environment
|
||||
- Start Phase 1: spawn a static bot
|
||||
- OR wait for dev team answers on open questions
|
||||
291
bot-docs/POC_PLAN.md
Normal file
291
bot-docs/POC_PLAN.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
# PlanetSide Bots - Proof of Concept Plan
|
||||
|
||||
## Goal
|
||||
Get a single bot to spawn, be visible to clients, and perform basic actions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Research & Setup (DONE)
|
||||
|
||||
- [x] Clone PSF-LoginServer
|
||||
- [x] Analyze codebase architecture
|
||||
- [x] Identify Player entity system
|
||||
- [x] Find existing AI patterns (AutomatedTurretBehavior)
|
||||
- [x] Document integration points
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: "Hello World" Bot
|
||||
|
||||
### Objective
|
||||
Spawn a bot entity that appears in the game world as a player character.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1.1 Understand Player Creation Flow
|
||||
- [ ] Trace how a real player is created when they log in
|
||||
- [ ] Find `Player` instantiation code
|
||||
- [ ] Find zone registration code (`Zone.AddPlayer` or similar)
|
||||
- [ ] Find GUID allocation for new entities
|
||||
|
||||
#### 1.2 Create Minimal Bot Infrastructure
|
||||
- [ ] Create `src/main/scala/net/psforever/objects/bot/` directory
|
||||
- [ ] Create `Bot.scala` - wrapper or extension of Player
|
||||
- [ ] Create `BotActor.scala` - minimal actor (just keeps bot alive)
|
||||
|
||||
#### 1.3 Bot Spawning
|
||||
- [ ] Find how to spawn a player at a SpawnPoint without client
|
||||
- [ ] Allocate GUID for bot
|
||||
- [ ] Register bot in zone
|
||||
- [ ] Broadcast bot's existence to other players
|
||||
|
||||
#### 1.4 Validation
|
||||
- [ ] Connect real client to server
|
||||
- [ ] Verify bot appears as a player character
|
||||
- [ ] Bot should be standing still at spawn location
|
||||
|
||||
### Questions to Answer
|
||||
- Can we create a Player without Avatar from database?
|
||||
- Do we need to fake an Account/Character entry?
|
||||
- What minimum packets must be sent to make bot visible?
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Moving Bot
|
||||
|
||||
### Objective
|
||||
Bot moves through the world and other players can see it moving.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 2.1 PlayerStateMessage Broadcasting
|
||||
- [ ] Understand how player position updates are broadcast
|
||||
- [ ] Find the packet: `PlayerStateMessage`
|
||||
- [ ] Implement position update loop in BotActor
|
||||
|
||||
#### 2.2 Simple Movement
|
||||
- [ ] Bot walks forward
|
||||
- [ ] Bot turns (change orientation)
|
||||
- [ ] Movement speed matches infantry walking
|
||||
|
||||
#### 2.3 Patrol Behavior
|
||||
- [ ] Bot walks between two points
|
||||
- [ ] Bot stops, turns, walks back
|
||||
|
||||
### Success Criteria
|
||||
- Real player sees bot walking around
|
||||
- Bot movement is smooth (not teleporting)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Combat Bot
|
||||
|
||||
### Objective
|
||||
Bot can detect enemies and shoot at them.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 3.1 Vision System
|
||||
- [ ] Implement FOV cone check (60-90 degrees)
|
||||
- [ ] Detect players in range
|
||||
- [ ] Filter by faction (don't shoot friendlies)
|
||||
|
||||
#### 3.2 Target Tracking
|
||||
- [ ] Store current target reference
|
||||
- [ ] Turn to face target (orientation updates)
|
||||
- [ ] Lose target when out of range/sight
|
||||
|
||||
#### 3.3 Shooting
|
||||
- [ ] Find weapon fire packets (`ChangeFireStateMessage_Start`, `WeaponFireMessage`)
|
||||
- [ ] Implement firing at target
|
||||
- [ ] Basic accuracy (not perfect, not terrible)
|
||||
|
||||
#### 3.4 Taking Damage
|
||||
- [ ] Bot receives damage normally (via PlayerControl)
|
||||
- [ ] Bot dies when HP reaches 0
|
||||
- [ ] Bot respawns after death
|
||||
|
||||
### Success Criteria
|
||||
- Bot shoots at enemy players
|
||||
- Bot can be killed
|
||||
- Bot respawns
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Smart Bot
|
||||
|
||||
### Objective
|
||||
Bot makes tactical decisions.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 4.1 Health-Based Retreat
|
||||
- [ ] Track bot's HP
|
||||
- [ ] Retreat when HP below threshold
|
||||
- [ ] Find cover or run toward spawn
|
||||
|
||||
#### 4.2 Ammunition Management
|
||||
- [ ] Track ammo count
|
||||
- [ ] Retreat to resupply when empty
|
||||
|
||||
#### 4.3 V-Menu Communication
|
||||
- [ ] Bot sends VVV when needs help
|
||||
- [ ] Bot sends VNG when in vehicle without gunner
|
||||
- [ ] Proper chat message format
|
||||
|
||||
### Success Criteria
|
||||
- Bot retreats when hurt
|
||||
- Bot calls for help appropriately
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Bot Classes
|
||||
|
||||
### Objective
|
||||
Different bot types with different behaviors.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 5.1 Class Definitions
|
||||
- [ ] Define loadouts for each class (Driver, Support, Hacker, AV, MAX)
|
||||
- [ ] Create certification sets per class
|
||||
- [ ] Assign behavior weights per class
|
||||
|
||||
#### 5.2 Class-Specific Behavior
|
||||
- [ ] Support: Prioritize healing/repairing
|
||||
- [ ] Hacker: Prioritize hacking doors/objectives
|
||||
- [ ] AV: Prioritize anti-vehicle combat
|
||||
- [ ] MAX: Heavy combat behavior
|
||||
|
||||
#### 5.3 Vet & Ace Classes
|
||||
- [ ] Vet: Multi-role capability
|
||||
- [ ] Ace: Command behavior, last to logout
|
||||
|
||||
### Success Criteria
|
||||
- Multiple bot types visible
|
||||
- Each type behaves differently
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Population Management
|
||||
|
||||
### Objective
|
||||
Bots scale with player population.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 6.1 BotManager
|
||||
- [ ] Track real player count per faction
|
||||
- [ ] Calculate target bot count
|
||||
- [ ] Spawn/despawn bots to maintain balance
|
||||
|
||||
#### 6.2 Graceful Despawn
|
||||
- [ ] Non-Ace bots leave first
|
||||
- [ ] Ace leaves last
|
||||
- [ ] Bots "log out" naturally (not poof)
|
||||
|
||||
#### 6.3 Faction Balance
|
||||
- [ ] Equal bots per faction
|
||||
- [ ] Adjust for real player imbalance?
|
||||
|
||||
### Success Criteria
|
||||
- Server maintains ~100 bots per faction when empty
|
||||
- Bots reduce as real players join
|
||||
- Ace is last bot standing per faction
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Team Coordination
|
||||
|
||||
### Objective
|
||||
Bots work together and follow orders.
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 7.1 Order System
|
||||
- [ ] Ace issues attack/defend orders
|
||||
- [ ] Bots receive and follow orders
|
||||
- [ ] Order priority system
|
||||
|
||||
#### 7.2 Help Response
|
||||
- [ ] Bots respond to VNG/VNH/VVV
|
||||
- [ ] Check if can help
|
||||
- [ ] Navigate to requester
|
||||
|
||||
#### 7.3 Celebration Coordination
|
||||
- [ ] Coordinate V-menu celebrations
|
||||
- [ ] Staggered timing
|
||||
- [ ] Limited responders (1-6)
|
||||
|
||||
### Success Criteria
|
||||
- Bots attack/defend as ordered
|
||||
- Bots help each other
|
||||
- Celebrations feel natural
|
||||
|
||||
---
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
- Java 8 JDK
|
||||
- sbt (Scala Build Tool)
|
||||
- PostgreSQL 10+
|
||||
- PlanetSide client (version 3.15.84.0)
|
||||
|
||||
### Build & Run
|
||||
```bash
|
||||
cd PSF-LoginServer
|
||||
sbt compile
|
||||
sbt server/run
|
||||
```
|
||||
|
||||
### Testing Approach
|
||||
1. Run server locally
|
||||
2. Connect with PS client
|
||||
3. Observe bot behavior
|
||||
4. Iterate
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Player without SessionActor breaks things | Medium | High | May need dummy SessionActor |
|
||||
| GUID allocation conflicts | Medium | High | Coordinate with existing GUID system |
|
||||
| Performance with 300+ bots | Medium | Medium | Profile early, optimize as needed |
|
||||
| Network overhead for bot updates | Low | Medium | Batch updates, reduce frequency |
|
||||
| Client can't render many players | Low | High | Test with max players early |
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Phase 1 Success
|
||||
- 1 bot visible in game world
|
||||
|
||||
### Phase 3 Success
|
||||
- Bot engages in combat
|
||||
|
||||
### Phase 5 Success
|
||||
- Multiple bot classes working
|
||||
|
||||
### Phase 7 Success (MVP)
|
||||
- 100+ bots per faction
|
||||
- Dynamic population scaling
|
||||
- Team coordination
|
||||
- "Feels like PlanetSide"
|
||||
|
||||
---
|
||||
|
||||
## Estimated Complexity
|
||||
|
||||
| Phase | Complexity | Notes |
|
||||
|-------|------------|-------|
|
||||
| 1 | Medium | Unknown integration points |
|
||||
| 2 | Low | Straightforward once Phase 1 works |
|
||||
| 3 | Medium | Combat systems are complex |
|
||||
| 4 | Low | Building on Phase 3 |
|
||||
| 5 | Medium | Many classes to implement |
|
||||
| 6 | Low | Straightforward management |
|
||||
| 7 | High | Coordination is complex |
|
||||
145
bot-docs/PROJECT.md
Normal file
145
bot-docs/PROJECT.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# PlanetSide Bots Project
|
||||
|
||||
## Overview
|
||||
Bringing bot support to PlanetSide through the PSForever server emulator project.
|
||||
|
||||
## Resources
|
||||
- **Server Emulator**: [PSF-LoginServer](https://github.com/psforever/PSF-LoginServer) (Scala)
|
||||
- **Wiki Reference**: https://www.psforever.net/PlanetSide/
|
||||
- **Community**: PSForever dev team and community
|
||||
|
||||
---
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
> **Important**: Keep documentation updated for context handoff between sessions.
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `HANDOFF.md` | **Start here** - Context for new Claude instances |
|
||||
| `PROJECT.md` | This file - Overview and status |
|
||||
| `GAME_FEEL.md` | Behavioral spec from user's notes |
|
||||
| `ARCHITECTURE.md` | Technical design decisions |
|
||||
| `CODEBASE_MAP.md` | Key files with line numbers |
|
||||
| `POC_PLAN.md` | Implementation milestones |
|
||||
| `DEV_SETUP.md` | Dev environment setup (verified working) |
|
||||
| `SKETCHES/` | Conceptual code (not production) |
|
||||
|
||||
### Documentation Practices
|
||||
- When discovering new code patterns, update `CODEBASE_MAP.md` with file paths and line numbers
|
||||
- Link related concepts (e.g., "PlayerControl handles damage, see line 50-80")
|
||||
- Write for a fresh Claude instance that has no prior context
|
||||
- Keep `HANDOFF.md` current with latest decisions and next steps
|
||||
|
||||
---
|
||||
|
||||
## Game Context (PlanetSide 1)
|
||||
- **Genre**: MMOFPS (Massively Multiplayer Online First Person Shooter)
|
||||
- **Factions**: Terran Republic (TR), New Conglomerate (NC), Vanu Sovereignty (VS)
|
||||
- **Scale**: Designed for hundreds to thousands of concurrent players
|
||||
- **Core Loop**: Territory control via base capture through lattice-linked continents
|
||||
- **Emulator Status**: ~98% complete - virtually indistinguishable from original to veteran players
|
||||
|
||||
### Key Gameplay Elements
|
||||
- **Combined Arms**: Infantry, ground vehicles, aircraft all operating together
|
||||
- **Armor Types**: Standard, Reinforced, Agile, Infiltration suits
|
||||
- **Certification System**: Players unlock equipment/vehicles via certs
|
||||
- **Base Capture**: Lattice-based progression, facility benefits cascade through links
|
||||
- **Continents**: 10 landmass continents + underground caverns
|
||||
|
||||
### Vehicle Categories
|
||||
- **Air**: Reaver, Mosquito, Liberator, Galaxy
|
||||
- **Ground**: Lightning, Magrider, Vanguard, Harasser, ATV, ANT, Sunderer
|
||||
- **Specialized**: BattleFrame Robotics (BFRs)
|
||||
|
||||
---
|
||||
|
||||
## Project Goals
|
||||
|
||||
### Primary Goal
|
||||
**Population bots** - Make the world feel alive even with low real player counts
|
||||
- Target: Hundreds of bots capable of running simultaneously
|
||||
- **Dynamic scaling**: Bots log out as real players join each faction
|
||||
- Bots should participate in the war naturally (capture bases, fight, move with purpose)
|
||||
|
||||
### Scope Phases
|
||||
1. **Phase 1**: Infantry bots only
|
||||
2. **Phase 2+**: Ground vehicles, aircraft, advanced behaviors (TBD)
|
||||
|
||||
### Intelligence Level
|
||||
- Start realistic, iterate based on what's achievable
|
||||
- Goal: Bots that don't break immersion, feel like "bad but trying" players
|
||||
|
||||
---
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### Decided: BotActor (Server-Side Native)
|
||||
After analyzing the codebase, we chose to create a new `BotActor` rather than emulating `SessionActor`:
|
||||
- SessionActor handles network I/O from clients - bots have no client
|
||||
- BotActor makes AI decisions internally, broadcasts via zone services
|
||||
- Less overhead, better scalability, full optimization control
|
||||
|
||||
### Architecture Overview
|
||||
```
|
||||
BotManager (per zone)
|
||||
└── BotActor (per bot)
|
||||
└── Controls Player entity
|
||||
└── PlayerControl (existing) handles damage/death
|
||||
```
|
||||
|
||||
### Key Integration Points
|
||||
- `Zone.Population` - Join/Spawn/Leave for bots
|
||||
- `zone.AvatarEvents` - Broadcast position/actions
|
||||
- `GUIDTask.registerPlayer()` - GUID allocation
|
||||
- `PlayerControl` - Handles damage/death (reused)
|
||||
|
||||
See `CODEBASE_MAP.md` for file locations and line numbers.
|
||||
|
||||
### Dev Team Support
|
||||
- Team wants this feature but bandwidth-limited
|
||||
- Will assist with major blockers
|
||||
- Full support expected as project nears completion
|
||||
- **Project Lead**: Community member (us!) taking point
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
**Phase**: POC Complete - Bots Spawn and Move!
|
||||
|
||||
### Completed
|
||||
- [x] Clone PSF-LoginServer codebase
|
||||
- [x] Analyze codebase architecture
|
||||
- [x] Map spawn/broadcast/GUID flows
|
||||
- [x] Document behavioral spec from user's notes
|
||||
- [x] Design bot architecture
|
||||
- [x] Create conceptual code sketches
|
||||
- [x] Build handoff documentation
|
||||
- [x] Set up dev environment (Java 8, sbt, PostgreSQL)
|
||||
- [x] Verify baseline compile and server run
|
||||
- [x] **Phase 1: Spawn a static bot** - WORKING!
|
||||
- [x] **Phase 2: Bot moves and broadcasts** - WORKING!
|
||||
|
||||
### Resolved Questions (Through Trial & Error)
|
||||
- Avatar IDs must be POSITIVE (900000+), not negative
|
||||
- PlayerControl works perfectly with stub BotAvatarActor
|
||||
- Must use `registerAvatar()` not `registerPlayer()` (locker needs GUID)
|
||||
|
||||
### Next Steps
|
||||
- [ ] Bot loadout/equipment (no backpack on death without items)
|
||||
- [ ] Phase 3: Bot detects enemies and shoots
|
||||
- [ ] Death/respawn cycle
|
||||
- [ ] Terrain following (Z height)
|
||||
|
||||
---
|
||||
|
||||
## Faction Behavior & Game Feel
|
||||
*See `GAME_FEEL.md` for detailed behavioral spec including:*
|
||||
- Vision system (60-90 degree FOV, partial/full spotting)
|
||||
- V-menu communication system
|
||||
- Bot classes (Driver, Support, Hacker, AV, MAX, Vet, Ace)
|
||||
- Movement patterns (newbie vs veteran)
|
||||
- Retreat behaviors
|
||||
- Attitude and vengeance system
|
||||
- Chaos factor
|
||||
367
bot-docs/SKETCHES/BotActor_v1.scala
Normal file
367
bot-docs/SKETCHES/BotActor_v1.scala
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
// SKETCH - NOT PRODUCTION CODE
|
||||
// This is a conceptual exploration of what BotActor might look like
|
||||
|
||||
package net.psforever.actors.bot
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* BotActor - Controls a single bot entity
|
||||
*
|
||||
* Unlike SessionActor (which handles network packets from a client),
|
||||
* BotActor makes decisions internally and broadcasts state to other players.
|
||||
*
|
||||
* Key differences from SessionActor:
|
||||
* - No middlewareActor (no network connection)
|
||||
* - No incoming packets to process
|
||||
* - Generates actions based on AI logic
|
||||
* - Still broadcasts via zone.AvatarEvents (same as real players)
|
||||
*/
|
||||
class BotActor(
|
||||
player: Player,
|
||||
avatar: Avatar,
|
||||
zone: Zone,
|
||||
botClass: BotClass,
|
||||
botPersonality: BotPersonality
|
||||
) extends Actor {
|
||||
|
||||
// Tick timer - how often bot makes decisions and broadcasts state
|
||||
private var tickTimer: Cancellable = _
|
||||
|
||||
// Timestamp counter for PlayerStateMessage
|
||||
private var timestamp: Int = 0
|
||||
|
||||
// Current AI state
|
||||
private var currentTarget: Option[Player] = None
|
||||
private var currentObjective: Option[BotObjective] = None
|
||||
private var attitude: Float = 0.5f // 0 = calm, 1 = raging
|
||||
|
||||
// Death memory for vengeance system
|
||||
private var lastDeathLocation: Option[Vector3] = None
|
||||
private var lastKiller: Option[PlanetSideGUID] = None
|
||||
|
||||
override def preStart(): Unit = {
|
||||
// Start the tick loop
|
||||
// 10 FPS = 100ms, could go lower for distant bots
|
||||
tickTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
initialDelay = 100.millis,
|
||||
delay = 100.millis, // 10 ticks per second
|
||||
receiver = self,
|
||||
message = BotActor.Tick
|
||||
)
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
tickTimer.cancel()
|
||||
}
|
||||
|
||||
def receive: Receive = {
|
||||
case BotActor.Tick =>
|
||||
tick()
|
||||
|
||||
case BotActor.TakeDamage(amount, source, sourcePosition) =>
|
||||
handleDamage(amount, source, sourcePosition)
|
||||
|
||||
case BotActor.Die(killer) =>
|
||||
handleDeath(killer)
|
||||
|
||||
case BotActor.Respawn(spawnPoint) =>
|
||||
handleRespawn(spawnPoint)
|
||||
|
||||
case BotActor.ReceiveOrder(order) =>
|
||||
handleOrder(order)
|
||||
|
||||
case BotActor.HelpRequest(requester, helpType, location) =>
|
||||
handleHelpRequest(requester, helpType, location)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main AI tick - called every 100ms (10 FPS)
|
||||
*/
|
||||
private def tick(): Unit = {
|
||||
if (!player.isAlive) return
|
||||
|
||||
timestamp = (timestamp + 1) % 65536
|
||||
|
||||
// 1. Perception - what can we see?
|
||||
val visibleTargets = detectTargets()
|
||||
|
||||
// 2. Decision - what should we do?
|
||||
val action = decideAction(visibleTargets)
|
||||
|
||||
// 3. Execute - do the thing
|
||||
executeAction(action)
|
||||
|
||||
// 4. Broadcast - tell everyone where we are
|
||||
broadcastState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect targets within vision cone
|
||||
*/
|
||||
private def detectTargets(): Seq[Player] = {
|
||||
val myPos = player.Position
|
||||
val myFacing = player.Orientation.z
|
||||
val fovHalf = botPersonality.fovDegrees / 2
|
||||
val maxRange = botPersonality.detectionRange
|
||||
|
||||
zone.LivePlayers
|
||||
.filter(p => p != player)
|
||||
.filter(p => p.Faction != player.Faction) // enemies only
|
||||
.filter(p => p.isAlive)
|
||||
.filter { target =>
|
||||
val targetPos = target.Position
|
||||
val distance = Vector3.Distance(myPos, targetPos)
|
||||
val angle = calculateAngle(myPos, targetPos, myFacing)
|
||||
|
||||
distance <= maxRange && math.abs(angle) <= fovHalf
|
||||
}
|
||||
.toSeq
|
||||
.sortBy(t => Vector3.DistanceSquared(myPos, t.Position))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide what action to take based on current state and perception
|
||||
*/
|
||||
private def decideAction(visibleTargets: Seq[Player]): BotAction = {
|
||||
// Check health - should we retreat?
|
||||
if (player.Health < player.MaxHealth * botPersonality.retreatThreshold) {
|
||||
return BotAction.Retreat
|
||||
}
|
||||
|
||||
// Check ammo - need resupply?
|
||||
if (isOutOfAmmo()) {
|
||||
return BotAction.Resupply
|
||||
}
|
||||
|
||||
// Have a target?
|
||||
visibleTargets.headOption match {
|
||||
case Some(target) =>
|
||||
currentTarget = Some(target)
|
||||
BotAction.Attack(target)
|
||||
|
||||
case None =>
|
||||
currentTarget = None
|
||||
// Follow objective or patrol
|
||||
currentObjective match {
|
||||
case Some(obj) => BotAction.FollowObjective(obj)
|
||||
case None => BotAction.Patrol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the decided action
|
||||
*/
|
||||
private def executeAction(action: BotAction): Unit = action match {
|
||||
case BotAction.Attack(target) =>
|
||||
// Turn toward target
|
||||
val newFacing = calculateFacingToward(player.Position, target.Position)
|
||||
player.Orientation = Vector3(0, 0, newFacing)
|
||||
|
||||
// Move based on bot type
|
||||
botPersonality.movementStyle match {
|
||||
case MovementStyle.Newbie =>
|
||||
// Run toward target with held strafe
|
||||
moveToward(target.Position, strafeOffset = 2f)
|
||||
case MovementStyle.Veteran =>
|
||||
// ADAD strafe
|
||||
moveWithADAD(target.Position)
|
||||
case MovementStyle.NCClose =>
|
||||
// Close distance aggressively (shotgun range)
|
||||
moveToward(target.Position, speed = 1.5f)
|
||||
}
|
||||
|
||||
// Fire if we have line of sight
|
||||
if (hasLineOfSight(target)) {
|
||||
fireWeapon(target)
|
||||
}
|
||||
|
||||
case BotAction.Retreat =>
|
||||
val retreatPosition = findRetreatPosition()
|
||||
moveToward(retreatPosition)
|
||||
|
||||
// Call for help
|
||||
if (shouldCallForHelp()) {
|
||||
sendVoiceCommand("VVV") // HELP!
|
||||
}
|
||||
|
||||
case BotAction.Resupply =>
|
||||
val terminal = findNearestTerminal()
|
||||
terminal.foreach(moveToward)
|
||||
|
||||
case BotAction.FollowObjective(obj) =>
|
||||
moveToward(obj.targetPosition)
|
||||
|
||||
case BotAction.Patrol =>
|
||||
patrol()
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast current state to other players via AvatarService
|
||||
*/
|
||||
private def broadcastState(): Unit = {
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlayerState(
|
||||
player.GUID,
|
||||
player.Position,
|
||||
Some(player.Velocity),
|
||||
player.Orientation.z, // facingYaw
|
||||
player.Orientation.x, // facingPitch
|
||||
player.facingYawUpper, // upper body yaw
|
||||
timestamp,
|
||||
player.Crouching,
|
||||
player.Jumping,
|
||||
jumpThrust = false,
|
||||
player.Cloaked,
|
||||
spectator = false,
|
||||
weaponInHand = player.DrawnSlot != Player.HandsDownSlot
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle taking damage
|
||||
*/
|
||||
private def handleDamage(amount: Int, source: PlanetSideGUID, sourcePosition: Vector3): Unit = {
|
||||
// If not in combat, react to damage
|
||||
if (currentTarget.isEmpty) {
|
||||
// Turn toward damage source
|
||||
val newFacing = calculateFacingToward(player.Position, sourcePosition)
|
||||
player.Orientation = Vector3(0, 0, newFacing)
|
||||
|
||||
// Panic behavior based on class
|
||||
if (botClass.role == BotRole.Support || botClass.role == BotRole.Hacker) {
|
||||
// Panic! Run to cover, swap to weapon
|
||||
// (Support was probably repairing/healing)
|
||||
}
|
||||
}
|
||||
|
||||
// Increase attitude if repeatedly dying to same source
|
||||
if (botPersonality.experienceLevel >= ExperienceLevel.Veteran) {
|
||||
lastKiller.foreach { killer =>
|
||||
if (killer == source) {
|
||||
attitude = math.min(1.0f, attitude + 0.1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle death
|
||||
*/
|
||||
private def handleDeath(killer: PlanetSideGUID): Unit = {
|
||||
lastDeathLocation = Some(player.Position)
|
||||
lastKiller = Some(killer)
|
||||
|
||||
// Veteran+ bots remember for vengeance
|
||||
if (botPersonality.experienceLevel >= ExperienceLevel.Veteran) {
|
||||
// Store vengeance target
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a V-menu voice command
|
||||
*/
|
||||
private def sendVoiceCommand(command: String): Unit = {
|
||||
// TODO: Send ChatMsg with voice command
|
||||
// zone.AvatarEvents ! ... ChatMsg ...
|
||||
}
|
||||
|
||||
// Helper methods (stubs)
|
||||
private def calculateAngle(from: Vector3, to: Vector3, facing: Float): Float = ???
|
||||
private def calculateFacingToward(from: Vector3, to: Vector3): Float = ???
|
||||
private def moveToward(target: Vector3, strafeOffset: Float = 0f, speed: Float = 1f): Unit = ???
|
||||
private def moveWithADAD(target: Vector3): Unit = ???
|
||||
private def hasLineOfSight(target: Player): Boolean = ???
|
||||
private def fireWeapon(target: Player): Unit = ???
|
||||
private def isOutOfAmmo(): Boolean = ???
|
||||
private def findRetreatPosition(): Vector3 = ???
|
||||
private def findNearestTerminal(): Option[Vector3] = ???
|
||||
private def shouldCallForHelp(): Boolean = ???
|
||||
private def patrol(): Unit = ???
|
||||
}
|
||||
|
||||
object BotActor {
|
||||
def props(player: Player, avatar: Avatar, zone: Zone, botClass: BotClass, personality: BotPersonality): Props =
|
||||
Props(classOf[BotActor], player, avatar, zone, botClass, personality)
|
||||
|
||||
// Messages
|
||||
case object Tick
|
||||
case class TakeDamage(amount: Int, source: PlanetSideGUID, sourcePosition: Vector3)
|
||||
case class Die(killer: PlanetSideGUID)
|
||||
case class Respawn(spawnPoint: Vector3)
|
||||
case class ReceiveOrder(order: BotOrder)
|
||||
case class HelpRequest(requester: PlanetSideGUID, helpType: String, location: Vector3)
|
||||
}
|
||||
|
||||
// Supporting types (would be in separate files)
|
||||
sealed trait BotAction
|
||||
object BotAction {
|
||||
case class Attack(target: Player) extends BotAction
|
||||
case object Retreat extends BotAction
|
||||
case object Resupply extends BotAction
|
||||
case class FollowObjective(objective: BotObjective) extends BotAction
|
||||
case object Patrol extends BotAction
|
||||
}
|
||||
|
||||
sealed trait BotRole
|
||||
object BotRole {
|
||||
case object Driver extends BotRole
|
||||
case object Support extends BotRole
|
||||
case object Hacker extends BotRole
|
||||
case object AV extends BotRole
|
||||
case object MAX extends BotRole
|
||||
case object Veteran extends BotRole
|
||||
case object Ace extends BotRole
|
||||
}
|
||||
|
||||
sealed trait ExperienceLevel
|
||||
object ExperienceLevel {
|
||||
case object Newbie extends ExperienceLevel
|
||||
case object Regular extends ExperienceLevel
|
||||
case object Veteran extends ExperienceLevel
|
||||
case object Ace extends ExperienceLevel
|
||||
}
|
||||
|
||||
sealed trait MovementStyle
|
||||
object MovementStyle {
|
||||
case object Newbie extends MovementStyle // straight run with held strafe
|
||||
case object Veteran extends MovementStyle // ADAD + crouch spam
|
||||
case object NCClose extends MovementStyle // aggressive closing for shotguns
|
||||
}
|
||||
|
||||
case class BotClass(
|
||||
name: String,
|
||||
role: BotRole,
|
||||
// certifications, loadout, etc.
|
||||
)
|
||||
|
||||
case class BotPersonality(
|
||||
experienceLevel: ExperienceLevel,
|
||||
movementStyle: MovementStyle,
|
||||
fovDegrees: Float = 60f,
|
||||
detectionRange: Float = 100f,
|
||||
retreatThreshold: Float = 0.25f, // retreat at 25% HP
|
||||
accuracyModifier: Float = 1.0f
|
||||
)
|
||||
|
||||
case class BotObjective(
|
||||
targetPosition: Vector3,
|
||||
objectiveType: String // "attack", "defend", "capture", etc.
|
||||
)
|
||||
|
||||
case class BotOrder(
|
||||
orderType: String,
|
||||
targetPosition: Option[Vector3],
|
||||
priority: Int
|
||||
)
|
||||
394
bot-docs/SKETCHES/BotSpawner_v1.scala
Normal file
394
bot-docs/SKETCHES/BotSpawner_v1.scala
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
// SKETCH - NOT PRODUCTION CODE
|
||||
// Bot spawning flow - what would it take to spawn a bot?
|
||||
|
||||
package net.psforever.actors.bot
|
||||
|
||||
import akka.actor.{Actor, ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.definition.ExoSuitDefinition
|
||||
import net.psforever.objects.guid.GUIDTask
|
||||
import net.psforever.objects.loadouts.InfantryLoadout
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.objectcreate.BasicCharacterData
|
||||
import net.psforever.types._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Success, Failure}
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* BotSpawner - Responsible for spawning bots into a zone
|
||||
*
|
||||
* Key insight: Looking at ZonePopulationActor.scala:
|
||||
* - Zone.Population.Join(avatar) -> registers avatar in playerMap
|
||||
* - Zone.Population.Spawn(avatar, player, avatarActor) -> creates PlayerControl
|
||||
* - GUIDTask.registerPlayer(zone.GUID, player) -> assigns GUIDs
|
||||
*
|
||||
* For bots, we need:
|
||||
* 1. Create an Avatar (normally from DB, but we can construct directly)
|
||||
* 2. Create a Player with that Avatar
|
||||
* 3. Equip the player with loadout
|
||||
* 4. Register GUIDs for player and equipment
|
||||
* 5. Join zone population
|
||||
* 6. Spawn player
|
||||
* 7. Create BotActor to control AI
|
||||
*/
|
||||
object BotSpawner {
|
||||
|
||||
// Counter for generating unique bot IDs
|
||||
// Using negative numbers to avoid collision with real player charIds
|
||||
private val botIdCounter = new AtomicInteger(-1)
|
||||
|
||||
/**
|
||||
* Spawn a single bot into a zone
|
||||
*
|
||||
* @param zone The zone to spawn into
|
||||
* @param faction Which empire (TR, NC, VS)
|
||||
* @param botClass The class/role of the bot
|
||||
* @param spawnPosition Where to spawn
|
||||
* @param context ActorContext for creating BotActor
|
||||
* @return The spawned player entity
|
||||
*/
|
||||
def spawnBot(
|
||||
zone: Zone,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
botClass: BotClass,
|
||||
spawnPosition: Vector3,
|
||||
context: ActorContext
|
||||
): Player = {
|
||||
|
||||
// 1. Generate unique bot ID (negative to avoid DB collision)
|
||||
val botId = botIdCounter.getAndDecrement()
|
||||
|
||||
// 2. Create Avatar
|
||||
val avatar = createBotAvatar(botId, faction, botClass)
|
||||
|
||||
// 3. Create Player entity
|
||||
val player = new Player(avatar)
|
||||
|
||||
// 4. Configure player
|
||||
configurePlayer(player, botClass, spawnPosition)
|
||||
|
||||
// 5. Register GUIDs for player and all equipment
|
||||
// This is async - we need to wait for it to complete
|
||||
val registerTask = GUIDTask.registerPlayer(zone.GUID, player)
|
||||
TaskWorkflow.execute(registerTask)
|
||||
|
||||
// 6. Join zone population (avatar-level)
|
||||
zone.Population ! Zone.Population.Join(avatar)
|
||||
|
||||
// 7. Create a placeholder BotAvatarActor
|
||||
// Real AvatarActor handles DB persistence - we don't need that
|
||||
val botAvatarActor = context.actorOf(
|
||||
BotAvatarActor.props(avatar),
|
||||
name = s"bot-avatar-$botId"
|
||||
)
|
||||
|
||||
// 8. Spawn player in zone (creates PlayerControl actor)
|
||||
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
|
||||
|
||||
// 9. Add to block map for spatial queries
|
||||
zone.actor ! ZoneActor.AddToBlockMap(player, spawnPosition)
|
||||
|
||||
// 10. Create BotActor for AI control
|
||||
val personality = createPersonality(botClass)
|
||||
val botActor = context.actorOf(
|
||||
BotActor.props(player, avatar, zone, botClass, personality),
|
||||
name = s"bot-ai-$botId"
|
||||
)
|
||||
|
||||
// 11. Broadcast player existence to all connected clients
|
||||
broadcastPlayerSpawn(zone, player)
|
||||
|
||||
player
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Avatar for a bot
|
||||
*/
|
||||
private def createBotAvatar(
|
||||
botId: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
botClass: BotClass
|
||||
): Avatar = {
|
||||
val name = generateBotName(faction, botClass, botId)
|
||||
val sex = if (scala.util.Random.nextBoolean()) CharacterSex.Male else CharacterSex.Female
|
||||
val head = scala.util.Random.nextInt(5) + 1
|
||||
val voice = CharacterVoice.values.toSeq(scala.util.Random.nextInt(CharacterVoice.values.size))
|
||||
|
||||
// Create avatar with predefined certifications for the class
|
||||
Avatar(
|
||||
id = botId,
|
||||
basic = BasicCharacterData(name, faction, sex, head, voice),
|
||||
bep = botClass.battleRank * 1000L, // Fake BEP for appearance
|
||||
cep = if (botClass.role == BotRole.Ace) 10000L else 0L, // CR for Ace only
|
||||
certifications = botClass.certifications
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a bot name like "[BOT]Grunt_TR_042"
|
||||
*/
|
||||
private def generateBotName(
|
||||
faction: PlanetSideEmpire.Value,
|
||||
botClass: BotClass,
|
||||
botId: Int
|
||||
): String = {
|
||||
val factionPrefix = faction match {
|
||||
case PlanetSideEmpire.TR => "TR"
|
||||
case PlanetSideEmpire.NC => "NC"
|
||||
case PlanetSideEmpire.VS => "VS"
|
||||
case _ => "XX"
|
||||
}
|
||||
val classPrefix = botClass.role match {
|
||||
case BotRole.Driver => "Driver"
|
||||
case BotRole.Support => "Medic"
|
||||
case BotRole.Hacker => "Hacker"
|
||||
case BotRole.AV => "Heavy"
|
||||
case BotRole.MAX => "MAX"
|
||||
case BotRole.Veteran => "Vet"
|
||||
case BotRole.Ace => "Ace"
|
||||
}
|
||||
f"[BOT]${classPrefix}_${factionPrefix}_${math.abs(botId)}%03d"
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure player entity with position, equipment, etc.
|
||||
*/
|
||||
private def configurePlayer(
|
||||
player: Player,
|
||||
botClass: BotClass,
|
||||
spawnPosition: Vector3
|
||||
): Unit = {
|
||||
// Set position and orientation
|
||||
player.Position = spawnPosition
|
||||
player.Orientation = Vector3(0, 0, scala.util.Random.nextFloat() * 360f)
|
||||
|
||||
// Set exosuit based on class
|
||||
val exosuit = botClass.role match {
|
||||
case BotRole.MAX => ExoSuitType.MAX
|
||||
case BotRole.Hacker => ExoSuitType.Agile // Infiltrators use Agile
|
||||
case _ => ExoSuitType.Reinforced
|
||||
}
|
||||
player.ExoSuit = exosuit
|
||||
|
||||
// Equip loadout
|
||||
equipLoadout(player, botClass)
|
||||
|
||||
// Spawn the player (set health, armor)
|
||||
player.Spawn()
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip player with class-appropriate loadout
|
||||
*/
|
||||
private def equipLoadout(player: Player, botClass: BotClass): Unit = {
|
||||
// TODO: Load from predefined loadouts
|
||||
// For now, just give basic equipment based on faction + class
|
||||
|
||||
// Example: Standard infantry loadout
|
||||
// player.Slot(0).Equipment = ... // Rifle
|
||||
// player.Slot(1).Equipment = ... // Sidearm
|
||||
// etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create personality/behavior weights for a bot class
|
||||
*/
|
||||
private def createPersonality(botClass: BotClass): BotPersonality = {
|
||||
botClass.role match {
|
||||
case BotRole.Veteran | BotRole.Ace =>
|
||||
BotPersonality(
|
||||
experienceLevel = if (botClass.role == BotRole.Ace) ExperienceLevel.Ace else ExperienceLevel.Veteran,
|
||||
movementStyle = MovementStyle.Veteran,
|
||||
fovDegrees = 75f, // Better awareness
|
||||
accuracyModifier = 1.2f,
|
||||
retreatThreshold = 0.3f
|
||||
)
|
||||
case BotRole.MAX =>
|
||||
BotPersonality(
|
||||
experienceLevel = ExperienceLevel.Regular,
|
||||
movementStyle = MovementStyle.Newbie, // MAXes are slower
|
||||
fovDegrees = 60f,
|
||||
accuracyModifier = 1.0f,
|
||||
retreatThreshold = 0.2f // MAXes don't retreat easily
|
||||
)
|
||||
case _ =>
|
||||
BotPersonality(
|
||||
experienceLevel = ExperienceLevel.Newbie,
|
||||
movementStyle = MovementStyle.Newbie,
|
||||
fovDegrees = 60f,
|
||||
accuracyModifier = 0.8f, // Worse accuracy
|
||||
retreatThreshold = 0.25f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast player spawn to all connected clients
|
||||
*/
|
||||
private def broadcastPlayerSpawn(zone: Zone, player: Player): Unit = {
|
||||
// Use ObjectCreateMessage to create the player on all clients
|
||||
// This is what makes the bot visible to everyone
|
||||
|
||||
import net.psforever.packet.game.ObjectCreateMessage
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
|
||||
// Build the player data for ObjectCreateMessage
|
||||
val playerData = PlayerData.create(player)
|
||||
|
||||
// Broadcast via AvatarService
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.LoadPlayer(
|
||||
player.GUID,
|
||||
player.Definition.ObjectId,
|
||||
player.GUID, // target_guid - same as player for self
|
||||
playerData,
|
||||
None // no parent (not in vehicle)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Despawn a bot from a zone (graceful logout)
|
||||
*/
|
||||
def despawnBot(zone: Zone, player: Player): Unit = {
|
||||
// 1. Stop BotActor
|
||||
|
||||
// 2. Notify zone population
|
||||
zone.Population ! Zone.Population.Leave(player.avatar)
|
||||
|
||||
// 3. Broadcast player deletion
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.ObjectDelete(player.GUID, player.GUID)
|
||||
)
|
||||
|
||||
// 4. Unregister GUIDs
|
||||
TaskWorkflow.execute(GUIDTask.unregisterPlayer(zone.GUID, player))
|
||||
|
||||
// 5. Remove from block map
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified AvatarActor for bots
|
||||
*
|
||||
* The real AvatarActor handles:
|
||||
* - DB persistence (saving stats, certs, etc.)
|
||||
* - Character selection
|
||||
* - Login/logout flow
|
||||
*
|
||||
* For bots, we don't need most of that. Just a stub actor
|
||||
* that can handle the messages PlayerControl expects.
|
||||
*/
|
||||
class BotAvatarActor(avatar: Avatar) extends Actor {
|
||||
// Minimal implementation - just accept messages and do nothing
|
||||
def receive: Receive = {
|
||||
case _ => // Ignore most messages - bots don't persist
|
||||
}
|
||||
}
|
||||
|
||||
object BotAvatarActor {
|
||||
def props(avatar: Avatar): Props = Props(classOf[BotAvatarActor], avatar)
|
||||
}
|
||||
|
||||
/**
|
||||
* BotManager - Manages bot population across a zone
|
||||
*/
|
||||
class BotManager(zone: Zone) extends Actor {
|
||||
import scala.concurrent.duration._
|
||||
|
||||
private val targetBotsPerFaction = 100
|
||||
private var bots: Map[PlanetSideGUID, Player] = Map.empty
|
||||
|
||||
// Population check timer
|
||||
context.system.scheduler.scheduleWithFixedDelay(
|
||||
initialDelay = 5.seconds,
|
||||
delay = 10.seconds,
|
||||
receiver = self,
|
||||
message = BotManager.CheckPopulation
|
||||
)(context.dispatcher)
|
||||
|
||||
def receive: Receive = {
|
||||
case BotManager.CheckPopulation =>
|
||||
balancePopulation()
|
||||
|
||||
case BotManager.SpawnBot(faction, botClass, position) =>
|
||||
val player = BotSpawner.spawnBot(zone, faction, botClass, position, context)
|
||||
bots += (player.GUID -> player)
|
||||
|
||||
case BotManager.DespawnBot(guid) =>
|
||||
bots.get(guid).foreach { player =>
|
||||
BotSpawner.despawnBot(zone, player)
|
||||
bots -= guid
|
||||
}
|
||||
|
||||
case BotManager.DespawnAll =>
|
||||
bots.values.foreach(player => BotSpawner.despawnBot(zone, player))
|
||||
bots = Map.empty
|
||||
}
|
||||
|
||||
private def balancePopulation(): Unit = {
|
||||
PlanetSideEmpire.values.foreach { faction =>
|
||||
if (faction != PlanetSideEmpire.NEUTRAL) {
|
||||
val realPlayers = zone.LivePlayers.count(p =>
|
||||
!isBotPlayer(p) && p.Faction == faction
|
||||
)
|
||||
val currentBots = bots.values.count(_.Faction == faction)
|
||||
val targetBots = math.max(0, targetBotsPerFaction - realPlayers)
|
||||
|
||||
if (currentBots < targetBots) {
|
||||
// Spawn more bots
|
||||
val toSpawn = targetBots - currentBots
|
||||
(0 until toSpawn).foreach { _ =>
|
||||
val position = findSpawnPosition(faction)
|
||||
val botClass = randomBotClass()
|
||||
self ! BotManager.SpawnBot(faction, botClass, position)
|
||||
}
|
||||
} else if (currentBots > targetBots) {
|
||||
// Despawn excess bots (non-Ace first)
|
||||
val toRemove = currentBots - targetBots
|
||||
val botsToRemove = bots.values
|
||||
.filter(_.Faction == faction)
|
||||
.toSeq
|
||||
.sortBy(p => if (isAce(p)) 1 else 0) // Ace last
|
||||
.take(toRemove)
|
||||
|
||||
botsToRemove.foreach(p => self ! BotManager.DespawnBot(p.GUID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def isBotPlayer(player: Player): Boolean = {
|
||||
// Check if name starts with [BOT]
|
||||
player.Name.startsWith("[BOT]")
|
||||
}
|
||||
|
||||
private def isAce(player: Player): Boolean = {
|
||||
player.Name.contains("Ace_")
|
||||
}
|
||||
|
||||
private def findSpawnPosition(faction: PlanetSideEmpire.Value): Vector3 = {
|
||||
// TODO: Find appropriate spawn point for faction
|
||||
// Could use owned bases, warpgates, etc.
|
||||
Vector3(100f, 100f, 10f) // Placeholder
|
||||
}
|
||||
|
||||
private def randomBotClass(): BotClass = {
|
||||
// Weighted random class selection
|
||||
// More grunts than specialists
|
||||
BotClass("Grunt", BotRole.Veteran, Set(), 10) // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
object BotManager {
|
||||
case object CheckPopulation
|
||||
case class SpawnBot(faction: PlanetSideEmpire.Value, botClass: BotClass, position: Vector3)
|
||||
case class DespawnBot(guid: PlanetSideGUID)
|
||||
case object DespawnAll
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue