All files / utils mobileLayoutHelpers.ts

100% Statements 20/20
100% Branches 12/12
100% Functions 1/1
100% Lines 20/20

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                                                                                                                              77x 77x 77x                   77x 6x 71x 47x 24x 3x 21x 5x     16x       77x       77x       77x       77x       77x 77x 77x   77x                    
/**
 * Mobile Layout Helpers
 *
 * Shared utilities for calculating mobile area bounds with consistent aspect ratios
 * and device-specific sizing. Used by both combat and training layout hooks.
 *
 * @module utils/mobileLayoutHelpers
 * @category Layout
 * @korean 모바일레이아웃도우미
 */
 
import { calculateArenaWorldDimensions } from "./arenaWorldDimensions";
 
/**
 * Mobile area bounds with world dimensions.
 *
 * @public
 */
export interface MobileAreaBounds {
  readonly x: number;
  readonly y: number;
  readonly width: number;
  readonly height: number;
  readonly scale: number;
  readonly worldWidthMeters: number;
  readonly worldDepthMeters: number;
}
 
/**
 * Calculate mobile area bounds with 4:3 aspect ratio
 *
 * Implements consistent mobile area sizing logic shared between combat and training screens.
 * Adapts to different device resolutions while maintaining a 4:3 aspect ratio.
 * Enhanced with extra-small device support (<380px) for low-end mobile devices.
 *
 * @param width - Screen width in pixels
 * @param height - Screen height in pixels
 * @param topClearance - Minimum space to reserve at top (for HUD/header)
 * @param bottomClearance - Minimum space to reserve at bottom (for controls)
 * @param yOffset - Y position offset (typically header height + padding)
 * @returns Mobile area bounds with position, dimensions, scale, and world dimensions
 *
 * @example
 * ```typescript
 * const bounds = calculateMobileAreaBounds(375, 667, 80, 120, 100);
 * // Returns: { x: ~37, y: 100, width: 300, height: 225, scale: 0.3125, worldWidthMeters: 6, worldDepthMeters: 6 }
 *
 * const boundsSmall = calculateMobileAreaBounds(320, 568, 75, 110, 90);
 * // Returns: { x: ~25, y: 90, width: 270, height: 202, scale: 0.28125, worldWidthMeters: 6, worldDepthMeters: 6 }
 * ```
 *
 * @public
 * @korean 모바일영역경계계산
 */
export function calculateMobileAreaBounds(
  width: number,
  height: number,
  topClearance: number,
  bottomClearance: number,
  yOffset: number,
): MobileAreaBounds {
  // Calculate available space for the area
  // Extra-small devices (<380px) use tighter margins for more screen real estate
  const horizontalMargin = width < 380 ? 30 : 40; // 15px vs 20px per side
  const availableHeight = height - topClearance - bottomClearance;
  const availableWidth = width - horizontalMargin;
 
  // Determine max width based on device resolution
  // Device-specific sizing with extra-small support:
  // - 4K/QHD+ (≥1440px): up to 800px
  // - 2K (1200-1439px): up to 600px
  // - Large phones (768-1199px): up to 500px
  // - Standard phones (380-767px): up to 400px
  // - Extra-small phones (<380px): up to 320px
  let maxMobileWidth: number;
  if (width >= 1440) {
    maxMobileWidth = Math.min(availableWidth, 800);
  } else if (width >= 1200) {
    maxMobileWidth = Math.min(availableWidth, 600);
  } else if (width >= 768) {
    maxMobileWidth = Math.min(availableWidth, 500);
  } else if (width >= 380) {
    maxMobileWidth = Math.min(availableWidth, 400);
  } else {
    // Extra-small devices (iPhone SE, old Android, budget phones)
    maxMobileWidth = Math.min(availableWidth, 320);
  }
 
  // Extra-small devices also get reduced max height for better fit
  const maxMobileHeight = Math.min(availableHeight, width < 380 ? 240 : 800);
 
  // Use 4:3 aspect ratio for arena (width > height)
  // Calculate area width first, constrained by available space
  const areaWidth = Math.max(
    Math.min(maxMobileWidth, maxMobileHeight * (4 / 3)),
    280,
  );
  const areaHeight = areaWidth * (3 / 4); // 4:3 aspect ratio
 
  // Calculate world dimensions based on RENDERED arena width (not screen width)
  // This ensures correct pixels-per-meter ratio for the actual visible arena
  const worldDimensions = calculateArenaWorldDimensions(areaWidth);
 
  // Calculate 3D scale factor based on reference arena
  // Reference: 10m arena at 1000px = 100 px/m
  const pixelsPerMeter = areaWidth / worldDimensions.widthMeters;
  const referencePixelsPerMeter = 100;
  const scale = pixelsPerMeter / referencePixelsPerMeter;
 
  return {
    x: (width - areaWidth) / 2, // Centered horizontally
    y: yOffset,
    width: areaWidth,
    height: areaHeight, // 4:3 aspect ratio
    scale,
    worldWidthMeters: worldDimensions.widthMeters,
    worldDepthMeters: worldDimensions.depthMeters,
  };
}