@almadar/ui 5.96.0 → 5.98.0
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/avl/index.cjs +50 -42
- package/dist/avl/index.js +52 -44
- package/dist/components/game/3d/index.cjs +376 -253
- package/dist/components/game/3d/index.js +377 -254
- package/dist/components/game/3d/molecules/ModelLoader.d.ts +3 -1
- package/dist/components/game/shared/isometricTypes.d.ts +2 -0
- package/dist/components/index.cjs +29 -32
- package/dist/components/index.js +29 -32
- package/dist/lib/index.cjs +5 -2
- package/dist/lib/index.js +5 -2
- package/dist/providers/index.cjs +40 -40
- package/dist/providers/index.js +40 -40
- package/dist/runtime/index.cjs +50 -42
- package/dist/runtime/index.js +52 -44
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ import { useThree, useFrame, Canvas, useLoader } from '@react-three/fiber';
|
|
|
4
4
|
import * as THREE6 from 'three';
|
|
5
5
|
import { Vector3, QuadraticBezierCurve3, MathUtils, Quaternion } from 'three';
|
|
6
6
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
|
-
import { OrbitControls, Billboard,
|
|
7
|
+
import { OrbitControls, Grid, Billboard, Text, Stars, Sparkles, Html, RoundedBox } from '@react-three/drei';
|
|
8
8
|
import { createLogger } from '@almadar/logger';
|
|
9
9
|
import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
10
10
|
import { clone } from 'three/examples/jsm/utils/SkeletonUtils';
|
|
@@ -307,6 +307,36 @@ var Canvas3DErrorBoundary = class extends Component {
|
|
|
307
307
|
}
|
|
308
308
|
};
|
|
309
309
|
var log2 = createLogger("almadar:ui:game:model-loader");
|
|
310
|
+
var gltfCache = /* @__PURE__ */ new Map();
|
|
311
|
+
function loadGltfCached(url, assetRoot) {
|
|
312
|
+
const key = `${assetRoot}|${url}`;
|
|
313
|
+
let entry = gltfCache.get(key);
|
|
314
|
+
if (!entry) {
|
|
315
|
+
const pending = {
|
|
316
|
+
promise: new Promise((resolveDone) => {
|
|
317
|
+
const loader = new GLTFLoader$1();
|
|
318
|
+
loader.setResourcePath(assetRoot);
|
|
319
|
+
loader.load(
|
|
320
|
+
url,
|
|
321
|
+
(gltf) => {
|
|
322
|
+
log2.debug("Loaded", { url, clips: gltf.animations.length });
|
|
323
|
+
pending.gltf = { scene: gltf.scene, animations: gltf.animations };
|
|
324
|
+
resolveDone();
|
|
325
|
+
},
|
|
326
|
+
void 0,
|
|
327
|
+
(err) => {
|
|
328
|
+
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
329
|
+
pending.error = err instanceof Error ? err : new Error(String(err));
|
|
330
|
+
resolveDone();
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
};
|
|
335
|
+
entry = pending;
|
|
336
|
+
gltfCache.set(key, entry);
|
|
337
|
+
}
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
310
340
|
function detectAssetRoot(modelUrl) {
|
|
311
341
|
const idx = modelUrl.indexOf("/3d/");
|
|
312
342
|
if (idx !== -1) {
|
|
@@ -314,45 +344,30 @@ function detectAssetRoot(modelUrl) {
|
|
|
314
344
|
}
|
|
315
345
|
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
316
346
|
}
|
|
347
|
+
function stateFromEntry(entry) {
|
|
348
|
+
if (!entry) return { model: null, clips: [], isLoading: false, error: null };
|
|
349
|
+
if (entry.gltf) return { model: entry.gltf.scene, clips: entry.gltf.animations, isLoading: false, error: null };
|
|
350
|
+
return { model: null, clips: [], isLoading: !entry.error, error: entry.error ?? null };
|
|
351
|
+
}
|
|
317
352
|
function useGLTFModel(url, resourceBasePath) {
|
|
318
|
-
const [state, setState] = useState(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
isLoading: false,
|
|
322
|
-
error: null
|
|
323
|
-
});
|
|
353
|
+
const [state, setState] = useState(
|
|
354
|
+
() => stateFromEntry(url ? loadGltfCached(url, resourceBasePath || detectAssetRoot(url)) : null)
|
|
355
|
+
);
|
|
324
356
|
useEffect(() => {
|
|
325
357
|
if (!url) {
|
|
326
358
|
setState({ model: null, clips: [], isLoading: false, error: null });
|
|
327
359
|
return;
|
|
328
360
|
}
|
|
329
|
-
|
|
330
|
-
setState((
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
model: gltf.scene,
|
|
340
|
-
clips: gltf.animations,
|
|
341
|
-
isLoading: false,
|
|
342
|
-
error: null
|
|
343
|
-
});
|
|
344
|
-
},
|
|
345
|
-
void 0,
|
|
346
|
-
(err) => {
|
|
347
|
-
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
348
|
-
setState({
|
|
349
|
-
model: null,
|
|
350
|
-
clips: [],
|
|
351
|
-
isLoading: false,
|
|
352
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
);
|
|
361
|
+
const entry = loadGltfCached(url, resourceBasePath || detectAssetRoot(url));
|
|
362
|
+
setState(stateFromEntry(entry));
|
|
363
|
+
if (entry.gltf || entry.error) return;
|
|
364
|
+
let cancelled = false;
|
|
365
|
+
void entry.promise.then(() => {
|
|
366
|
+
if (!cancelled) setState(stateFromEntry(entry));
|
|
367
|
+
});
|
|
368
|
+
return () => {
|
|
369
|
+
cancelled = true;
|
|
370
|
+
};
|
|
356
371
|
}, [url, resourceBasePath]);
|
|
357
372
|
return state;
|
|
358
373
|
}
|
|
@@ -369,21 +384,28 @@ function ModelLoader({
|
|
|
369
384
|
castShadow = true,
|
|
370
385
|
receiveShadow = true,
|
|
371
386
|
resourceBasePath,
|
|
372
|
-
animation
|
|
387
|
+
animation,
|
|
388
|
+
tint
|
|
373
389
|
}) {
|
|
374
390
|
const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
375
391
|
const model = useMemo(() => {
|
|
376
392
|
if (!loadedModel) return null;
|
|
377
393
|
const cloned = clone(loadedModel);
|
|
378
394
|
cloned.updateMatrixWorld(true);
|
|
395
|
+
const tintColor = tint ? new THREE6.Color(tint) : null;
|
|
379
396
|
cloned.traverse((child) => {
|
|
380
397
|
if (child instanceof THREE6.Mesh) {
|
|
381
398
|
child.castShadow = castShadow;
|
|
382
399
|
child.receiveShadow = receiveShadow;
|
|
400
|
+
if (tintColor && child.material instanceof THREE6.MeshStandardMaterial) {
|
|
401
|
+
const mat = child.material.clone();
|
|
402
|
+
mat.color.multiply(tintColor);
|
|
403
|
+
child.material = mat;
|
|
404
|
+
}
|
|
383
405
|
}
|
|
384
406
|
});
|
|
385
407
|
return cloned;
|
|
386
|
-
}, [loadedModel, castShadow, receiveShadow]);
|
|
408
|
+
}, [loadedModel, castShadow, receiveShadow, tint]);
|
|
387
409
|
const mixer = useMemo(() => model ? new THREE6.AnimationMixer(model) : null, [model]);
|
|
388
410
|
useEffect(() => {
|
|
389
411
|
if (!mixer || !animation || clips.length === 0) return;
|
|
@@ -1760,7 +1782,10 @@ function SideScene({
|
|
|
1760
1782
|
ppu,
|
|
1761
1783
|
playerSprite,
|
|
1762
1784
|
tileSprites,
|
|
1763
|
-
interpolate
|
|
1785
|
+
interpolate,
|
|
1786
|
+
features = [],
|
|
1787
|
+
events = [],
|
|
1788
|
+
assetManifest
|
|
1764
1789
|
}) {
|
|
1765
1790
|
const pw = player.width ?? 32;
|
|
1766
1791
|
const ph = player.height ?? 48;
|
|
@@ -1792,6 +1817,21 @@ function SideScene({
|
|
|
1792
1817
|
`plat-${i}`
|
|
1793
1818
|
);
|
|
1794
1819
|
}),
|
|
1820
|
+
features.map((feature, i) => {
|
|
1821
|
+
const model = feature.assetUrl ?? assetManifest?.features?.[feature.type];
|
|
1822
|
+
const fx = feature.x / ppu;
|
|
1823
|
+
const fy = (worldHeight - feature.y) / ppu;
|
|
1824
|
+
if (!model?.url) return null;
|
|
1825
|
+
return /* @__PURE__ */ jsx("group", { position: [fx, fy, 0], children: /* @__PURE__ */ jsx(ModelLoader, { url: model.url, scale: 0.6, tint: feature.color, fallbackGeometry: "sphere", castShadow: true }) }, feature.id ?? `sfeat-${i}`);
|
|
1826
|
+
}),
|
|
1827
|
+
events.map((ev, i) => /* @__PURE__ */ jsx(
|
|
1828
|
+
EventMarker,
|
|
1829
|
+
{
|
|
1830
|
+
event: ev,
|
|
1831
|
+
position: [ev.x / ppu, (worldHeight - (ev.y ?? 0)) / ppu + 0.8, 0.2]
|
|
1832
|
+
},
|
|
1833
|
+
ev.id ?? `sev-${i}`
|
|
1834
|
+
)),
|
|
1795
1835
|
/* @__PURE__ */ jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsx(
|
|
1796
1836
|
ModelLoader,
|
|
1797
1837
|
{
|
|
@@ -1808,6 +1848,245 @@ function SideScene({
|
|
|
1808
1848
|
] }) })
|
|
1809
1849
|
] });
|
|
1810
1850
|
}
|
|
1851
|
+
var UNIT_BASE_MODEL_SCALE = 0.5;
|
|
1852
|
+
var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
1853
|
+
var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
1854
|
+
var EVENT_COLORS = {
|
|
1855
|
+
hit: "#ff5544",
|
|
1856
|
+
damage: "#ff5544",
|
|
1857
|
+
attack: "#ff8844",
|
|
1858
|
+
heal: "#44dd66",
|
|
1859
|
+
pickup: "#ffcc33",
|
|
1860
|
+
score: "#ffcc33",
|
|
1861
|
+
death: "#aa3333",
|
|
1862
|
+
win: "#66ff88",
|
|
1863
|
+
lose: "#ff6666"
|
|
1864
|
+
};
|
|
1865
|
+
function EventMarker({
|
|
1866
|
+
event,
|
|
1867
|
+
position
|
|
1868
|
+
}) {
|
|
1869
|
+
return /* @__PURE__ */ jsx(Billboard, { position, children: /* @__PURE__ */ jsx(
|
|
1870
|
+
Text,
|
|
1871
|
+
{
|
|
1872
|
+
fontSize: 0.32,
|
|
1873
|
+
color: EVENT_COLORS[event.type] ?? "#ffffff",
|
|
1874
|
+
outlineWidth: 0.02,
|
|
1875
|
+
outlineColor: "#000000",
|
|
1876
|
+
anchorX: "center",
|
|
1877
|
+
anchorY: "middle",
|
|
1878
|
+
children: event.message ?? event.type
|
|
1879
|
+
}
|
|
1880
|
+
) });
|
|
1881
|
+
}
|
|
1882
|
+
function DefaultTile({
|
|
1883
|
+
tile,
|
|
1884
|
+
position,
|
|
1885
|
+
model,
|
|
1886
|
+
isSelected,
|
|
1887
|
+
isHovered,
|
|
1888
|
+
isValidMove,
|
|
1889
|
+
isAttackTarget,
|
|
1890
|
+
onTileClick,
|
|
1891
|
+
onTileHover
|
|
1892
|
+
}) {
|
|
1893
|
+
let color = 8421504;
|
|
1894
|
+
if (tile.type === "water") color = 4491468;
|
|
1895
|
+
else if (tile.type === "grass") color = 4500036;
|
|
1896
|
+
else if (tile.type === "sand") color = 14535816;
|
|
1897
|
+
else if (tile.type === "rock") color = 8947848;
|
|
1898
|
+
else if (tile.type === "snow") color = 15658734;
|
|
1899
|
+
let emissive = 0;
|
|
1900
|
+
if (isSelected) emissive = 4473924;
|
|
1901
|
+
else if (isAttackTarget) emissive = 4456448;
|
|
1902
|
+
else if (isValidMove) emissive = 17408;
|
|
1903
|
+
else if (isHovered) emissive = 2236962;
|
|
1904
|
+
if (model?.url) {
|
|
1905
|
+
return /* @__PURE__ */ jsx(
|
|
1906
|
+
"group",
|
|
1907
|
+
{
|
|
1908
|
+
position,
|
|
1909
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1910
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1911
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1912
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1913
|
+
children: /* @__PURE__ */ jsx(
|
|
1914
|
+
ModelLoader,
|
|
1915
|
+
{
|
|
1916
|
+
url: model.url,
|
|
1917
|
+
scale: 0.95,
|
|
1918
|
+
rotation: [0, tile.rotation ?? 0, 0],
|
|
1919
|
+
fallbackGeometry: "box",
|
|
1920
|
+
castShadow: true,
|
|
1921
|
+
receiveShadow: true
|
|
1922
|
+
}
|
|
1923
|
+
)
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
return /* @__PURE__ */ jsxs(
|
|
1928
|
+
"mesh",
|
|
1929
|
+
{
|
|
1930
|
+
position,
|
|
1931
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1932
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1933
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1934
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1935
|
+
children: [
|
|
1936
|
+
/* @__PURE__ */ jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
1937
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color, emissive })
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
function DefaultUnit({
|
|
1943
|
+
unit,
|
|
1944
|
+
position,
|
|
1945
|
+
model,
|
|
1946
|
+
isSelected,
|
|
1947
|
+
unitScale,
|
|
1948
|
+
cellSize,
|
|
1949
|
+
resolveUnitFrame,
|
|
1950
|
+
onUnitClick
|
|
1951
|
+
}) {
|
|
1952
|
+
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
1953
|
+
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
1954
|
+
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
1955
|
+
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * cellSize;
|
|
1956
|
+
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * cellSize;
|
|
1957
|
+
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * cellSize;
|
|
1958
|
+
return /* @__PURE__ */ jsxs(
|
|
1959
|
+
"group",
|
|
1960
|
+
{
|
|
1961
|
+
position,
|
|
1962
|
+
onClick: (e) => onUnitClick(unit, e),
|
|
1963
|
+
userData: { type: "unit", unitId: unit.id },
|
|
1964
|
+
children: [
|
|
1965
|
+
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1966
|
+
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1967
|
+
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1968
|
+
] }),
|
|
1969
|
+
hasAtlas && initialFrame ? (
|
|
1970
|
+
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
1971
|
+
/* @__PURE__ */ jsx(Billboard, { children: /* @__PURE__ */ jsx(
|
|
1972
|
+
UnitSpriteBillboard,
|
|
1973
|
+
{
|
|
1974
|
+
sheetUrl: initialFrame.sheetUrl,
|
|
1975
|
+
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
1976
|
+
height: billboardHeight
|
|
1977
|
+
}
|
|
1978
|
+
) })
|
|
1979
|
+
) : model?.url ? (
|
|
1980
|
+
/* GLB unit model — LOLO's `animation` field drives the named clip */
|
|
1981
|
+
/* @__PURE__ */ jsx(
|
|
1982
|
+
ModelLoader,
|
|
1983
|
+
{
|
|
1984
|
+
url: model.url,
|
|
1985
|
+
scale: modelScale,
|
|
1986
|
+
animation: unit.animation ?? "idle",
|
|
1987
|
+
fallbackGeometry: "box",
|
|
1988
|
+
castShadow: true
|
|
1989
|
+
}
|
|
1990
|
+
)
|
|
1991
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1992
|
+
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
1993
|
+
/* @__PURE__ */ jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
1994
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1995
|
+
] }),
|
|
1996
|
+
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
1997
|
+
/* @__PURE__ */ jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
1998
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1999
|
+
] }),
|
|
2000
|
+
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2001
|
+
/* @__PURE__ */ jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2002
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
2003
|
+
] })
|
|
2004
|
+
] }),
|
|
2005
|
+
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2006
|
+
/* @__PURE__ */ jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2007
|
+
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2008
|
+
/* @__PURE__ */ jsx("meshBasicMaterial", { color: 3355443 })
|
|
2009
|
+
] }),
|
|
2010
|
+
/* @__PURE__ */ jsxs(
|
|
2011
|
+
"mesh",
|
|
2012
|
+
{
|
|
2013
|
+
position: [
|
|
2014
|
+
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2015
|
+
0,
|
|
2016
|
+
0.01
|
|
2017
|
+
],
|
|
2018
|
+
children: [
|
|
2019
|
+
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2020
|
+
/* @__PURE__ */ jsx(
|
|
2021
|
+
"meshBasicMaterial",
|
|
2022
|
+
{
|
|
2023
|
+
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2024
|
+
}
|
|
2025
|
+
)
|
|
2026
|
+
]
|
|
2027
|
+
}
|
|
2028
|
+
)
|
|
2029
|
+
] })
|
|
2030
|
+
]
|
|
2031
|
+
}
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
function DefaultFeature({
|
|
2035
|
+
feature,
|
|
2036
|
+
position,
|
|
2037
|
+
model,
|
|
2038
|
+
onFeatureClick
|
|
2039
|
+
}) {
|
|
2040
|
+
if (model?.url) {
|
|
2041
|
+
return /* @__PURE__ */ jsx(
|
|
2042
|
+
ModelLoader,
|
|
2043
|
+
{
|
|
2044
|
+
url: model.url,
|
|
2045
|
+
position,
|
|
2046
|
+
scale: 0.5,
|
|
2047
|
+
rotation: [0, feature.rotation ?? 0, 0],
|
|
2048
|
+
tint: feature.color,
|
|
2049
|
+
onClick: () => onFeatureClick(feature, null),
|
|
2050
|
+
fallbackGeometry: "box"
|
|
2051
|
+
}
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
if (feature.type === "tree") {
|
|
2055
|
+
return /* @__PURE__ */ jsxs(
|
|
2056
|
+
"group",
|
|
2057
|
+
{
|
|
2058
|
+
position,
|
|
2059
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2060
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2061
|
+
children: [
|
|
2062
|
+
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2063
|
+
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2064
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 9127187 })
|
|
2065
|
+
] }),
|
|
2066
|
+
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2067
|
+
/* @__PURE__ */ jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2068
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 2263842 })
|
|
2069
|
+
] })
|
|
2070
|
+
]
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
if (feature.type === "rock") {
|
|
2075
|
+
return /* @__PURE__ */ jsxs(
|
|
2076
|
+
"mesh",
|
|
2077
|
+
{
|
|
2078
|
+
position: [position[0], position[1] + 0.3, position[2]],
|
|
2079
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2080
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2081
|
+
children: [
|
|
2082
|
+
/* @__PURE__ */ jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2083
|
+
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 8421504 })
|
|
2084
|
+
]
|
|
2085
|
+
}
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
1811
2090
|
var GameCanvas3D = forwardRef(
|
|
1812
2091
|
({
|
|
1813
2092
|
tiles = [],
|
|
@@ -2094,216 +2373,6 @@ var GameCanvas3D = forwardRef(
|
|
|
2094
2373
|
}
|
|
2095
2374
|
return cameraTarget;
|
|
2096
2375
|
}, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
|
|
2097
|
-
const DefaultTileRenderer = useCallback(
|
|
2098
|
-
({ tile, position }) => {
|
|
2099
|
-
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
2100
|
-
const isHovered = hoveredTile?.id === tile.id;
|
|
2101
|
-
const isValidMove = validMoves.some(
|
|
2102
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
2103
|
-
);
|
|
2104
|
-
const isAttackTarget = attackTargets.some(
|
|
2105
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
2106
|
-
);
|
|
2107
|
-
let color = 8421504;
|
|
2108
|
-
if (tile.type === "water") color = 4491468;
|
|
2109
|
-
else if (tile.type === "grass") color = 4500036;
|
|
2110
|
-
else if (tile.type === "sand") color = 14535816;
|
|
2111
|
-
else if (tile.type === "rock") color = 8947848;
|
|
2112
|
-
else if (tile.type === "snow") color = 15658734;
|
|
2113
|
-
let emissive = 0;
|
|
2114
|
-
if (isSelected) emissive = 4473924;
|
|
2115
|
-
else if (isAttackTarget) emissive = 4456448;
|
|
2116
|
-
else if (isValidMove) emissive = 17408;
|
|
2117
|
-
else if (isHovered) emissive = 2236962;
|
|
2118
|
-
const tileModel = tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""];
|
|
2119
|
-
if (tileModel?.url) {
|
|
2120
|
-
return /* @__PURE__ */ jsx(
|
|
2121
|
-
"group",
|
|
2122
|
-
{
|
|
2123
|
-
position,
|
|
2124
|
-
onClick: (e) => handleTileClick(tile, e),
|
|
2125
|
-
onPointerEnter: (e) => handleTileHover(tile, e),
|
|
2126
|
-
onPointerLeave: (e) => handleTileHover(null, e),
|
|
2127
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
2128
|
-
children: /* @__PURE__ */ jsx(
|
|
2129
|
-
ModelLoader,
|
|
2130
|
-
{
|
|
2131
|
-
url: tileModel.url,
|
|
2132
|
-
scale: 0.95,
|
|
2133
|
-
fallbackGeometry: "box",
|
|
2134
|
-
castShadow: true,
|
|
2135
|
-
receiveShadow: true
|
|
2136
|
-
}
|
|
2137
|
-
)
|
|
2138
|
-
}
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
return /* @__PURE__ */ jsxs(
|
|
2142
|
-
"mesh",
|
|
2143
|
-
{
|
|
2144
|
-
position,
|
|
2145
|
-
onClick: (e) => handleTileClick(tile, e),
|
|
2146
|
-
onPointerEnter: (e) => handleTileHover(tile, e),
|
|
2147
|
-
onPointerLeave: (e) => handleTileHover(null, e),
|
|
2148
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
2149
|
-
children: [
|
|
2150
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
2151
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color, emissive })
|
|
2152
|
-
]
|
|
2153
|
-
}
|
|
2154
|
-
);
|
|
2155
|
-
},
|
|
2156
|
-
[selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover, assetManifest]
|
|
2157
|
-
);
|
|
2158
|
-
const UNIT_BASE_MODEL_SCALE = 0.5;
|
|
2159
|
-
const UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
2160
|
-
const UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
2161
|
-
const DefaultUnitRenderer = useCallback(
|
|
2162
|
-
({ unit, position }) => {
|
|
2163
|
-
const isSelected = selectedUnitId === unit.id;
|
|
2164
|
-
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
2165
|
-
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
2166
|
-
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
2167
|
-
const unitModel = unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""];
|
|
2168
|
-
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * gridConfig.cellSize;
|
|
2169
|
-
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * gridConfig.cellSize;
|
|
2170
|
-
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * gridConfig.cellSize;
|
|
2171
|
-
return /* @__PURE__ */ jsxs(
|
|
2172
|
-
"group",
|
|
2173
|
-
{
|
|
2174
|
-
position,
|
|
2175
|
-
onClick: (e) => handleUnitClick(unit, e),
|
|
2176
|
-
userData: { type: "unit", unitId: unit.id },
|
|
2177
|
-
children: [
|
|
2178
|
-
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2179
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2180
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2181
|
-
] }),
|
|
2182
|
-
hasAtlas && initialFrame ? (
|
|
2183
|
-
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
2184
|
-
/* @__PURE__ */ jsx(Billboard, { children: /* @__PURE__ */ jsx(
|
|
2185
|
-
UnitSpriteBillboard,
|
|
2186
|
-
{
|
|
2187
|
-
sheetUrl: initialFrame.sheetUrl,
|
|
2188
|
-
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
2189
|
-
height: billboardHeight
|
|
2190
|
-
}
|
|
2191
|
-
) })
|
|
2192
|
-
) : unitModel?.url ? (
|
|
2193
|
-
/* GLB unit model — LOLO's `animation` field drives the named clip */
|
|
2194
|
-
/* @__PURE__ */ jsx(
|
|
2195
|
-
ModelLoader,
|
|
2196
|
-
{
|
|
2197
|
-
url: unitModel.url,
|
|
2198
|
-
scale: modelScale,
|
|
2199
|
-
animation: unit.animation ?? "idle",
|
|
2200
|
-
fallbackGeometry: "box",
|
|
2201
|
-
castShadow: true
|
|
2202
|
-
}
|
|
2203
|
-
)
|
|
2204
|
-
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2205
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
2206
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
2207
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
2208
|
-
] }),
|
|
2209
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
2210
|
-
/* @__PURE__ */ jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
2211
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
2212
|
-
] }),
|
|
2213
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2214
|
-
/* @__PURE__ */ jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2215
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
2216
|
-
] })
|
|
2217
|
-
] }),
|
|
2218
|
-
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2219
|
-
/* @__PURE__ */ jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2220
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2221
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: 3355443 })
|
|
2222
|
-
] }),
|
|
2223
|
-
/* @__PURE__ */ jsxs(
|
|
2224
|
-
"mesh",
|
|
2225
|
-
{
|
|
2226
|
-
position: [
|
|
2227
|
-
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2228
|
-
0,
|
|
2229
|
-
0.01
|
|
2230
|
-
],
|
|
2231
|
-
children: [
|
|
2232
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2233
|
-
/* @__PURE__ */ jsx(
|
|
2234
|
-
"meshBasicMaterial",
|
|
2235
|
-
{
|
|
2236
|
-
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2237
|
-
}
|
|
2238
|
-
)
|
|
2239
|
-
]
|
|
2240
|
-
}
|
|
2241
|
-
)
|
|
2242
|
-
] })
|
|
2243
|
-
]
|
|
2244
|
-
}
|
|
2245
|
-
);
|
|
2246
|
-
},
|
|
2247
|
-
[selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig, assetManifest]
|
|
2248
|
-
);
|
|
2249
|
-
const DefaultFeatureRenderer = useCallback(
|
|
2250
|
-
({
|
|
2251
|
-
feature,
|
|
2252
|
-
position
|
|
2253
|
-
}) => {
|
|
2254
|
-
const featureModel = feature.assetUrl ?? assetManifest?.features?.[feature.type];
|
|
2255
|
-
if (featureModel?.url) {
|
|
2256
|
-
return /* @__PURE__ */ jsx(
|
|
2257
|
-
ModelLoader,
|
|
2258
|
-
{
|
|
2259
|
-
url: featureModel.url,
|
|
2260
|
-
position,
|
|
2261
|
-
scale: 0.5,
|
|
2262
|
-
rotation: [0, feature.rotation ?? 0, 0],
|
|
2263
|
-
onClick: () => handleFeatureClick(feature, null),
|
|
2264
|
-
fallbackGeometry: "box"
|
|
2265
|
-
},
|
|
2266
|
-
feature.id
|
|
2267
|
-
);
|
|
2268
|
-
}
|
|
2269
|
-
if (feature.type === "tree") {
|
|
2270
|
-
return /* @__PURE__ */ jsxs(
|
|
2271
|
-
"group",
|
|
2272
|
-
{
|
|
2273
|
-
position,
|
|
2274
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2275
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2276
|
-
children: [
|
|
2277
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2278
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2279
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 9127187 })
|
|
2280
|
-
] }),
|
|
2281
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2282
|
-
/* @__PURE__ */ jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2283
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 2263842 })
|
|
2284
|
-
] })
|
|
2285
|
-
]
|
|
2286
|
-
}
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
if (feature.type === "rock") {
|
|
2290
|
-
return /* @__PURE__ */ jsxs(
|
|
2291
|
-
"mesh",
|
|
2292
|
-
{
|
|
2293
|
-
position: [position[0], position[1] + 0.3, position[2]],
|
|
2294
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2295
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2296
|
-
children: [
|
|
2297
|
-
/* @__PURE__ */ jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2298
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 8421504 })
|
|
2299
|
-
]
|
|
2300
|
-
}
|
|
2301
|
-
);
|
|
2302
|
-
}
|
|
2303
|
-
return null;
|
|
2304
|
-
},
|
|
2305
|
-
[handleFeatureClick, assetManifest]
|
|
2306
|
-
);
|
|
2307
2376
|
if (externalLoading || assetsLoading && preloadAssets.length > 0) {
|
|
2308
2377
|
return /* @__PURE__ */ jsx(
|
|
2309
2378
|
Canvas3DLoadingState,
|
|
@@ -2409,7 +2478,10 @@ var GameCanvas3D = forwardRef(
|
|
|
2409
2478
|
ppu: pixelsPerUnit,
|
|
2410
2479
|
playerSprite,
|
|
2411
2480
|
tileSprites,
|
|
2412
|
-
interpolate: interpolateUnits
|
|
2481
|
+
interpolate: interpolateUnits,
|
|
2482
|
+
features,
|
|
2483
|
+
events,
|
|
2484
|
+
assetManifest
|
|
2413
2485
|
}
|
|
2414
2486
|
)
|
|
2415
2487
|
) : /* @__PURE__ */ jsxs("group", { children: [
|
|
@@ -2419,8 +2491,25 @@ var GameCanvas3D = forwardRef(
|
|
|
2419
2491
|
tile.z ?? tile.y ?? 0,
|
|
2420
2492
|
tile.elevation ?? 0
|
|
2421
2493
|
);
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2494
|
+
const key = tile.id ?? `tile-${index}`;
|
|
2495
|
+
if (CustomTileRenderer) {
|
|
2496
|
+
return /* @__PURE__ */ jsx(CustomTileRenderer, { tile, position }, key);
|
|
2497
|
+
}
|
|
2498
|
+
return /* @__PURE__ */ jsx(
|
|
2499
|
+
DefaultTile,
|
|
2500
|
+
{
|
|
2501
|
+
tile,
|
|
2502
|
+
position,
|
|
2503
|
+
model: tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""],
|
|
2504
|
+
isSelected: tile.id ? selectedTileIds.includes(tile.id) : false,
|
|
2505
|
+
isHovered: hoveredTile?.id === tile.id,
|
|
2506
|
+
isValidMove: validMoves.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2507
|
+
isAttackTarget: attackTargets.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2508
|
+
onTileClick: handleTileClick,
|
|
2509
|
+
onTileHover: handleTileHover
|
|
2510
|
+
},
|
|
2511
|
+
key
|
|
2512
|
+
);
|
|
2424
2513
|
}),
|
|
2425
2514
|
features.map((feature, index) => {
|
|
2426
2515
|
const position = gridToWorld2(
|
|
@@ -2428,8 +2517,20 @@ var GameCanvas3D = forwardRef(
|
|
|
2428
2517
|
feature.z ?? feature.y ?? 0,
|
|
2429
2518
|
(feature.elevation ?? 0) + 0.5
|
|
2430
2519
|
);
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2520
|
+
const key = feature.id ?? `feature-${index}`;
|
|
2521
|
+
if (CustomFeatureRenderer) {
|
|
2522
|
+
return /* @__PURE__ */ jsx(CustomFeatureRenderer, { feature, position }, key);
|
|
2523
|
+
}
|
|
2524
|
+
return /* @__PURE__ */ jsx(
|
|
2525
|
+
DefaultFeature,
|
|
2526
|
+
{
|
|
2527
|
+
feature,
|
|
2528
|
+
position,
|
|
2529
|
+
model: feature.assetUrl ?? assetManifest?.features?.[feature.type],
|
|
2530
|
+
onFeatureClick: handleFeatureClick
|
|
2531
|
+
},
|
|
2532
|
+
key
|
|
2533
|
+
);
|
|
2433
2534
|
}),
|
|
2434
2535
|
units.map((unit) => {
|
|
2435
2536
|
const position = gridToWorld2(
|
|
@@ -2437,8 +2538,30 @@ var GameCanvas3D = forwardRef(
|
|
|
2437
2538
|
unit.z ?? unit.y ?? unit.position?.y ?? 0,
|
|
2438
2539
|
(unit.elevation ?? 0) + 0.5
|
|
2439
2540
|
);
|
|
2440
|
-
|
|
2441
|
-
|
|
2541
|
+
return /* @__PURE__ */ jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsx(
|
|
2542
|
+
DefaultUnit,
|
|
2543
|
+
{
|
|
2544
|
+
unit,
|
|
2545
|
+
position: [0, 0, 0],
|
|
2546
|
+
model: unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""],
|
|
2547
|
+
isSelected: selectedUnitId === unit.id,
|
|
2548
|
+
unitScale,
|
|
2549
|
+
cellSize: gridConfig.cellSize,
|
|
2550
|
+
resolveUnitFrame,
|
|
2551
|
+
onUnitClick: handleUnitClick
|
|
2552
|
+
}
|
|
2553
|
+
) }, unit.id);
|
|
2554
|
+
}),
|
|
2555
|
+
events.map((ev, i) => {
|
|
2556
|
+
const position = gridToWorld2(ev.x, ev.z ?? ev.y ?? 0, 0);
|
|
2557
|
+
return /* @__PURE__ */ jsx(
|
|
2558
|
+
EventMarker,
|
|
2559
|
+
{
|
|
2560
|
+
event: ev,
|
|
2561
|
+
position: [position[0], position[1] + 1.4, position[2]]
|
|
2562
|
+
},
|
|
2563
|
+
ev.id ?? `ev-${i}`
|
|
2564
|
+
);
|
|
2442
2565
|
})
|
|
2443
2566
|
] }),
|
|
2444
2567
|
children,
|