« get me outta code hell

csb-game - Pixelly spin-off of the Command Synergy Battle system used in Final Fantasy XIII
summary refs log tree commit diff
path: root/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'index.js')
-rw-r--r--index.js148
1 files changed, 122 insertions, 26 deletions
diff --git a/index.js b/index.js
index 2c4ed29..2762bf6 100644
--- a/index.js
+++ b/index.js
@@ -126,40 +126,57 @@ class ATBBar {
   }
 
   update(dt) {
-    // ATB gauge progressing
-    if (!this.battleCharacter.isExecutingChain) {
-      this.progress = Math.min(1, this.progress + dt * 1.5 / this.segmentCount)
-    }
-
-    // ATB gauge visual update
-    this.visualProgress += 8 * dt * (this.progress - this.visualProgress)
-
     if (this.battle.playerCharacter !== this.battleCharacter) {
       this.aiUpdate(dt)
     }
 
-    // Start action chain once user has confirmed (willExecuteChain = true) and IP gauge is full enough to execute all actions
-    if (this.battleCharacter.willExecuteChain && this.progress >= 1 - this.getRemainingSpace() / this.segmentCount) {
-      this.battleCharacter.willExecuteChain = false
-      this.battleCharacter.isExecutingChain = true // Will be acknowledged by next if statement
+    // Decrease stun time
+    if (this.battleCharacter.stunTime) {
+      this.battleCharacter.stunTime -= dt
+      if (this.battleCharacter.stunTime <= 0) {
+        this.battleCharacter.stunTime = 0
+        this.battleCharacter.stunIsSignificant = null
+      }
     }
 
-    // Action chaining
-    if (this.battleCharacter.isExecutingChain && !this.battleCharacter.isExecutingAction) {
-      if (this.queuedActions.length) {
-        const action = this.queuedActions.shift()
-        this.battleCharacter.executeAction(action)
-        this.progress -= 1 / this.segmentCount * action.size
-      } else {
-        this.battleCharacter.isExecutingChain = false
-        if (this.battle.playerCharacter === this.battleCharacter && !this.battle.currentMenu) {
-          this.battle.showActionMenu({direction: 1})
+    if (this.battleCharacter.stunIsSignificant) {
+      // Do nothing. Ow!
+    } else {
+      // ATB gauge progressing
+      if (!this.battleCharacter.isExecutingChain) {
+        this.progress = Math.min(1, this.progress + dt * 1.5 / this.segmentCount)
+      }
+
+      // Start action chain once user has confirmed (willExecuteChain = true) and IP gauge is full enough to execute all actions
+      if (this.battleCharacter.willExecuteChain && this.progress >= 1 - this.getRemainingSpace() / this.segmentCount) {
+        this.battleCharacter.willExecuteChain = false
+        this.battleCharacter.isExecutingChain = true // Will be acknowledged by next if statement
+      }
+
+      // Action chaining
+      if (this.battleCharacter.isExecutingChain && !this.battleCharacter.isExecutingAction) {
+        if (this.queuedActions.length) {
+          const action = this.queuedActions.shift()
+          this.battleCharacter.executeAction(action)
+          this.progress -= 1 / this.segmentCount * action.size
+        } else {
+          this.battleCharacter.isExecutingChain = false
+          if (this.battle.playerCharacter === this.battleCharacter && !this.battle.currentMenu) {
+            this.battle.showActionMenu({direction: 1})
+          }
         }
       }
     }
+
+    // ATB gauge visual update
+    this.visualProgress += 8 * dt * (this.progress - this.visualProgress)
   }
 
   aiUpdate(dt) {
+    if (this.battleCharacter.stunIsSignificant) {
+      return
+    }
+
     // Determine a target
     if (!this.battleCharacter.targetCharacter) {
       this.battleCharacter.determineNewTarget('enemy')
@@ -169,7 +186,7 @@ class ATBBar {
     if (!this.battleCharacter.isExecutingChain && this.battleCharacter.targetCharacter && this.getRemainingSpace() >= 0) {
       // TODO: More moves
       while (this.getRemainingSpace()) {
-        this.queuedActions.push(actionDatabase.fire[0])
+        this.enqueue(actionDatabase.fire[0])
       }
     }
 
@@ -187,6 +204,15 @@ class ATBBar {
     this.queuedActions.pop()
   }
 
+  enqueue(action) {
+    if (this.stunIsSignificant) {
+      return false
+    } else {
+      this.queuedActions.push(action)
+      return true
+    }
+  }
+
   activate() {
     if (this.queuedActions.length) {
       // Cut off the actions that we don't have enough ATB charge for
@@ -433,7 +459,7 @@ class ActionMenu extends BaseBattleMenu {
     const remainingSpace = atbBar.getRemainingSpace()
 
     if (effectiveLevel <= remainingSpace) {
-      atbBar.queuedActions.push(option.chain[effectiveLevel - 1])
+      atbBar.enqueue(option.chain[effectiveLevel - 1])
 
       if (effectiveLevel === remainingSpace) {
         this.battle.showTargetMenu()
@@ -486,6 +512,37 @@ class BattleCharacter extends Sprite {
     this.dead = false
 
     this.hpBar = new HPBar(this)
+
+    // "Stun" is not an actual status effect of paralysis, but refers to how
+    // long a character is prevented from acting after having been hit.
+    // Effects during when stunTime is non-zero:
+    // * The character cannot execute any actions. Enqueued actions are delayed
+    //   until stun time ends.
+    // * The character's ATB gauge continues progressing UNLESS the stun is
+    //   "significant" (see stunIsSignificant).
+    // * The character's actionExecuteTime continues decreasing. Therefore,
+    //   if a character is hit by a strong attack and stunned for some time,
+    //   they will immediately activate the next queued action once the stun
+    //   time has passed (assuming actionExecuteTime has reached zero).
+    // * The character can still input (enqueue) actions UNLESS the stun is
+    //   "significant". Newly-queued actions will not be executed until the
+    //   stun time has passed.
+    // The property stunTime usually should not be set directly. Instead, use
+    // the addStunTime function, which contains additional accounting for
+    // stunIsSignificant.
+    this.stunTime = 0
+
+    // This property refers to whether a character's stun time is considered
+    // "significant". If a stun is significant, the character's ATB gauge will
+    // not progress during the stun. Furthermore, the character's queued
+    // actions will all be removed (but the ATB gauge itself will not be
+    // reset), and the character will be prevented from enqueueing any actions
+    // until the stun has passed. Significant stuns create an effect of the
+    // character "losing focus", and give incentive to avoid heavy blows.
+    // A stun will automatically be considered significant if the stun time
+    // passes a certain threshold. For example, while a single light attack may
+    // not significantly stun a character, a persistent barrage may be able to.
+    this.stunIsSignificant = null
   }
 
   update(dt) {
@@ -502,7 +559,11 @@ class BattleCharacter extends Sprite {
     this.atbBar.update(dt)
 
     if (this.isExecutingChain && !this.isExecutingAction) {
-      throw new Error('Executing chain but not action for more than one update.. ATB Bar should have queued an action or ended the chain')
+      // It's okay to be executing a chain but not any particular action if we
+      // are significantly stunned.
+      if (!this.stunIsSignificant) {
+        throw new Error('Executing chain but not action for more than one update.. ATB Bar should have queued an action or ended the chain')
+      }
     }
 
     if (this.actionExecuteTime) {
@@ -520,9 +581,19 @@ class BattleCharacter extends Sprite {
   }
 
   applySpriteEffects(ctx) {
+    let filter = ''
     if (this.dead) {
-      ctx.filter = 'grayscale(100%)'
+      filter += 'grayscale(100%) '
+    }
+    // TODO: Just for debugging, change this later
+    if (false && this.stunTime) {
+      if (this.stunIsSignificant) {
+        filter += 'blur(2px) '
+      } else {
+        filter += 'blur(1px) '
+      }
     }
+    ctx.filter = filter.trim()
   }
 
   executeAction(action) {
@@ -575,12 +646,36 @@ class BattleCharacter extends Sprite {
     this.hp = 0
     this.dead = true
 
+    this.stunTime = 0
+    this.stunIsSignificant = null
+
     for (const battleCharacter of this.battle.getAllBattleCharacters()) {
       if (battleCharacter.targetCharacter === this) {
         battleCharacter.determineNewTarget('enemy') // TODO: battleCharacter.targetType
       }
     }
   }
+
+  addStunTime(time, forceSignificant = false) {
+    // Takes the optional argument forceSignificant. This should almost always
+    // be left as false, even if a move seems like it should be considered
+    // "heavy" (e.g. getting whacked by the swipe of a huge claw). Instead, let
+    // the logic in this function decide whether a stun is significant. Doing
+    // so lets the character's own stun threshold be taken into account.
+
+    this.stunTime += time
+
+    // TODO: Generate this somehow.
+    const significantStunThreshold = 1
+
+    if (forceSignificant || this.stunTime >= significantStunThreshold) {
+      // Only set stunIsSignificant here. ATBBar will control the resulting
+      // effects of having been significantly stunned.
+      this.stunIsSignificant = true
+    } else {
+      this.stunIsSignificant = false
+    }
+  }
 }
 
 class Battle {
@@ -885,6 +980,7 @@ class MagicProjectile {
     if (Math.abs(this.target.x - this.x) <= 5 && Math.abs(this.target.y - this.y) <= 5) {
       if (['fire', 'blizz'].includes(this.action.chain)) {
         this.target.takeDamage(this.action.size * 20)
+        this.target.addStunTime([0.1, 0.25, 0.4][this.action.size - 1])
       } else if (this.action.id === 'cure') {
         this.target.recoverHP(200)
       }