@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.
- package/dist/{chunk-VJP2HCLY.js → chunk-NES4SBB7.js} +6607 -3398
- package/dist/components/index.d.ts +3644 -3089
- package/dist/components/index.js +20 -1840
- package/dist/lib/index.js +1 -1
- package/dist/providers/index.js +2 -2
- package/package.json +1 -1
- package/dist/{chunk-RPYMP7ZC.js → chunk-A5J5CNCU.js} +26 -26
package/dist/components/index.js
CHANGED
|
@@ -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,
|
|
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-
|
|
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,
|
|
14
|
-
export { cn } from '../chunk-
|
|
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
|
|
20
|
-
import
|
|
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] =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 };
|