@almadar/ui 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,23 +2,22 @@ import { useAuthContext } from '../chunk-2QM732NQ.js';
2
2
  export { ENTITY_EVENTS, useAgentChat, useAuthContext, useCompile, useConnectGitHub, useCreateEntity, useDeepAgentGeneration, useDeleteEntity, useDisconnectGitHub, useEntities, useEntitiesByType, useEntity as useEntityById, useEntityMutations, useExtensions, useFileEditor, useFileSystem, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useInput, useOrbitalHistory, useOrbitalMutations, usePhysics, usePinchZoom, usePlayer, usePreview, useResolvedEntity, useSelectedEntity, useSendOrbitalEvent, useSingletonEntity, useUIEvents, useUpdateEntity, useValidation } from '../chunk-2QM732NQ.js';
3
3
  import { DEFAULT_CONFIG, renderStateMachineToDomData, parseContentSegments } from '../chunk-N6DJVKZ6.js';
4
4
  import '../chunk-3HJHHULT.js';
5
- import { VStack, HStack, Typography, Button, Icon, Box, Card, Avatar, Badge, SearchInput, Checkbox, Menu as Menu$1, Pagination, LoadingState, EmptyState, Modal, ErrorState, QuizBlock, CodeBlock, ScaledDiagram, MarkdownContent, Divider, ProgressBar, Stack, StatBadge, Select, Drawer, Toast, Tabs, Input, ThemeToggle, EntityDisplayEvents, StateIndicator, Container } from '../chunk-VJP2HCLY.js';
6
- export { Accordion, ActionButtons, Card2 as ActionCard, Alert, AnimatedCounter, Avatar, Badge, Box, Breadcrumb, Button, ButtonGroup, CalendarGrid, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, Center, ChartLegend, Checkbox, CodeBlock, ConditionalWrapper, ConfettiEffect, Container, ControlButton, DPad, DataGrid, DataList, DataTable, DateRangeSelector, DayCell, DetailPanel, Divider, Drawer, EmptyState, EntityDisplayEvents, ErrorBoundary, ErrorState, FilterGroup, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormField, FormSectionHeader, GraphView, Grid, HStack, Heading, HealthBar, Icon, InfiniteScrollSentinel, Input, InputGroup, Label, LawReferenceTooltip, Lightbox, LineChart, LoadingState, MapView, MarkdownContent, MasterDetail, Menu, Meter, Modal, NumberStepper, Overlay, PageHeader, Pagination, Popover, ProgressBar, ProgressDots, PullToRefresh, QuizBlock, Radio, RangeSlider, RelationSelect, RepeatableFormSection, ScaledDiagram, ScoreDisplay, SearchInput, Select, SidePanel, SimpleGrid, Skeleton, SlotContentRenderer, SortableList, Spacer, Spinner, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateIndicator, StatusDot, SwipeableRow, Switch, Tabs, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, TimeSlotCell, Toast, Tooltip, TrendIndicator, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UploadDropZone, VStack, ViolationAlert, WizardNavigation, WizardProgress, drawSprite } from '../chunk-VJP2HCLY.js';
5
+ import { VStack, HStack, Typography, Button, Icon, Box, Card, Avatar, Badge, SearchInput, Checkbox, Menu as Menu$1, Pagination, LoadingState, EmptyState, Modal, ErrorState, QuizBlock, CodeBlock, ScaledDiagram, MarkdownContent, Divider, ProgressBar, isoToScreen, IsometricCanvas_default, Stack, Select, Drawer, Toast, Tabs, Input, ThemeToggle, TILE_WIDTH, EntityDisplayEvents, StateIndicator, Container } from '../chunk-NES4SBB7.js';
6
+ export { Accordion, ActionButton, ActionButtons, Card2 as ActionCard, Alert, AnimatedCounter, Avatar, Badge, Box, Breadcrumb, Button, ButtonGroup, CalendarGrid, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, Center, ChartLegend, Checkbox, ChoiceButton, CodeBlock, CombatLog, ComboCounter, ConditionalWrapper, ConfettiEffect, Container, ControlButton, CraftingRecipe, DIAMOND_TOP_Y, DPad, DamageNumber, DataGrid, DataList, DataTable, DateRangeSelector, DayCell, DetailPanel, DialogueBox, DialogueBubble, Divider, Drawer, EmptyState, EnemyPlate, EntityDisplayEvents, ErrorBoundary, ErrorState, FEATURE_COLORS, FLOOR_HEIGHT, FilterGroup, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormField, FormSectionHeader, GameCanvas2D, GameHud, GameMenu, GameOverScreen, GraphView, Grid, HStack, Heading, HealthBar, HealthPanel, Icon, InfiniteScrollSentinel, Input, InputGroup, InventoryGrid, InventoryPanel, IsometricCanvas, ItemSlot, Label, LawReferenceTooltip, Lightbox, LineChart, LoadingState, MapView, MarkdownContent, MasterDetail, Menu, Meter, MiniMap, Modal, NumberStepper, Overlay, PageHeader, Pagination, PlatformerCanvas, Popover, PowerupSlots, ProgressBar, ProgressDots, PullToRefresh, QuestTracker, QuizBlock, Radio, RangeSlider, RelationSelect, RepeatableFormSection, ResourceBar, ResourceCounter, ScaledDiagram, ScoreBoard, ScoreDisplay, SearchInput, Select, SidePanel, SimpleGrid, Skeleton, SlotContentRenderer, SortableList, Spacer, Spinner, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateIndicator, StatusDot, StatusEffect, SwipeableRow, Switch, TILE_HEIGHT, TILE_WIDTH, Tabs, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, TimeSlotCell, TimerDisplay, Toast, Tooltip, TrendIndicator, TurnIndicator, TurnPanel, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UnitCommandBar, UploadDropZone, VStack, ViolationAlert, WaypointMarker, WizardNavigation, WizardProgress, XPBar, drawSprite, isoToScreen, screenToIso, useCamera, useImageCache } from '../chunk-NES4SBB7.js';
7
7
  import '../chunk-DKQN5FVU.js';
8
8
  import { useTranslate } from '../chunk-WGJIL4YR.js';
9
9
  export { EntityDataProvider, I18nProvider, createTranslate, entityDataKeys, parseQueryBinding, useDragReorder, useEntity, useEntityDataAdapter, useEntityDetail, useEntityList, useEntityListSuspense, useEntitySuspense, useInfiniteScroll, useLongPress, usePullToRefresh, useQuerySingleton, useSwipeGesture, useTranslate } from '../chunk-WGJIL4YR.js';
10
10
  import { useEventBus, useEventListener } from '../chunk-YXZM3WCF.js';
11
11
  export { useEmitEvent, useEventBus, useEventListener } from '../chunk-YXZM3WCF.js';
12
12
  export { DEFAULT_SLOTS, useUISlotManager } from '../chunk-3JGAROCW.js';
13
- import { cn, getNestedValue, updateAssetStatus, bindCanvasCapture } from '../chunk-RPYMP7ZC.js';
14
- export { cn } from '../chunk-RPYMP7ZC.js';
13
+ import { cn, getNestedValue, bindCanvasCapture } from '../chunk-A5J5CNCU.js';
14
+ export { cn } from '../chunk-A5J5CNCU.js';
15
15
  import '../chunk-TSETXL2E.js';
16
16
  import '../chunk-K2D5D3WK.js';
17
17
  export { clearEntities, getAllEntities, getByType, getEntity, getSingleton, removeEntity, spawnEntity, updateEntity, updateSingleton } from '../chunk-N7MVUW4R.js';
18
18
  import { __publicField } from '../chunk-PKBMQBKP.js';
19
- import * as React from 'react';
20
- import React__default, { createContext, useState, useCallback, useMemo, useEffect, useRef, useContext } from 'react';
21
- import { ChevronDown, X, Menu, ChevronRight, ChevronLeft, ArrowUp, ArrowDown, MoreVertical, Package, Check, AlertTriangle, Trash2, List as List$1, Printer, CheckCircle, XCircle, Play, RotateCcw, Send, Wrench, Bug, ArrowRight, Pause, SkipForward, Zap, Sword, Move, Heart, Shield, AlertCircle, Circle, Clock, CheckCircle2, Image as Image$1, Upload, ZoomIn, Eraser, FileText, ZoomOut, Download, Code, WrapText, Copy, Settings, Search, Bell, LogOut, Calendar, Pencil, Eye, MoreHorizontal, Minus, Plus } from 'lucide-react';
19
+ import React, { createContext, useState, useCallback, useMemo, useEffect, useRef, useContext } from 'react';
20
+ import { ChevronDown, X, Menu, ChevronRight, ChevronLeft, ArrowUp, ArrowDown, MoreVertical, Package, Check, AlertTriangle, Trash2, List as List$1, Printer, CheckCircle, XCircle, Play, RotateCcw, Send, Wrench, Bug, ArrowRight, Pause, SkipForward, AlertCircle, Circle, Clock, CheckCircle2, Image as Image$1, Upload, ZoomIn, Eraser, FileText, ZoomOut, Download, Code, WrapText, Copy, Settings, Search, Bell, LogOut, Calendar, Pencil, Eye, MoreHorizontal, Minus, Plus } from 'lucide-react';
22
21
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
23
22
  import { createPortal } from 'react-dom';
24
23
  import { useLocation, Link, Outlet } from 'react-router-dom';
@@ -33,7 +32,7 @@ var FormSection = ({
33
32
  columns = 1,
34
33
  className
35
34
  }) => {
36
- const [collapsed, setCollapsed] = React__default.useState(defaultCollapsed);
35
+ const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
37
36
  const { t } = useTranslate();
38
37
  const eventBus = useEventBus();
39
38
  const gridClass = {
@@ -41,7 +40,7 @@ var FormSection = ({
41
40
  2: "grid-cols-1 md:grid-cols-2",
42
41
  3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
43
42
  }[columns];
44
- React__default.useCallback(() => {
43
+ React.useCallback(() => {
45
44
  if (collapsible) {
46
45
  setCollapsed((prev) => !prev);
47
46
  eventBus.emit("UI:TOGGLE_COLLAPSE", { collapsed: !collapsed });
@@ -1013,7 +1012,7 @@ var List = ({
1013
1012
  if (entity && typeof entity === "object" && "id" in entity) return [entity];
1014
1013
  return [];
1015
1014
  }, [entity]);
1016
- const getItemActions = React__default.useCallback(
1015
+ const getItemActions = React.useCallback(
1017
1016
  (item) => {
1018
1017
  if (!itemActions) return [];
1019
1018
  if (typeof itemActions === "function") {
@@ -1518,7 +1517,7 @@ var WizardContainer = ({
1518
1517
  const isCompleted = index < currentStep;
1519
1518
  const stepKey = step.id ?? step.tabId ?? `step-${index}`;
1520
1519
  const stepTitle = step.title ?? step.name ?? `Step ${index + 1}`;
1521
- return /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
1520
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
1522
1521
  /* @__PURE__ */ jsx(
1523
1522
  Button,
1524
1523
  {
@@ -2742,7 +2741,7 @@ var StateMachineView = ({
2742
2741
  style: { top: title ? 30 : 0 },
2743
2742
  children: [
2744
2743
  entity && /* @__PURE__ */ jsx(EntityBox, { entity, config }),
2745
- states.map((state) => renderStateNode ? /* @__PURE__ */ jsx(React__default.Fragment, { children: renderStateNode(state, config) }, state.id) : /* @__PURE__ */ jsx(
2744
+ states.map((state) => renderStateNode ? /* @__PURE__ */ jsx(React.Fragment, { children: renderStateNode(state, config) }, state.id) : /* @__PURE__ */ jsx(
2746
2745
  StateNode,
2747
2746
  {
2748
2747
  state,
@@ -3746,1124 +3745,6 @@ var TabbedContainer = ({
3746
3745
  );
3747
3746
  };
3748
3747
  TabbedContainer.displayName = "TabbedContainer";
3749
- function useImageCache(urls) {
3750
- const cacheRef = useRef(/* @__PURE__ */ new Map());
3751
- const loadingRef = useRef(/* @__PURE__ */ new Set());
3752
- const [pendingCount, setPendingCount] = useState(0);
3753
- useEffect(() => {
3754
- const cache = cacheRef.current;
3755
- const loading = loadingRef.current;
3756
- let cancelled = false;
3757
- const newUrls = urls.filter((url) => url && !cache.has(url) && !loading.has(url));
3758
- if (newUrls.length === 0) return;
3759
- setPendingCount((prev) => prev + newUrls.length);
3760
- for (const url of newUrls) {
3761
- loading.add(url);
3762
- const img = new Image();
3763
- img.crossOrigin = "anonymous";
3764
- img.onload = () => {
3765
- if (cancelled) return;
3766
- cache.set(url, img);
3767
- loading.delete(url);
3768
- setPendingCount((prev) => Math.max(0, prev - 1));
3769
- updateAssetStatus(url, "loaded");
3770
- };
3771
- img.onerror = () => {
3772
- if (cancelled) return;
3773
- loading.delete(url);
3774
- setPendingCount((prev) => Math.max(0, prev - 1));
3775
- updateAssetStatus(url, "failed");
3776
- };
3777
- updateAssetStatus(url, "pending");
3778
- img.src = url;
3779
- }
3780
- return () => {
3781
- cancelled = true;
3782
- };
3783
- }, [urls.join(",")]);
3784
- const getImage = useCallback((url) => {
3785
- return cacheRef.current.get(url);
3786
- }, []);
3787
- return {
3788
- getImage,
3789
- isLoaded: pendingCount === 0,
3790
- pendingCount
3791
- };
3792
- }
3793
- function useCamera() {
3794
- const cameraRef = useRef({ x: 0, y: 0, zoom: 1 });
3795
- const targetCameraRef = useRef(null);
3796
- const isDraggingRef = useRef(false);
3797
- const dragDistanceRef = useRef(0);
3798
- const lastMouseRef = useRef({ x: 0, y: 0 });
3799
- const handleMouseDown = useCallback((e) => {
3800
- isDraggingRef.current = true;
3801
- dragDistanceRef.current = 0;
3802
- lastMouseRef.current = { x: e.clientX, y: e.clientY };
3803
- if (e.button === 1 || e.button === 2) {
3804
- e.preventDefault();
3805
- }
3806
- }, []);
3807
- const handleMouseUp = useCallback(() => {
3808
- isDraggingRef.current = false;
3809
- }, []);
3810
- const handleMouseMove = useCallback((e, drawFn) => {
3811
- if (!isDraggingRef.current) return false;
3812
- const dx = e.clientX - lastMouseRef.current.x;
3813
- const dy = e.clientY - lastMouseRef.current.y;
3814
- dragDistanceRef.current += Math.abs(dx) + Math.abs(dy);
3815
- cameraRef.current.x -= dx;
3816
- cameraRef.current.y -= dy;
3817
- lastMouseRef.current = { x: e.clientX, y: e.clientY };
3818
- targetCameraRef.current = null;
3819
- drawFn?.();
3820
- return dragDistanceRef.current > 5;
3821
- }, []);
3822
- const handleMouseLeave = useCallback(() => {
3823
- isDraggingRef.current = false;
3824
- }, []);
3825
- const handleWheel = useCallback((e, drawFn) => {
3826
- e.preventDefault();
3827
- const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
3828
- cameraRef.current.zoom = Math.max(0.5, Math.min(3, cameraRef.current.zoom * zoomDelta));
3829
- drawFn?.();
3830
- }, []);
3831
- const screenToWorld = useCallback((clientX, clientY, canvas, viewportSize) => {
3832
- const rect = canvas.getBoundingClientRect();
3833
- const screenX = clientX - rect.left;
3834
- const screenY = clientY - rect.top;
3835
- const cam = cameraRef.current;
3836
- const worldX = (screenX - viewportSize.width / 2) / cam.zoom + viewportSize.width / 2 + cam.x;
3837
- const worldY = (screenY - viewportSize.height / 2) / cam.zoom + viewportSize.height / 2 + cam.y;
3838
- return { x: worldX, y: worldY };
3839
- }, []);
3840
- const lerpToTarget = useCallback((t = 0.08) => {
3841
- if (!targetCameraRef.current) return false;
3842
- const cam = cameraRef.current;
3843
- cam.x += (targetCameraRef.current.x - cam.x) * t;
3844
- cam.y += (targetCameraRef.current.y - cam.y) * t;
3845
- if (Math.abs(cam.x - targetCameraRef.current.x) < 0.5 && Math.abs(cam.y - targetCameraRef.current.y) < 0.5) {
3846
- cam.x = targetCameraRef.current.x;
3847
- cam.y = targetCameraRef.current.y;
3848
- targetCameraRef.current = null;
3849
- }
3850
- return true;
3851
- }, []);
3852
- return {
3853
- cameraRef,
3854
- targetCameraRef,
3855
- isDragging: () => isDraggingRef.current,
3856
- dragDistance: () => dragDistanceRef.current,
3857
- handleMouseDown,
3858
- handleMouseUp,
3859
- handleMouseMove,
3860
- handleMouseLeave,
3861
- handleWheel,
3862
- screenToWorld,
3863
- lerpToTarget
3864
- };
3865
- }
3866
-
3867
- // components/organisms/game/utils/isometric.ts
3868
- var TILE_WIDTH = 256;
3869
- var TILE_HEIGHT = 512;
3870
- var FLOOR_HEIGHT = 128;
3871
- var DIAMOND_TOP_Y = 374;
3872
- var FEATURE_COLORS = {
3873
- goldMine: "#fbbf24",
3874
- resonanceCrystal: "#a78bfa",
3875
- traitCache: "#60a5fa",
3876
- salvageYard: "#6b7280",
3877
- portal: "#c084fc",
3878
- castle: "#f59e0b",
3879
- mountain: "#78716c",
3880
- default: "#9ca3af"
3881
- };
3882
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
3883
- const scaledTileWidth = TILE_WIDTH * scale;
3884
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
3885
- const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
3886
- const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
3887
- return { x: screenX, y: screenY };
3888
- }
3889
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
3890
- const scaledTileWidth = TILE_WIDTH * scale;
3891
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
3892
- const adjustedX = screenX - baseOffsetX;
3893
- const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
3894
- const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
3895
- return { x: Math.round(tileX), y: Math.round(tileY) };
3896
- }
3897
- function IsometricCanvas({
3898
- // Closed-circuit
3899
- className,
3900
- isLoading = false,
3901
- error = null,
3902
- entity,
3903
- // Grid data
3904
- tiles: tilesProp = [],
3905
- units = [],
3906
- features = [],
3907
- // Interaction state
3908
- selectedUnitId = null,
3909
- validMoves = [],
3910
- attackTargets = [],
3911
- hoveredTile = null,
3912
- // Event handlers
3913
- onTileClick,
3914
- onUnitClick,
3915
- onTileHover,
3916
- onTileLeave,
3917
- // Declarative event props
3918
- tileClickEvent,
3919
- unitClickEvent,
3920
- tileHoverEvent,
3921
- tileLeaveEvent,
3922
- // Rendering options
3923
- scale = 0.4,
3924
- debug = false,
3925
- backgroundImage,
3926
- showMinimap = true,
3927
- enableCamera = true,
3928
- unitScale = 1,
3929
- // Asset resolution
3930
- getTerrainSprite,
3931
- getFeatureSprite,
3932
- getUnitSprite,
3933
- resolveUnitFrame,
3934
- effectSpriteUrls = [],
3935
- onDrawEffects,
3936
- hasActiveEffects: hasActiveEffects2 = false,
3937
- // Tuning
3938
- diamondTopY: diamondTopYProp,
3939
- // Remote asset loading
3940
- assetBaseUrl,
3941
- assetManifest
3942
- }) {
3943
- const eventBus = useEventBus();
3944
- const { t } = useTranslate();
3945
- const canvasRef = useRef(null);
3946
- const containerRef = useRef(null);
3947
- const minimapRef = useRef(null);
3948
- const animTimeRef = useRef(0);
3949
- const rafIdRef = useRef(0);
3950
- const [viewportSize, setViewportSize] = useState({ width: 800, height: 600 });
3951
- useEffect(() => {
3952
- const el = containerRef.current;
3953
- if (!el) return;
3954
- if (typeof ResizeObserver === "undefined") return;
3955
- const observer = new ResizeObserver((entries) => {
3956
- const entry = entries[0];
3957
- if (entry) {
3958
- setViewportSize({
3959
- width: entry.contentRect.width || 800,
3960
- height: entry.contentRect.height || 600
3961
- });
3962
- }
3963
- });
3964
- observer.observe(el);
3965
- return () => observer.disconnect();
3966
- }, []);
3967
- const sortedTiles = useMemo(() => {
3968
- const tiles = [...tilesProp];
3969
- tiles.sort((a, b) => {
3970
- const depthA = a.x + a.y;
3971
- const depthB = b.x + b.y;
3972
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
3973
- });
3974
- return tiles;
3975
- }, [tilesProp]);
3976
- const gridWidth = useMemo(() => {
3977
- if (sortedTiles.length === 0) return 0;
3978
- return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
3979
- }, [sortedTiles]);
3980
- const gridHeight = useMemo(() => {
3981
- if (sortedTiles.length === 0) return 0;
3982
- return Math.max(...sortedTiles.map((t2) => t2.y)) + 1;
3983
- }, [sortedTiles]);
3984
- const scaledTileWidth = TILE_WIDTH * scale;
3985
- const scaledTileHeight = TILE_HEIGHT * scale;
3986
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
3987
- const effectiveDiamondTopY = diamondTopYProp ?? DIAMOND_TOP_Y;
3988
- const scaledDiamondTopY = effectiveDiamondTopY * scale;
3989
- const baseOffsetX = useMemo(() => {
3990
- return (gridHeight - 1) * (scaledTileWidth / 2);
3991
- }, [gridHeight, scaledTileWidth]);
3992
- const validMoveSet = useMemo(() => {
3993
- return new Set(validMoves.map((p2) => `${p2.x},${p2.y}`));
3994
- }, [validMoves]);
3995
- const attackTargetSet = useMemo(() => {
3996
- return new Set(attackTargets.map((p2) => `${p2.x},${p2.y}`));
3997
- }, [attackTargets]);
3998
- const resolveManifestUrl = useCallback((relativePath) => {
3999
- if (!relativePath) return void 0;
4000
- if (assetBaseUrl) return `${assetBaseUrl.replace(/\/$/, "")}${relativePath.startsWith("/") ? "" : "/"}${relativePath}`;
4001
- return relativePath;
4002
- }, [assetBaseUrl]);
4003
- const spriteUrls = useMemo(() => {
4004
- const urls = [];
4005
- for (const tile of sortedTiles) {
4006
- if (tile.terrainSprite) urls.push(tile.terrainSprite);
4007
- else if (getTerrainSprite) {
4008
- const url = getTerrainSprite(tile.terrain ?? "");
4009
- if (url) urls.push(url);
4010
- } else {
4011
- const url = resolveManifestUrl(assetManifest?.terrains?.[tile.terrain ?? ""]);
4012
- if (url) urls.push(url);
4013
- }
4014
- }
4015
- for (const feature of features) {
4016
- if (feature.sprite) urls.push(feature.sprite);
4017
- else if (getFeatureSprite) {
4018
- const url = getFeatureSprite(feature.type);
4019
- if (url) urls.push(url);
4020
- } else {
4021
- const url = resolveManifestUrl(assetManifest?.features?.[feature.type]);
4022
- if (url) urls.push(url);
4023
- }
4024
- }
4025
- for (const unit of units) {
4026
- if (unit.sprite) urls.push(unit.sprite);
4027
- else if (getUnitSprite) {
4028
- const url = getUnitSprite(unit);
4029
- if (url) urls.push(url);
4030
- } else if (unit.unitType) {
4031
- const url = resolveManifestUrl(assetManifest?.units?.[unit.unitType]);
4032
- if (url) urls.push(url);
4033
- }
4034
- }
4035
- if (assetManifest?.effects) {
4036
- for (const path of Object.values(assetManifest.effects)) {
4037
- const url = resolveManifestUrl(path);
4038
- if (url) urls.push(url);
4039
- }
4040
- }
4041
- urls.push(...effectSpriteUrls);
4042
- if (backgroundImage) urls.push(backgroundImage);
4043
- return [...new Set(urls.filter(Boolean))];
4044
- }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, backgroundImage, assetManifest, resolveManifestUrl]);
4045
- const { getImage } = useImageCache(spriteUrls);
4046
- useEffect(() => {
4047
- if (typeof window === "undefined") return;
4048
- const canvas = canvasRef.current;
4049
- if (!canvas) return;
4050
- bindCanvasCapture(() => canvas.toDataURL("image/png"));
4051
- return () => {
4052
- bindCanvasCapture(() => null);
4053
- };
4054
- }, []);
4055
- const {
4056
- cameraRef,
4057
- targetCameraRef,
4058
- isDragging,
4059
- dragDistance,
4060
- handleMouseDown,
4061
- handleMouseUp,
4062
- handleMouseMove,
4063
- handleMouseLeave,
4064
- handleWheel,
4065
- screenToWorld,
4066
- lerpToTarget
4067
- } = useCamera();
4068
- const resolveTerrainSpriteUrl = useCallback((tile) => {
4069
- return tile.terrainSprite || getTerrainSprite?.(tile.terrain ?? "") || resolveManifestUrl(assetManifest?.terrains?.[tile.terrain ?? ""]);
4070
- }, [getTerrainSprite, assetManifest, resolveManifestUrl]);
4071
- const resolveFeatureSpriteUrl = useCallback((featureType) => {
4072
- return getFeatureSprite?.(featureType) || resolveManifestUrl(assetManifest?.features?.[featureType]);
4073
- }, [getFeatureSprite, assetManifest, resolveManifestUrl]);
4074
- const resolveUnitSpriteUrl = useCallback((unit) => {
4075
- return unit.sprite || getUnitSprite?.(unit) || (unit.unitType ? resolveManifestUrl(assetManifest?.units?.[unit.unitType]) : void 0);
4076
- }, [getUnitSprite, assetManifest, resolveManifestUrl]);
4077
- const drawMinimap = useCallback(() => {
4078
- if (!showMinimap) return;
4079
- const miniCanvas = minimapRef.current;
4080
- if (!miniCanvas || sortedTiles.length === 0) return;
4081
- const mCtx = miniCanvas.getContext("2d");
4082
- if (!mCtx) return;
4083
- const mW = 150;
4084
- const mH = 100;
4085
- miniCanvas.width = mW;
4086
- miniCanvas.height = mH;
4087
- mCtx.clearRect(0, 0, mW, mH);
4088
- const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX));
4089
- const minX = Math.min(...allScreenPos.map((p2) => p2.x));
4090
- const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
4091
- const minY = Math.min(...allScreenPos.map((p2) => p2.y));
4092
- const maxY = Math.max(...allScreenPos.map((p2) => p2.y + scaledTileHeight));
4093
- const worldW = maxX - minX;
4094
- const worldH = maxY - minY;
4095
- const scaleM = Math.min(mW / worldW, mH / worldH) * 0.9;
4096
- const offsetMx = (mW - worldW * scaleM) / 2;
4097
- const offsetMy = (mH - worldH * scaleM) / 2;
4098
- for (const tile of sortedTiles) {
4099
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
4100
- const mx = (pos.x - minX) * scaleM + offsetMx;
4101
- const my = (pos.y - minY) * scaleM + offsetMy;
4102
- const mTileW = scaledTileWidth * scaleM;
4103
- const mFloorH = scaledFloorHeight * scaleM;
4104
- mCtx.fillStyle = tile.terrain === "water" ? "#3b82f6" : tile.terrain === "mountain" ? "#78716c" : "#4ade80";
4105
- mCtx.beginPath();
4106
- mCtx.moveTo(mx + mTileW / 2, my);
4107
- mCtx.lineTo(mx + mTileW, my + mFloorH / 2);
4108
- mCtx.lineTo(mx + mTileW / 2, my + mFloorH);
4109
- mCtx.lineTo(mx, my + mFloorH / 2);
4110
- mCtx.closePath();
4111
- mCtx.fill();
4112
- }
4113
- for (const unit of units) {
4114
- if (!unit.position) continue;
4115
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
4116
- const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
4117
- const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
4118
- mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
4119
- mCtx.beginPath();
4120
- mCtx.arc(mx, my, 3, 0, Math.PI * 2);
4121
- mCtx.fill();
4122
- }
4123
- const cam = cameraRef.current;
4124
- const vLeft = (cam.x - minX) * scaleM + offsetMx;
4125
- const vTop = (cam.y - minY) * scaleM + offsetMy;
4126
- const vW = viewportSize.width / cam.zoom * scaleM;
4127
- const vH = viewportSize.height / cam.zoom * scaleM;
4128
- mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
4129
- mCtx.lineWidth = 1;
4130
- mCtx.strokeRect(vLeft, vTop, vW, vH);
4131
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
4132
- const draw = useCallback((animTime) => {
4133
- const canvas = canvasRef.current;
4134
- if (!canvas) return;
4135
- const ctx = canvas.getContext("2d");
4136
- if (!ctx) return;
4137
- const dpr = window.devicePixelRatio || 1;
4138
- canvas.width = viewportSize.width * dpr;
4139
- canvas.height = viewportSize.height * dpr;
4140
- ctx.scale(dpr, dpr);
4141
- ctx.clearRect(0, 0, viewportSize.width, viewportSize.height);
4142
- if (backgroundImage) {
4143
- const bgImg = getImage(backgroundImage);
4144
- if (bgImg) {
4145
- const cam2 = cameraRef.current;
4146
- const patW = bgImg.naturalWidth;
4147
- const patH = bgImg.naturalHeight;
4148
- const startX = -(cam2.x % patW + patW) % patW;
4149
- const startY = -(cam2.y % patH + patH) % patH;
4150
- for (let y = startY - patH; y < viewportSize.height; y += patH) {
4151
- for (let x = startX - patW; x < viewportSize.width; x += patW) {
4152
- ctx.drawImage(bgImg, x, y);
4153
- }
4154
- }
4155
- }
4156
- } else {
4157
- ctx.fillStyle = "#1a1a2e";
4158
- ctx.fillRect(0, 0, viewportSize.width, viewportSize.height);
4159
- }
4160
- if (sortedTiles.length === 0) return;
4161
- ctx.save();
4162
- const cam = cameraRef.current;
4163
- ctx.translate(viewportSize.width / 2, viewportSize.height / 2);
4164
- ctx.scale(cam.zoom, cam.zoom);
4165
- ctx.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
4166
- const visLeft = cam.x - viewportSize.width / cam.zoom;
4167
- const visRight = cam.x + viewportSize.width * 2 / cam.zoom;
4168
- const visTop = cam.y - viewportSize.height / cam.zoom;
4169
- const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
4170
- for (const tile of sortedTiles) {
4171
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
4172
- if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
4173
- continue;
4174
- }
4175
- const spriteUrl = resolveTerrainSpriteUrl(tile);
4176
- const img = spriteUrl ? getImage(spriteUrl) : null;
4177
- if (img) {
4178
- ctx.drawImage(img, pos.x, pos.y, scaledTileWidth, scaledTileHeight);
4179
- } else {
4180
- const centerX = pos.x + scaledTileWidth / 2;
4181
- const topY = pos.y + scaledDiamondTopY;
4182
- ctx.fillStyle = tile.terrain === "water" ? "#3b82f6" : tile.terrain === "mountain" ? "#78716c" : tile.terrain === "stone" ? "#9ca3af" : "#4ade80";
4183
- ctx.beginPath();
4184
- ctx.moveTo(centerX, topY);
4185
- ctx.lineTo(pos.x + scaledTileWidth, topY + scaledFloorHeight / 2);
4186
- ctx.lineTo(centerX, topY + scaledFloorHeight);
4187
- ctx.lineTo(pos.x, topY + scaledFloorHeight / 2);
4188
- ctx.closePath();
4189
- ctx.fill();
4190
- ctx.strokeStyle = "rgba(0,0,0,0.2)";
4191
- ctx.lineWidth = 1;
4192
- ctx.stroke();
4193
- }
4194
- if (hoveredTile && hoveredTile.x === tile.x && hoveredTile.y === tile.y) {
4195
- const centerX = pos.x + scaledTileWidth / 2;
4196
- const topY = pos.y + scaledDiamondTopY;
4197
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
4198
- ctx.beginPath();
4199
- ctx.moveTo(centerX, topY);
4200
- ctx.lineTo(pos.x + scaledTileWidth, topY + scaledFloorHeight / 2);
4201
- ctx.lineTo(centerX, topY + scaledFloorHeight);
4202
- ctx.lineTo(pos.x, topY + scaledFloorHeight / 2);
4203
- ctx.closePath();
4204
- ctx.fill();
4205
- }
4206
- const tileKey = `${tile.x},${tile.y}`;
4207
- if (validMoveSet.has(tileKey)) {
4208
- const centerX = pos.x + scaledTileWidth / 2;
4209
- const topY = pos.y + scaledDiamondTopY;
4210
- const pulse = 0.15 + 0.1 * Math.sin(animTime * 4e-3);
4211
- ctx.fillStyle = `rgba(74, 222, 128, ${pulse})`;
4212
- ctx.beginPath();
4213
- ctx.moveTo(centerX, topY);
4214
- ctx.lineTo(pos.x + scaledTileWidth, topY + scaledFloorHeight / 2);
4215
- ctx.lineTo(centerX, topY + scaledFloorHeight);
4216
- ctx.lineTo(pos.x, topY + scaledFloorHeight / 2);
4217
- ctx.closePath();
4218
- ctx.fill();
4219
- }
4220
- if (attackTargetSet.has(tileKey)) {
4221
- const centerX = pos.x + scaledTileWidth / 2;
4222
- const topY = pos.y + scaledDiamondTopY;
4223
- const pulse = 0.2 + 0.15 * Math.sin(animTime * 5e-3);
4224
- ctx.fillStyle = `rgba(239, 68, 68, ${pulse})`;
4225
- ctx.beginPath();
4226
- ctx.moveTo(centerX, topY);
4227
- ctx.lineTo(pos.x + scaledTileWidth, topY + scaledFloorHeight / 2);
4228
- ctx.lineTo(centerX, topY + scaledFloorHeight);
4229
- ctx.lineTo(pos.x, topY + scaledFloorHeight / 2);
4230
- ctx.closePath();
4231
- ctx.fill();
4232
- }
4233
- if (debug) {
4234
- const centerX = pos.x + scaledTileWidth / 2;
4235
- const centerY = pos.y + scaledFloorHeight / 2 + scaledDiamondTopY;
4236
- ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
4237
- ctx.font = `${12 * scale * 2}px monospace`;
4238
- ctx.textAlign = "center";
4239
- ctx.fillText(`${tile.x},${tile.y}`, centerX, centerY + 4);
4240
- }
4241
- }
4242
- const sortedFeatures = [...features].sort((a, b) => {
4243
- const depthA = a.x + a.y;
4244
- const depthB = b.x + b.y;
4245
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
4246
- });
4247
- for (const feature of sortedFeatures) {
4248
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
4249
- if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
4250
- continue;
4251
- }
4252
- const spriteUrl = feature.sprite || resolveFeatureSpriteUrl(feature.type);
4253
- const img = spriteUrl ? getImage(spriteUrl) : null;
4254
- const centerX = pos.x + scaledTileWidth / 2;
4255
- const featureGroundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
4256
- const isCastle = feature.type === "castle";
4257
- const featureDrawH = isCastle ? scaledFloorHeight * 3.5 : scaledFloorHeight * 1.6;
4258
- const maxFeatureW = isCastle ? scaledTileWidth * 1.8 : scaledTileWidth * 0.7;
4259
- if (img) {
4260
- const ar = img.naturalWidth / img.naturalHeight;
4261
- let drawH = featureDrawH;
4262
- let drawW = featureDrawH * ar;
4263
- if (drawW > maxFeatureW) {
4264
- drawW = maxFeatureW;
4265
- drawH = maxFeatureW / ar;
4266
- }
4267
- const drawX = centerX - drawW / 2;
4268
- const drawY = featureGroundY - drawH;
4269
- ctx.drawImage(img, drawX, drawY, drawW, drawH);
4270
- } else {
4271
- const color = FEATURE_COLORS[feature.type] || FEATURE_COLORS.default;
4272
- ctx.beginPath();
4273
- ctx.arc(centerX, featureGroundY - 8 * scale, 16 * scale, 0, Math.PI * 2);
4274
- ctx.fillStyle = color;
4275
- ctx.fill();
4276
- ctx.strokeStyle = "rgba(0,0,0,0.5)";
4277
- ctx.lineWidth = 2;
4278
- ctx.stroke();
4279
- }
4280
- }
4281
- const unitsWithPosition = units.filter((u) => !!u.position);
4282
- const sortedUnits = [...unitsWithPosition].sort((a, b) => {
4283
- const depthA = a.position.x + a.position.y;
4284
- const depthB = b.position.x + b.position.y;
4285
- return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
4286
- });
4287
- for (const unit of sortedUnits) {
4288
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
4289
- if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
4290
- continue;
4291
- }
4292
- const isSelected = unit.id === selectedUnitId;
4293
- const centerX = pos.x + scaledTileWidth / 2;
4294
- const groundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
4295
- const breatheOffset = 0.8 * scale * (1 + Math.sin(animTime * 2e-3 + (unit.position.x * 3.7 + unit.position.y * 5.3)));
4296
- const unitSpriteUrl = resolveUnitSpriteUrl(unit);
4297
- const img = unitSpriteUrl ? getImage(unitSpriteUrl) : null;
4298
- const unitDrawH = scaledFloorHeight * 1.5 * unitScale;
4299
- const maxUnitW = scaledTileWidth * 0.6 * unitScale;
4300
- const ar = img ? img.naturalWidth / img.naturalHeight : 0.5;
4301
- let drawH = unitDrawH;
4302
- let drawW = unitDrawH * ar;
4303
- if (drawW > maxUnitW) {
4304
- drawW = maxUnitW;
4305
- drawH = maxUnitW / ar;
4306
- }
4307
- if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
4308
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
4309
- const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
4310
- const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
4311
- ctx.save();
4312
- ctx.globalAlpha = 0.25;
4313
- if (img) {
4314
- ctx.drawImage(img, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
4315
- } else {
4316
- const color = unit.team === "player" ? "#3b82f6" : unit.team === "enemy" ? "#ef4444" : "#6b7280";
4317
- ctx.beginPath();
4318
- ctx.arc(ghostCenterX, ghostGroundY - 16 * scale, 20 * scale, 0, Math.PI * 2);
4319
- ctx.fillStyle = color;
4320
- ctx.fill();
4321
- }
4322
- ctx.restore();
4323
- }
4324
- if (isSelected) {
4325
- const ringAlpha = 0.6 + 0.3 * Math.sin(animTime * 4e-3);
4326
- ctx.beginPath();
4327
- ctx.ellipse(centerX, groundY, drawW / 2 + 4 * scale, 12 * scale, 0, 0, Math.PI * 2);
4328
- ctx.strokeStyle = `rgba(0, 200, 255, ${ringAlpha})`;
4329
- ctx.lineWidth = 3;
4330
- ctx.stroke();
4331
- }
4332
- const frame = resolveUnitFrame?.(unit.id) ?? null;
4333
- const frameImg = frame ? getImage(frame.sheetUrl) : null;
4334
- if (frame && frameImg) {
4335
- const frameAr = frame.sw / frame.sh;
4336
- let fDrawH = unitDrawH;
4337
- let fDrawW = unitDrawH * frameAr;
4338
- if (fDrawW > maxUnitW) {
4339
- fDrawW = maxUnitW;
4340
- fDrawH = maxUnitW / frameAr;
4341
- }
4342
- const spriteY = groundY - fDrawH - (frame.applyBreathing ? breatheOffset : 0);
4343
- ctx.save();
4344
- if (unit.team) {
4345
- ctx.shadowColor = unit.team === "player" ? "rgba(0, 150, 255, 0.6)" : "rgba(255, 50, 50, 0.6)";
4346
- ctx.shadowBlur = 12 * scale;
4347
- }
4348
- if (frame.flipX) {
4349
- ctx.translate(centerX, 0);
4350
- ctx.scale(-1, 1);
4351
- ctx.drawImage(frameImg, frame.sx, frame.sy, frame.sw, frame.sh, -fDrawW / 2, spriteY, fDrawW, fDrawH);
4352
- } else {
4353
- ctx.drawImage(frameImg, frame.sx, frame.sy, frame.sw, frame.sh, centerX - fDrawW / 2, spriteY, fDrawW, fDrawH);
4354
- }
4355
- ctx.restore();
4356
- } else if (img) {
4357
- const spriteY = groundY - drawH - breatheOffset;
4358
- if (unit.team) {
4359
- ctx.save();
4360
- ctx.shadowColor = unit.team === "player" ? "rgba(0, 150, 255, 0.6)" : "rgba(255, 50, 50, 0.6)";
4361
- ctx.shadowBlur = 12 * scale;
4362
- ctx.drawImage(img, centerX - drawW / 2, spriteY, drawW, drawH);
4363
- ctx.restore();
4364
- } else {
4365
- ctx.drawImage(img, centerX - drawW / 2, spriteY, drawW, drawH);
4366
- }
4367
- } else {
4368
- const color = unit.team === "player" ? "#3b82f6" : unit.team === "enemy" ? "#ef4444" : "#6b7280";
4369
- ctx.beginPath();
4370
- ctx.arc(centerX, groundY - 20 * scale - breatheOffset, 20 * scale, 0, Math.PI * 2);
4371
- ctx.fillStyle = color;
4372
- ctx.fill();
4373
- ctx.strokeStyle = "rgba(255,255,255,0.8)";
4374
- ctx.lineWidth = 2;
4375
- ctx.stroke();
4376
- }
4377
- if (unit.name) {
4378
- const labelBg = unit.team === "player" ? "rgba(59, 130, 246, 0.9)" : unit.team === "enemy" ? "rgba(239, 68, 68, 0.9)" : "rgba(107, 114, 128, 0.9)";
4379
- ctx.font = `bold ${10 * scale * 2.5}px system-ui`;
4380
- ctx.textAlign = "center";
4381
- const textWidth = ctx.measureText(unit.name).width;
4382
- const labelY = groundY + 14 * scale - breatheOffset;
4383
- ctx.fillStyle = labelBg;
4384
- ctx.beginPath();
4385
- ctx.roundRect(centerX - textWidth / 2 - 6 * scale, labelY - 8 * scale, textWidth + 12 * scale, 16 * scale, 4 * scale);
4386
- ctx.fill();
4387
- ctx.fillStyle = "white";
4388
- ctx.fillText(unit.name, centerX, labelY + 4 * scale);
4389
- }
4390
- if (unit.health !== void 0 && unit.maxHealth !== void 0) {
4391
- const barWidth = 40 * scale;
4392
- const barHeight = 6 * scale;
4393
- const barX = centerX - barWidth / 2;
4394
- const barY = groundY - drawH - 2 * scale - breatheOffset;
4395
- const healthRatio = unit.health / unit.maxHealth;
4396
- const barRadius = barHeight / 2;
4397
- ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
4398
- ctx.beginPath();
4399
- ctx.roundRect(barX, barY, barWidth, barHeight, barRadius);
4400
- ctx.fill();
4401
- if (healthRatio > 0) {
4402
- const fillWidth = barWidth * healthRatio;
4403
- const gradient = ctx.createLinearGradient(barX, barY, barX, barY + barHeight);
4404
- if (healthRatio > 0.6) {
4405
- gradient.addColorStop(0, "#4ade80");
4406
- gradient.addColorStop(1, "#22c55e");
4407
- } else if (healthRatio > 0.3) {
4408
- gradient.addColorStop(0, "#fde047");
4409
- gradient.addColorStop(1, "#eab308");
4410
- } else {
4411
- gradient.addColorStop(0, "#f87171");
4412
- gradient.addColorStop(1, "#ef4444");
4413
- }
4414
- ctx.fillStyle = gradient;
4415
- ctx.save();
4416
- ctx.beginPath();
4417
- ctx.roundRect(barX, barY, fillWidth, barHeight, barRadius);
4418
- ctx.clip();
4419
- ctx.fillRect(barX, barY, fillWidth, barHeight);
4420
- ctx.restore();
4421
- }
4422
- ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
4423
- ctx.lineWidth = 1;
4424
- ctx.beginPath();
4425
- ctx.roundRect(barX, barY, barWidth, barHeight, barRadius);
4426
- ctx.stroke();
4427
- }
4428
- }
4429
- onDrawEffects?.(ctx, animTime, getImage);
4430
- ctx.restore();
4431
- drawMinimap();
4432
- }, [
4433
- sortedTiles,
4434
- units,
4435
- features,
4436
- selectedUnitId,
4437
- scale,
4438
- debug,
4439
- resolveTerrainSpriteUrl,
4440
- resolveFeatureSpriteUrl,
4441
- resolveUnitSpriteUrl,
4442
- resolveUnitFrame,
4443
- getImage,
4444
- gridWidth,
4445
- gridHeight,
4446
- baseOffsetX,
4447
- scaledTileWidth,
4448
- scaledTileHeight,
4449
- scaledFloorHeight,
4450
- scaledDiamondTopY,
4451
- validMoveSet,
4452
- attackTargetSet,
4453
- hoveredTile,
4454
- viewportSize,
4455
- drawMinimap,
4456
- onDrawEffects,
4457
- backgroundImage,
4458
- cameraRef,
4459
- unitScale
4460
- ]);
4461
- useEffect(() => {
4462
- if (!selectedUnitId) return;
4463
- const unit = units.find((u) => u.id === selectedUnitId);
4464
- if (!unit?.position) return;
4465
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
4466
- const centerX = pos.x + scaledTileWidth / 2;
4467
- const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
4468
- targetCameraRef.current = {
4469
- x: centerX - viewportSize.width / 2,
4470
- y: centerY - viewportSize.height / 2
4471
- };
4472
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
4473
- useEffect(() => {
4474
- const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2;
4475
- draw(animTimeRef.current);
4476
- if (!hasAnimations) return;
4477
- let running = true;
4478
- const animate = (timestamp) => {
4479
- if (!running) return;
4480
- animTimeRef.current = timestamp;
4481
- lerpToTarget();
4482
- draw(timestamp);
4483
- rafIdRef.current = requestAnimationFrame(animate);
4484
- };
4485
- rafIdRef.current = requestAnimationFrame(animate);
4486
- return () => {
4487
- running = false;
4488
- cancelAnimationFrame(rafIdRef.current);
4489
- };
4490
- }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, lerpToTarget, targetCameraRef]);
4491
- const handleMouseMoveWithCamera = useCallback((e) => {
4492
- if (enableCamera) {
4493
- const wasPanning = handleMouseMove(e, () => draw(animTimeRef.current));
4494
- if (wasPanning) return;
4495
- }
4496
- if (!onTileHover && !tileHoverEvent || !canvasRef.current) return;
4497
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
4498
- const adjustedX = world.x - scaledTileWidth / 2;
4499
- const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
4500
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
4501
- const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
4502
- if (tileExists) {
4503
- if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
4504
- onTileHover?.(isoPos.x, isoPos.y);
4505
- }
4506
- }, [enableCamera, handleMouseMove, draw, onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
4507
- const handleMouseLeaveWithCamera = useCallback(() => {
4508
- handleMouseLeave();
4509
- if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
4510
- onTileLeave?.();
4511
- }, [handleMouseLeave, onTileLeave, tileLeaveEvent, eventBus]);
4512
- const handleWheelWithCamera = useCallback((e) => {
4513
- if (enableCamera) {
4514
- handleWheel(e, () => draw(animTimeRef.current));
4515
- }
4516
- }, [enableCamera, handleWheel, draw]);
4517
- const handleClick = useCallback((e) => {
4518
- if (dragDistance() > 5) return;
4519
- if (!canvasRef.current) return;
4520
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
4521
- const adjustedX = world.x - scaledTileWidth / 2;
4522
- const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
4523
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
4524
- const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
4525
- if (clickedUnit && (onUnitClick || unitClickEvent)) {
4526
- if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
4527
- onUnitClick?.(clickedUnit.id);
4528
- } else if (onTileClick || tileClickEvent) {
4529
- const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
4530
- if (tileExists) {
4531
- if (tileClickEvent) eventBus.emit(`UI:${tileClickEvent}`, { x: isoPos.x, y: isoPos.y });
4532
- onTileClick?.(isoPos.x, isoPos.y);
4533
- }
4534
- }
4535
- }, [dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
4536
- if (error) {
4537
- return /* @__PURE__ */ jsx(ErrorState, { title: t("canvas.errorTitle"), message: error.message, className });
4538
- }
4539
- if (isLoading) {
4540
- return /* @__PURE__ */ jsx(LoadingState, { className });
4541
- }
4542
- if (sortedTiles.length === 0) {
4543
- return /* @__PURE__ */ jsx(
4544
- Box,
4545
- {
4546
- className: cn("relative w-full overflow-hidden rounded-lg", className),
4547
- style: { height: viewportSize.height },
4548
- "data-testid": "game-canvas-empty",
4549
- children: /* @__PURE__ */ jsx(Box, { className: "flex items-center justify-center h-full bg-slate-800 rounded-lg", children: /* @__PURE__ */ jsxs(Stack, { direction: "vertical", gap: "md", align: "center", children: [
4550
- /* @__PURE__ */ jsx(Icon, { name: "map", size: "xl" }),
4551
- /* @__PURE__ */ jsx(Typography, { variant: "body", className: "text-slate-400", children: t("canvas.emptyMessage") || "No map data loaded" })
4552
- ] }) })
4553
- }
4554
- );
4555
- }
4556
- return /* @__PURE__ */ jsxs(
4557
- Box,
4558
- {
4559
- ref: containerRef,
4560
- className: cn("relative overflow-hidden w-full h-full", className),
4561
- children: [
4562
- /* @__PURE__ */ jsx(
4563
- "canvas",
4564
- {
4565
- ref: canvasRef,
4566
- "data-testid": "game-canvas",
4567
- onClick: handleClick,
4568
- onMouseDown: enableCamera ? handleMouseDown : void 0,
4569
- onMouseMove: handleMouseMoveWithCamera,
4570
- onMouseUp: enableCamera ? handleMouseUp : void 0,
4571
- onMouseLeave: handleMouseLeaveWithCamera,
4572
- onWheel: handleWheelWithCamera,
4573
- onContextMenu: (e) => e.preventDefault(),
4574
- className: "cursor-pointer",
4575
- style: {
4576
- width: viewportSize.width,
4577
- height: viewportSize.height
4578
- }
4579
- }
4580
- ),
4581
- process.env.NODE_ENV !== "production" && /* @__PURE__ */ jsxs("div", { "data-game-actions": "", className: "sr-only", "aria-hidden": "true", children: [
4582
- tileClickEvent && /* @__PURE__ */ jsx(
4583
- "button",
4584
- {
4585
- "data-event": tileClickEvent,
4586
- "data-x": "0",
4587
- "data-y": "0",
4588
- onClick: () => eventBus.emit(`UI:${tileClickEvent}`, { x: 0, y: 0 }),
4589
- children: tileClickEvent
4590
- }
4591
- ),
4592
- unitClickEvent && units && units.length > 0 && /* @__PURE__ */ jsx(
4593
- "button",
4594
- {
4595
- "data-event": unitClickEvent,
4596
- "data-unit-id": units[0].id,
4597
- onClick: () => eventBus.emit(`UI:${unitClickEvent}`, { unitId: units[0].id }),
4598
- children: unitClickEvent
4599
- }
4600
- )
4601
- ] }),
4602
- showMinimap && /* @__PURE__ */ jsx(
4603
- "canvas",
4604
- {
4605
- ref: minimapRef,
4606
- className: "absolute bottom-2 right-2 border border-border rounded bg-background/80 pointer-events-none",
4607
- style: { width: 150, height: 100, zIndex: 10 }
4608
- }
4609
- )
4610
- ]
4611
- }
4612
- );
4613
- }
4614
- IsometricCanvas.displayName = "IsometricCanvas";
4615
- var IsometricCanvas_default = IsometricCanvas;
4616
- var PLATFORM_COLORS = {
4617
- ground: "#4a7c59",
4618
- platform: "#7c6b4a",
4619
- hazard: "#c0392b",
4620
- goal: "#f1c40f"
4621
- };
4622
- var PLAYER_COLOR = "#3498db";
4623
- var PLAYER_EYE_COLOR = "#ffffff";
4624
- var SKY_GRADIENT_TOP = "#1a1a2e";
4625
- var SKY_GRADIENT_BOTTOM = "#16213e";
4626
- var GRID_COLOR = "rgba(255, 255, 255, 0.03)";
4627
- function PlatformerCanvas({
4628
- player,
4629
- platforms = [],
4630
- worldWidth = 800,
4631
- worldHeight = 400,
4632
- canvasWidth = 800,
4633
- canvasHeight = 400,
4634
- followCamera = true,
4635
- bgColor,
4636
- playerSprite,
4637
- tileSprites,
4638
- backgroundImage,
4639
- assetBaseUrl = "",
4640
- leftEvent = "MOVE_LEFT",
4641
- rightEvent = "MOVE_RIGHT",
4642
- jumpEvent = "JUMP",
4643
- stopEvent = "STOP",
4644
- className,
4645
- entity
4646
- }) {
4647
- const canvasRef = useRef(null);
4648
- const eventBus = useEventBus();
4649
- const keysRef = useRef(/* @__PURE__ */ new Set());
4650
- const imageCache = useRef(/* @__PURE__ */ new Map());
4651
- const loadImage = useCallback((url) => {
4652
- const fullUrl = url.startsWith("http") ? url : `${assetBaseUrl}${url}`;
4653
- const cached = imageCache.current.get(fullUrl);
4654
- if (cached?.complete && cached.naturalWidth > 0) return cached;
4655
- if (!cached) {
4656
- const img = new Image();
4657
- img.crossOrigin = "anonymous";
4658
- img.src = fullUrl;
4659
- img.onload = () => {
4660
- updateAssetStatus(fullUrl, "loaded");
4661
- };
4662
- img.onerror = () => {
4663
- updateAssetStatus(fullUrl, "failed");
4664
- };
4665
- imageCache.current.set(fullUrl, img);
4666
- updateAssetStatus(fullUrl, "pending");
4667
- }
4668
- return null;
4669
- }, [assetBaseUrl]);
4670
- useEffect(() => {
4671
- if (typeof window === "undefined") return;
4672
- const canvas = canvasRef.current;
4673
- if (!canvas) return;
4674
- bindCanvasCapture(() => canvas.toDataURL("image/png"));
4675
- return () => {
4676
- bindCanvasCapture(() => null);
4677
- };
4678
- }, []);
4679
- const resolvedPlayer = player ?? {
4680
- x: entity?.x ?? 50,
4681
- y: entity?.y ?? 300,
4682
- width: entity?.width ?? 24,
4683
- height: entity?.height ?? 32,
4684
- vx: entity?.vx ?? 0,
4685
- vy: entity?.vy ?? 0,
4686
- grounded: entity?.grounded ?? false,
4687
- facingRight: entity?.facingRight ?? true
4688
- };
4689
- const handleKeyDown = useCallback((e) => {
4690
- if (keysRef.current.has(e.code)) return;
4691
- keysRef.current.add(e.code);
4692
- switch (e.code) {
4693
- case "ArrowLeft":
4694
- case "KeyA":
4695
- eventBus.emit(`UI:${leftEvent}`, { direction: -1 });
4696
- break;
4697
- case "ArrowRight":
4698
- case "KeyD":
4699
- eventBus.emit(`UI:${rightEvent}`, { direction: 1 });
4700
- break;
4701
- case "ArrowUp":
4702
- case "KeyW":
4703
- case "Space":
4704
- eventBus.emit(`UI:${jumpEvent}`, {});
4705
- e.preventDefault();
4706
- break;
4707
- }
4708
- }, [eventBus, leftEvent, rightEvent, jumpEvent]);
4709
- const handleKeyUp = useCallback((e) => {
4710
- keysRef.current.delete(e.code);
4711
- switch (e.code) {
4712
- case "ArrowLeft":
4713
- case "KeyA":
4714
- case "ArrowRight":
4715
- case "KeyD":
4716
- eventBus.emit(`UI:${stopEvent}`, {});
4717
- break;
4718
- }
4719
- }, [eventBus, stopEvent]);
4720
- useEffect(() => {
4721
- window.addEventListener("keydown", handleKeyDown);
4722
- window.addEventListener("keyup", handleKeyUp);
4723
- return () => {
4724
- window.removeEventListener("keydown", handleKeyDown);
4725
- window.removeEventListener("keyup", handleKeyUp);
4726
- };
4727
- }, [handleKeyDown, handleKeyUp]);
4728
- useEffect(() => {
4729
- const canvas = canvasRef.current;
4730
- if (!canvas) return;
4731
- const ctx = canvas.getContext("2d");
4732
- if (!ctx) return;
4733
- const dpr = window.devicePixelRatio || 1;
4734
- canvas.width = canvasWidth * dpr;
4735
- canvas.height = canvasHeight * dpr;
4736
- ctx.scale(dpr, dpr);
4737
- let camX = 0;
4738
- let camY = 0;
4739
- if (followCamera) {
4740
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
4741
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
4742
- }
4743
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
4744
- if (bgImg) {
4745
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
4746
- } else if (bgColor) {
4747
- ctx.fillStyle = bgColor;
4748
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
4749
- } else {
4750
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
4751
- grad.addColorStop(0, SKY_GRADIENT_TOP);
4752
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
4753
- ctx.fillStyle = grad;
4754
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
4755
- }
4756
- ctx.strokeStyle = GRID_COLOR;
4757
- ctx.lineWidth = 1;
4758
- const gridSize = 32;
4759
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
4760
- ctx.beginPath();
4761
- ctx.moveTo(gx, 0);
4762
- ctx.lineTo(gx, canvasHeight);
4763
- ctx.stroke();
4764
- }
4765
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
4766
- ctx.beginPath();
4767
- ctx.moveTo(0, gy);
4768
- ctx.lineTo(canvasWidth, gy);
4769
- ctx.stroke();
4770
- }
4771
- for (const plat of platforms) {
4772
- const px = plat.x - camX;
4773
- const py = plat.y - camY;
4774
- const platType = plat.type ?? "ground";
4775
- const spriteUrl = tileSprites?.[platType];
4776
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
4777
- if (tileImg) {
4778
- const tileW = tileImg.naturalWidth;
4779
- const tileH = tileImg.naturalHeight;
4780
- const scaleH = plat.height / tileH;
4781
- const scaledW = tileW * scaleH;
4782
- for (let tx = 0; tx < plat.width; tx += scaledW) {
4783
- const drawW = Math.min(scaledW, plat.width - tx);
4784
- const srcW = drawW / scaleH;
4785
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
4786
- }
4787
- } else {
4788
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
4789
- ctx.fillStyle = color;
4790
- ctx.fillRect(px, py, plat.width, plat.height);
4791
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
4792
- ctx.fillRect(px, py, plat.width, 3);
4793
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
4794
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
4795
- if (platType === "hazard") {
4796
- ctx.strokeStyle = "#e74c3c";
4797
- ctx.lineWidth = 2;
4798
- for (let sx = px; sx < px + plat.width; sx += 12) {
4799
- ctx.beginPath();
4800
- ctx.moveTo(sx, py);
4801
- ctx.lineTo(sx + 6, py + plat.height);
4802
- ctx.stroke();
4803
- }
4804
- }
4805
- if (platType === "goal") {
4806
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
4807
- ctx.beginPath();
4808
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
4809
- ctx.fill();
4810
- }
4811
- }
4812
- }
4813
- const pw = resolvedPlayer.width ?? 24;
4814
- const ph = resolvedPlayer.height ?? 32;
4815
- const ppx = resolvedPlayer.x - camX;
4816
- const ppy = resolvedPlayer.y - camY;
4817
- const facingRight = resolvedPlayer.facingRight ?? true;
4818
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
4819
- if (playerImg) {
4820
- ctx.save();
4821
- if (!facingRight) {
4822
- ctx.translate(ppx + pw, ppy);
4823
- ctx.scale(-1, 1);
4824
- ctx.drawImage(playerImg, 0, 0, pw, ph);
4825
- } else {
4826
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
4827
- }
4828
- ctx.restore();
4829
- } else {
4830
- ctx.fillStyle = PLAYER_COLOR;
4831
- const radius = Math.min(pw, ph) * 0.25;
4832
- ctx.beginPath();
4833
- ctx.moveTo(ppx + radius, ppy);
4834
- ctx.lineTo(ppx + pw - radius, ppy);
4835
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
4836
- ctx.lineTo(ppx + pw, ppy + ph - radius);
4837
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
4838
- ctx.lineTo(ppx + radius, ppy + ph);
4839
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
4840
- ctx.lineTo(ppx, ppy + radius);
4841
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
4842
- ctx.fill();
4843
- const eyeY = ppy + ph * 0.3;
4844
- const eyeSize = 3;
4845
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
4846
- ctx.fillStyle = PLAYER_EYE_COLOR;
4847
- ctx.beginPath();
4848
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
4849
- ctx.fill();
4850
- ctx.beginPath();
4851
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
4852
- ctx.fill();
4853
- }
4854
- });
4855
- return /* @__PURE__ */ jsx(
4856
- "canvas",
4857
- {
4858
- ref: canvasRef,
4859
- style: { width: canvasWidth, height: canvasHeight },
4860
- className: cn("block rounded-lg border border-white/10", className),
4861
- "data-testid": "platformer-canvas",
4862
- tabIndex: 0
4863
- }
4864
- );
4865
- }
4866
- PlatformerCanvas.displayName = "PlatformerCanvas";
4867
3748
 
4868
3749
  // components/organisms/game/types/effects.ts
4869
3750
  var EMPTY_EFFECT_STATE = {
@@ -6910,629 +5791,6 @@ function usePhysics2D(options = {}) {
6910
5791
  reset
6911
5792
  };
6912
5793
  }
6913
- var positionMap = {
6914
- top: "top-0 left-0 right-0 flex justify-between items-start p-4",
6915
- bottom: "bottom-0 left-0 right-0 flex justify-between items-end p-4",
6916
- corners: "inset-0 pointer-events-none"
6917
- };
6918
- function convertElementsToStats(elements) {
6919
- return elements.map((el) => {
6920
- const [source, field] = el.bind?.split(".") ?? [];
6921
- const labelMap = {
6922
- "health-bar": "Health",
6923
- "score-display": "Score",
6924
- lives: "Lives",
6925
- timer: "Time"
6926
- };
6927
- return {
6928
- label: el.label || labelMap[el.type ?? ""] || el.type || "",
6929
- source,
6930
- field,
6931
- // Pass through direct values from compiled render-ui effects
6932
- ...el.value !== void 0 && { value: el.value },
6933
- ...el.icon !== void 0 && { icon: el.icon },
6934
- ...el.format !== void 0 && { format: el.format },
6935
- ...el.max !== void 0 && { max: el.max }
6936
- };
6937
- });
6938
- }
6939
- function GameHud({
6940
- position: propPosition,
6941
- stats: propStats,
6942
- items,
6943
- elements,
6944
- size = "md",
6945
- className,
6946
- transparent = true
6947
- }) {
6948
- const rawStats = propStats ?? items ?? (elements && Array.isArray(elements) ? convertElementsToStats(elements) : []);
6949
- const stats = Array.isArray(rawStats) ? rawStats : [];
6950
- const position = propPosition ?? "corners";
6951
- if (position === "corners") {
6952
- const leftStats = stats.slice(0, Math.ceil(stats.length / 2));
6953
- const rightStats = stats.slice(Math.ceil(stats.length / 2));
6954
- return /* @__PURE__ */ jsxs("div", { className: cn("absolute", positionMap[position], className), children: [
6955
- /* @__PURE__ */ jsx("div", { className: "absolute top-4 left-4 flex flex-col gap-2 pointer-events-auto", children: leftStats.map((stat, i) => /* @__PURE__ */ jsx(StatBadge, { ...stat, size }, i)) }),
6956
- /* @__PURE__ */ jsx("div", { className: "absolute top-4 right-4 flex flex-col gap-2 items-end pointer-events-auto", children: rightStats.map((stat, i) => /* @__PURE__ */ jsx(StatBadge, { ...stat, size }, i)) })
6957
- ] });
6958
- }
6959
- return /* @__PURE__ */ jsx(
6960
- "div",
6961
- {
6962
- className: cn(
6963
- "absolute z-10",
6964
- positionMap[position],
6965
- transparent && "bg-gradient-to-b from-black/50 to-transparent",
6966
- position === "bottom" && "bg-gradient-to-t from-black/50 to-transparent",
6967
- className
6968
- ),
6969
- children: /* @__PURE__ */ jsx("div", { className: "flex gap-4", children: stats.map((stat, i) => /* @__PURE__ */ jsx(StatBadge, { ...stat, size }, i)) })
6970
- }
6971
- );
6972
- }
6973
- GameHud.displayName = "GameHud";
6974
- var variantMap = {
6975
- primary: "bg-blue-600 hover:bg-blue-500 text-white border-blue-400 shadow-lg shadow-blue-500/25",
6976
- secondary: "bg-gray-700 hover:bg-gray-600 text-white border-gray-500",
6977
- ghost: "bg-transparent hover:bg-white/10 text-white border-white/20"
6978
- };
6979
- function GameMenu({
6980
- title,
6981
- subtitle,
6982
- options,
6983
- menuItems,
6984
- onSelect,
6985
- eventBus: eventBusProp,
6986
- background,
6987
- logo,
6988
- className
6989
- }) {
6990
- const resolvedOptions = options ?? menuItems ?? [];
6991
- let eventBusFromHook = null;
6992
- try {
6993
- eventBusFromHook = useEventBus();
6994
- } catch {
6995
- }
6996
- const eventBus = eventBusProp || eventBusFromHook;
6997
- const handleOptionClick = React.useCallback(
6998
- (option) => {
6999
- if (option.event && eventBus) {
7000
- eventBus.emit(`UI:${option.event}`, { option });
7001
- }
7002
- if (onSelect) {
7003
- onSelect(option);
7004
- }
7005
- if (option.navigatesTo && eventBus) {
7006
- eventBus.emit("UI:NAVIGATE", { url: option.navigatesTo, option });
7007
- }
7008
- },
7009
- [eventBus, onSelect]
7010
- );
7011
- return /* @__PURE__ */ jsxs(
7012
- "div",
7013
- {
7014
- className: cn(
7015
- "min-h-screen w-full flex flex-col items-center justify-center p-8",
7016
- className
7017
- ),
7018
- style: {
7019
- background: background ?? "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0e17 100%)"
7020
- },
7021
- children: [
7022
- /* @__PURE__ */ jsxs("div", { className: "text-center mb-12 animate-fade-in", children: [
7023
- logo && /* @__PURE__ */ jsx(
7024
- "img",
7025
- {
7026
- src: logo,
7027
- alt: title,
7028
- className: "h-24 w-auto mx-auto mb-6 drop-shadow-2xl"
7029
- }
7030
- ),
7031
- /* @__PURE__ */ jsx(
7032
- "h1",
7033
- {
7034
- className: "text-5xl md:text-7xl font-bold text-white tracking-tight",
7035
- style: {
7036
- textShadow: "0 4px 12px rgba(0,0,0,0.5)"
7037
- },
7038
- children: title
7039
- }
7040
- ),
7041
- subtitle && /* @__PURE__ */ jsx("p", { className: "mt-2 text-lg text-gray-400 tracking-widest uppercase", children: subtitle })
7042
- ] }),
7043
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 w-full max-w-md", children: resolvedOptions.map((option, index) => /* @__PURE__ */ jsx(
7044
- "button",
7045
- {
7046
- onClick: () => handleOptionClick(option),
7047
- disabled: option.disabled,
7048
- className: cn(
7049
- "w-full py-4 px-8 rounded-xl border-2 font-bold text-lg",
7050
- "transition-all duration-200 transform",
7051
- "hover:scale-105 active:scale-95",
7052
- "focus:outline-none focus:ring-4 focus:ring-white/25",
7053
- variantMap[option.variant ?? "secondary"] ?? variantMap.secondary,
7054
- option.disabled && "opacity-50 cursor-not-allowed hover:scale-100"
7055
- ),
7056
- style: {
7057
- animationDelay: `${index * 100}ms`
7058
- },
7059
- children: option.label
7060
- },
7061
- index
7062
- )) }),
7063
- /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none overflow-hidden", children: [
7064
- /* @__PURE__ */ jsx("div", { className: "absolute top-1/4 left-1/4 w-64 h-64 bg-blue-500/10 rounded-full blur-3xl" }),
7065
- /* @__PURE__ */ jsx("div", { className: "absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" })
7066
- ] })
7067
- ]
7068
- }
7069
- );
7070
- }
7071
- GameMenu.displayName = "GameMenu";
7072
- var variantColors = {
7073
- victory: {
7074
- bg: "from-green-900/90 to-emerald-950/90",
7075
- title: "text-green-400",
7076
- accent: "border-green-500"
7077
- },
7078
- defeat: {
7079
- bg: "from-red-900/90 to-gray-950/90",
7080
- title: "text-red-400",
7081
- accent: "border-red-500"
7082
- },
7083
- neutral: {
7084
- bg: "from-gray-900/90 to-gray-950/90",
7085
- title: "text-white",
7086
- accent: "border-gray-500"
7087
- }
7088
- };
7089
- var buttonVariants = {
7090
- primary: "bg-blue-600 hover:bg-blue-500 text-white border-blue-400",
7091
- secondary: "bg-gray-700 hover:bg-gray-600 text-white border-gray-500",
7092
- ghost: "bg-transparent hover:bg-white/10 text-white border-white/20"
7093
- };
7094
- function formatTime(ms) {
7095
- const seconds = Math.floor(ms / 1e3);
7096
- const minutes = Math.floor(seconds / 60);
7097
- const remainingSeconds = seconds % 60;
7098
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
7099
- }
7100
- function GameOverScreen({
7101
- title,
7102
- message,
7103
- stats = [],
7104
- actions,
7105
- menuItems,
7106
- onAction,
7107
- eventBus: eventBusProp,
7108
- variant = "neutral",
7109
- highScore,
7110
- currentScore,
7111
- className
7112
- }) {
7113
- const resolvedActions = actions ?? menuItems ?? [];
7114
- let eventBusFromHook = null;
7115
- try {
7116
- eventBusFromHook = useEventBus();
7117
- } catch {
7118
- }
7119
- const eventBus = eventBusProp || eventBusFromHook;
7120
- const handleActionClick = React.useCallback(
7121
- (action) => {
7122
- if (action.event && eventBus) {
7123
- eventBus.emit(`UI:${action.event}`, { action });
7124
- }
7125
- if (onAction) {
7126
- onAction(action);
7127
- }
7128
- },
7129
- [eventBus, onAction]
7130
- );
7131
- const colors = variantColors[variant];
7132
- const numericCurrentScore = typeof currentScore === "string" ? parseFloat(currentScore) : currentScore;
7133
- const numericHighScore = typeof highScore === "string" ? parseFloat(highScore) : highScore;
7134
- const isNewHighScore = numericHighScore !== void 0 && numericCurrentScore !== void 0 && !isNaN(numericCurrentScore) && !isNaN(numericHighScore) && numericCurrentScore > numericHighScore;
7135
- return /* @__PURE__ */ jsxs(
7136
- "div",
7137
- {
7138
- className: cn(
7139
- "min-h-screen w-full flex flex-col items-center justify-center p-8",
7140
- "bg-gradient-to-b",
7141
- colors.bg,
7142
- className
7143
- ),
7144
- children: [
7145
- variant === "victory" && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(34,197,94,0.2),transparent_70%)]" }) }),
7146
- /* @__PURE__ */ jsx(
7147
- "h1",
7148
- {
7149
- className: cn(
7150
- "text-6xl md:text-8xl font-bold mb-4 tracking-tight animate-bounce-once",
7151
- colors.title
7152
- ),
7153
- style: { textShadow: "0 4px 20px rgba(0,0,0,0.5)" },
7154
- children: title
7155
- }
7156
- ),
7157
- message && /* @__PURE__ */ jsx("p", { className: "text-xl text-gray-300 mb-8 text-center max-w-md", children: message }),
7158
- isNewHighScore && /* @__PURE__ */ jsx("div", { className: "mb-6 px-6 py-2 bg-yellow-500/20 border-2 border-yellow-500 rounded-full animate-pulse", children: /* @__PURE__ */ jsx("span", { className: "text-yellow-400 font-bold text-lg tracking-wide", children: "\u{1F3C6} NEW HIGH SCORE! \u{1F3C6}" }) }),
7159
- stats.length > 0 && /* @__PURE__ */ jsx(
7160
- "div",
7161
- {
7162
- className: cn(
7163
- "mb-8 p-6 rounded-xl border-2 bg-black/30",
7164
- colors.accent
7165
- ),
7166
- children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 md:grid-cols-3 gap-4", children: stats.map((stat, index) => {
7167
- let displayValue = stat.value ?? 0;
7168
- if (stat.format === "time" && typeof displayValue === "number") {
7169
- displayValue = formatTime(displayValue);
7170
- }
7171
- return /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
7172
- /* @__PURE__ */ jsx("div", { className: "text-gray-400 text-sm mb-1", children: stat.label }),
7173
- /* @__PURE__ */ jsxs("div", { className: "text-white text-2xl font-bold flex items-center justify-center gap-2", children: [
7174
- stat.icon && /* @__PURE__ */ jsx("span", { children: stat.icon }),
7175
- /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: displayValue })
7176
- ] })
7177
- ] }, index);
7178
- }) })
7179
- }
7180
- ),
7181
- /* @__PURE__ */ jsx("div", { className: "flex flex-col sm:flex-row gap-4", children: resolvedActions.map((action, index) => /* @__PURE__ */ jsx(
7182
- "button",
7183
- {
7184
- onClick: () => handleActionClick(action),
7185
- className: cn(
7186
- "px-8 py-4 rounded-xl border-2 font-bold text-lg",
7187
- "transition-all duration-200",
7188
- "hover:scale-105 active:scale-95",
7189
- "focus:outline-none focus:ring-4 focus:ring-white/25",
7190
- buttonVariants[action.variant ?? "secondary"]
7191
- ),
7192
- children: action.label
7193
- },
7194
- index
7195
- )) })
7196
- ]
7197
- }
7198
- );
7199
- }
7200
- GameOverScreen.displayName = "GameOverScreen";
7201
- function InventoryPanel({
7202
- items,
7203
- slots,
7204
- columns,
7205
- selectedSlot,
7206
- onSelectSlot,
7207
- onUseItem,
7208
- onDropItem,
7209
- selectSlotEvent,
7210
- useItemEvent,
7211
- dropItemEvent,
7212
- showTooltips = true,
7213
- className,
7214
- slotSize = 48
7215
- }) {
7216
- const eventBus = useEventBus();
7217
- const [hoveredSlot, setHoveredSlot] = useState(null);
7218
- const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
7219
- const safeItems = Array.isArray(items) ? items : [];
7220
- const safeSlots = typeof slots === "number" && slots > 0 ? slots : 0;
7221
- const safeColumns = typeof columns === "number" && columns > 0 ? columns : 1;
7222
- const slotArray = Array.from({ length: safeSlots }, (_, index) => {
7223
- return safeItems[index] ?? null;
7224
- });
7225
- const rows = Math.ceil(safeSlots / safeColumns);
7226
- const handleSlotClick = useCallback((index) => {
7227
- if (selectSlotEvent) eventBus.emit(`UI:${selectSlotEvent}`, { index });
7228
- onSelectSlot?.(index);
7229
- }, [onSelectSlot, selectSlotEvent, eventBus]);
7230
- const handleSlotDoubleClick = useCallback((index) => {
7231
- const item = slotArray[index];
7232
- if (item) {
7233
- if (useItemEvent) eventBus.emit(`UI:${useItemEvent}`, { item });
7234
- onUseItem?.(item);
7235
- }
7236
- }, [slotArray, onUseItem, useItemEvent, eventBus]);
7237
- const handleKeyDown = useCallback((e, index) => {
7238
- const item = slotArray[index];
7239
- switch (e.key) {
7240
- case "Enter":
7241
- case " ":
7242
- if (item) {
7243
- e.preventDefault();
7244
- if (useItemEvent) eventBus.emit(`UI:${useItemEvent}`, { item });
7245
- onUseItem?.(item);
7246
- }
7247
- break;
7248
- case "Delete":
7249
- case "Backspace":
7250
- if (item) {
7251
- e.preventDefault();
7252
- if (dropItemEvent) eventBus.emit(`UI:${dropItemEvent}`, { item });
7253
- onDropItem?.(item);
7254
- }
7255
- break;
7256
- case "ArrowRight":
7257
- e.preventDefault();
7258
- onSelectSlot?.(Math.min(index + 1, safeSlots - 1));
7259
- break;
7260
- case "ArrowLeft":
7261
- e.preventDefault();
7262
- onSelectSlot?.(Math.max(index - 1, 0));
7263
- break;
7264
- case "ArrowDown":
7265
- e.preventDefault();
7266
- onSelectSlot?.(Math.min(index + safeColumns, safeSlots - 1));
7267
- break;
7268
- case "ArrowUp":
7269
- e.preventDefault();
7270
- onSelectSlot?.(Math.max(index - safeColumns, 0));
7271
- break;
7272
- }
7273
- }, [slotArray, onUseItem, onDropItem, onSelectSlot, safeColumns, safeSlots, useItemEvent, dropItemEvent, eventBus]);
7274
- const handleMouseEnter = useCallback((e, index) => {
7275
- if (showTooltips && slotArray[index]) {
7276
- setHoveredSlot(index);
7277
- setTooltipPosition({
7278
- x: e.clientX + 10,
7279
- y: e.clientY + 10
7280
- });
7281
- }
7282
- }, [showTooltips, slotArray]);
7283
- const handleMouseLeave = useCallback(() => {
7284
- setHoveredSlot(null);
7285
- }, []);
7286
- const hoveredItem = hoveredSlot !== null ? slotArray[hoveredSlot] : null;
7287
- return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
7288
- /* @__PURE__ */ jsx(
7289
- "div",
7290
- {
7291
- className: "grid gap-1 bg-gray-900 p-2 rounded-lg border border-gray-700",
7292
- style: {
7293
- gridTemplateColumns: `repeat(${safeColumns}, ${slotSize}px)`,
7294
- gridTemplateRows: `repeat(${rows}, ${slotSize}px)`
7295
- },
7296
- children: slotArray.map((item, index) => /* @__PURE__ */ jsx(
7297
- "button",
7298
- {
7299
- type: "button",
7300
- className: cn(
7301
- "relative flex items-center justify-center",
7302
- "bg-gray-800 border rounded transition-colors",
7303
- "hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500",
7304
- selectedSlot === index ? "border-yellow-400 bg-gray-700" : "border-gray-600"
7305
- ),
7306
- style: { width: slotSize, height: slotSize },
7307
- onClick: () => handleSlotClick(index),
7308
- onDoubleClick: () => handleSlotDoubleClick(index),
7309
- onKeyDown: (e) => handleKeyDown(e, index),
7310
- onMouseEnter: (e) => handleMouseEnter(e, index),
7311
- onMouseLeave: handleMouseLeave,
7312
- tabIndex: 0,
7313
- "aria-label": item ? `${item.name || item.type}, quantity: ${item.quantity}` : `Empty slot ${index + 1}`,
7314
- children: item && /* @__PURE__ */ jsxs(Fragment, { children: [
7315
- item.sprite ? /* @__PURE__ */ jsx(
7316
- "img",
7317
- {
7318
- src: item.sprite,
7319
- alt: item.name || item.type,
7320
- className: "w-8 h-8 object-contain",
7321
- style: { imageRendering: "pixelated" }
7322
- }
7323
- ) : /* @__PURE__ */ jsx("div", { className: "w-8 h-8 bg-gray-600 rounded flex items-center justify-center text-xs text-gray-300", children: item.type.charAt(0).toUpperCase() }),
7324
- item.quantity > 1 && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 right-0 bg-black bg-opacity-70 text-white text-xs px-1 rounded-tl", children: item.quantity })
7325
- ] })
7326
- },
7327
- index
7328
- ))
7329
- }
7330
- ),
7331
- showTooltips && hoveredItem && /* @__PURE__ */ jsxs(
7332
- "div",
7333
- {
7334
- className: "fixed z-50 bg-gray-900 border border-gray-600 rounded-lg p-2 shadow-lg pointer-events-none",
7335
- style: {
7336
- left: tooltipPosition.x,
7337
- top: tooltipPosition.y,
7338
- maxWidth: 200
7339
- },
7340
- children: [
7341
- /* @__PURE__ */ jsx("div", { className: "font-semibold text-white", children: hoveredItem.name || hoveredItem.type }),
7342
- hoveredItem.description && /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-400 mt-1", children: hoveredItem.description }),
7343
- /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500 mt-1", children: [
7344
- "Quantity: ",
7345
- hoveredItem.quantity
7346
- ] })
7347
- ]
7348
- }
7349
- )
7350
- ] });
7351
- }
7352
- function DialogueBox({
7353
- dialogue,
7354
- typewriterSpeed = 30,
7355
- position = "bottom",
7356
- onComplete,
7357
- onChoice,
7358
- onAdvance,
7359
- completeEvent,
7360
- choiceEvent,
7361
- advanceEvent,
7362
- className
7363
- }) {
7364
- const eventBus = useEventBus();
7365
- const [displayedText, setDisplayedText] = useState("");
7366
- const [isTyping, setIsTyping] = useState(false);
7367
- const [selectedChoice, setSelectedChoice] = useState(0);
7368
- const textRef = useRef(dialogue.text);
7369
- const charIndexRef = useRef(0);
7370
- const autoAdvanceTimerRef = useRef(null);
7371
- useEffect(() => {
7372
- textRef.current = dialogue.text;
7373
- charIndexRef.current = 0;
7374
- setDisplayedText("");
7375
- setSelectedChoice(0);
7376
- if (typewriterSpeed === 0) {
7377
- setDisplayedText(dialogue.text);
7378
- setIsTyping(false);
7379
- if (completeEvent) eventBus.emit(`UI:${completeEvent}`, {});
7380
- onComplete?.();
7381
- } else {
7382
- setIsTyping(true);
7383
- }
7384
- return () => {
7385
- if (autoAdvanceTimerRef.current) {
7386
- clearTimeout(autoAdvanceTimerRef.current);
7387
- }
7388
- };
7389
- }, [dialogue, typewriterSpeed, onComplete]);
7390
- useEffect(() => {
7391
- if (!isTyping || typewriterSpeed === 0) return;
7392
- const interval = setInterval(() => {
7393
- if (charIndexRef.current < textRef.current.length) {
7394
- charIndexRef.current++;
7395
- setDisplayedText(textRef.current.slice(0, charIndexRef.current));
7396
- } else {
7397
- setIsTyping(false);
7398
- clearInterval(interval);
7399
- if (completeEvent) eventBus.emit(`UI:${completeEvent}`, {});
7400
- onComplete?.();
7401
- if (dialogue.autoAdvance && !dialogue.choices?.length) {
7402
- autoAdvanceTimerRef.current = setTimeout(() => {
7403
- if (advanceEvent) eventBus.emit(`UI:${advanceEvent}`, {});
7404
- onAdvance?.();
7405
- }, dialogue.autoAdvance);
7406
- }
7407
- }
7408
- }, typewriterSpeed);
7409
- return () => clearInterval(interval);
7410
- }, [isTyping, typewriterSpeed, dialogue.autoAdvance, dialogue.choices, onComplete, onAdvance]);
7411
- const skipTypewriter = useCallback(() => {
7412
- if (isTyping) {
7413
- charIndexRef.current = textRef.current.length;
7414
- setDisplayedText(textRef.current);
7415
- setIsTyping(false);
7416
- if (completeEvent) eventBus.emit(`UI:${completeEvent}`, {});
7417
- onComplete?.();
7418
- }
7419
- }, [isTyping, onComplete, completeEvent, eventBus]);
7420
- const handleClick = useCallback(() => {
7421
- if (isTyping) {
7422
- skipTypewriter();
7423
- } else if (!dialogue.choices?.length) {
7424
- if (advanceEvent) eventBus.emit(`UI:${advanceEvent}`, {});
7425
- onAdvance?.();
7426
- }
7427
- }, [isTyping, skipTypewriter, dialogue.choices, onAdvance, advanceEvent, eventBus]);
7428
- const handleKeyDown = useCallback((e) => {
7429
- if (isTyping) {
7430
- if (e.key === " " || e.key === "Enter") {
7431
- e.preventDefault();
7432
- skipTypewriter();
7433
- }
7434
- return;
7435
- }
7436
- if (dialogue.choices?.length) {
7437
- const enabledChoices2 = dialogue.choices.filter((c) => !c.disabled);
7438
- switch (e.key) {
7439
- case "ArrowUp":
7440
- e.preventDefault();
7441
- setSelectedChoice((prev) => Math.max(0, prev - 1));
7442
- break;
7443
- case "ArrowDown":
7444
- e.preventDefault();
7445
- setSelectedChoice((prev) => Math.min(enabledChoices2.length - 1, prev + 1));
7446
- break;
7447
- case "Enter":
7448
- case " ":
7449
- e.preventDefault();
7450
- const choice = enabledChoices2[selectedChoice];
7451
- if (choice) {
7452
- if (choiceEvent) eventBus.emit(`UI:${choiceEvent}`, { choice });
7453
- onChoice?.(choice);
7454
- }
7455
- break;
7456
- case "1":
7457
- case "2":
7458
- case "3":
7459
- case "4":
7460
- const choiceIndex = parseInt(e.key) - 1;
7461
- if (choiceIndex < enabledChoices2.length) {
7462
- e.preventDefault();
7463
- if (choiceEvent) eventBus.emit(`UI:${choiceEvent}`, { choice: enabledChoices2[choiceIndex] });
7464
- onChoice?.(enabledChoices2[choiceIndex]);
7465
- }
7466
- break;
7467
- }
7468
- } else {
7469
- if (e.key === " " || e.key === "Enter") {
7470
- e.preventDefault();
7471
- if (advanceEvent) eventBus.emit(`UI:${advanceEvent}`, {});
7472
- onAdvance?.();
7473
- }
7474
- }
7475
- }, [isTyping, skipTypewriter, dialogue.choices, selectedChoice, onChoice, onAdvance, choiceEvent, advanceEvent, eventBus]);
7476
- const enabledChoices = dialogue.choices?.filter((c) => !c.disabled) ?? [];
7477
- return /* @__PURE__ */ jsx(
7478
- "div",
7479
- {
7480
- className: cn(
7481
- "fixed left-0 right-0 z-40",
7482
- position === "top" ? "top-0" : "bottom-0",
7483
- className
7484
- ),
7485
- onClick: handleClick,
7486
- onKeyDown: handleKeyDown,
7487
- tabIndex: 0,
7488
- role: "dialog",
7489
- "aria-label": "Dialogue",
7490
- children: /* @__PURE__ */ jsx("div", { className: "mx-4 my-4 bg-gray-900 bg-opacity-95 border-2 border-gray-600 rounded-lg overflow-hidden", children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
7491
- dialogue.portrait && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 p-4 border-r border-gray-700", children: /* @__PURE__ */ jsx(
7492
- "img",
7493
- {
7494
- src: dialogue.portrait,
7495
- alt: dialogue.speaker,
7496
- className: "w-24 h-24 object-contain",
7497
- style: { imageRendering: "pixelated" }
7498
- }
7499
- ) }),
7500
- /* @__PURE__ */ jsxs("div", { className: "flex-1 p-4", children: [
7501
- /* @__PURE__ */ jsx("div", { className: "text-yellow-400 font-bold mb-2", children: dialogue.speaker }),
7502
- /* @__PURE__ */ jsxs("div", { className: "text-white text-lg leading-relaxed min-h-[60px]", children: [
7503
- displayedText,
7504
- isTyping && /* @__PURE__ */ jsx("span", { className: "animate-pulse", children: "\u258C" })
7505
- ] }),
7506
- !isTyping && enabledChoices.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-4 space-y-2", children: enabledChoices.map((choice, index) => /* @__PURE__ */ jsxs(
7507
- "button",
7508
- {
7509
- type: "button",
7510
- className: cn(
7511
- "block w-full text-left px-4 py-2 rounded transition-colors",
7512
- "hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400",
7513
- selectedChoice === index ? "bg-gray-700 text-yellow-400" : "bg-gray-800 text-white"
7514
- ),
7515
- onClick: (e) => {
7516
- e.stopPropagation();
7517
- if (choiceEvent) eventBus.emit(`UI:${choiceEvent}`, { choice });
7518
- onChoice?.(choice);
7519
- },
7520
- children: [
7521
- /* @__PURE__ */ jsxs("span", { className: "text-gray-500 mr-2", children: [
7522
- index + 1,
7523
- "."
7524
- ] }),
7525
- choice.text
7526
- ]
7527
- },
7528
- index
7529
- )) }),
7530
- !isTyping && !dialogue.choices?.length && /* @__PURE__ */ jsx("div", { className: "mt-4 text-gray-500 text-sm animate-pulse", children: "Press SPACE or click to continue..." })
7531
- ] })
7532
- ] }) })
7533
- }
7534
- );
7535
- }
7536
5794
  function BattleBoard({
7537
5795
  entity,
7538
5796
  scale = 0.45,
@@ -8392,7 +6650,7 @@ function LinearView({
8392
6650
  /* @__PURE__ */ jsx(HStack, { className: "flex-wrap items-center", gap: "xs", children: trait.states.map((state, i) => {
8393
6651
  const isDone = i < currentIdx;
8394
6652
  const isCurrent = i === currentIdx;
8395
- return /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
6653
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
8396
6654
  i > 0 && /* @__PURE__ */ jsx(
8397
6655
  Typography,
8398
6656
  {
@@ -9046,7 +7304,7 @@ function SequenceBar({
9046
7304
  onSlotRemove(index);
9047
7305
  }, [onSlotRemove, playing]);
9048
7306
  const paddedSlots = Array.from({ length: maxSlots }, (_, i) => slots[i]);
9049
- return /* @__PURE__ */ jsx(HStack, { className: cn("items-center", className), gap: "sm", children: paddedSlots.map((slot, i) => /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
7307
+ return /* @__PURE__ */ jsx(HStack, { className: cn("items-center", className), gap: "sm", children: paddedSlots.map((slot, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
9050
7308
  i > 0 && /* @__PURE__ */ jsx(
9051
7309
  Typography,
9052
7310
  {
@@ -11160,84 +9418,6 @@ function SimulationGraph({
11160
9418
  ] }) });
11161
9419
  }
11162
9420
  SimulationGraph.displayName = "SimulationGraph";
11163
- var eventIcons = {
11164
- attack: Sword,
11165
- defend: Shield,
11166
- heal: Heart,
11167
- move: Move,
11168
- special: Zap,
11169
- death: Sword,
11170
- spawn: Zap
11171
- };
11172
- var eventColors = {
11173
- attack: "text-error",
11174
- defend: "text-info",
11175
- heal: "text-success",
11176
- move: "text-primary",
11177
- special: "text-warning",
11178
- death: "text-muted-foreground",
11179
- spawn: "text-accent"
11180
- };
11181
- var eventBadgeVariants = {
11182
- attack: "danger",
11183
- defend: "primary",
11184
- heal: "success",
11185
- move: "warning",
11186
- special: "secondary",
11187
- death: "secondary",
11188
- spawn: "secondary"
11189
- };
11190
- function CombatLog({
11191
- events,
11192
- maxVisible = 50,
11193
- autoScroll = true,
11194
- showTimestamps = false,
11195
- className,
11196
- title = "Combat Log"
11197
- }) {
11198
- const scrollRef = useRef(null);
11199
- useEffect(() => {
11200
- if (autoScroll && scrollRef.current) {
11201
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
11202
- }
11203
- }, [events, autoScroll]);
11204
- const visibleEvents = events.slice(-maxVisible);
11205
- return /* @__PURE__ */ jsxs(Card, { variant: "default", className: cn("flex flex-col", className), children: [
11206
- /* @__PURE__ */ jsx(Box, { padding: "sm", border: true, className: "border-b-2 border-x-0 border-t-0 border-[var(--color-border)]", children: /* @__PURE__ */ jsxs(Box, { display: "flex", className: "items-center justify-between", children: [
11207
- /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "font-bold", children: title }),
11208
- /* @__PURE__ */ jsxs(Badge, { variant: "neutral", size: "sm", children: [
11209
- events.length,
11210
- " events"
11211
- ] })
11212
- ] }) }),
11213
- /* @__PURE__ */ jsx(Box, { ref: scrollRef, overflow: "auto", className: "flex-1 max-h-64", children: visibleEvents.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: "md", className: "text-center opacity-50", children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: "No events yet" }) }) : /* @__PURE__ */ jsx(Box, { padding: "xs", className: "space-y-1", children: visibleEvents.map((event) => {
11214
- const EventIcon = eventIcons[event.type];
11215
- const colorClass = eventColors[event.type];
11216
- return /* @__PURE__ */ jsxs(
11217
- Box,
11218
- {
11219
- display: "flex",
11220
- padding: "xs",
11221
- rounded: "sm",
11222
- className: cn("items-start gap-2 hover:bg-[var(--color-muted)] transition-colors", event.type === "death" && "opacity-60"),
11223
- children: [
11224
- /* @__PURE__ */ jsx(Box, { className: cn("flex-shrink-0 mt-0.5", colorClass), children: /* @__PURE__ */ jsx(EventIcon, { className: "h-4 w-4" }) }),
11225
- /* @__PURE__ */ jsxs(Box, { className: "flex-1 min-w-0", children: [
11226
- /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "block", children: event.message }),
11227
- event.value !== void 0 && /* @__PURE__ */ jsxs(Badge, { variant: eventBadgeVariants[event.type], size: "sm", className: "mt-1", children: [
11228
- event.type === "heal" ? "+" : event.type === "attack" ? "-" : "",
11229
- event.value
11230
- ] })
11231
- ] }),
11232
- (event.turn || showTimestamps) && /* @__PURE__ */ jsx(Box, { className: "flex-shrink-0", children: /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "opacity-40", children: event.turn ? `T${event.turn}` : "" }) })
11233
- ]
11234
- },
11235
- event.id
11236
- );
11237
- }) }) })
11238
- ] });
11239
- }
11240
- CombatLog.displayName = "CombatLog";
11241
9421
 
11242
9422
  // components/organisms/game/types/game.ts
11243
9423
  function createInitialGameState(width, height, units, defaultTerrain = "floorStone") {
@@ -11378,7 +9558,7 @@ function generateCombatMessage(event) {
11378
9558
  return event.message;
11379
9559
  }
11380
9560
  function extractTitle(children) {
11381
- if (!React__default.isValidElement(children)) return void 0;
9561
+ if (!React.isValidElement(children)) return void 0;
11382
9562
  const props = children.props;
11383
9563
  if (typeof props.title === "string") {
11384
9564
  return props.title;
@@ -11413,7 +9593,7 @@ var ModalSlot = ({
11413
9593
  };
11414
9594
  ModalSlot.displayName = "ModalSlot";
11415
9595
  function extractTitle2(children) {
11416
- if (!React__default.isValidElement(children)) return void 0;
9596
+ if (!React.isValidElement(children)) return void 0;
11417
9597
  const props = children.props;
11418
9598
  if (typeof props.title === "string") {
11419
9599
  return props.title;
@@ -11450,7 +9630,7 @@ var DrawerSlot = ({
11450
9630
  };
11451
9631
  DrawerSlot.displayName = "DrawerSlot";
11452
9632
  function extractToastProps(children) {
11453
- if (!React__default.isValidElement(children)) {
9633
+ if (!React.isValidElement(children)) {
11454
9634
  if (typeof children === "string") {
11455
9635
  return { message: children };
11456
9636
  }
@@ -11481,7 +9661,7 @@ var ToastSlot = ({
11481
9661
  eventBus.emit("UI:CLOSE");
11482
9662
  };
11483
9663
  if (!isVisible) return null;
11484
- const isCustomContent = React__default.isValidElement(children) && !message;
9664
+ const isCustomContent = React.isValidElement(children) && !message;
11485
9665
  return /* @__PURE__ */ jsx(Box, { className: "fixed bottom-4 right-4 z-50", children: isCustomContent ? children : /* @__PURE__ */ jsx(
11486
9666
  Toast,
11487
9667
  {
@@ -11832,7 +10012,7 @@ var Timeline = ({
11832
10012
  }) => {
11833
10013
  const { t } = useTranslate();
11834
10014
  const entityData = Array.isArray(entity) ? entity : [];
11835
- const items = React__default.useMemo(() => {
10015
+ const items = React.useMemo(() => {
11836
10016
  if (propItems) return propItems;
11837
10017
  if (entityData.length === 0) return [];
11838
10018
  return entityData.map((record, idx) => {
@@ -11973,7 +10153,7 @@ var MediaGallery = ({
11973
10153
  [selectable, selectedItems, selectionEvent, eventBus]
11974
10154
  );
11975
10155
  const entityData = Array.isArray(entity) ? entity : [];
11976
- const items = React__default.useMemo(() => {
10156
+ const items = React.useMemo(() => {
11977
10157
  if (propItems) return propItems;
11978
10158
  if (entityData.length === 0) return [];
11979
10159
  return entityData.map((record, idx) => ({
@@ -13961,4 +12141,4 @@ function WorldMapTemplate({
13961
12141
  }
13962
12142
  WorldMapTemplate.displayName = "WorldMapTemplate";
13963
12143
 
13964
- export { ALL_PRESETS, AR_BOOK_FIELDS, ActionPalette, ActionTile, AuthLayout, BattleBoard, BattleTemplate, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, BuilderBoard, CanvasEffect, CastleBoard, CastleTemplate, Chart, ClassifierBoard, CodeView, CodeViewer, CollapsibleSection, CombatLog, ConfirmDialog, ContentRenderer, CounterTemplate, DIAMOND_TOP_Y, DashboardGrid, DashboardLayout, DebuggerBoard, DialogueBox, DocumentViewer, StateMachineView as DomStateMachineVisualizer, DrawerSlot, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EventHandlerBoard, EventLog, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FormActions, FormLayout, FormSection, GameAudioContext, GameAudioProvider, GameAudioToggle, GameHud, GameMenu, GameOverScreen, GameShell, GameTemplate, GenericAppTemplate, GraphCanvas, Header, IDENTITY_BOOK_FIELDS, InventoryPanel, IsometricCanvas, JazariStateMachine, List, MediaGallery, ModalSlot, Navigation, NegotiatorBoard, NotifyListener, ObjectRulePanel, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, PhysicsManager, PlatformerCanvas, RuleEditor, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, Section, SequenceBar, SequencerBoard, Sidebar, SignaturePad, SimulationCanvas, SimulationControls, SimulationGraph, SimulatorBoard, Split, SplitPane, StateArchitectBoard, StateMachineView, StateNode2 as StateNode, StatusBar, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, Table, TerrainPalette, Timeline, ToastSlot, TraitSlot, TraitStateViewer, TransitionArrow, UncontrolledBattleBoard, VariablePanel, WizardContainer, WorldMapBoard, WorldMapTemplate, applyTemporaryEffect, calculateAttackTargets, calculateDamage, calculateValidMoves, combatAnimations, combatClasses, combatEffects, createInitialGameState, createUnitAnimationState, generateCombatMessage, getCurrentFrame, inferDirection, isoToScreen, mapBookData, pendulum, projectileMotion, resolveFieldMap, resolveFrame, resolveSheetDirection, screenToIso, springOscillator, tickAnimationState, transitionAnimation, useBattleState, useCamera, useGameAudio, useGameAudioContext, useImageCache, usePhysics2D, useSpriteAnimations };
12144
+ export { ALL_PRESETS, AR_BOOK_FIELDS, ActionPalette, ActionTile, AuthLayout, BattleBoard, BattleTemplate, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, BuilderBoard, CanvasEffect, CastleBoard, CastleTemplate, Chart, ClassifierBoard, CodeView, CodeViewer, CollapsibleSection, ConfirmDialog, ContentRenderer, CounterTemplate, DashboardGrid, DashboardLayout, DebuggerBoard, DocumentViewer, StateMachineView as DomStateMachineVisualizer, DrawerSlot, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EventHandlerBoard, EventLog, FEATURE_TYPES, FormActions, FormLayout, FormSection, GameAudioContext, GameAudioProvider, GameAudioToggle, GameShell, GameTemplate, GenericAppTemplate, GraphCanvas, Header, IDENTITY_BOOK_FIELDS, JazariStateMachine, List, MediaGallery, ModalSlot, Navigation, NegotiatorBoard, NotifyListener, ObjectRulePanel, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, PhysicsManager, RuleEditor, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, Section, SequenceBar, SequencerBoard, Sidebar, SignaturePad, SimulationCanvas, SimulationControls, SimulationGraph, SimulatorBoard, Split, SplitPane, StateArchitectBoard, StateMachineView, StateNode2 as StateNode, StatusBar, TERRAIN_COLORS, TabbedContainer, Table, TerrainPalette, Timeline, ToastSlot, TraitSlot, TraitStateViewer, TransitionArrow, UncontrolledBattleBoard, VariablePanel, WizardContainer, WorldMapBoard, WorldMapTemplate, applyTemporaryEffect, calculateAttackTargets, calculateDamage, calculateValidMoves, combatAnimations, combatClasses, combatEffects, createInitialGameState, createUnitAnimationState, generateCombatMessage, getCurrentFrame, inferDirection, mapBookData, pendulum, projectileMotion, resolveFieldMap, resolveFrame, resolveSheetDirection, springOscillator, tickAnimationState, transitionAnimation, useBattleState, useGameAudio, useGameAudioContext, usePhysics2D, useSpriteAnimations };