aappeell commited on
Commit
26272c4
·
verified ·
1 Parent(s): b93eb69

Dont have the table at the top right display if you are close to a bench, or if you have the lantern on. Also make the lantern not so bright where I turn into the holy spirit. The dropping function doesnt work (it might but I just instant pick it back up) and onluy show the accessible crafting and dont put (need bench) just only show it when a bench is near (within 3 blocks on the x cordinate)

Browse files
Files changed (2) hide show
  1. README.md +8 -5
  2. index.html +1457 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Blocky Builder Deluxe
3
- emoji:
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Blocky Builder Deluxe 🧱
3
+ colorFrom: pink
4
+ colorTo: blue
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
index.html CHANGED
@@ -1,19 +1,1457 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Advanced 2D Sandbox Game (v7)</title>
7
+ <!-- Load Tailwind CSS for styling -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ /* Use Inter font */
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #3b82f6; /* Sky Blue - Will be darkened by JS overlay */
15
+ color: white;
16
+ margin: 0;
17
+ padding: 0;
18
+ overflow: hidden;
19
+ display: flex;
20
+ justify-content: center;
21
+ align-items: center;
22
+ min-height: 10vh;
23
+ }
24
+ canvas {
25
+ display: block;
26
+ background: transparent;
27
+ cursor: crosshair;
28
+ }
29
+ /* UI container styles */
30
+ #ui-overlay {
31
+ position: fixed;
32
+ top: 0;
33
+ left: 0;
34
+ width: 100%;
35
+ height: 100%;
36
+ pointer-events: none; /* Allows mouse to pass through */
37
+ }
38
+ #hotbar, #instructions, #coordinates, #crafting-menu {
39
+ pointer-events: auto; /* Re-enable pointer events for UI elements */
40
+ }
41
+ /* Hotbar styles */
42
+ #hotbar {
43
+ position: fixed;
44
+ bottom: 20px;
45
+ left: 50%;
46
+ transform: translateX(-50%);
47
+ display: flex;
48
+ gap: 8px;
49
+ background-color: rgba(0, 0, 0, 0.5);
50
+ padding: 8px;
51
+ border-radius: 12px;
52
+ border: 2px solid rgba(255, 255, 255, 0.3);
53
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
54
+ transition: all 0.1s ease;
55
+ }
56
+ .hotbar-slot {
57
+ width: 50px;
58
+ height: 50px;
59
+ border: 2px solid #888;
60
+ border-radius: 8px;
61
+ background-size: cover;
62
+ background-position: center;
63
+ display: flex;
64
+ align-items: flex-end;
65
+ justify-content: flex-end;
66
+ font-weight: bold;
67
+ font-size: 14px;
68
+ text-shadow: 1px 1px 2px #000;
69
+ position: relative;
70
+ cursor: pointer;
71
+ box-shadow: inset 0 0 5px rgba(0,0,0,0.5);
72
+ }
73
+ .hotbar-slot.active {
74
+ border-color: #fde047; /* Yellow */
75
+ transform: scale(1.1);
76
+ box-shadow: 0 0 15px #fde047;
77
+ }
78
+ .hotbar-slot .count {
79
+ position: absolute;
80
+ bottom: 2px;
81
+ right: 4px;
82
+ font-size: 16px;
83
+ }
84
+ .hotbar-slot .item-name {
85
+ position: absolute;
86
+ top: 2px;
87
+ left: 50%;
88
+ transform: translateX(-50%);
89
+ font-size: 10px;
90
+ white-space: nowrap;
91
+ color: #ccc;
92
+ }
93
+ /* Crafting panel styles */
94
+ #crafting-menu {
95
+ position: fixed;
96
+ top: 50%;
97
+ left: 50%;
98
+ transform: translate(-50%, -50%);
99
+ width: 400px;
100
+ max-height: 80vh;
101
+ background-color: #2c3e50;
102
+ border: 4px solid #fde047;
103
+ border-radius: 15px;
104
+ padding: 20px;
105
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
106
+ z-index: 1000;
107
+ display: none; /* Hidden by default */
108
+ overflow-y: auto;
109
+ pointer-events: auto;
110
+ }
111
+ .recipe-item {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ background-color: #34495e;
116
+ padding: 10px;
117
+ margin-bottom: 8px;
118
+ border-radius: 8px;
119
+ transition: background-color 0.2s;
120
+ cursor: pointer;
121
+ border: 1px solid #4a637c;
122
+ }
123
+ .recipe-item:hover:not(.disabled) {
124
+ background-color: #4a637c;
125
+ }
126
+ .recipe-item.disabled {
127
+ opacity: 0.5;
128
+ cursor: not-allowed;
129
+ background-color: #1e2a38;
130
+ }
131
+ .recipe-info {
132
+ display: flex;
133
+ flex-direction: column;
134
+ flex-grow: 1;
135
+ }
136
+ .recipe-name {
137
+ font-size: 1.1rem;
138
+ font-weight: bold;
139
+ color: #ecf0f1;
140
+ }
141
+ .recipe-materials {
142
+ font-size: 0.8rem;
143
+ color: #bdc3c7;
144
+ }
145
+ .craft-button {
146
+ background-color: #2ecc71;
147
+ color: white;
148
+ padding: 8px 15px;
149
+ border-radius: 6px;
150
+ font-weight: bold;
151
+ transition: background-color 0.2s;
152
+ }
153
+ .recipe-item.disabled .craft-button {
154
+ background-color: #e74c3c;
155
+ }
156
+ </style>
157
+ </head>
158
+ <body>
159
+
160
+ <!-- Canvas for the game world -->
161
+ <canvas id="gameCanvas"></canvas>
162
+
163
+ <!-- UI Overlay Container -->
164
+ <div id="ui-overlay">
165
+ <!-- UI: Hotbar for selecting items -->
166
+ <div id="hotbar">
167
+ <!-- Slots will be generated by JS -->
168
+ </div>
169
+
170
+ <!-- UI: Instructions -->
171
+ <div id="instructions" class="fixed top-4 left-4 p-3 bg-gray-800 bg-opacity-70 rounded-lg text-sm text-gray-200">
172
+ <strong>Controls:</strong> [A][D] Move | [Space] Jump | [1-7] Select | [Mouse Wheel] Scroll | **[E]** Crafting Menu | **[F]** Loot Chest | **[G]** Drop Item
173
+ </div>
174
+
175
+ <!-- UI: Crafting Menu -->
176
+ <div id="crafting-menu">
177
+ <h2 class="text-xl font-bold mb-4 text-center">Crafting Bench</h2>
178
+ <div id="recipe-list">
179
+ <!-- Recipes will be generated by JS -->
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <script type="module">
185
+ // --- Perlin Noise Helper Functions ---
186
+ const PERLIN_SIZE = 256;
187
+ const PERLIN_WRAP = PERLIN_SIZE - 1;
188
+ const p = new Array(PERLIN_SIZE);
189
+ for(let i = 0; i < PERLIN_SIZE; i++) p[i] = Math.floor(Math.random() * 256);
190
+
191
+ function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
192
+ function lerp(t, a, b) { return a + t * (b - a); }
193
+ function grad(hash, x, y) {
194
+ const h = hash & 15;
195
+ const u = h < 8 ? x : y;
196
+ const v = h < 4 ? y : h === 12 || h === 14 ? x : 0;
197
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
198
+ }
199
+
200
+ function noise2D(x, y) {
201
+ let X = Math.floor(x) & PERLIN_WRAP;
202
+ let Y = Math.floor(y) & PERLIN_WRAP;
203
+ x -= Math.floor(x);
204
+ y -= Math.floor(y);
205
+
206
+ let u = fade(x);
207
+ let v = fade(y);
208
+
209
+ let A = (p[X] + Y) & PERLIN_WRAP;
210
+ let B = (p[X + 1] + Y) & PERLIN_WRAP;
211
+
212
+ let h_A = p[A];
213
+ let h_B = p[B];
214
+ let h_A1 = p[A + 1];
215
+ let h_B1 = p[B + 1];
216
+
217
+ return lerp(v,
218
+ lerp(u, grad(h_A, x, y), grad(h_B, x - 1, y)),
219
+ lerp(u, grad(h_A1, x, y - 1), grad(h_B1, x - 1, y - 1))
220
+ );
221
+ }
222
+
223
+ // --- Game Setup ---
224
+ const canvas = document.getElementById('gameCanvas');
225
+ const ctx = canvas.getContext('2d');
226
+ const hotbarContainer = document.getElementById('hotbar');
227
+ const craftingMenu = document.getElementById('crafting-menu');
228
+ const recipeList = document.getElementById('recipe-list');
229
+
230
+ // --- Constants ---
231
+ const TILE_SIZE = 32;
232
+ const WORLD_WIDTH = 300; // In tiles
233
+ const WORLD_HEIGHT = 300; // Increased depth!
234
+ const GRAVITY = 0.5;
235
+ const PLAYER_SPEED = 5;
236
+ const JUMP_FORCE = 12;
237
+ const REACH = 5 * TILE_SIZE; // How many tiles away the player can build/mine
238
+ const E = 0.01; // Epsilon for collision buffer to prevent sticking
239
+
240
+ const NOISE_SCALE = 0.02;
241
+ const CAVE_THRESHOLD = 0.1;
242
+ const SURFACE_Y = 60; // Y coordinate (in tiles) where the surface generally starts
243
+
244
+ // --- Asset Definitions (Tiles and Items) ---
245
+ const ASSETS = {
246
+ // Tiles (Placeable Blocks, ID < 100)
247
+ AIR: 0,
248
+ GRASS: 1,
249
+ DIRT: 2,
250
+ STONE: 3,
251
+ WOOD: 4,
252
+ LEAVES: 5,
253
+ COPPER_ORE: 6,
254
+ IRON_ORE: 7,
255
+ GOLD_ORE: 8,
256
+ DIAMOND_ORE: 9,
257
+ PLATFORM_WOOD: 10,
258
+ CHEST: 11,
259
+ CRAFTING_TABLE: 12, // New Block
260
+
261
+ // Items (Non-placeable, ID >= 100)
262
+ PICKAXE_WOOD: 100,
263
+ LANTERN: 101, // New Crafted Light Source
264
+ };
265
+
266
+ const ASSET_INFO = {
267
+ [ASSETS.AIR]: { name: 'Air', color: 'rgba(0,0,0,0)', hp: 0, stackable: false, placeable: false },
268
+ [ASSETS.GRASS]: { name: 'Grass', color: '#6a994e', hp: 20, stackable: true, placeable: true },
269
+ [ASSETS.DIRT]: { name: 'Dirt', color: '#785532', hp: 20, stackable: true, placeable: true },
270
+ [ASSETS.STONE]: { name: 'Stone', color: '#8a8d91', hp: 50, stackable: true, placeable: true },
271
+ [ASSETS.WOOD]: { name: 'Wood', color: '#6f4518', hp: 40, stackable: true, placeable: true },
272
+ [ASSETS.LEAVES]: { name: 'Leaves', color: '#386641', hp: 10, stackable: true, placeable: true },
273
+ [ASSETS.COPPER_ORE]: { name: 'Copper Ore', color: '#b87333', hp: 80, stackable: true, placeable: true, highContrast: true },
274
+ [ASSETS.IRON_ORE]: { name: 'Iron Ore', color: '#a8a8a0', hp: 120, stackable: true, placeable: true, highContrast: true },
275
+ [ASSETS.GOLD_ORE]: { name: 'Gold Ore', color: '#ffd700', hp: 150, stackable: true, placeable: true, highContrast: true },
276
+ [ASSETS.DIAMOND_ORE]: { name: 'Diamond Ore', color: '#b9f2ff', hp: 250, stackable: true, placeable: true, highContrast: true },
277
+ [ASSETS.PLATFORM_WOOD]: { name: 'Wood Platform', color: '#a07850', hp: 5, stackable: true, placeable: true, isPlatform: true },
278
+ [ASSETS.CHEST]: { name: 'Chest', color: '#964b00', hp: 1000, stackable: false, placeable: false, highContrast: true },
279
+ [ASSETS.CRAFTING_TABLE]: { name: 'Crafting Table', color: '#964b00', hp: 100, stackable: true, placeable: true },
280
+
281
+ // Items
282
+ [ASSETS.PICKAXE_WOOD]: { name: 'W. Pickaxe', color: '#b69255', hp: 0, stackable: false, placeable: false },
283
+ [ASSETS.LANTERN]: { name: 'Lantern', color: '#fde047', hp: 0, stackable: false, placeable: false },
284
+ };
285
+
286
+ const RECIPES = [
287
+ {
288
+ name: "Crafting Table (Basic)",
289
+ result: { type: ASSETS.CRAFTING_TABLE, count: 1 },
290
+ materials: { [ASSETS.WOOD]: 4 },
291
+ requiresBench: false // FIX: Can be crafted anywhere
292
+ },
293
+ {
294
+ name: "Wooden Pickaxe",
295
+ result: { type: ASSETS.PICKAXE_WOOD, count: 1 },
296
+ materials: { [ASSETS.WOOD]: 10, [ASSETS.DIRT]: 5 },
297
+ requiresBench: true
298
+ },
299
+ {
300
+ name: "Wood Platform (x10)",
301
+ result: { type: ASSETS.PLATFORM_WOOD, count: 10 },
302
+ materials: { [ASSETS.WOOD]: 1 },
303
+ requiresBench: true
304
+ },
305
+ {
306
+ name: "Lantern",
307
+ result: { type: ASSETS.LANTERN, count: 1 },
308
+ materials: { [ASSETS.IRON_ORE]: 1, [ASSETS.WOOD]: 4 },
309
+ requiresBench: true
310
+ },
311
+ {
312
+ name: "Dirt Block",
313
+ result: { type: ASSETS.DIRT, count: 1 },
314
+ materials: { [ASSETS.GRASS]: 1 },
315
+ requiresBench: false
316
+ }
317
+ ];
318
+
319
+ // --- Game State ---
320
+ let world = [];
321
+ let worldChests = {};
322
+ let droppedItems = [];
323
+ let gameTime = 0;
324
+ let isCraftingOpen = false;
325
+ let lastGPress = 0;
326
+ let hasPlacedWorkbench = false; // New state flag for placed bench
327
+
328
+ let player = {
329
+ x: (WORLD_WIDTH * TILE_SIZE) / 2,
330
+ y: SURFACE_Y * TILE_SIZE,
331
+ width: TILE_SIZE * 0.8,
332
+ height: TILE_SIZE * 1.8,
333
+ vx: 0,
334
+ vy: 0,
335
+ onGround: false,
336
+ animState: 'idle',
337
+ hp: 100,
338
+ maxHp: 100,
339
+ hasLantern: false,
340
+ };
341
+
342
+ let camera = { x: 0, y: 0 };
343
+
344
+ let keys = {};
345
+
346
+ let inventory = {
347
+ [ASSETS.PICKAXE_WOOD]: 1,
348
+ };
349
+ const hotbar = [ASSETS.PICKAXE_WOOD, ASSETS.WOOD, ASSETS.DIRT, ASSETS.STONE, ASSETS.CRAFTING_TABLE, ASSETS.PLATFORM_WOOD, ASSETS.LANTERN];
350
+ let activeSlot = 0;
351
+
352
+ let mouse = {
353
+ x: 0,
354
+ y: 0,
355
+ worldX: 0,
356
+ worldY: 0,
357
+ tileX: 0,
358
+ tileY: 0,
359
+ isDown: false,
360
+ breaking: null,
361
+ };
362
+
363
+ // --- Utility Functions ---
364
+
365
+ // FIX: updateMousePos defined here to resolve ReferenceError
366
+ function updateMousePos(e) {
367
+ mouse.x = e.clientX;
368
+ mouse.y = e.clientY;
369
+ mouse.worldX = mouse.x + camera.x;
370
+ mouse.worldY = mouse.y + camera.y;
371
+ mouse.tileX = Math.floor(mouse.worldX / TILE_SIZE);
372
+ mouse.tileY = Math.floor(mouse.worldY / TILE_SIZE);
373
+ }
374
+
375
+ function drawTile(x, y, type) {
376
+ const drawX = x * TILE_SIZE;
377
+ const drawY = y * TILE_SIZE;
378
+ const color = ASSET_INFO[type].color;
379
+
380
+ // Base fill
381
+ ctx.fillStyle = color;
382
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
383
+
384
+ // Apply texture patterns
385
+ switch (type) {
386
+ case ASSETS.GRASS:
387
+ // Grass top and shade
388
+ ctx.fillStyle = '#4c7b39';
389
+ ctx.fillRect(drawX, drawY, TILE_SIZE, 5);
390
+ ctx.fillStyle = '#3f6631';
391
+ for (let i = 0; i < TILE_SIZE; i += 4) {
392
+ ctx.fillRect(drawX + i, drawY, 2, 8);
393
+ }
394
+ break;
395
+ case ASSETS.WOOD:
396
+ // Wood grain
397
+ ctx.fillStyle = '#83653b';
398
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
399
+ ctx.strokeStyle = '#5c4521';
400
+ ctx.lineWidth = 1;
401
+ for (let i = 0; i < TILE_SIZE; i += 8) {
402
+ ctx.beginPath();
403
+ ctx.moveTo(drawX, drawY + i);
404
+ ctx.lineTo(drawX + TILE_SIZE, drawY + i);
405
+ ctx.stroke();
406
+ }
407
+ break;
408
+ case ASSETS.STONE:
409
+ // Stone speckles
410
+ ctx.fillStyle = '#787a7d';
411
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
412
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
413
+ for (let i = 0; i < 10; i++) {
414
+ ctx.fillRect(drawX + Math.random() * TILE_SIZE, drawY + Math.random() * TILE_SIZE, 2, 2);
415
+ }
416
+ break;
417
+ case ASSETS.COPPER_ORE:
418
+ case ASSETS.IRON_ORE:
419
+ case ASSETS.GOLD_ORE:
420
+ case ASSETS.DIAMOND_ORE:
421
+ // Ore glow/speckles over stone base
422
+ ctx.fillStyle = ASSET_INFO[ASSETS.STONE].color;
423
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
424
+ ctx.fillStyle = color;
425
+ for (let i = 0; i < 5; i++) {
426
+ ctx.fillRect(drawX + TILE_SIZE/4 + Math.random() * TILE_SIZE/2, drawY + TILE_SIZE/4 + Math.random() * TILE_SIZE/2, 4, 4);
427
+ }
428
+ break;
429
+ case ASSETS.CHEST:
430
+ // Chest texture
431
+ ctx.fillStyle = '#964b00'; // Dark brown
432
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
433
+ ctx.fillStyle = '#ffd700'; // Gold latch
434
+ ctx.fillRect(drawX + TILE_SIZE * 0.4, drawY + 2, TILE_SIZE * 0.2, TILE_SIZE * 0.1);
435
+ ctx.fillRect(drawX + TILE_SIZE * 0.45, drawY + TILE_SIZE * 0.1, TILE_SIZE * 0.1, TILE_SIZE * 0.4);
436
+ break;
437
+ case ASSETS.CRAFTING_TABLE:
438
+ // Crafting table texture
439
+ ctx.fillStyle = '#6f4518'; // Wood color
440
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
441
+ ctx.fillStyle = '#2c3e50'; // Top surface
442
+ ctx.fillRect(drawX, drawY, TILE_SIZE, 5);
443
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; // Detail
444
+ ctx.fillRect(drawX + 5, drawY + 1, 2, 2);
445
+ ctx.fillRect(drawX + TILE_SIZE - 7, drawY + 1, 2, 2);
446
+ break;
447
+ case ASSETS.PLATFORM_WOOD:
448
+ // Platform (Drawn thinner later)
449
+ ctx.fillStyle = ASSET_INFO[type].color;
450
+ break;
451
+ default:
452
+ // Use fallback color
453
+ ctx.fillStyle = color;
454
+ ctx.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
455
+ }
456
+
457
+ // Draw Platforms thin
458
+ if (ASSET_INFO[type].isPlatform) {
459
+ ctx.fillStyle = ASSET_INFO[type].color;
460
+ ctx.fillRect(drawX, drawY + TILE_SIZE * 0.8, TILE_SIZE, TILE_SIZE * 0.2);
461
+ }
462
+ }
463
+
464
+ // --- Initialization ---
465
+
466
+ function generateWorld() {
467
+ console.log("Generating world...");
468
+ world = new Array(WORLD_WIDTH).fill(0).map(() => new Array(WORLD_HEIGHT).fill(ASSETS.AIR));
469
+ worldChests = {};
470
+
471
+ for (let x = 0; x < WORLD_WIDTH; x++) {
472
+ let heightNoise = (noise2D(x * NOISE_SCALE, 0) + 1) / 2;
473
+ let terrainHeight = Math.floor(SURFACE_Y + heightNoise * 30 - 15);
474
+
475
+ for (let y = 0; y < WORLD_HEIGHT; y++) {
476
+ if (y > terrainHeight) {
477
+ let caveNoise = Math.abs(noise2D(x * 0.08, y * 0.08));
478
+ let deepCaveNoise = Math.abs(noise2D(x * 0.03, y * 0.03));
479
+
480
+ if (caveNoise < CAVE_THRESHOLD * 1.2 && y > SURFACE_Y + 5 || deepCaveNoise < 0.03 && y > SURFACE_Y + 50) {
481
+ world[x][y] = ASSETS.AIR; // Cave
482
+
483
+ // Chest placement check: Must be on a solid block
484
+ if (deepCaveNoise < 0.03 && Math.random() < 0.01 && y + 1 < WORLD_HEIGHT && world[x][y+1] !== ASSETS.AIR) {
485
+ world[x][y] = ASSETS.CHEST;
486
+ worldChests[`${x},${y}`] = generateLoot();
487
+ }
488
+
489
+ } else if (y === terrainHeight + 1) {
490
+ world[x][y] = ASSETS.GRASS;
491
+ } else if (y < terrainHeight + 8) {
492
+ world[x][y] = ASSETS.DIRT;
493
+ } else {
494
+ if (y < SURFACE_Y + 30) {
495
+ world[x][y] = Math.random() < 0.03 ? ASSETS.COPPER_ORE : ASSETS.STONE;
496
+ } else if (y < SURFACE_Y + 80) {
497
+ world[x][y] = Math.random() < 0.02 ? ASSETS.IRON_ORE : ASSETS.STONE;
498
+ } else if (y < SURFACE_Y + 150) {
499
+ world[x][y] = Math.random() < 0.01 ? ASSETS.GOLD_ORE : ASSETS.STONE;
500
+ } else {
501
+ world[x][y] = Math.random() < 0.005 ? ASSETS.DIAMOND_ORE : ASSETS.STONE;
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ // Tree Generation
508
+ if (Math.random() < 0.05 && x > 10 && x < WORLD_WIDTH - 10 && world[x][terrainHeight + 1] === ASSETS.GRASS) {
509
+ let treeHeight = 5 + Math.floor(Math.random() * 5);
510
+ let treeTopY = terrainHeight - treeHeight;
511
+
512
+ for (let y = terrainHeight; y >= treeTopY; y--) {
513
+ if (world[x] && world[x][y] !== undefined) {
514
+ world[x][y] = ASSETS.WOOD;
515
+ }
516
+ }
517
+
518
+ for (let lx = -2; lx <= 2; lx++) {
519
+ for (let ly = -2; ly <= 0; ly++) {
520
+ if (Math.random() < 0.8) {
521
+ let leafX = x + lx;
522
+ let leafY = treeTopY + ly;
523
+ if (world[leafX] && world[leafX][leafY] === ASSETS.AIR) {
524
+ world[leafX][leafY] = ASSETS.LEAVES;
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ console.log("World generated.");
532
+
533
+ player.y = (SURFACE_Y - 5) * TILE_SIZE;
534
+ }
535
+
536
+ function generateLoot() {
537
+ const possibleLoot = [
538
+ [ASSETS.WOOD, 10 + Math.floor(Math.random() * 20)],
539
+ [ASSETS.IRON_ORE, 3 + Math.floor(Math.random() * 7)],
540
+ [ASSETS.GOLD_ORE, 1 + Math.floor(Math.random() * 5)],
541
+ [ASSETS.DIAMOND_ORE, Math.random() < 0.2 ? 1 : 0],
542
+ [ASSETS.LANTERN, 1]
543
+ ];
544
+
545
+ const loot = [];
546
+ for(let i = 0; i < 3; i++) {
547
+ if(Math.random() < 0.6) {
548
+ loot.push(possibleLoot[Math.floor(Math.random() * possibleLoot.length)]);
549
+ }
550
+ }
551
+ return loot.filter(item => item[1] > 0);
552
+ }
553
+
554
+ function resizeCanvas() {
555
+ canvas.width = window.innerWidth;
556
+ canvas.height = window.innerHeight;
557
+ }
558
+
559
+ // --- Game Loop ---
560
+
561
+ function update() {
562
+ if (isCraftingOpen) return;
563
+ player.hasLantern = (inventory[ASSETS.LANTERN] || 0) > 0;
564
+ hasPlacedWorkbench = getNearestCraftingTable(); // Update workbench status
565
+ handleInput();
566
+ handleMining();
567
+ applyPhysics(player);
568
+ updateDroppedItems();
569
+ updateCamera();
570
+ }
571
+
572
+ function draw() {
573
+ // Clear canvas (the sky is the body background)
574
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
575
+
576
+ ctx.save();
577
+ ctx.translate(-Math.floor(camera.x), -Math.floor(camera.y));
578
+
579
+ // --- Draw World (Base Tiles) ---
580
+ let startX = Math.floor(camera.x / TILE_SIZE);
581
+ let endX = Math.min(WORLD_WIDTH, startX + Math.ceil(canvas.width / TILE_SIZE) + 1);
582
+ let startY = Math.floor(camera.y / TILE_SIZE);
583
+ let endY = Math.min(WORLD_HEIGHT, startY + Math.ceil(canvas.height / TILE_SIZE) + 1);
584
+
585
+ for (let x = startX; x < endX; x++) {
586
+ for (let y = startY; y < endY; y++) {
587
+ let tileType = world[x][y];
588
+ if (tileType !== ASSETS.AIR) {
589
+ drawTile(x, y, tileType);
590
+ }
591
+ }
592
+ }
593
+
594
+ // --- Draw Block Breaking Animation ---
595
+ drawBreakingDamage();
596
+
597
+ // --- Draw Dropped Items ---
598
+ drawDroppedItems();
599
+
600
+ // --- Draw Player ---
601
+ drawPlayer(player);
602
+
603
+ // --- Draw Mouse Selector ---
604
+ let dist = Math.hypot(mouse.worldX - (player.x + player.width / 2), mouse.worldY - (player.y + player.height / 2));
605
+ if (dist <= REACH) {
606
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
607
+ ctx.lineWidth = 2;
608
+ ctx.strokeRect(mouse.tileX * TILE_SIZE, mouse.tileY * TILE_SIZE, TILE_SIZE, TILE_SIZE);
609
+ }
610
+
611
+ ctx.restore();
612
+
613
+ // --- Lighting Overlay (FIX for bright sky & grainy textures) ---
614
+
615
+ const playerCenterWorldY = player.y + player.height / 2;
616
+ const depthInTiles = playerCenterWorldY / TILE_SIZE;
617
+
618
+ // Calculate ambient darkness (Max 70% dark at 50 tiles below surface)
619
+ const maxDarknessDepth = SURFACE_Y + 50;
620
+ let ambientDarkness = Math.min(1, Math.max(0, (depthInTiles - SURFACE_Y) / (maxDarknessDepth - SURFACE_Y))) * 0.7;
621
+
622
+ // 1. Apply Global Ambient Darkness (Darkens the entire canvas, including the sky area)
623
+ if (ambientDarkness > 0.05) {
624
+ ctx.fillStyle = `rgba(0, 0, 0, ${ambientDarkness})`;
625
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
626
+ }
627
+
628
+ // 2. Apply Local Lighting Brightness (Cut out a light circle)
629
+ const LIGHT_RADIUS = player.hasLantern ? 250 : 0;
630
+
631
+ if (LIGHT_RADIUS > 0) {
632
+ const playerCenterScreenX = canvas.width / 2;
633
+ const playerCenterScreenY = canvas.height / 2;
634
+
635
+ // Create a soft light gradient effect
636
+ const gradient = ctx.createRadialGradient(
637
+ playerCenterScreenX, playerCenterScreenY, 0,
638
+ playerCenterScreenX, playerCenterScreenY, LIGHT_RADIUS
639
+ );
640
+ // More subtle lighting
641
+ gradient.addColorStop(0, 'rgba(255, 255, 200, 0.8)');
642
+ gradient.addColorStop(0.5, 'rgba(255, 255, 200, 0.4)');
643
+ gradient.addColorStop(1, 'rgba(255, 255, 200, 0)');
644
+ ctx.fillStyle = gradient;
645
+ ctx.globalCompositeOperation = 'lighter'; // Use 'lighter' to brighten the area
646
+ ctx.beginPath();
647
+ ctx.arc(playerCenterScreenX, playerCenterScreenY, LIGHT_RADIUS, 0, Math.PI * 2);
648
+ ctx.fill();
649
+ ctx.globalCompositeOperation = 'source-over'; // Reset
650
+ }
651
+
652
+ // --- Draw UI ---
653
+ drawHotbar();
654
+ drawHealthBar();
655
+ drawCoordinates();
656
+ drawAlert();
657
+ }
658
+
659
+ function drawHealthBar() {
660
+ const barWidth = 200;
661
+ const barHeight = 20;
662
+ const x = canvas.width - barWidth - 20;
663
+ const y = 20;
664
+
665
+ ctx.fillStyle = '#2c3e50';
666
+ ctx.fillRect(x, y, barWidth, barHeight);
667
+
668
+ const healthPercent = player.hp / player.maxHp;
669
+ ctx.fillStyle = '#e74c3c';
670
+ ctx.fillRect(x, y, barWidth * healthPercent, barHeight);
671
+
672
+ ctx.strokeStyle = 'white';
673
+ ctx.lineWidth = 2;
674
+ ctx.strokeRect(x, y, barWidth, barHeight);
675
+
676
+ ctx.fillStyle = 'white';
677
+ ctx.font = '14px Inter';
678
+ ctx.textAlign = 'center';
679
+ ctx.fillText(`HP: ${player.hp}/${player.maxHp}`, x + barWidth / 2, y + barHeight - 5);
680
+ }
681
+
682
+ function drawCoordinates() {
683
+ const x = canvas.width - 20;
684
+ const y = 50;
685
+
686
+ const tileX = Math.floor(player.x / TILE_SIZE);
687
+ const tileY = Math.floor(player.y / TILE_SIZE);
688
+
689
+ const depthLevel = Math.max(0, tileY - SURFACE_Y);
690
+
691
+ const depthColor = depthLevel > 10 ? '#fde047' : 'white';
692
+
693
+ ctx.fillStyle = depthColor;
694
+ ctx.font = '16px Inter';
695
+ ctx.textAlign = 'right';
696
+ ctx.fillText(`X: ${tileX}`, x, y + 20);
697
+ ctx.fillText(`Depth: ${depthLevel}`, x, y + 40);
698
+ // Don't show lantern/bench status anymore
699
+ }
700
+
701
+ function drawPlayer(p) {
702
+ let yOffset = 0;
703
+ let pHeight = p.height;
704
+ let pWidth = p.width;
705
+
706
+ if (p.animState === 'walking') {
707
+ yOffset = Math.sin(gameTime * 0.3) * 2;
708
+ } else if (p.animState === 'jumping') {
709
+ if (p.vy > 1) { pHeight = p.height * 0.95; pWidth = p.width * 1.05; }
710
+ else if (p.vy < -1) { pHeight = p.height * 1.05; pWidth = p.width * 0.95; }
711
+ }
712
+
713
+ const headSize = TILE_SIZE * 0.8;
714
+ const headX = p.x + (p.width - headSize) / 2;
715
+ const headY = p.y + yOffset;
716
+ const bodyY = headY + headSize;
717
+ const bodyHeight = pHeight - headSize;
718
+
719
+ ctx.fillStyle = '#8b5e3c';
720
+ ctx.fillRect(p.x + (p.width - pWidth) / 2, bodyY, pWidth, bodyHeight);
721
+
722
+ ctx.fillStyle = '#f3bf9e';
723
+ ctx.fillRect(headX, headY, headSize, headSize);
724
+
725
+ const headCenterXScreen = p.x + p.width / 2 - camera.x;
726
+ const headCenterYScreen = headY + headSize / 2 - camera.y;
727
+
728
+ const dx = mouse.x - headCenterXScreen;
729
+ const dy = mouse.y - headCenterYScreen;
730
+
731
+ const angle = Math.atan2(dy, dx);
732
+ const eyeMovementRadius = 3;
733
+ const pupilOffsetX = Math.cos(angle) * eyeMovementRadius;
734
+ const pupilOffsetY = Math.sin(angle) * eyeMovementRadius;
735
+
736
+ const eyeWidth = 4;
737
+ const eyeHeight = 4;
738
+
739
+ const leftEyeX = headX + headSize * 0.2;
740
+ const rightEyeX = headX + headSize * 0.6;
741
+ const eyeY = headY + headSize * 0.3;
742
+
743
+ ctx.fillStyle = 'white';
744
+ ctx.fillRect(leftEyeX, eyeY, eyeWidth, eyeHeight);
745
+ ctx.fillRect(rightEyeX, eyeY, eyeWidth, eyeHeight);
746
+ ctx.fillStyle = 'black';
747
+ ctx.fillRect(leftEyeX + eyeWidth/2 - 1 + pupilOffsetX, eyeY + eyeHeight/2 - 1 + pupilOffsetY, 2, 2);
748
+ ctx.fillRect(rightEyeX + eyeWidth/2 - 1 + pupilOffsetX, eyeY + eyeHeight/2 - 1 + pupilOffsetY, 2, 2);
749
+ }
750
+
751
+ function drawBreakingDamage() {
752
+ if (!mouse.breaking) return;
753
+ const { x, y, damage } = mouse.breaking;
754
+ const tileType = world[x][y];
755
+ if (tileType === ASSETS.AIR) return;
756
+
757
+ const maxHp = ASSET_INFO[tileType].hp || 20;
758
+ const percent = damage / maxHp;
759
+
760
+ if (percent <= 0) return;
761
+
762
+ const drawX = x * TILE_SIZE;
763
+ const drawY = y * TILE_SIZE;
764
+
765
+ ctx.strokeStyle = 'rgba(255, 0, 0, 0.6)'; // Redder damage indicator for visibility
766
+ ctx.lineWidth = 2;
767
+ ctx.beginPath();
768
+
769
+ if (percent > 0.2) {
770
+ ctx.moveTo(drawX + TILE_SIZE * 0.1, drawY + TILE_SIZE * 0.1);
771
+ ctx.lineTo(drawX + TILE_SIZE * 0.9, drawY + TILE_SIZE * 0.9);
772
+ }
773
+ if (percent > 0.5) {
774
+ ctx.moveTo(drawX + TILE_SIZE * 0.9, drawY + TILE_SIZE * 0.1);
775
+ ctx.lineTo(drawX + TILE_SIZE * 0.1, drawY + TILE_SIZE * 0.9);
776
+ }
777
+ if (percent > 0.8) {
778
+ ctx.moveTo(drawX + TILE_SIZE * 0.5, drawY + TILE_SIZE * 0.1);
779
+ ctx.lineTo(drawX + TILE_SIZE * 0.5, drawY + TILE_SIZE * 0.9);
780
+ }
781
+ ctx.stroke();
782
+ }
783
+
784
+ function drawHotbar() {
785
+ hotbarContainer.innerHTML = '';
786
+ hotbar.forEach((assetType, index) => {
787
+ const info = ASSET_INFO[assetType];
788
+ const countValue = inventory[assetType] || 0;
789
+
790
+ // Only show slot if we have the item or it's a non-stackable tool (pickaxe)
791
+ if (countValue === 0 && info.stackable && assetType !== ASSETS.AIR) return;
792
+
793
+ const slot = document.createElement('div');
794
+ slot.className = 'hotbar-slot';
795
+ if (index === activeSlot) {
796
+ slot.classList.add('active');
797
+ }
798
+
799
+ // Draw Item Icon inside the slot (simplified)
800
+ slot.style.backgroundColor = info.color;
801
+
802
+ // Add item name
803
+ const name = document.createElement('span');
804
+ name.className = 'item-name';
805
+ name.textContent = info.name;
806
+ slot.appendChild(name);
807
+
808
+ // Add count
809
+ const count = document.createElement('span');
810
+ count.className = 'count';
811
+ count.textContent = info.stackable ? countValue : '∞'; // Show infinity for tools
812
+ slot.appendChild(count);
813
+
814
+ slot.onclick = () => {
815
+ activeSlot = index;
816
+ drawHotbar();
817
+ };
818
+
819
+ hotbarContainer.appendChild(slot);
820
+ });
821
+ }
822
+
823
+ function drawCrafting() {
824
+ recipeList.innerHTML = '';
825
+ const workbenchPresent = getNearestCraftingTable();
826
+
827
+ RECIPES.forEach((recipe, index) => {
828
+ const recipeDiv = document.createElement('div');
829
+ recipeDiv.className = 'recipe-item';
830
+
831
+ let canCraft = true;
832
+ let materialsHtml = '';
833
+
834
+ for (const type in recipe.materials) {
835
+ const required = recipe.materials[type];
836
+ const held = inventory[type] || 0;
837
+ const info = ASSET_INFO[type];
838
+
839
+ if (held < required) {
840
+ canCraft = false;
841
+ }
842
+
843
+ materialsHtml += `<span style="color: ${held >= required ? '#2ecc71' : '#e74c3c'};">${required} ${info.name}</span>, `;
844
+ }
845
+ materialsHtml = materialsHtml.slice(0, -2);
846
+ // Only show recipes that are available (either don't need bench or bench is nearby)
847
+ const requiresBench = recipe.requiresBench !== false;
848
+ const recipeAvailable = !requiresBench || workbenchPresent;
849
+
850
+ if (!recipeAvailable) {
851
+ return; // Skip adding this recipe to the list
852
+ }
853
+
854
+ let buttonText = 'Craft';
855
+ if (!canCraft) {
856
+ recipeDiv.classList.add('disabled');
857
+ buttonText = 'Missing Mats';
858
+ }
859
+ recipeDiv.innerHTML = `
860
+ <div class="recipe-info">
861
+ <div class="recipe-name">${recipe.name} (x${recipe.result.count})</div>
862
+ <div class="recipe-materials">Requires: ${materialsHtml}</div>
863
+ </div>
864
+ <button class="craft-button" data-index="${index}" ${canCraft ? '' : 'disabled'}>
865
+ ${buttonText}
866
+ </button>
867
+ `;
868
+
869
+ recipeDiv.querySelector('.craft-button').onclick = (e) => {
870
+ e.stopPropagation();
871
+ if (canCraft) {
872
+ tryCraft(recipe);
873
+ }
874
+ };
875
+
876
+ recipeList.appendChild(recipeDiv);
877
+ });
878
+ }
879
+
880
+ function drawDroppedItems() {
881
+ droppedItems.forEach(item => {
882
+ const info = ASSET_INFO[item.type];
883
+
884
+ // Draw icon (simplified small rectangle with item color)
885
+ ctx.fillStyle = info.color;
886
+ ctx.fillRect(item.x, item.y, item.width, item.height);
887
+
888
+ // Draw label (Name and count)
889
+ ctx.fillStyle = 'white';
890
+ ctx.font = '12px Inter';
891
+ ctx.textAlign = 'center';
892
+ ctx.shadowColor = 'black';
893
+ ctx.shadowBlur = 4;
894
+ ctx.fillText(`${info.name} x${item.count}`, item.x + item.width / 2, item.y - 10);
895
+ ctx.shadowBlur = 0;
896
+ });
897
+ }
898
+
899
+ function gameLoop() {
900
+ gameTime++;
901
+ update();
902
+ draw();
903
+ requestAnimationFrame(gameLoop);
904
+ }
905
+
906
+ // --- Core Game Logic ---
907
+
908
+ function updateDroppedItems() {
909
+ for (let i = droppedItems.length - 1; i >= 0; i--) {
910
+ const item = droppedItems[i];
911
+
912
+ // Bobbing animation logic
913
+ if (item.onGround) {
914
+ item.vy = 0;
915
+ // Reset to base position then apply bob
916
+ item.y = item.baseY + Math.sin(gameTime * 0.05) * 2;
917
+ } else {
918
+ applyPhysics(item);
919
+ }
920
+
921
+ // Check if player picks it up
922
+ if (item.pickupCooldown > 0) {
923
+ item.pickupCooldown--;
924
+ } else if (checkCollision(player, item)) {
925
+ inventory[item.type] = (inventory[item.type] || 0) + item.count;
926
+
927
+ // Add to hotbar if new and stackable
928
+ if (!hotbar.includes(item.type) && ASSET_INFO[item.type].stackable) {
929
+ // Find the first empty hotbar slot that isn't a tool, or append
930
+ const emptySlotIndex = hotbar.findIndex(t => t === ASSETS.AIR || inventory[t] === 0);
931
+ if (emptySlotIndex !== -1 && hotbar[emptySlotIndex] < 100) {
932
+ hotbar[emptySlotIndex] = item.type;
933
+ } else {
934
+ // Fallback: append it
935
+ hotbar.push(item.type);
936
+ }
937
+ }
938
+
939
+ alert(`Picked up +${item.count} ${ASSET_INFO[item.type].name}`);
940
+ droppedItems.splice(i, 1);
941
+ drawHotbar();
942
+ }
943
+ }
944
+ }
945
+
946
+ function checkCollision(r1, r2) {
947
+ return r1.x < r2.x + r2.width &&
948
+ r1.x + r1.width > r2.x &&
949
+ r1.y < r2.y + r2.height &&
950
+ r1.y + r1.height > r2.y;
951
+ }
952
+
953
+ function handleInput() {
954
+ // Horizontal movement
955
+ if (keys['a'] || keys['ArrowLeft']) {
956
+ player.vx = -PLAYER_SPEED;
957
+ if(player.onGround) player.animState = 'walking';
958
+ } else if (keys['d'] || keys['ArrowRight']) {
959
+ player.vx = PLAYER_SPEED;
960
+ if(player.onGround) player.animState = 'walking';
961
+ } else if (player.onGround) {
962
+ player.animState = 'idle';
963
+ }
964
+
965
+ // Jumping
966
+ if ((keys[' '] || keys['w'] || keys['ArrowUp']) && player.onGround) {
967
+ player.vy = -JUMP_FORCE;
968
+ player.onGround = false;
969
+ player.animState = 'jumping';
970
+ }
971
+ }
972
+
973
+ function applyPhysics(entity) {
974
+ const isPlayer = entity.type === undefined;
975
+
976
+ if (entity.onGround && isPlayer) {
977
+ if (!keys['a'] && !keys['d'] && !keys['ArrowLeft'] && !keys['ArrowRight']) {
978
+ entity.vx *= 0.8;
979
+ if (Math.abs(entity.vx) < 0.1) entity.vx = 0;
980
+ }
981
+ } else if (entity.onGround && !isPlayer) {
982
+ entity.vx *= 0.95;
983
+ if (Math.abs(entity.vx) < 0.05) entity.vx = 0;
984
+ }
985
+
986
+ entity.vy += GRAVITY;
987
+ entity.onGround = false;
988
+
989
+ // 1. Horizontal Collision
990
+ entity.x += entity.vx;
991
+ let dx = entity.vx;
992
+
993
+ let playerLeftTile = Math.floor(entity.x / TILE_SIZE);
994
+ let playerRightTile = Math.floor((entity.x + entity.width - E) / TILE_SIZE);
995
+ let playerTopTile = Math.floor((entity.y + E) / TILE_SIZE);
996
+ let playerBottomTile = Math.floor((entity.y + entity.height - E) / TILE_SIZE);
997
+
998
+ for (let y = playerTopTile; y <= playerBottomTile; y++) {
999
+ if (y < 0 || y >= WORLD_HEIGHT) continue;
1000
+
1001
+ if (dx < 0 && playerLeftTile >= 0 && world[playerLeftTile] && ASSET_INFO[world[playerLeftTile][y]].placeable && !ASSET_INFO[world[playerLeftTile][y]].isPlatform) {
1002
+ entity.x = (playerLeftTile + 1) * TILE_SIZE;
1003
+ entity.vx = 0;
1004
+ break;
1005
+ }
1006
+ if (dx > 0 && playerRightTile < WORLD_WIDTH && world[playerRightTile] && ASSET_INFO[world[playerRightTile][y]].placeable && !ASSET_INFO[world[playerRightTile][y]].isPlatform) {
1007
+ entity.x = playerRightTile * TILE_SIZE - entity.width;
1008
+ entity.vx = 0;
1009
+ break;
1010
+ }
1011
+ }
1012
+
1013
+ // 2. Vertical Collision
1014
+ entity.y += entity.vy;
1015
+
1016
+ playerLeftTile = Math.floor((entity.x + E) / TILE_SIZE);
1017
+ playerRightTile = Math.floor((entity.x + entity.width - E) / TILE_SIZE);
1018
+ playerTopTile = Math.floor(entity.y / TILE_SIZE);
1019
+ playerBottomTile = Math.floor((entity.y + entity.height) / TILE_SIZE);
1020
+
1021
+ for (let x = playerLeftTile; x <= playerRightTile; x++) {
1022
+ if (x < 0 || x >= WORLD_WIDTH) continue;
1023
+
1024
+ if (entity.vy < 0 && world[x] && ASSET_INFO[world[x][playerTopTile]].placeable && !ASSET_INFO[world[x][playerTopTile]].isPlatform) {
1025
+ entity.y = (playerTopTile + 1) * TILE_SIZE;
1026
+ entity.vy = 0;
1027
+ break;
1028
+ }
1029
+
1030
+ if (entity.vy > 0 && world[x]) {
1031
+ const tileType = world[x][playerBottomTile];
1032
+ const isPlaceable = ASSET_INFO[tileType] && ASSET_INFO[tileType].placeable;
1033
+
1034
+ if (isPlaceable) {
1035
+ if (ASSET_INFO[tileType].isPlatform) {
1036
+ if (entity.y + entity.height <= (playerBottomTile * TILE_SIZE) + (TILE_SIZE * 0.2) + GRAVITY) {
1037
+ entity.y = (playerBottomTile * TILE_SIZE) + (TILE_SIZE * 0.2) - entity.height;
1038
+ entity.vy = 0;
1039
+ entity.onGround = true;
1040
+ if (!isPlayer) entity.baseY = entity.y; // For dropped items
1041
+ }
1042
+ } else {
1043
+ entity.y = playerBottomTile * TILE_SIZE - entity.height;
1044
+ entity.vy = 0;
1045
+ entity.onGround = true;
1046
+ if (!isPlayer) entity.baseY = entity.y; // For dropped items
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ if (!entity.onGround && entity.vy > 0 && isPlayer) {
1053
+ entity.animState = 'jumping';
1054
+ }
1055
+
1056
+ // World bounds
1057
+ if (entity.x < 0) entity.x = 0;
1058
+ if (entity.x + entity.width > WORLD_WIDTH * TILE_SIZE) {
1059
+ entity.x = WORLD_WIDTH * TILE_SIZE - entity.width;
1060
+ }
1061
+ if (entity.y < 0) {
1062
+ entity.y = 0;
1063
+ entity.vy = 0;
1064
+ }
1065
+ }
1066
+
1067
+ function updateCamera() {
1068
+ camera.x = player.x - canvas.width / 2 + player.width / 2;
1069
+ camera.y = player.y - canvas.height / 2 + player.height / 2;
1070
+
1071
+ camera.x = Math.max(0, Math.min(camera.x, WORLD_WIDTH * TILE_SIZE - canvas.width));
1072
+ camera.y = Math.max(0, Math.min(camera.y, WORLD_HEIGHT * TILE_SIZE - canvas.height));
1073
+ }
1074
+
1075
+ // --- Interaction ---
1076
+
1077
+ function handleMining() {
1078
+ if (!mouse.isDown) {
1079
+ mouse.breaking = null;
1080
+ return;
1081
+ }
1082
+
1083
+ const activeItem = hotbar[activeSlot];
1084
+ if (activeItem !== ASSETS.PICKAXE_WOOD) {
1085
+ mouse.breaking = null;
1086
+ return;
1087
+ }
1088
+
1089
+ let dist = Math.hypot(mouse.worldX - (player.x + player.width / 2), mouse.worldY - (player.y + player.height / 2));
1090
+ if (dist > REACH) {
1091
+ mouse.breaking = null;
1092
+ return;
1093
+ }
1094
+
1095
+ let tileX = mouse.tileX;
1096
+ let tileY = mouse.tileY;
1097
+
1098
+ if (tileX < 0 || tileX >= WORLD_WIDTH || tileY < 0 || tileY >= WORLD_HEIGHT) return;
1099
+
1100
+ let tileType = world[tileX][tileY];
1101
+
1102
+ if (tileType === ASSETS.AIR || tileType === ASSETS.CHEST) {
1103
+ mouse.breaking = null;
1104
+ return;
1105
+ }
1106
+
1107
+ if (!mouse.breaking || mouse.breaking.x !== tileX || mouse.breaking.y !== tileY) {
1108
+ mouse.breaking = { x: tileX, y: tileY, damage: 0 };
1109
+ }
1110
+
1111
+ mouse.breaking.damage += 5;
1112
+ let maxHp = ASSET_INFO[tileType].hp || 20;
1113
+
1114
+ if (mouse.breaking.damage >= maxHp) {
1115
+ mineBlock(tileX, tileY);
1116
+ mouse.breaking = null; // Reset
1117
+ }
1118
+ }
1119
+
1120
+ function mineBlock(tileX, tileY) {
1121
+ let tileType = world[tileX][tileY];
1122
+ if (tileType !== ASSETS.AIR) {
1123
+ world[tileX][tileY] = ASSETS.AIR;
1124
+
1125
+ if(ASSET_INFO[tileType].stackable) {
1126
+ inventory[tileType] = (inventory[tileType] || 0) + 1;
1127
+
1128
+ // Add newly acquired stackable item to hotbar if not present
1129
+ if (!hotbar.includes(tileType)) {
1130
+ const emptySlotIndex = hotbar.findIndex(t => t === ASSETS.AIR || (inventory[t] || 0) === 0);
1131
+ if (emptySlotIndex !== -1) {
1132
+ hotbar[emptySlotIndex] = tileType;
1133
+ } else {
1134
+ hotbar.push(tileType);
1135
+ }
1136
+ }
1137
+ }
1138
+ }
1139
+ drawHotbar();
1140
+ }
1141
+
1142
+ function placeBlock(tileX, tileY) {
1143
+ if (tileX < 0 || tileX >= WORLD_WIDTH || tileY < 0 || tileY >= WORLD_HEIGHT) return;
1144
+
1145
+ let assetToPlace = hotbar[activeSlot];
1146
+ const assetInfo = ASSET_INFO[assetToPlace];
1147
+
1148
+ if (assetToPlace < 100 && assetInfo.placeable && world[tileX][tileY] === ASSETS.AIR) {
1149
+
1150
+ if ((inventory[assetToPlace] || 0) > 0) {
1151
+ let playerRect = { x: player.x, y: player.y, width: player.width, height: player.height };
1152
+ let tileRect = { x: tileX * TILE_SIZE, y: tileY * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE };
1153
+
1154
+ if (!(playerRect.x < tileRect.x + tileRect.width &&
1155
+ playerRect.x + playerRect.width > tileRect.x &&
1156
+ playerRect.y < tileRect.y + tileRect.height &&
1157
+ playerRect.y + playerRect.height > tileRect.y)) {
1158
+
1159
+ world[tileX][tileY] = assetToPlace;
1160
+ inventory[assetToPlace]--;
1161
+ drawHotbar();
1162
+ }
1163
+ } else {
1164
+ alert(`You are out of ${assetInfo.name}!`);
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ function tryCraft(recipe) {
1170
+ const workbenchPresent = getNearestCraftingTable();
1171
+ const requiresBench = recipe.requiresBench !== false;
1172
+
1173
+ if (requiresBench && !workbenchPresent) {
1174
+ alert("You need a placed Crafting Table nearby for this recipe!");
1175
+ return;
1176
+ }
1177
+
1178
+ let canCraft = true;
1179
+ for (const type in recipe.materials) {
1180
+ if ((inventory[type] || 0) < recipe.materials[type]) {
1181
+ canCraft = false;
1182
+ break;
1183
+ }
1184
+ }
1185
+
1186
+ if (canCraft) {
1187
+ for (const type in recipe.materials) {
1188
+ inventory[type] -= recipe.materials[type];
1189
+ }
1190
+ const resultType = recipe.result.type;
1191
+ const resultCount = recipe.result.count;
1192
+ inventory[resultType] = (inventory[resultType] || 0) + resultCount;
1193
+
1194
+ // Update hotbar if newly crafted item is not present
1195
+ if (!hotbar.includes(resultType) && ASSET_INFO[resultType].stackable) {
1196
+ const emptySlotIndex = hotbar.findIndex(t => t === ASSETS.AIR || (inventory[t] || 0) === 0);
1197
+ if (emptySlotIndex !== -1) {
1198
+ hotbar[emptySlotIndex] = resultType;
1199
+ } else {
1200
+ hotbar.push(resultType);
1201
+ }
1202
+ }
1203
+
1204
+ alert(`Crafted ${recipe.name}!`);
1205
+
1206
+ drawHotbar();
1207
+ drawCrafting();
1208
+ } else {
1209
+ alert("Cannot craft: missing materials.");
1210
+ }
1211
+ }
1212
+
1213
+ function dropItem() {
1214
+ const assetType = hotbar[activeSlot];
1215
+ const info = ASSET_INFO[assetType];
1216
+ const countToDrop = 1;
1217
+
1218
+ if (assetType >= 100 || !info.stackable || (inventory[assetType] || 0) < countToDrop) {
1219
+ alert("Cannot drop this item or stack is empty.");
1220
+ return;
1221
+ }
1222
+ // Drop item further away from player
1223
+ const dropOffsetX = (Math.random() - 0.5) * 40;
1224
+ const dropOffsetY = -20;
1225
+
1226
+ const itemEntity = {
1227
+ x: player.x + player.width / 2 - TILE_SIZE / 4 + dropOffsetX,
1228
+ y: player.y + player.height - TILE_SIZE / 4 + dropOffsetY,
1229
+ width: TILE_SIZE / 2,
1230
+ height: TILE_SIZE / 2,
1231
+ vx: (Math.random() - 0.5) * 5,
1232
+ vy: -5,
1233
+ onGround: false,
1234
+ type: assetType,
1235
+ count: countToDrop,
1236
+ baseY: 0,
1237
+ pickupCooldown: 30 // Prevent instant pickup
1238
+ };
1239
+ inventory[assetType] -= countToDrop;
1240
+ droppedItems.push(itemEntity);
1241
+ alert(`Dropped 1 ${info.name}.`);
1242
+ drawHotbar();
1243
+ }
1244
+ function getNearestCraftingTable() {
1245
+ const ptx = Math.floor((player.x + player.width / 2) / TILE_SIZE);
1246
+ const pty = Math.floor((player.y + player.height / 2) / TILE_SIZE);
1247
+
1248
+ // Check 3 blocks left/right, 2 blocks up/down
1249
+ for (let x = ptx - 3; x <= ptx + 3; x++) {
1250
+ for (let y = pty - 2; y <= pty + 2; y++) {
1251
+ if (x >= 0 && x < WORLD_WIDTH && y >= 0 && y < WORLD_HEIGHT && world[x][y] === ASSETS.CRAFTING_TABLE) {
1252
+ return true;
1253
+ }
1254
+ }
1255
+ }
1256
+ return false;
1257
+ }
1258
+ // FIX: Always open the menu, but rely on drawCrafting to disable advanced recipes if no table is placed.
1259
+ function toggleCrafting() {
1260
+ if (isCraftingOpen) {
1261
+ isCraftingOpen = false;
1262
+ craftingMenu.style.display = 'none';
1263
+ } else {
1264
+ isCraftingOpen = true;
1265
+ craftingMenu.style.display = 'block';
1266
+ drawCrafting();
1267
+ }
1268
+ }
1269
+
1270
+ function checkChestInteraction() {
1271
+ const ptx = Math.floor((player.x + player.width / 2) / TILE_SIZE);
1272
+ const pty = Math.floor(player.y / TILE_SIZE);
1273
+
1274
+ const checks = [
1275
+ {x: ptx, y: pty},
1276
+ {x: ptx, y: pty + 1},
1277
+ {x: ptx, y: pty + 2},
1278
+ {x: ptx + 1, y: pty + 1},
1279
+ {x: ptx - 1, y: pty + 1}
1280
+ ];
1281
+
1282
+ let foundChest = false;
1283
+ for (const { x, y } of checks) {
1284
+ if (x >= 0 && x < WORLD_WIDTH && y >= 0 && y < WORLD_HEIGHT && world[x][y] === ASSETS.CHEST) {
1285
+ const chestKey = `${x},${y}`;
1286
+ if (worldChests[chestKey]) {
1287
+ lootChest(chestKey, x, y);
1288
+ foundChest = true;
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ if (!foundChest) {
1294
+ alert("No nearby chest found.");
1295
+ }
1296
+ }
1297
+
1298
+ function lootChest(chestKey, x, y) {
1299
+ const loot = worldChests[chestKey];
1300
+ let lootMessage = "Looted Chest:\n";
1301
+
1302
+ if (loot.length === 0) {
1303
+ lootMessage += "The chest was empty!";
1304
+ } else {
1305
+ loot.forEach(([itemType, count]) => {
1306
+ const info = ASSET_INFO[itemType];
1307
+ inventory[itemType] = (inventory[itemType] || 0) + count;
1308
+ lootMessage += `+${count} ${info.name}\n`;
1309
+
1310
+ if (!hotbar.includes(itemType) && ASSET_INFO[itemType].stackable) {
1311
+ // Add new loot items to hotbar if not present
1312
+ const emptySlotIndex = hotbar.findIndex(t => t === ASSETS.AIR || (inventory[t] || 0) === 0);
1313
+ if (emptySlotIndex !== -1 && hotbar[emptySlotIndex] < 100) {
1314
+ hotbar[emptySlotIndex] = itemType;
1315
+ } else {
1316
+ hotbar.push(itemType);
1317
+ }
1318
+ }
1319
+ });
1320
+ }
1321
+
1322
+ alert(lootMessage);
1323
+
1324
+ world[x][y] = ASSETS.AIR;
1325
+ delete worldChests[chestKey];
1326
+ drawHotbar();
1327
+ }
1328
+
1329
+ // QoL: Simple on-screen alert for game messages
1330
+ let messageTimer = 0;
1331
+ let currentMessage = '';
1332
+
1333
+ function alert(message) {
1334
+ console.log("--- MESSAGE BOX ---");
1335
+ console.log(message);
1336
+ console.log("-------------------");
1337
+ alertUserOnScreen(message);
1338
+ }
1339
+
1340
+ function alertUserOnScreen(message) {
1341
+ currentMessage = message;
1342
+ messageTimer = 180;
1343
+ }
1344
+
1345
+ function drawAlert() {
1346
+ if (messageTimer <= 0) return;
1347
+
1348
+ ctx.globalAlpha = messageTimer / 60;
1349
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
1350
+ const x = canvas.width / 2;
1351
+ const y = canvas.height / 2;
1352
+
1353
+ ctx.beginPath();
1354
+ ctx.roundRect(x - 200, y - 50, 400, 100, 10);
1355
+ ctx.fill();
1356
+
1357
+ ctx.fillStyle = 'white';
1358
+ ctx.font = '20px Inter';
1359
+ ctx.textAlign = 'center';
1360
+ currentMessage.split('\n').forEach((line, index) => {
1361
+ ctx.fillText(line, x, y + index * 25 - 20);
1362
+ });
1363
+
1364
+ ctx.globalAlpha = 1.0;
1365
+ messageTimer--;
1366
+ }
1367
+
1368
+ // --- Event Listeners ---
1369
+ window.addEventListener('keydown', (e) => {
1370
+ keys[e.key.toLowerCase()] = true;
1371
+
1372
+ if (e.key >= '1' && e.key <= '7') {
1373
+ let slotIndex = parseInt(e.key) - 1;
1374
+ if(slotIndex < hotbar.length) {
1375
+ activeSlot = slotIndex;
1376
+ drawHotbar();
1377
+ }
1378
+ }
1379
+
1380
+ // Crafting Toggle (E)
1381
+ if (e.key.toLowerCase() === 'e' && !e.repeat) {
1382
+ toggleCrafting();
1383
+ }
1384
+
1385
+ // Loot Chest (F)
1386
+ if (e.key.toLowerCase() === 'f' && !isCraftingOpen && !e.repeat) {
1387
+ checkChestInteraction();
1388
+ }
1389
+
1390
+ // Drop Item (G)
1391
+ if (e.key.toLowerCase() === 'g' && !isCraftingOpen && gameTime > lastGPress + 10) {
1392
+ dropItem();
1393
+ lastGPress = gameTime;
1394
+ }
1395
+ });
1396
+
1397
+ window.addEventListener('keyup', (e) => {
1398
+ keys[e.key.toLowerCase()] = false;
1399
+ });
1400
+
1401
+ canvas.addEventListener('mousemove', updateMousePos);
1402
+
1403
+ canvas.addEventListener('mousedown', (e) => {
1404
+ updateMousePos(e);
1405
+
1406
+ if (isCraftingOpen) return;
1407
+
1408
+ let dist = Math.hypot(mouse.worldX - (player.x + player.width / 2), mouse.worldY - (player.y + player.height / 2));
1409
+ if (dist > REACH) return;
1410
+
1411
+ if (e.button === 2) { // Right click (Place Block)
1412
+ placeBlock(mouse.tileX, mouse.tileY);
1413
+ }
1414
+ });
1415
+
1416
+ window.addEventListener('mousedown', (e) => {
1417
+ if (e.button === 0 && e.target === canvas && !isCraftingOpen) {
1418
+ mouse.isDown = true;
1419
+ e.preventDefault();
1420
+ }
1421
+ });
1422
+
1423
+ window.addEventListener('mouseup', (e) => {
1424
+ if (e.button === 0) {
1425
+ mouse.isDown = false;
1426
+ mouse.breaking = null;
1427
+ }
1428
+ });
1429
+
1430
+ canvas.addEventListener('wheel', (e) => {
1431
+ if (isCraftingOpen) return;
1432
+
1433
+ if (e.deltaY > 0) {
1434
+ activeSlot = (activeSlot + 1) % hotbar.length;
1435
+ } else {
1436
+ activeSlot = (activeSlot - 1 + hotbar.length) % hotbar.length;
1437
+ }
1438
+ drawHotbar();
1439
+ e.preventDefault();
1440
+ });
1441
+
1442
+ canvas.addEventListener('contextmenu', (e) => {
1443
+ e.preventDefault();
1444
+ });
1445
+
1446
+ window.addEventListener('resize', resizeCanvas);
1447
+
1448
+ // --- Start Game ---
1449
+ resizeCanvas();
1450
+ generateWorld();
1451
+ alert("Welcome to Survival! Press [E] to open the Crafting Menu anywhere.\n[G] to drop items. [F] to loot chests.");
1452
+ gameLoop();
1453
+
1454
+ </script>
1455
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
1456
+ </body>
1457
+ </html>