aether-engine 1.0.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.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/biome.json +51 -0
  3. package/bun.lock +192 -0
  4. package/index.ts +1 -0
  5. package/package.json +25 -0
  6. package/serve.ts +125 -0
  7. package/src/audio/AudioEngine.ts +61 -0
  8. package/src/components/Animator3D.ts +65 -0
  9. package/src/components/AudioSource.ts +26 -0
  10. package/src/components/BitmapText.ts +25 -0
  11. package/src/components/Camera.ts +33 -0
  12. package/src/components/CameraFollow.ts +5 -0
  13. package/src/components/Collider.ts +16 -0
  14. package/src/components/Components.test.ts +68 -0
  15. package/src/components/Light.ts +15 -0
  16. package/src/components/MeshRenderer.ts +58 -0
  17. package/src/components/ParticleEmitter.ts +59 -0
  18. package/src/components/RigidBody.ts +9 -0
  19. package/src/components/ShadowCaster.ts +3 -0
  20. package/src/components/SkinnedMeshRenderer.ts +25 -0
  21. package/src/components/SpriteAnimator.ts +42 -0
  22. package/src/components/SpriteRenderer.ts +26 -0
  23. package/src/components/Transform.test.ts +39 -0
  24. package/src/components/Transform.ts +54 -0
  25. package/src/core/AssetManager.ts +123 -0
  26. package/src/core/Input.test.ts +67 -0
  27. package/src/core/Input.ts +94 -0
  28. package/src/core/Scene.ts +24 -0
  29. package/src/core/SceneManager.ts +57 -0
  30. package/src/core/Storage.ts +161 -0
  31. package/src/desktop/SteamClient.ts +52 -0
  32. package/src/ecs/System.ts +11 -0
  33. package/src/ecs/World.test.ts +29 -0
  34. package/src/ecs/World.ts +149 -0
  35. package/src/index.ts +115 -0
  36. package/src/math/Color.ts +100 -0
  37. package/src/math/Vector2.ts +96 -0
  38. package/src/math/Vector3.ts +103 -0
  39. package/src/math/math.test.ts +168 -0
  40. package/src/renderer/GlowMaterial.ts +66 -0
  41. package/src/renderer/LitMaterial.ts +337 -0
  42. package/src/renderer/Material.test.ts +23 -0
  43. package/src/renderer/Material.ts +80 -0
  44. package/src/renderer/OcclusionMaterial.ts +43 -0
  45. package/src/renderer/ParticleMaterial.ts +66 -0
  46. package/src/renderer/Shader.ts +44 -0
  47. package/src/renderer/SkinnedLitMaterial.ts +55 -0
  48. package/src/renderer/WaterMaterial.ts +298 -0
  49. package/src/renderer/WebGLRenderer.ts +917 -0
  50. package/src/systems/Animation3DSystem.ts +148 -0
  51. package/src/systems/AnimationSystem.ts +58 -0
  52. package/src/systems/AudioSystem.ts +62 -0
  53. package/src/systems/LightingSystem.ts +114 -0
  54. package/src/systems/ParticleSystem.ts +278 -0
  55. package/src/systems/PhysicsSystem.ts +211 -0
  56. package/src/systems/Systems.test.ts +165 -0
  57. package/src/systems/TextSystem.ts +153 -0
  58. package/src/ui/AnimationEditor.tsx +639 -0
  59. package/src/ui/BottomPanel.tsx +443 -0
  60. package/src/ui/EntityExplorer.tsx +420 -0
  61. package/src/ui/GameState.ts +286 -0
  62. package/src/ui/Icons.tsx +239 -0
  63. package/src/ui/InventoryPanel.tsx +335 -0
  64. package/src/ui/PlayerHUD.tsx +250 -0
  65. package/src/ui/SpriteEditor.tsx +3241 -0
  66. package/src/ui/SpriteSheetManager.tsx +198 -0
  67. package/src/utils/GLTFLoader.ts +257 -0
  68. package/src/utils/ObjLoader.ts +81 -0
  69. package/src/utils/idb.ts +137 -0
  70. package/src/utils/packer.ts +85 -0
  71. package/test_obj.ts +12 -0
  72. package/tsconfig.json +21 -0
  73. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,239 @@
1
+ import type { JSX } from "solid-js";
2
+
3
+ export function IconBase(
4
+ props: JSX.SvgSVGAttributes<SVGSVGElement> & {
5
+ children?: any;
6
+ size?: number | string;
7
+ },
8
+ ) {
9
+ return (
10
+ <svg
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width={props.size || 24}
13
+ height={props.size || 24}
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ {...props}
21
+ >
22
+ {props.children}
23
+ </svg>
24
+ );
25
+ }
26
+
27
+ export const IconPencil = (props: any) => (
28
+ <IconBase {...props}>
29
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
30
+ <path d="m15 5 4 4" />
31
+ </IconBase>
32
+ );
33
+
34
+ export const IconEraser = (props: any) => (
35
+ <IconBase {...props}>
36
+ <path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21" />
37
+ <path d="M22 21H7" />
38
+ <path d="m5 11 9 9" />
39
+ </IconBase>
40
+ );
41
+
42
+ export const IconBucket = (props: any) => (
43
+ <IconBase {...props}>
44
+ <path d="m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z" />
45
+ <path d="m5 2 5 5" />
46
+ <path d="M2 13h15" />
47
+ <path d="M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.4 2-4 .3 1.6 2 2.4 2 4Z" />
48
+ </IconBase>
49
+ );
50
+
51
+ export const IconLine = (props: any) => (
52
+ <IconBase {...props}>
53
+ <line x1="3" y1="21" x2="21" y2="3" />
54
+ </IconBase>
55
+ );
56
+
57
+ export const IconRect = (props: any) => (
58
+ <IconBase {...props}>
59
+ <rect width="18" height="18" x="3" y="3" rx="2" />
60
+ </IconBase>
61
+ );
62
+
63
+ export const IconCircle = (props: any) => (
64
+ <IconBase {...props}>
65
+ <circle cx="12" cy="12" r="10" />
66
+ </IconBase>
67
+ );
68
+
69
+ export const IconSelect = (props: any) => (
70
+ <IconBase {...props}>
71
+ <rect width="18" height="18" x="3" y="3" rx="2" stroke-dasharray="4 4" />
72
+ </IconBase>
73
+ );
74
+
75
+ export const IconPan = (props: any) => (
76
+ <IconBase {...props}>
77
+ <path d="M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0" />
78
+ <path d="M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2" />
79
+ <path d="M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8" />
80
+ <path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" />
81
+ </IconBase>
82
+ );
83
+
84
+ export const IconCopy = (props: any) => (
85
+ <IconBase {...props}>
86
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
87
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
88
+ </IconBase>
89
+ );
90
+
91
+ export const IconPaste = (props: any) => (
92
+ <IconBase {...props}>
93
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
94
+ <rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
95
+ </IconBase>
96
+ );
97
+
98
+ export const IconDuplicate = (props: any) => (
99
+ <IconBase {...props}>
100
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
101
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
102
+ <line x1="15" x2="15" y1="11" y2="17" />
103
+ <line x1="12" x2="18" y1="14" y2="14" />
104
+ </IconBase>
105
+ );
106
+
107
+ export const IconTrash = (props: any) => (
108
+ <IconBase {...props}>
109
+ <path d="M3 6h18" />
110
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
111
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
112
+ </IconBase>
113
+ );
114
+
115
+ export const IconEye = (props: any) => (
116
+ <IconBase {...props}>
117
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
118
+ <circle cx="12" cy="12" r="3" />
119
+ </IconBase>
120
+ );
121
+
122
+ export const IconEyeOff = (props: any) => (
123
+ <IconBase {...props}>
124
+ <path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
125
+ <path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
126
+ <path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
127
+ <line x1="2" x2="22" y1="2" y2="22" />
128
+ </IconBase>
129
+ );
130
+
131
+ export const IconUp = (props: any) => (
132
+ <IconBase {...props}>
133
+ <path d="m18 15-6-6-6 6" />
134
+ </IconBase>
135
+ );
136
+
137
+ export const IconDown = (props: any) => (
138
+ <IconBase {...props}>
139
+ <path d="m6 9 6 6 6-6" />
140
+ </IconBase>
141
+ );
142
+
143
+ export const IconMergeDown = (props: any) => (
144
+ <IconBase {...props}>
145
+ <path d="M12 3v14" />
146
+ <path d="m19 10-7 7-7-7" />
147
+ <path d="M5 21h14" />
148
+ </IconBase>
149
+ );
150
+
151
+ export const IconPlus = (props: any) => (
152
+ <IconBase {...props}>
153
+ <path d="M5 12h14" />
154
+ <path d="M12 5v14" />
155
+ </IconBase>
156
+ );
157
+
158
+ export const IconX = (props: any) => (
159
+ <IconBase {...props}>
160
+ <path d="M18 6 6 18" />
161
+ <path d="m6 6 12 12" />
162
+ </IconBase>
163
+ );
164
+
165
+ export const IconCheck = (props: any) => (
166
+ <IconBase {...props}>
167
+ <path d="M20 6 9 17l-5-5" />
168
+ </IconBase>
169
+ );
170
+
171
+ export const IconSave = (props: any) => (
172
+ <IconBase {...props}>
173
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
174
+ <polyline points="17 21 17 13 7 13 7 21" />
175
+ <polyline points="7 3 7 8 15 8" />
176
+ </IconBase>
177
+ );
178
+
179
+ export const IconPlay = (props: any) => (
180
+ <IconBase {...props}>
181
+ <polygon points="5 3 19 12 5 21 5 3" />
182
+ </IconBase>
183
+ );
184
+
185
+ export const IconPause = (props: any) => (
186
+ <IconBase {...props}>
187
+ <rect width="4" height="16" x="6" y="4" />
188
+ <rect width="4" height="16" x="14" y="4" />
189
+ </IconBase>
190
+ );
191
+
192
+ export const IconStepForward = (props: any) => (
193
+ <IconBase {...props}>
194
+ <line x1="19" x2="19" y1="5" y2="19" />
195
+ <polygon points="5 4 15 12 5 20 5 4" />
196
+ </IconBase>
197
+ );
198
+
199
+ export const IconChevronUp = (props: any) => (
200
+ <IconBase {...props}>
201
+ <path d="m18 15-6-6-6 6" />
202
+ </IconBase>
203
+ );
204
+
205
+ export const IconChevronDown = (props: any) => (
206
+ <IconBase {...props}>
207
+ <path d="m6 9 6 6 6-6" />
208
+ </IconBase>
209
+ );
210
+
211
+ export const IconChevronRight = (props: any) => (
212
+ <IconBase {...props}>
213
+ <path d="m9 18 6-6-6-6" />
214
+ </IconBase>
215
+ );
216
+
217
+ export const IconFlipHorizontal = (props: any) => (
218
+ <IconBase {...props}>
219
+ <path d="M8 21.5 4.5 18 8 14.5" />
220
+ <path d="M16 21.5l3.5-3.5L16 14.5" />
221
+ <line x1="12" x2="12" y1="2" y2="22" />
222
+ </IconBase>
223
+ );
224
+
225
+ export const IconFlipVertical = (props: any) => (
226
+ <IconBase {...props}>
227
+ <path d="M14.5 8 18 4.5 21.5 8" />
228
+ <path d="M14.5 16 18 19.5 21.5 16" />
229
+ <line x1="2" x2="22" y1="12" y2="12" />
230
+ </IconBase>
231
+ );
232
+
233
+ export const IconAetherEngine = (props: any) => (
234
+ <IconBase {...props}>
235
+ <path d="M12 2L2 20h20L12 2z" />
236
+ <path d="M12 2v20" />
237
+ <path d="M6 14h12" />
238
+ </IconBase>
239
+ );
@@ -0,0 +1,335 @@
1
+ import { Index, onCleanup, onMount, Show } from "solid-js";
2
+ import {
3
+ chestInv,
4
+ draggedItem,
5
+ dragHoverTarget,
6
+ getInvAccessor,
7
+ handleDrop,
8
+ type InventorySource,
9
+ type Item,
10
+ initInventory,
11
+ isDropHandled,
12
+ playerInv,
13
+ setDraggedItem,
14
+ setDragHoverTarget,
15
+ setInvAccessor,
16
+ setIsDropHandled,
17
+ } from "./GameState";
18
+
19
+ export function InventoryPanel(props: {
20
+ isOpen: boolean;
21
+ hasChest?: boolean;
22
+ onClose: () => void;
23
+ }) {
24
+ onMount(() => {
25
+ initInventory(); // We call init here temporarily just to test
26
+
27
+ const handleKeyDown = (e: KeyboardEvent) => {
28
+ if (e.key === "Escape" && props.isOpen) {
29
+ props.onClose();
30
+ }
31
+ };
32
+ window.addEventListener("keydown", handleKeyDown);
33
+ onCleanup(() => {
34
+ window.removeEventListener("keydown", handleKeyDown);
35
+ });
36
+ });
37
+
38
+ const handleDragStart = (
39
+ e: DragEvent,
40
+ source: InventorySource,
41
+ index: number,
42
+ ) => {
43
+ setDraggedItem({ source, index });
44
+ if (e.dataTransfer) {
45
+ e.dataTransfer.effectAllowed = "move";
46
+ e.dataTransfer.setData("text/plain", "");
47
+ }
48
+ };
49
+
50
+ const handleDragOver = (e: DragEvent) => {
51
+ e.preventDefault();
52
+ if (e.dataTransfer) {
53
+ e.dataTransfer.dropEffect = "move";
54
+ }
55
+ };
56
+
57
+ const onDropHandler = (
58
+ e: DragEvent,
59
+ targetSource: InventorySource,
60
+ targetIndex: number,
61
+ ) => {
62
+ e.preventDefault();
63
+ handleDrop(targetSource, targetIndex);
64
+ };
65
+
66
+ const handleDragEnter = (
67
+ e: DragEvent,
68
+ source: InventorySource,
69
+ index: number,
70
+ ) => {
71
+ e.preventDefault();
72
+ setDragHoverTarget({ source, index });
73
+ };
74
+
75
+ const handleDragLeave = (
76
+ _e: DragEvent,
77
+ source: InventorySource,
78
+ index: number,
79
+ ) => {
80
+ const currentTarget = dragHoverTarget();
81
+ if (
82
+ currentTarget &&
83
+ currentTarget.source === source &&
84
+ currentTarget.index === index
85
+ ) {
86
+ setDragHoverTarget(null);
87
+ }
88
+ };
89
+
90
+ const handleDragEnd = (_e: DragEvent) => {
91
+ if (!isDropHandled() && draggedItem()) {
92
+ // Dropped on ground
93
+ const itemInfo = draggedItem()!;
94
+ const invAccess = getInvAccessor(itemInfo.source)();
95
+ const item = invAccess[itemInfo.index];
96
+ if (item) {
97
+ window.dispatchEvent(
98
+ new CustomEvent("DropItemOnGround", { detail: { item } }),
99
+ );
100
+ const newInv = [...invAccess];
101
+ newInv[itemInfo.index] = null;
102
+ setInvAccessor(itemInfo.source, newInv);
103
+ }
104
+ }
105
+ setDraggedItem(null);
106
+ setDragHoverTarget(null);
107
+ setIsDropHandled(false);
108
+ };
109
+
110
+ const renderSlot = (
111
+ itemAccessor: () => Item | null,
112
+ source: InventorySource,
113
+ index: number,
114
+ ) => {
115
+ return (
116
+ <div
117
+ style={{
118
+ width: "48px",
119
+ height: "48px",
120
+ background:
121
+ dragHoverTarget()?.source === source &&
122
+ dragHoverTarget()?.index === index &&
123
+ !(
124
+ draggedItem()?.source === source && draggedItem()?.index === index
125
+ )
126
+ ? "rgba(255, 255, 255, 0.2)"
127
+ : "rgba(0, 0, 0, 0.4)",
128
+ border:
129
+ dragHoverTarget()?.source === source &&
130
+ dragHoverTarget()?.index === index &&
131
+ !(
132
+ draggedItem()?.source === source && draggedItem()?.index === index
133
+ )
134
+ ? "1px solid rgba(255, 255, 255, 0.8)"
135
+ : "1px solid rgba(255, 255, 255, 0.1)",
136
+ "border-radius": "6px",
137
+ position: "relative",
138
+ display: "flex",
139
+ "justify-content": "center",
140
+ "align-items": "center",
141
+ cursor: itemAccessor() ? "grab" : "default",
142
+ "box-shadow":
143
+ dragHoverTarget()?.source === source &&
144
+ dragHoverTarget()?.index === index &&
145
+ !(
146
+ draggedItem()?.source === source && draggedItem()?.index === index
147
+ )
148
+ ? "0 0 8px rgba(255,255,255,0.5)"
149
+ : "inset 0px 2px 4px rgba(0,0,0,0.3)",
150
+ opacity:
151
+ draggedItem()?.source === source && draggedItem()?.index === index
152
+ ? 0.3
153
+ : 1,
154
+ transition: "all 0.1s ease-in-out",
155
+ }}
156
+ draggable={!!itemAccessor()}
157
+ onDragStart={(e) => itemAccessor() && handleDragStart(e, source, index)}
158
+ onDragOver={handleDragOver}
159
+ onDragEnter={(e) => handleDragEnter(e, source, index)}
160
+ onDragLeave={(e) => handleDragLeave(e, source, index)}
161
+ onDrop={(e) => onDropHandler(e, source, index)}
162
+ onDragEnd={handleDragEnd}
163
+ >
164
+ <Show when={itemAccessor()}>
165
+ {(it) => {
166
+ const item = () => it();
167
+ return (
168
+ <>
169
+ <Show
170
+ when={item().iconX !== undefined}
171
+ fallback={
172
+ <img
173
+ src={item().icon}
174
+ style={{
175
+ "max-width": "32px",
176
+ "max-height": "32px",
177
+ "object-fit": "contain",
178
+ "pointer-events": "none",
179
+ }}
180
+ />
181
+ }
182
+ >
183
+ <div
184
+ style={{
185
+ width: "32px",
186
+ height: "32px",
187
+ "background-image": `url(${item().icon})`,
188
+ "background-position": `-${item().iconX! * (32 / item().iconW!)}px -${item().iconY! * (32 / item().iconH!)}px`,
189
+ "background-size": `${item().atlasW! * (32 / item().iconW!)}px ${item().atlasH! * (32 / item().iconH!)}px`,
190
+ "pointer-events": "none",
191
+ "image-rendering": "pixelated",
192
+ }}
193
+ ></div>
194
+ </Show>
195
+ <div
196
+ style={{
197
+ position: "absolute",
198
+ bottom: "2px",
199
+ right: "4px",
200
+ "font-size": "11px",
201
+ color: "white",
202
+ "font-weight": "bold",
203
+ "text-shadow": "1px 1px 0px black",
204
+ }}
205
+ >
206
+ {item().quantity > 1 ? item().quantity : ""}
207
+ </div>
208
+ </>
209
+ );
210
+ }}
211
+ </Show>
212
+ </div>
213
+ );
214
+ };
215
+
216
+ return (
217
+ <Show when={props.isOpen}>
218
+ <div
219
+ style={{
220
+ position: "absolute",
221
+ top: 0,
222
+ left: 0,
223
+ width: "100%",
224
+ height: "100%",
225
+ background: "rgba(0, 0, 0, 0.6)",
226
+ display: "flex",
227
+ "justify-content": "center",
228
+ "align-items": "center",
229
+ "z-index": 5000,
230
+ "backdrop-filter": "blur(4px)",
231
+ "font-family": "Arial, Helvetica, sans-serif",
232
+ "user-select": "none",
233
+ }}
234
+ >
235
+ <div
236
+ style={{
237
+ display: "flex",
238
+ gap: "40px",
239
+ background: "rgba(35, 35, 45, 0.9)",
240
+ padding: "30px",
241
+ "border-radius": "12px",
242
+ border: "1px solid rgba(255,255,255,0.15)",
243
+ "box-shadow": "0 10px 30px rgba(0,0,0,0.5)",
244
+ position: "relative",
245
+ }}
246
+ >
247
+ <button
248
+ onClick={props.onClose}
249
+ style={{
250
+ position: "absolute",
251
+ top: "10px",
252
+ right: "10px",
253
+ background: "transparent",
254
+ border: "none",
255
+ color: "#AAA",
256
+ cursor: "pointer",
257
+ "font-size": "16px",
258
+ "font-weight": "bold",
259
+ }}
260
+ >
261
+ X
262
+ </button>
263
+
264
+ {/* Chest Inventory */}
265
+ <Show when={props.hasChest !== false}>
266
+ <div
267
+ style={{
268
+ display: "flex",
269
+ "flex-direction": "column",
270
+ gap: "10px",
271
+ }}
272
+ >
273
+ <h2
274
+ style={{
275
+ color: "white",
276
+ margin: 0,
277
+ "font-size": "18px",
278
+ "text-align": "center",
279
+ "text-shadow": "0 2px 2px rgba(0,0,0,0.5)",
280
+ }}
281
+ >
282
+ Chest
283
+ </h2>
284
+ <div
285
+ style={{
286
+ display: "grid",
287
+ "grid-template-columns": "repeat(4, 1fr)",
288
+ gap: "8px",
289
+ background: "rgba(0,0,0,0.2)",
290
+ padding: "15px",
291
+ "border-radius": "8px",
292
+ }}
293
+ >
294
+ <Index each={chestInv()}>
295
+ {(item, i) => renderSlot(item, "chest", i)}
296
+ </Index>
297
+ </div>
298
+ </div>
299
+ </Show>
300
+
301
+ {/* Player Inventory */}
302
+ <div
303
+ style={{ display: "flex", "flex-direction": "column", gap: "10px" }}
304
+ >
305
+ <h2
306
+ style={{
307
+ color: "white",
308
+ margin: 0,
309
+ "font-size": "18px",
310
+ "text-align": "center",
311
+ "text-shadow": "0 2px 2px rgba(0,0,0,0.5)",
312
+ }}
313
+ >
314
+ Inventory
315
+ </h2>
316
+ <div
317
+ style={{
318
+ display: "grid",
319
+ "grid-template-columns": "repeat(4, 1fr)",
320
+ gap: "8px",
321
+ background: "rgba(0,0,0,0.2)",
322
+ padding: "15px",
323
+ "border-radius": "8px",
324
+ }}
325
+ >
326
+ <Index each={playerInv()}>
327
+ {(item, i) => renderSlot(item, "player", i)}
328
+ </Index>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </Show>
334
+ );
335
+ }