---
# System prepended metadata

title: Unified Character Architecture (Player + Enemy)

---

# Unified Character Architecture (Player + Enemy)

> **Design goal**  
> One Brain. One State system. One execution pipeline.  
> Player input and Enemy AI differ only in **how Facts are produced**, not in how decisions are made.

---

## 1. Core Architectural Decisions

- **States decide actions**
- **Brain mediates decisions**
- **Character executes**
- **Policy validates actions**
- **Transitions are control-flow, not actions**
- **Animation is presentation, not gameplay**
- **Enemy AI replaces input, not states**

---

## 2. High-Level Data Flow

```mermaid
flowchart LR
    InputOrAI[Input / AI Sensors]
    InputOrAI --> FactsProvider
    FactsProvider --> Facts

    Facts --> Brain

    %% State execution
    Brain --> CurrentState[IState.update]
    CurrentState --> StateResult

    %% Transition path
    StateResult -->|target_state| TransitionCheck[State Graph]
    TransitionCheck -->|allowed| BrainChange[Brain.change_state]
    BrainChange --> CurrentState

    %% Action path
    StateResult -->|intent| ActionPolicy
    ActionPolicy -->|approved| Character

    %% Presentation path
    StateResult -->|animation| AnimationController

```

## 3. Core Data Objects
### 3.1 Facts (World Snapshot)
```gdscript=
class_name Facts

var input: InputSnapshot = InputSnapshot.new()

# Character physical state
var velocity: Vector2
var is_on_floor: bool

# Combat / control state
var is_locked: bool = false
var stamina: float = 0.0

# Time
var frame: int = 0

# Inventory
var inventory_items: Dictionary = {}

func inventory_has_item(item_id: StringName, quantity := 1) -> bool:
	return inventory_items.get(item_id, 0) >= quantity
```

**Invariant:**
Facts must always be complete and non-null.
>For enemy, the AI and stack FSM logic, will resolve the actions of enemy into simple behavioral states as same in enemy, i.e. idle, walk, attack etc.

### 3.2 InputSnapshot (Control Abstraction)

```gdscript=
class_name InputSnapshot

var just_pressed: Dictionary = {}
var just_released: Dictionary = {}
var is_pressed: Dictionary = {}

var horizontal: float = 0.0

```

**Used by:**
- Player (keyboard / controller)
- Enemy (AI-generated virtual input)

**For player, autoload of InputManager will handle filling the InputSnapshot instance**

InputManager:
```gdscript=
extends Node

signal input_snapshot_ready(snapshot)

const ACTIONS := [
	"move_left", "move_right",
	"jump", "attack", "interact",
	"dash", "inventory"
]

func _physics_process(_delta: float) -> void:
	var snapshot := InputSnapshot.new()

	for a in ACTIONS:
		snapshot.just_pressed[a] = Input.is_action_just_pressed(a)
		snapshot.just_released[a] = Input.is_action_just_released(a)
		snapshot.is_pressed[a] = Input.is_action_pressed(a)

	snapshot.horizontal = (
		Input.get_action_strength("move_right")
		- Input.get_action_strength("move_left")
	)

	input_snapshot_ready.emit(snapshot)
```

**For enemy, AI + stack FSM will be responsible for filling this snapshot**

### 3.3 Intent (Gameplay Action Only)
```gdscript=
class_name Intent

enum Type {
    NONE,
    MOVE,
    JUMP,
    ATTACK
}

var type: Type = Type.NONE
var move_axis: float = 0.0
```

Intent contains **ONLY** things that affect the world.
It is only responsible for handling **behavioural** work, not animation and transitions.

## 4. State Output Contract
### 4.1 StateResult (State → Brain)
```gdscript=
class_name StateResult

var intent: Intent = null        # gameplay action
var next_state: StringName = &"" # requested transition
var animation: StringName = &""  # presentation
```

**Why this exists**
| Field      | Why it is NOT in Intent       |
| ---------- | ----------------------------- |
| intent     | (behavioural) validated + executed          |
| next_state | control-flow, validated by stateGraph or TransitionGraphPolicy  |
| animation  | presentation |

## 5. State System (Decision Layer)
### 5.1 IState Interface
- IState is **behavioural**, not **perceptual**.
- States should not “reason about the world”
- States should reason about intent-ready signals
```gdscript=
class_name IState

func get_id() -> StringName
func enter(facts: Facts) -> void
func update(facts: Facts) -> StateResult
func exit() -> void
```

**States:**
- read Facts
- return StateResult
- never execute logic directly

There would be two types of states:
- States which do not consume any sort of resource on `enter()`, would just extend from `IState`.
- But `IActionState`, extended from `IState`, would cost resouces, and would be validated by policy instance of its correspondance.
#### 5.1.1 Example: IdleState
```gdscript=
extends IState

func get_id() -> StringName:
    return &"idle"

func update(facts: Facts) -> StateResult:
    var r := StateResult.new()
    r.animation = &"idle"

    if facts.input.horizontal != 0:
        r.next_state = &"walk"

    return r
```
#### 5.1.2 Example: WalkState
```gdscript=
extends IState

func get_id() -> StringName:
    return &"walk"

func update(facts: Facts) -> StateResult:
    var r := StateResult.new()

    if facts.input.horizontal == 0:
        r.next_state = &"idle"
        return r

    var i := Intent.new()
    i.type = Intent.Type.MOVE
    i.move_axis = facts.input.horizontal

    r.intent = i
    r.animation = &"walk"
    return r
```
**Invariant:**
Idle never moves. Walk owns movement.
Non-resource consuming states, do not require any policy validation.

### 5.2 IActionState
```gdscript=
# core/states/action_state_interface.gd
class_name IActionState
extends IState

## Return the resource / commitment this state requires on entry
func get_entry_cost(facts: Facts) -> Dictionary:
	# Example return:
	# { "stamina": 20 }
	return {}

```
#### 5.2.1 Example Action State
```gdscript=
# core/states/combat/light_attack_state.gd
extends IActionState
class_name LightAttackState

const STAMINA_COST := 15
const HIT_FRAME := 6
const RECOVERY_FRAME := 18

var frame := 0

func get_id() -> StringName:
	return &"light_attack"

func get_entry_cost(_facts: Facts) -> Dictionary:
	return { "stamina": STAMINA_COST }

func enter(_facts: Facts) -> void:
	frame = 0
	# stamina is assumed to be RESERVED already by StateGraph

func update(facts: Facts) -> StateResult:
	frame += 1
	var r := StateResult.new()

	# Animation always reflects current state
	r.animation = &"light_attack"

	# Hit frame → emit gameplay effect
	if frame == HIT_FRAME:
		var i := Intent.new()
		i.type = Intent.Type.ATTACK
		r.intent = i

	# Recovery done → leave state
	if frame >= RECOVERY_FRAME:
		r.next_state = &"idle"

	return r

func exit() -> void:
	pass

```

## 6. Brain (Decision Mediator)
**Brain Flow**
```mermaid
%%{init: {
  "theme": "forest"
}}%%

flowchart TD
    Facts["Facts"]
    ForceCheck["Forced Transition Check<br/>(Die, Stun, Override)"]
    Update["IState.update(facts)"]
    Result["StateResult"]

    HasNext{next_state != empty?}
    GraphCheck["StateGraph.can_transition"]
    ChangeState["Brain.change_state"]

    NoTransition["No Transition"]

    EmitAnim["Emit animation_requested"]
    HasIntent{intent != null?}
    PolicyExec["ActionPolicy.validate(intent)"]
    EmitIntent["intent_approved.emit"]
    EndTick["End Tick"]

    %% Entry
    Facts --> ForceCheck
    ForceCheck -->|triggered| ChangeState
    ChangeState --> EndTick
    ForceCheck -->|not triggered| Update

    %% State evaluation
    Update --> Result
    Result --> HasNext

    %% Transition path (ONLY StateGraph involved)
    HasNext -->|yes| GraphCheck
    GraphCheck -->|allowed| ChangeState
    GraphCheck -->|denied| NoTransition

    %% No-transition path
    HasNext -->|no| NoTransition
    NoTransition --> EmitAnim
    EmitAnim --> HasIntent

    %% Execution path (ONLY Policy involved)
    HasIntent -->|yes| PolicyExec
    PolicyExec -->|approved| EmitIntent
    PolicyExec -->|denied| EndTick
    HasIntent -->|no| EndTick

    EmitIntent --> EndTick

```
## Brain Tick Invariants

**1. StateResult is evaluated exactly once**
- `IState.update(facts)` runs once per tick
- Brain never re-enters state logic in the same frame
- All decisions are derived from a single `StateResult`
---
**2. Transitions strictly short-circuit execution**
- If `next_state` exists **and StateGraph allows it**:
  - Brain changes state
  - **No animation is emitted**
  - **No intent is validated or executed**
- This guarantees:
  - no “Idle but moving”
  - no “Attack animation without attack”
  - no partial-frame side effects

---

**3. StateGraph owns *all* transition legality**
- StateGraph decides whether a state change is allowed
- This includes:
  - lockouts
  - stamina / cooldown checks *for entering a state*
  - forced states (Die, Stun)
- Policy is **not involved** in state transitions

---

**4. Animation and Intent are parallel but ordered outputs**
- Animation is emitted **only if the state remains active**
- Animation is **never validated**, as it is attached to current state. Current states are validate before entering, hence are always plausible
- Intent is validated **only after animation emission**
- Animation reflects *current state*, not successful execution

---

**5. Policy validates execution, nothing else**
- Policy answers only:
  > “May this intent produce gameplay effects right now?”
- Policy does **not**:
  - decide states
  - block animations
  - affect transitions

---

**6. Intent.NONE is meaningful**
- States may emit:
  - animation only
  - neither animation nor intent
- Brain does not early-return on `Intent.NONE`
- Presentation continues independently of gameplay actions

---

**Brain Responsibilities**
- Hold the current state
- Invoke `IState.update(facts)`
- Apply forced and requested transitions (via StateGraph)
- Emit animation requests
- Validate and emit intents (via Policy)



**Brain Signals**

```gdscript=
signal intent_approved(intent: Intent)
signal animation_requested(anim: StringName)
```

## 7. Transition Validation (Updated)

**All state transitions are validated exclusively by the StateGraph.**  
>The **difference** between **policyValidation** and **stateGraph** validation is that, **later** will check if the **transition is plausible**, while the **former** will handle if the execution of **current state is plausible**.


Intent is *never* involved in transition decisions.

### 7.1 Responsibility Split

- **StateGraph**
  - Decides whether a state change is allowed
  - Validates *commitment-level* rules:
    - resource availability (stamina, cooldowns)
    - global locks (stun, death)
    - forced overrides (Die, Stun, Scripted)
  - Guarantees that any active state is always *plausible*

- **Brain**
  - Queries the StateGraph
  - Applies transitions
  - Never decides legality itself

- **Policy**
  - Never validates transitions
  - Validates only *execution-time effects* (Intents)

---

### 7.2 Transition Validation API

```gdscript
class_name StateGraph

func can_transition(
    from: StringName,
    to: StringName,
    facts: Facts
) -> bool:
    return true
```

### StateGraph Example
```gdscript=
func can_transition(from: StringName, to: StringName, facts: Facts) -> bool:
	var next_state := state_map[to]

	# Handle action states
	if next_state is IActionState:
		var cost := next_state.get_entry_cost(facts)

		if cost.has("stamina"):
			if facts.stamina < cost["stamina"]:
				return false

	# structural rules
	return true

```

## 8. Character (Execution Layer)
**Responsibilities**
- calls brain ticks
- Apply velocity
- Run physics
- Execute approved intents

Character:
- does NOT read input
- does NOT change states
- does NOT validate actions
- does NOT manage animations

## 9. Interface Action Policy (Gameplay Rules)
This is the interface for Action policies, which is used to maintain coherent rules for characters.
Player Policy and Enemy Policy will inherit from these.
```gdscript=
class_name IActionPolicy

func validate(
    state_id: StringName,
    intent: Intent,
    facts: Facts
) -> bool
```

**Used for:**
- stamina
- hitstun
- lockouts
- combat rules

## 10. Animation System (Presentation Layer)
**Why Separate**
- Animation must never be blocked by policy
- Animation must work even when no actions/state happens.
- 1 state = 1 animation doesn't really scale well.
- Animation ≠ gameplay authority

**AnimationSystem**
```gdscript=
class_name AnimationController

func request(anim: StringName) -> void
```

Connected to:
```gdscript=
brain.animation_requested.connect(animation_controller)
```

## 11. Input & Facts Providers
Player
```
InputService → InputSnapshot → Facts → Brain
```

Enemy
```
AI Sensors → AI FSM → InputSnapshot → Facts → Brain
```

## 12. Enemy AI Architecture
**Key Rule**
> AI replaces input, not states

Enemy uses:
- same Brain
- same Behavioural States
- different Policies
- same execution

Inside the player/enemy/boss/etc. inherited from base character class, following policies can be attached.
```gdscript=
player.brain.policy = PlayerPolicy.new()
grunt.brain.policy = EnemyPolicy.new()
boss.brain.policy = BossPolicy.new()
```

### 12.1 Stack-Based AI FSM (Directive Layer)

Purpose:
- choose which behavioral state runs
- override via push / pop
- react to events

Examples:
- PatrolMode
- ChaseMode
- CombatMode

FSM does NOT:
- emit intents
- move character
- bypass Brain

### 12.2 Enemy Flow
```mermaid
flowchart TB
    Sensors --> DirectiveFSM
    DirectiveFSM --> AISnapshot
    AISnapshot --> Facts
    Facts --> Brain
    Brain --> State
    State --> Intent
    Intent --> Character
```

## 13. Authority Summary
```mermaid
flowchart LR
    FSM[AI FSM] -->|chooses| State
    State -->|decides| Intent
    Brain -->|validates| Intent
    Character -->|executes| Intent
```

## 14. Player vs Enemy Comparison
| Layer          | Player              | Enemy               |
| -------------- | ------------------- | ------------------- |
| Input Source   | Keyboard            | AI FSM              |
| Facts Provider | InputService | AIService  |
| Brain          | Shared              | Shared              |
| States         | Shared              | Shared + Enemy-only |
| Policy         | Player Policy              | Enemy Policy              |
| Animation      | Shared              | Shared + Enemy only              |

## 15. Non-Negotiable Invariants
- Intent contains ONLY gameplay actions
- Transitions are control-flow, not actions
- Animation is never validated
- AI never emits intents, just directive states
- Brain never touches engine APIs
- State ownership is absolute

## 16. Final Mental Model

> AI decides inputs
States decide actions
Brain mediates
Policy validates
StateGraph validates transitions
Character executes
Animation presents

--- 


## Some decisions to change
- [ ] `validate()` is a boolean. Returning string, will help to pass it through the UI, showing the message on screen to user. Also, help with debugging
- [ ] Guarantee Immutability of Facts and SnapShots

