All files / utils combatReadiness.ts

100% Statements 54/54
97.43% Branches 38/39
100% Functions 6/6
100% Lines 52/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209                                    4x                     4x                             105x 2x     103x                   721x 103x   103x                                               1131x 4x           1127x 99x     1028x 1028x   1127x     1127x 1127x     1127x 1127x     1127x 1127x     1127x   1127x                     192x 1x     191x 167x   24x 6x   18x 6x   12x 6x   6x                     182x 1x     181x 165x   16x 4x   12x 4x   8x 4x   4x                                             195x 1x   194x 3x     191x     191x     188x    
/**
 * Combat Readiness Calculation System
 * 
 * Calculates overall combat readiness from multiple factors:
 * - Body part health (40% weight)
 * - Pain level (20% weight)
 * - Consciousness (20% weight)
 * - Balance state (20% weight)
 * 
 * @module utils/combatReadiness
 */
 
import type { PlayerState } from "../systems/player";
import type { BodyPartHealth } from "../systems/bodypart/types";
 
/**
 * Combat readiness level thresholds and colors
 */
export const COMBAT_READINESS_THRESHOLDS = {
  FULL_CAPABILITY: { min: 80, max: 100, color: 0x00ff00, label: { korean: "전투 준비", english: "Combat Ready" } },
  LIGHT_IMPAIRMENT: { min: 60, max: 79, color: 0xffff00, label: { korean: "경미 손상", english: "Light Damage" } },
  MODERATE_IMPAIRMENT: { min: 40, max: 59, color: 0xff8800, label: { korean: "중간 손상", english: "Moderate Damage" } },
  HEAVY_IMPAIRMENT: { min: 20, max: 39, color: 0xff3333, label: { korean: "중증 손상", english: "Heavy Damage" } },
  CRITICAL: { min: 0, max: 19, color: 0x990000, label: { korean: "위급 상태", english: "Critical" } },
} as const;
 
/**
 * Weight factors for combat readiness calculation
 */
const READINESS_WEIGHTS = {
  BODY_HEALTH: 0.4,  // 40% - Primary survival metric
  PAIN: 0.2,         // 20% - Affects performance
  CONSCIOUSNESS: 0.2, // 20% - Awareness and response
  BALANCE: 0.2,      // 20% - Stability and mobility
} as const;
 
/**
 * Calculate average body part health percentage
 * 
 * @param bodyHealth - Body part health state
 * @returns Average health percentage (0-100)
 * @throws {Error} If bodyHealth is null or undefined
 */
export function calculateBodyHealthPercentage(bodyHealth: BodyPartHealth): number {
  if (!bodyHealth) {
    throw new Error("bodyHealth cannot be null or undefined");
  }
 
  const parts = [
    bodyHealth.head,
    bodyHealth.torsoUpper,
    bodyHealth.torsoLower,
    bodyHealth.armLeft,
    bodyHealth.armRight,
    bodyHealth.legLeft,
    bodyHealth.legRight,
  ];
  
  const total = parts.reduce((sum, hp) => sum + hp, 0);
  const average = total / parts.length;
  
  return Math.max(0, Math.min(100, average));
}
 
/**
 * Calculate combat readiness from player state
 * 
 * Combines multiple factors with weighted importance:
 * - Body part health (40%): Average health of all body parts
 * - Pain (20%): Inverted pain level (0 pain = 100% contribution)
 * - Consciousness (20%): Direct consciousness level
 * - Balance (20%): Direct balance level
 * 
 * @param player - Current player state
 * @returns Combat readiness percentage (0-100)
 * @throws {Error} If player is null or undefined
 * 
 * @example
 * ```typescript
 * const readiness = calculateCombatReadiness(playerState);
 * console.log(`Combat Readiness: ${readiness}%`);
 * // Output: "Combat Readiness: 85%"
 * ```
 */
export function calculateCombatReadiness(player: PlayerState): number {
  if (!player) {
    throw new Error("player cannot be null or undefined");
  }
 
  // 1. Body health contribution (40%)
  // Use bodyPartHealth if available, otherwise use aggregate health
  let bodyHealthPercent: number;
  if (player.bodyPartHealth) {
    bodyHealthPercent = calculateBodyHealthPercentage(player.bodyPartHealth);
  } else {
    // Fall back to aggregate health percentage
    const maxHealth = player.maxHealth || 100; // Prevent division by zero
    bodyHealthPercent = maxHealth > 0 ? (player.health / maxHealth) * 100 : 0;
  }
  const bodyHealthScore = bodyHealthPercent * READINESS_WEIGHTS.BODY_HEALTH;
  
  // 2. Pain contribution (20%) - inverted (less pain = better readiness)
  const painPercent = Math.max(0, Math.min(100, player.pain));
  const painScore = (100 - painPercent) * READINESS_WEIGHTS.PAIN;
  
  // 3. Consciousness contribution (20%)
  const consciousnessPercent = Math.max(0, Math.min(100, player.consciousness));
  const consciousnessScore = consciousnessPercent * READINESS_WEIGHTS.CONSCIOUSNESS;
  
  // 4. Balance contribution (20%)
  const balancePercent = Math.max(0, Math.min(100, player.balance));
  const balanceScore = balancePercent * READINESS_WEIGHTS.BALANCE;
  
  // Combine all factors
  const totalReadiness = bodyHealthScore + painScore + consciousnessScore + balanceScore;
  
  return Math.max(0, Math.min(100, Math.round(totalReadiness)));
}
 
/**
 * Get color for combat readiness level
 * 
 * @param readiness - Combat readiness percentage (0-100)
 * @returns Hex color code
 * @throws {Error} If readiness is NaN
 */
export function getCombatReadinessColor(readiness: number): number {
  if (Number.isNaN(readiness)) {
    throw new Error("readiness cannot be NaN");
  }
 
  if (readiness >= COMBAT_READINESS_THRESHOLDS.FULL_CAPABILITY.min) {
    return COMBAT_READINESS_THRESHOLDS.FULL_CAPABILITY.color;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.LIGHT_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.LIGHT_IMPAIRMENT.color;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.MODERATE_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.MODERATE_IMPAIRMENT.color;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.HEAVY_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.HEAVY_IMPAIRMENT.color;
  }
  return COMBAT_READINESS_THRESHOLDS.CRITICAL.color;
}
 
/**
 * Get label for combat readiness level
 * 
 * @param readiness - Combat readiness percentage (0-100)
 * @returns Korean and English labels
 * @throws {Error} If readiness is NaN
 */
export function getCombatReadinessLabel(readiness: number): { korean: string; english: string } {
  if (Number.isNaN(readiness)) {
    throw new Error("readiness cannot be NaN");
  }
 
  if (readiness >= COMBAT_READINESS_THRESHOLDS.FULL_CAPABILITY.min) {
    return COMBAT_READINESS_THRESHOLDS.FULL_CAPABILITY.label;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.LIGHT_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.LIGHT_IMPAIRMENT.label;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.MODERATE_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.MODERATE_IMPAIRMENT.label;
  }
  if (readiness >= COMBAT_READINESS_THRESHOLDS.HEAVY_IMPAIRMENT.min) {
    return COMBAT_READINESS_THRESHOLDS.HEAVY_IMPAIRMENT.label;
  }
  return COMBAT_READINESS_THRESHOLDS.CRITICAL.label;
}
 
/**
 * Get number of filled bars for combat readiness
 * 
 * Uses ceiling behavior: any non-zero readiness shows at least 1 bar.
 * This provides visual feedback even for minimal combat capability.
 * 
 * @param readiness - Combat readiness percentage (0-100)
 * @param totalBars - Total number of bars (default: 10)
 * @returns Number of filled bars (0-totalBars)
 * @throws {Error} If readiness is NaN or totalBars is not positive
 * 
 * @example
 * ```typescript
 * getCombatReadinessBars(0, 10);   // Returns 0 bars
 * getCombatReadinessBars(5, 10);   // Returns 1 bar (ceil behavior)
 * getCombatReadinessBars(50, 10);  // Returns 5 bars
 * getCombatReadinessBars(100, 10); // Returns 10 bars
 * ```
 */
export function getCombatReadinessBars(readiness: number, totalBars: number = 10): number {
  if (Number.isNaN(readiness)) {
    throw new Error("readiness cannot be NaN");
  }
  if (totalBars <= 0 || !Number.isInteger(totalBars)) {
    throw new Error("totalBars must be a positive integer");
  }
 
  const percentage = Math.max(0, Math.min(100, readiness));
  
  // Return 0 bars only for exactly 0% readiness
  if (percentage === 0) return 0;
  
  // Use ceiling for any non-zero value to provide visual feedback
  return Math.ceil((percentage / 100) * totalBars);
}