@almadar/ui 5.96.0 → 5.97.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.
|
@@ -328,6 +328,36 @@ var Canvas3DErrorBoundary = class extends React8.Component {
|
|
|
328
328
|
}
|
|
329
329
|
};
|
|
330
330
|
var log2 = logger.createLogger("almadar:ui:game:model-loader");
|
|
331
|
+
var gltfCache = /* @__PURE__ */ new Map();
|
|
332
|
+
function loadGltfCached(url, assetRoot) {
|
|
333
|
+
const key = `${assetRoot}|${url}`;
|
|
334
|
+
let entry = gltfCache.get(key);
|
|
335
|
+
if (!entry) {
|
|
336
|
+
const pending = {
|
|
337
|
+
promise: new Promise((resolveDone) => {
|
|
338
|
+
const loader = new GLTFLoader.GLTFLoader();
|
|
339
|
+
loader.setResourcePath(assetRoot);
|
|
340
|
+
loader.load(
|
|
341
|
+
url,
|
|
342
|
+
(gltf) => {
|
|
343
|
+
log2.debug("Loaded", { url, clips: gltf.animations.length });
|
|
344
|
+
pending.gltf = { scene: gltf.scene, animations: gltf.animations };
|
|
345
|
+
resolveDone();
|
|
346
|
+
},
|
|
347
|
+
void 0,
|
|
348
|
+
(err) => {
|
|
349
|
+
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
350
|
+
pending.error = err instanceof Error ? err : new Error(String(err));
|
|
351
|
+
resolveDone();
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
entry = pending;
|
|
357
|
+
gltfCache.set(key, entry);
|
|
358
|
+
}
|
|
359
|
+
return entry;
|
|
360
|
+
}
|
|
331
361
|
function detectAssetRoot(modelUrl) {
|
|
332
362
|
const idx = modelUrl.indexOf("/3d/");
|
|
333
363
|
if (idx !== -1) {
|
|
@@ -335,45 +365,30 @@ function detectAssetRoot(modelUrl) {
|
|
|
335
365
|
}
|
|
336
366
|
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
337
367
|
}
|
|
368
|
+
function stateFromEntry(entry) {
|
|
369
|
+
if (!entry) return { model: null, clips: [], isLoading: false, error: null };
|
|
370
|
+
if (entry.gltf) return { model: entry.gltf.scene, clips: entry.gltf.animations, isLoading: false, error: null };
|
|
371
|
+
return { model: null, clips: [], isLoading: !entry.error, error: entry.error ?? null };
|
|
372
|
+
}
|
|
338
373
|
function useGLTFModel(url, resourceBasePath) {
|
|
339
|
-
const [state, setState] = React8.useState(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
isLoading: false,
|
|
343
|
-
error: null
|
|
344
|
-
});
|
|
374
|
+
const [state, setState] = React8.useState(
|
|
375
|
+
() => stateFromEntry(url ? loadGltfCached(url, resourceBasePath || detectAssetRoot(url)) : null)
|
|
376
|
+
);
|
|
345
377
|
React8.useEffect(() => {
|
|
346
378
|
if (!url) {
|
|
347
379
|
setState({ model: null, clips: [], isLoading: false, error: null });
|
|
348
380
|
return;
|
|
349
381
|
}
|
|
350
|
-
|
|
351
|
-
setState((
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
model: gltf.scene,
|
|
361
|
-
clips: gltf.animations,
|
|
362
|
-
isLoading: false,
|
|
363
|
-
error: null
|
|
364
|
-
});
|
|
365
|
-
},
|
|
366
|
-
void 0,
|
|
367
|
-
(err) => {
|
|
368
|
-
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
369
|
-
setState({
|
|
370
|
-
model: null,
|
|
371
|
-
clips: [],
|
|
372
|
-
isLoading: false,
|
|
373
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
);
|
|
382
|
+
const entry = loadGltfCached(url, resourceBasePath || detectAssetRoot(url));
|
|
383
|
+
setState(stateFromEntry(entry));
|
|
384
|
+
if (entry.gltf || entry.error) return;
|
|
385
|
+
let cancelled = false;
|
|
386
|
+
void entry.promise.then(() => {
|
|
387
|
+
if (!cancelled) setState(stateFromEntry(entry));
|
|
388
|
+
});
|
|
389
|
+
return () => {
|
|
390
|
+
cancelled = true;
|
|
391
|
+
};
|
|
377
392
|
}, [url, resourceBasePath]);
|
|
378
393
|
return state;
|
|
379
394
|
}
|
|
@@ -390,21 +405,28 @@ function ModelLoader({
|
|
|
390
405
|
castShadow = true,
|
|
391
406
|
receiveShadow = true,
|
|
392
407
|
resourceBasePath,
|
|
393
|
-
animation
|
|
408
|
+
animation,
|
|
409
|
+
tint
|
|
394
410
|
}) {
|
|
395
411
|
const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
396
412
|
const model = React8.useMemo(() => {
|
|
397
413
|
if (!loadedModel) return null;
|
|
398
414
|
const cloned = SkeletonUtils.clone(loadedModel);
|
|
399
415
|
cloned.updateMatrixWorld(true);
|
|
416
|
+
const tintColor = tint ? new THREE6__namespace.Color(tint) : null;
|
|
400
417
|
cloned.traverse((child) => {
|
|
401
418
|
if (child instanceof THREE6__namespace.Mesh) {
|
|
402
419
|
child.castShadow = castShadow;
|
|
403
420
|
child.receiveShadow = receiveShadow;
|
|
421
|
+
if (tintColor && child.material instanceof THREE6__namespace.MeshStandardMaterial) {
|
|
422
|
+
const mat = child.material.clone();
|
|
423
|
+
mat.color.multiply(tintColor);
|
|
424
|
+
child.material = mat;
|
|
425
|
+
}
|
|
404
426
|
}
|
|
405
427
|
});
|
|
406
428
|
return cloned;
|
|
407
|
-
}, [loadedModel, castShadow, receiveShadow]);
|
|
429
|
+
}, [loadedModel, castShadow, receiveShadow, tint]);
|
|
408
430
|
const mixer = React8.useMemo(() => model ? new THREE6__namespace.AnimationMixer(model) : null, [model]);
|
|
409
431
|
React8.useEffect(() => {
|
|
410
432
|
if (!mixer || !animation || clips.length === 0) return;
|
|
@@ -1781,7 +1803,10 @@ function SideScene({
|
|
|
1781
1803
|
ppu,
|
|
1782
1804
|
playerSprite,
|
|
1783
1805
|
tileSprites,
|
|
1784
|
-
interpolate
|
|
1806
|
+
interpolate,
|
|
1807
|
+
features = [],
|
|
1808
|
+
events = [],
|
|
1809
|
+
assetManifest
|
|
1785
1810
|
}) {
|
|
1786
1811
|
const pw = player.width ?? 32;
|
|
1787
1812
|
const ph = player.height ?? 48;
|
|
@@ -1813,6 +1838,21 @@ function SideScene({
|
|
|
1813
1838
|
`plat-${i}`
|
|
1814
1839
|
);
|
|
1815
1840
|
}),
|
|
1841
|
+
features.map((feature, i) => {
|
|
1842
|
+
const model = feature.assetUrl ?? assetManifest?.features?.[feature.type];
|
|
1843
|
+
const fx = feature.x / ppu;
|
|
1844
|
+
const fy = (worldHeight - feature.y) / ppu;
|
|
1845
|
+
if (!model?.url) return null;
|
|
1846
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { position: [fx, fy, 0], children: /* @__PURE__ */ jsxRuntime.jsx(ModelLoader, { url: model.url, scale: 0.6, tint: feature.color, fallbackGeometry: "sphere", castShadow: true }) }, feature.id ?? `sfeat-${i}`);
|
|
1847
|
+
}),
|
|
1848
|
+
events.map((ev, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1849
|
+
EventMarker,
|
|
1850
|
+
{
|
|
1851
|
+
event: ev,
|
|
1852
|
+
position: [ev.x / ppu, (worldHeight - (ev.y ?? 0)) / ppu + 0.8, 0.2]
|
|
1853
|
+
},
|
|
1854
|
+
ev.id ?? `sev-${i}`
|
|
1855
|
+
)),
|
|
1816
1856
|
/* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsxRuntime.jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1817
1857
|
ModelLoader,
|
|
1818
1858
|
{
|
|
@@ -1829,6 +1869,245 @@ function SideScene({
|
|
|
1829
1869
|
] }) })
|
|
1830
1870
|
] });
|
|
1831
1871
|
}
|
|
1872
|
+
var UNIT_BASE_MODEL_SCALE = 0.5;
|
|
1873
|
+
var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
1874
|
+
var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
1875
|
+
var EVENT_COLORS = {
|
|
1876
|
+
hit: "#ff5544",
|
|
1877
|
+
damage: "#ff5544",
|
|
1878
|
+
attack: "#ff8844",
|
|
1879
|
+
heal: "#44dd66",
|
|
1880
|
+
pickup: "#ffcc33",
|
|
1881
|
+
score: "#ffcc33",
|
|
1882
|
+
death: "#aa3333",
|
|
1883
|
+
win: "#66ff88",
|
|
1884
|
+
lose: "#ff6666"
|
|
1885
|
+
};
|
|
1886
|
+
function EventMarker({
|
|
1887
|
+
event,
|
|
1888
|
+
position
|
|
1889
|
+
}) {
|
|
1890
|
+
return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1891
|
+
drei.Text,
|
|
1892
|
+
{
|
|
1893
|
+
fontSize: 0.32,
|
|
1894
|
+
color: EVENT_COLORS[event.type] ?? "#ffffff",
|
|
1895
|
+
outlineWidth: 0.02,
|
|
1896
|
+
outlineColor: "#000000",
|
|
1897
|
+
anchorX: "center",
|
|
1898
|
+
anchorY: "middle",
|
|
1899
|
+
children: event.message ?? event.type
|
|
1900
|
+
}
|
|
1901
|
+
) });
|
|
1902
|
+
}
|
|
1903
|
+
function DefaultTile({
|
|
1904
|
+
tile,
|
|
1905
|
+
position,
|
|
1906
|
+
model,
|
|
1907
|
+
isSelected,
|
|
1908
|
+
isHovered,
|
|
1909
|
+
isValidMove,
|
|
1910
|
+
isAttackTarget,
|
|
1911
|
+
onTileClick,
|
|
1912
|
+
onTileHover
|
|
1913
|
+
}) {
|
|
1914
|
+
let color = 8421504;
|
|
1915
|
+
if (tile.type === "water") color = 4491468;
|
|
1916
|
+
else if (tile.type === "grass") color = 4500036;
|
|
1917
|
+
else if (tile.type === "sand") color = 14535816;
|
|
1918
|
+
else if (tile.type === "rock") color = 8947848;
|
|
1919
|
+
else if (tile.type === "snow") color = 15658734;
|
|
1920
|
+
let emissive = 0;
|
|
1921
|
+
if (isSelected) emissive = 4473924;
|
|
1922
|
+
else if (isAttackTarget) emissive = 4456448;
|
|
1923
|
+
else if (isValidMove) emissive = 17408;
|
|
1924
|
+
else if (isHovered) emissive = 2236962;
|
|
1925
|
+
if (model?.url) {
|
|
1926
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1927
|
+
"group",
|
|
1928
|
+
{
|
|
1929
|
+
position,
|
|
1930
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1931
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1932
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1933
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1934
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1935
|
+
ModelLoader,
|
|
1936
|
+
{
|
|
1937
|
+
url: model.url,
|
|
1938
|
+
scale: 0.95,
|
|
1939
|
+
rotation: [0, tile.rotation ?? 0, 0],
|
|
1940
|
+
fallbackGeometry: "box",
|
|
1941
|
+
castShadow: true,
|
|
1942
|
+
receiveShadow: true
|
|
1943
|
+
}
|
|
1944
|
+
)
|
|
1945
|
+
}
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1949
|
+
"mesh",
|
|
1950
|
+
{
|
|
1951
|
+
position,
|
|
1952
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1953
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1954
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1955
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1956
|
+
children: [
|
|
1957
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
1958
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, emissive })
|
|
1959
|
+
]
|
|
1960
|
+
}
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
function DefaultUnit({
|
|
1964
|
+
unit,
|
|
1965
|
+
position,
|
|
1966
|
+
model,
|
|
1967
|
+
isSelected,
|
|
1968
|
+
unitScale,
|
|
1969
|
+
cellSize,
|
|
1970
|
+
resolveUnitFrame,
|
|
1971
|
+
onUnitClick
|
|
1972
|
+
}) {
|
|
1973
|
+
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
1974
|
+
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
1975
|
+
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
1976
|
+
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * cellSize;
|
|
1977
|
+
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * cellSize;
|
|
1978
|
+
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * cellSize;
|
|
1979
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1980
|
+
"group",
|
|
1981
|
+
{
|
|
1982
|
+
position,
|
|
1983
|
+
onClick: (e) => onUnitClick(unit, e),
|
|
1984
|
+
userData: { type: "unit", unitId: unit.id },
|
|
1985
|
+
children: [
|
|
1986
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1988
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1989
|
+
] }),
|
|
1990
|
+
hasAtlas && initialFrame ? (
|
|
1991
|
+
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
1992
|
+
/* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1993
|
+
UnitSpriteBillboard,
|
|
1994
|
+
{
|
|
1995
|
+
sheetUrl: initialFrame.sheetUrl,
|
|
1996
|
+
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
1997
|
+
height: billboardHeight
|
|
1998
|
+
}
|
|
1999
|
+
) })
|
|
2000
|
+
) : model?.url ? (
|
|
2001
|
+
/* GLB unit model — LOLO's `animation` field drives the named clip */
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2003
|
+
ModelLoader,
|
|
2004
|
+
{
|
|
2005
|
+
url: model.url,
|
|
2006
|
+
scale: modelScale,
|
|
2007
|
+
animation: unit.animation ?? "idle",
|
|
2008
|
+
fallbackGeometry: "box",
|
|
2009
|
+
castShadow: true
|
|
2010
|
+
}
|
|
2011
|
+
)
|
|
2012
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2013
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
2014
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2016
|
+
] }),
|
|
2017
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
2018
|
+
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2020
|
+
] }),
|
|
2021
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2022
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2023
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2024
|
+
] })
|
|
2025
|
+
] }),
|
|
2026
|
+
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2027
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2028
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2029
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: 3355443 })
|
|
2030
|
+
] }),
|
|
2031
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2032
|
+
"mesh",
|
|
2033
|
+
{
|
|
2034
|
+
position: [
|
|
2035
|
+
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2036
|
+
0,
|
|
2037
|
+
0.01
|
|
2038
|
+
],
|
|
2039
|
+
children: [
|
|
2040
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2042
|
+
"meshBasicMaterial",
|
|
2043
|
+
{
|
|
2044
|
+
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2045
|
+
}
|
|
2046
|
+
)
|
|
2047
|
+
]
|
|
2048
|
+
}
|
|
2049
|
+
)
|
|
2050
|
+
] })
|
|
2051
|
+
]
|
|
2052
|
+
}
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
function DefaultFeature({
|
|
2056
|
+
feature,
|
|
2057
|
+
position,
|
|
2058
|
+
model,
|
|
2059
|
+
onFeatureClick
|
|
2060
|
+
}) {
|
|
2061
|
+
if (model?.url) {
|
|
2062
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2063
|
+
ModelLoader,
|
|
2064
|
+
{
|
|
2065
|
+
url: model.url,
|
|
2066
|
+
position,
|
|
2067
|
+
scale: 0.5,
|
|
2068
|
+
rotation: [0, feature.rotation ?? 0, 0],
|
|
2069
|
+
tint: feature.color,
|
|
2070
|
+
onClick: () => onFeatureClick(feature, null),
|
|
2071
|
+
fallbackGeometry: "box"
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
if (feature.type === "tree") {
|
|
2076
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2077
|
+
"group",
|
|
2078
|
+
{
|
|
2079
|
+
position,
|
|
2080
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2081
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2082
|
+
children: [
|
|
2083
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2084
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2085
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
|
|
2086
|
+
] }),
|
|
2087
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2088
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2089
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 2263842 })
|
|
2090
|
+
] })
|
|
2091
|
+
]
|
|
2092
|
+
}
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
if (feature.type === "rock") {
|
|
2096
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2097
|
+
"mesh",
|
|
2098
|
+
{
|
|
2099
|
+
position: [position[0], position[1] + 0.3, position[2]],
|
|
2100
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2101
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2102
|
+
children: [
|
|
2103
|
+
/* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2104
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8421504 })
|
|
2105
|
+
]
|
|
2106
|
+
}
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
1832
2111
|
var GameCanvas3D = React8.forwardRef(
|
|
1833
2112
|
({
|
|
1834
2113
|
tiles = [],
|
|
@@ -2115,216 +2394,6 @@ var GameCanvas3D = React8.forwardRef(
|
|
|
2115
2394
|
}
|
|
2116
2395
|
return cameraTarget;
|
|
2117
2396
|
}, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
|
|
2118
|
-
const DefaultTileRenderer = React8.useCallback(
|
|
2119
|
-
({ tile, position }) => {
|
|
2120
|
-
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
2121
|
-
const isHovered = hoveredTile?.id === tile.id;
|
|
2122
|
-
const isValidMove = validMoves.some(
|
|
2123
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
2124
|
-
);
|
|
2125
|
-
const isAttackTarget = attackTargets.some(
|
|
2126
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
2127
|
-
);
|
|
2128
|
-
let color = 8421504;
|
|
2129
|
-
if (tile.type === "water") color = 4491468;
|
|
2130
|
-
else if (tile.type === "grass") color = 4500036;
|
|
2131
|
-
else if (tile.type === "sand") color = 14535816;
|
|
2132
|
-
else if (tile.type === "rock") color = 8947848;
|
|
2133
|
-
else if (tile.type === "snow") color = 15658734;
|
|
2134
|
-
let emissive = 0;
|
|
2135
|
-
if (isSelected) emissive = 4473924;
|
|
2136
|
-
else if (isAttackTarget) emissive = 4456448;
|
|
2137
|
-
else if (isValidMove) emissive = 17408;
|
|
2138
|
-
else if (isHovered) emissive = 2236962;
|
|
2139
|
-
const tileModel = tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""];
|
|
2140
|
-
if (tileModel?.url) {
|
|
2141
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2142
|
-
"group",
|
|
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: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2150
|
-
ModelLoader,
|
|
2151
|
-
{
|
|
2152
|
-
url: tileModel.url,
|
|
2153
|
-
scale: 0.95,
|
|
2154
|
-
fallbackGeometry: "box",
|
|
2155
|
-
castShadow: true,
|
|
2156
|
-
receiveShadow: true
|
|
2157
|
-
}
|
|
2158
|
-
)
|
|
2159
|
-
}
|
|
2160
|
-
);
|
|
2161
|
-
}
|
|
2162
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2163
|
-
"mesh",
|
|
2164
|
-
{
|
|
2165
|
-
position,
|
|
2166
|
-
onClick: (e) => handleTileClick(tile, e),
|
|
2167
|
-
onPointerEnter: (e) => handleTileHover(tile, e),
|
|
2168
|
-
onPointerLeave: (e) => handleTileHover(null, e),
|
|
2169
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
2170
|
-
children: [
|
|
2171
|
-
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
2172
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, emissive })
|
|
2173
|
-
]
|
|
2174
|
-
}
|
|
2175
|
-
);
|
|
2176
|
-
},
|
|
2177
|
-
[selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover, assetManifest]
|
|
2178
|
-
);
|
|
2179
|
-
const UNIT_BASE_MODEL_SCALE = 0.5;
|
|
2180
|
-
const UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
2181
|
-
const UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
2182
|
-
const DefaultUnitRenderer = React8.useCallback(
|
|
2183
|
-
({ unit, position }) => {
|
|
2184
|
-
const isSelected = selectedUnitId === unit.id;
|
|
2185
|
-
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
2186
|
-
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
2187
|
-
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
2188
|
-
const unitModel = unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""];
|
|
2189
|
-
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * gridConfig.cellSize;
|
|
2190
|
-
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * gridConfig.cellSize;
|
|
2191
|
-
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * gridConfig.cellSize;
|
|
2192
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2193
|
-
"group",
|
|
2194
|
-
{
|
|
2195
|
-
position,
|
|
2196
|
-
onClick: (e) => handleUnitClick(unit, e),
|
|
2197
|
-
userData: { type: "unit", unitId: unit.id },
|
|
2198
|
-
children: [
|
|
2199
|
-
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2200
|
-
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2201
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2202
|
-
] }),
|
|
2203
|
-
hasAtlas && initialFrame ? (
|
|
2204
|
-
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
2205
|
-
/* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2206
|
-
UnitSpriteBillboard,
|
|
2207
|
-
{
|
|
2208
|
-
sheetUrl: initialFrame.sheetUrl,
|
|
2209
|
-
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
2210
|
-
height: billboardHeight
|
|
2211
|
-
}
|
|
2212
|
-
) })
|
|
2213
|
-
) : unitModel?.url ? (
|
|
2214
|
-
/* GLB unit model — LOLO's `animation` field drives the named clip */
|
|
2215
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2216
|
-
ModelLoader,
|
|
2217
|
-
{
|
|
2218
|
-
url: unitModel.url,
|
|
2219
|
-
scale: modelScale,
|
|
2220
|
-
animation: unit.animation ?? "idle",
|
|
2221
|
-
fallbackGeometry: "box",
|
|
2222
|
-
castShadow: true
|
|
2223
|
-
}
|
|
2224
|
-
)
|
|
2225
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2226
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
2227
|
-
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
2228
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2229
|
-
] }),
|
|
2230
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
2231
|
-
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
2232
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2233
|
-
] }),
|
|
2234
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2235
|
-
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2236
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2237
|
-
] })
|
|
2238
|
-
] }),
|
|
2239
|
-
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2240
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2241
|
-
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2242
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: 3355443 })
|
|
2243
|
-
] }),
|
|
2244
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2245
|
-
"mesh",
|
|
2246
|
-
{
|
|
2247
|
-
position: [
|
|
2248
|
-
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2249
|
-
0,
|
|
2250
|
-
0.01
|
|
2251
|
-
],
|
|
2252
|
-
children: [
|
|
2253
|
-
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2254
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2255
|
-
"meshBasicMaterial",
|
|
2256
|
-
{
|
|
2257
|
-
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2258
|
-
}
|
|
2259
|
-
)
|
|
2260
|
-
]
|
|
2261
|
-
}
|
|
2262
|
-
)
|
|
2263
|
-
] })
|
|
2264
|
-
]
|
|
2265
|
-
}
|
|
2266
|
-
);
|
|
2267
|
-
},
|
|
2268
|
-
[selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig, assetManifest]
|
|
2269
|
-
);
|
|
2270
|
-
const DefaultFeatureRenderer = React8.useCallback(
|
|
2271
|
-
({
|
|
2272
|
-
feature,
|
|
2273
|
-
position
|
|
2274
|
-
}) => {
|
|
2275
|
-
const featureModel = feature.assetUrl ?? assetManifest?.features?.[feature.type];
|
|
2276
|
-
if (featureModel?.url) {
|
|
2277
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2278
|
-
ModelLoader,
|
|
2279
|
-
{
|
|
2280
|
-
url: featureModel.url,
|
|
2281
|
-
position,
|
|
2282
|
-
scale: 0.5,
|
|
2283
|
-
rotation: [0, feature.rotation ?? 0, 0],
|
|
2284
|
-
onClick: () => handleFeatureClick(feature, null),
|
|
2285
|
-
fallbackGeometry: "box"
|
|
2286
|
-
},
|
|
2287
|
-
feature.id
|
|
2288
|
-
);
|
|
2289
|
-
}
|
|
2290
|
-
if (feature.type === "tree") {
|
|
2291
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2292
|
-
"group",
|
|
2293
|
-
{
|
|
2294
|
-
position,
|
|
2295
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2296
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2297
|
-
children: [
|
|
2298
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2299
|
-
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2300
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
|
|
2301
|
-
] }),
|
|
2302
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2303
|
-
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2304
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 2263842 })
|
|
2305
|
-
] })
|
|
2306
|
-
]
|
|
2307
|
-
}
|
|
2308
|
-
);
|
|
2309
|
-
}
|
|
2310
|
-
if (feature.type === "rock") {
|
|
2311
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2312
|
-
"mesh",
|
|
2313
|
-
{
|
|
2314
|
-
position: [position[0], position[1] + 0.3, position[2]],
|
|
2315
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2316
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2317
|
-
children: [
|
|
2318
|
-
/* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2319
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8421504 })
|
|
2320
|
-
]
|
|
2321
|
-
}
|
|
2322
|
-
);
|
|
2323
|
-
}
|
|
2324
|
-
return null;
|
|
2325
|
-
},
|
|
2326
|
-
[handleFeatureClick, assetManifest]
|
|
2327
|
-
);
|
|
2328
2397
|
if (externalLoading || assetsLoading && preloadAssets.length > 0) {
|
|
2329
2398
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2330
2399
|
Canvas3DLoadingState,
|
|
@@ -2430,7 +2499,10 @@ var GameCanvas3D = React8.forwardRef(
|
|
|
2430
2499
|
ppu: pixelsPerUnit,
|
|
2431
2500
|
playerSprite,
|
|
2432
2501
|
tileSprites,
|
|
2433
|
-
interpolate: interpolateUnits
|
|
2502
|
+
interpolate: interpolateUnits,
|
|
2503
|
+
features,
|
|
2504
|
+
events,
|
|
2505
|
+
assetManifest
|
|
2434
2506
|
}
|
|
2435
2507
|
)
|
|
2436
2508
|
) : /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
|
|
@@ -2440,8 +2512,25 @@ var GameCanvas3D = React8.forwardRef(
|
|
|
2440
2512
|
tile.z ?? tile.y ?? 0,
|
|
2441
2513
|
tile.elevation ?? 0
|
|
2442
2514
|
);
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2515
|
+
const key = tile.id ?? `tile-${index}`;
|
|
2516
|
+
if (CustomTileRenderer) {
|
|
2517
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CustomTileRenderer, { tile, position }, key);
|
|
2518
|
+
}
|
|
2519
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2520
|
+
DefaultTile,
|
|
2521
|
+
{
|
|
2522
|
+
tile,
|
|
2523
|
+
position,
|
|
2524
|
+
model: tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""],
|
|
2525
|
+
isSelected: tile.id ? selectedTileIds.includes(tile.id) : false,
|
|
2526
|
+
isHovered: hoveredTile?.id === tile.id,
|
|
2527
|
+
isValidMove: validMoves.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2528
|
+
isAttackTarget: attackTargets.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2529
|
+
onTileClick: handleTileClick,
|
|
2530
|
+
onTileHover: handleTileHover
|
|
2531
|
+
},
|
|
2532
|
+
key
|
|
2533
|
+
);
|
|
2445
2534
|
}),
|
|
2446
2535
|
features.map((feature, index) => {
|
|
2447
2536
|
const position = gridToWorld2(
|
|
@@ -2449,8 +2538,20 @@ var GameCanvas3D = React8.forwardRef(
|
|
|
2449
2538
|
feature.z ?? feature.y ?? 0,
|
|
2450
2539
|
(feature.elevation ?? 0) + 0.5
|
|
2451
2540
|
);
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2541
|
+
const key = feature.id ?? `feature-${index}`;
|
|
2542
|
+
if (CustomFeatureRenderer) {
|
|
2543
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CustomFeatureRenderer, { feature, position }, key);
|
|
2544
|
+
}
|
|
2545
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2546
|
+
DefaultFeature,
|
|
2547
|
+
{
|
|
2548
|
+
feature,
|
|
2549
|
+
position,
|
|
2550
|
+
model: feature.assetUrl ?? assetManifest?.features?.[feature.type],
|
|
2551
|
+
onFeatureClick: handleFeatureClick
|
|
2552
|
+
},
|
|
2553
|
+
key
|
|
2554
|
+
);
|
|
2454
2555
|
}),
|
|
2455
2556
|
units.map((unit) => {
|
|
2456
2557
|
const position = gridToWorld2(
|
|
@@ -2458,8 +2559,30 @@ var GameCanvas3D = React8.forwardRef(
|
|
|
2458
2559
|
unit.z ?? unit.y ?? unit.position?.y ?? 0,
|
|
2459
2560
|
(unit.elevation ?? 0) + 0.5
|
|
2460
2561
|
);
|
|
2461
|
-
|
|
2462
|
-
|
|
2562
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsxRuntime.jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2563
|
+
DefaultUnit,
|
|
2564
|
+
{
|
|
2565
|
+
unit,
|
|
2566
|
+
position: [0, 0, 0],
|
|
2567
|
+
model: unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""],
|
|
2568
|
+
isSelected: selectedUnitId === unit.id,
|
|
2569
|
+
unitScale,
|
|
2570
|
+
cellSize: gridConfig.cellSize,
|
|
2571
|
+
resolveUnitFrame,
|
|
2572
|
+
onUnitClick: handleUnitClick
|
|
2573
|
+
}
|
|
2574
|
+
) }, unit.id);
|
|
2575
|
+
}),
|
|
2576
|
+
events.map((ev, i) => {
|
|
2577
|
+
const position = gridToWorld2(ev.x, ev.z ?? ev.y ?? 0, 0);
|
|
2578
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2579
|
+
EventMarker,
|
|
2580
|
+
{
|
|
2581
|
+
event: ev,
|
|
2582
|
+
position: [position[0], position[1] + 1.4, position[2]]
|
|
2583
|
+
},
|
|
2584
|
+
ev.id ?? `ev-${i}`
|
|
2585
|
+
);
|
|
2463
2586
|
})
|
|
2464
2587
|
] }),
|
|
2465
2588
|
children,
|