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 | 96x 96x 96x 96x 96x 66x 66x 66x 66x 66x 25x 25x 25x 25x 25x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 96x | /**
* useCombatLayout Hook - Enhanced Responsive Combat Layout
*
* Custom hook for managing responsive combat screen layout calculations with
* comprehensive support for all screen sizes from mobile to ultra-wide displays.
*
* Enhanced Features:
* - Five screen size categories (mobile, tablet, desktop, large, xlarge)
* - Proportional scaling for consistent sizing across devices
* - Optimized arena sizing for each device category
* - Smooth transitions for resize operations
* - 60fps performance maintained
*
* Uses robust device detection combining user-agent and screen size to ensure
* mobile controls are shown on all mobile devices, including high-resolution phones.
*
* Performance:
* - Reduces recalculations by checking only breakpoint changes, not exact dimensions
* - Memoizes arena bounds to prevent cascading re-renders
* - Targets <1ms execution time for layout calculations
*
* @param width - Screen width
* @param height - Screen height
*
* @returns Layout constants and arena bounds
*
* @example
* ```typescript
* const { layoutConstants, arenaBounds, isMobile, screenSize } = useCombatLayout(1200, 800);
* ```
*/
import { useMemo } from "react";
import { getScreenSize } from "../../../../systems/ResponsiveScaling";
import { calculateArenaWorldDimensions } from "../../../../utils/arenaWorldDimensions";
import { shouldUseMobileControls } from "../../../../utils/deviceDetection";
import { calculateMobileAreaBounds } from "../../../../utils/mobileLayoutHelpers";
import {
PORTRAIT_FORCE_MAX_WIDTH_PX,
PORTRAIT_HYSTERESIS_FACTOR,
portraitMobileControlsBottomBand,
} from "../../../../utils/responsiveOrientationConstants";
import { getCombatLayoutConstants } from "../../../../utils/responsiveLayoutHelpers";
import type { ScreenSize } from "../../../../systems/ResponsiveScaling";
export interface LayoutConstants {
readonly padding: number;
readonly hudHeight: number;
readonly controlsHeight: number;
readonly footerHeight: number;
readonly healthBarHeight: number;
}
export interface ArenaBounds {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
readonly scale: number; // 3D scale factor for arena (1.0 = desktop, <1.0 = mobile)
readonly worldWidthMeters: number; // Physical arena width in meters
readonly worldDepthMeters: number; // Physical arena depth in meters
}
export interface CombatLayout {
readonly layoutConstants: LayoutConstants;
readonly arenaBounds: ArenaBounds;
readonly isMobile: boolean;
readonly isPortrait: boolean;
readonly screenSize: ScreenSize;
}
/**
* Custom hook for combat screen layout calculations
* Enhanced with centralized responsive scaling system
* Optimized to reduce recalculations and improve 60fps performance
*/
export function useCombatLayout(width: number, height: number): CombatLayout {
// Determine screen size category using centralized scaling system
const screenSize = useMemo(() => getScreenSize(width), [width]);
// Portrait orientation detection. The hysteresis factor provides stability
// so viewports near 1:1 don't flap on every resize event.
// 세로 모드 감지
const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;
// Device detection has its own internal caching based on screen dimensions.
// In addition to its user-agent result we force the mobile branch for any
// narrow portrait viewport so that devtools emulation and real rotated
// phones both render the mobile-optimized layout.
// 모바일 레이아웃 강제: 세로 + 좁은 화면
const isMobile =
shouldUseMobileControls() ||
(isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);
// Centralized layout constants for easier tweaking
// Enhanced with tablet-specific values for better responsive support
// Updated mobile controls height for new sizing: D-Pad (140px), buttons (80px+70px)
// Uses centralized responsive helper for consistent scaling
// Now passes isMobile flag to ensure high-res mobile devices get mobile layouts
const layoutConstants = useMemo<LayoutConstants>(
() => getCombatLayoutConstants(width, isMobile),
[width, isMobile],
);
// Arena bounds calculation using physics-first aspect-ratio sizing
// Landscape mobile: 4:3 (width > height)
// Portrait mobile: 3:4 (height > width) — fits both fighters vertically
// without being occluded by bottom HUD + D-Pad
const arenaBounds = useMemo<ArenaBounds>(() => {
// In portrait mobile we render a compact two-player status strip
// directly below the top HUD to replace the collapsed side HUDs.
// Reserve its height here so the arena is pushed below it instead of
// being drawn underneath. Use a tighter strip on extra-small phones
// (< 380 px wide) to preserve the playable arena area.
const isExtraSmallWidth = width < 380;
const portraitStatusStripHeight =
isMobile && isPortrait
? Math.max(isExtraSmallWidth ? 28 : 36, Math.round(height * 0.055))
: 0;
const arenaY =
layoutConstants.hudHeight +
portraitStatusStripHeight +
layoutConstants.padding;
// Calculate world dimensions based on screen resolution (not device type)
// All arenas are SQUARE for consistent combat mechanics
const worldDimensions = calculateArenaWorldDimensions(width);
// Mobile-specific arena sizing for better screen fit
if (isMobile) {
const isExtraSmall = isExtraSmallWidth;
const minTopClearance =
(isExtraSmall ? 75 : 80) + portraitStatusStripHeight;
// In portrait we must reserve space for the whole bottom band
// (technique bar + mobile controls + footer) or the arena ends up
// behind the D-Pad. See responsiveOrientationConstants.ts for the
// derivation of the mobile-controls reservation.
const minBottomClearance = isPortrait
? portraitMobileControlsBottomBand(
layoutConstants.controlsHeight,
layoutConstants.footerHeight,
isExtraSmall,
"combat",
)
: isExtraSmall
? 110
: 120;
const mobileBounds = calculateMobileAreaBounds(
width,
height,
minTopClearance,
minBottomClearance,
arenaY,
isPortrait ? "portrait" : "landscape",
);
// Mobile bounds already include world dimensions from resolution
return mobileBounds;
}
// Desktop arena sizing - create 4:3 aspect ratio arena
const totalReservedHeight =
layoutConstants.hudHeight +
layoutConstants.controlsHeight +
layoutConstants.footerHeight;
const totalPadding = layoutConstants.padding * 3;
const availableHeight = height - totalReservedHeight - totalPadding;
const availableWidth = width * 0.8;
// Calculate arena dimensions with 4:3 aspect ratio (width > height)
// Start with available width, constrain by height if needed
let arenaWidth = availableWidth;
let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio
// If height is constrained, recalculate from height
Eif (arenaHeight > availableHeight) {
arenaHeight = availableHeight;
arenaWidth = arenaHeight * (4 / 3);
}
// Calculate pixels-per-meter and scale
const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;
const referencePixelsPerMeter = 100;
const scale = pixelsPerMeter / referencePixelsPerMeter;
return {
x: (width - arenaWidth) / 2, // Center horizontally
y: arenaY,
width: arenaWidth,
height: arenaHeight, // 4:3 aspect ratio
scale,
worldWidthMeters: worldDimensions.widthMeters,
worldDepthMeters: worldDimensions.depthMeters,
};
}, [width, height, layoutConstants, isMobile, isPortrait]);
return {
layoutConstants,
arenaBounds,
isMobile,
isPortrait,
screenSize,
};
}
|