File size: 3,380 Bytes
c670567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * StateMachine - A simple, reusable finite state machine
 *
 * This is a core utility that can be used by any game type.
 * For player-specific FSM with animation support, see PlayerFSM in modules.
 *
 * Usage:
 *   const fsm = new StateMachine(owner, 'idle');
 *   fsm.addState('idle', {
 *     onEnter: () => console.log('entering idle'),
 *     onUpdate: () => { ... },
 *     onExit: () => console.log('exiting idle'),
 *   });
 *   fsm.addState('moving', { ... });
 *   fsm.setState('moving');
 *   fsm.update(); // call every frame
 */

export interface IStateConfig {
  onEnter?: () => void;
  onUpdate?: () => void;
  onExit?: () => void;
}

export class StateMachine {
  private owner: any;
  private states: Map<string, IStateConfig> = new Map();
  private currentStateName: string | null = null;
  private currentState: IStateConfig | null = null;
  private isChangingState: boolean = false;

  /**
   * Create a new StateMachine
   * @param owner - The object that owns this state machine
   * @param initialState - Optional initial state name (will call onEnter if state exists)
   */
  constructor(owner: any, initialState?: string) {
    this.owner = owner;
    if (initialState) {
      this.currentStateName = initialState;
    }
  }

  /**
   * Add a state to the state machine
   * @param name - Unique state name
   * @param config - State configuration with onEnter, onUpdate, onExit callbacks
   */
  addState(name: string, config: IStateConfig): this {
    this.states.set(name, config);

    // If this is the initial state and we haven't entered yet, enter it now
    if (this.currentStateName === name && !this.currentState) {
      this.currentState = config;
      config.onEnter?.call(this.owner);
    }

    return this;
  }

  /**
   * Transition to a new state
   * @param name - Name of the state to transition to
   * @returns true if transition was successful
   */
  setState(name: string): boolean {
    // Prevent recursive state changes
    if (this.isChangingState) {
      console.warn(
        `StateMachine: Cannot change state while already changing state`,
      );
      return false;
    }

    // Check if state exists
    const newState = this.states.get(name);
    if (!newState) {
      console.warn(`StateMachine: State '${name}' does not exist`);
      return false;
    }

    // Skip if already in this state
    if (this.currentStateName === name) {
      return false;
    }

    this.isChangingState = true;

    // Exit current state
    if (this.currentState) {
      this.currentState.onExit?.call(this.owner);
    }

    // Update state references
    this.currentStateName = name;
    this.currentState = newState;

    // Enter new state
    this.currentState.onEnter?.call(this.owner);

    this.isChangingState = false;
    return true;
  }

  /**
   * Update the current state (call every frame)
   */
  update(): void {
    if (this.currentState && !this.isChangingState) {
      this.currentState.onUpdate?.call(this.owner);
    }
  }

  /**
   * Get the current state name
   */
  get state(): string | null {
    return this.currentStateName;
  }

  /**
   * Check if currently in a specific state
   */
  isState(name: string): boolean {
    return this.currentStateName === name;
  }

  /**
   * Check if a state exists
   */
  hasState(name: string): boolean {
    return this.states.has(name);
  }
}