kimhyunwoo commited on
Commit
0563b4d
Β·
verified Β·
1 Parent(s): f3dde3c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +183 -254
index.html CHANGED
@@ -3,41 +3,29 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Thronglets Simulation (Improved)</title>
7
  <style>
8
  :root {
9
- --grass-dark: #2a6141;
10
- --grass-light: #3a815b;
11
- --water: #4171a7;
12
- --rock-dark: #6b727c;
13
- --rock-light: #89919c;
14
- --tree-trunk: #6f4e37;
15
- --tree-leaves: #2b602c;
16
- --ui-bg: #e0cda9; /* Parchment/Light wood */
17
- --ui-border: #8b4513; /* SaddleBrown */
18
- --ui-accent: #6d8c4f; /* Green accent */
19
- --ui-text: #3a2e20; /* Dark brown text */
20
  }
21
 
22
  body {
23
  margin: 0; overflow: hidden; background: var(--grass-dark);
24
- color: #eee; font-family: 'Verdana', sans-serif;
25
- display: flex; flex-direction: column; align-items: center;
26
- justify-content: center; min-height: 100vh; position: relative;
27
  }
28
 
29
  .game-container {
30
  position: relative; width: 95vmin; height: 70vmin;
31
  max-width: 1000px; max-height: 700px;
32
- background-color: var(--grass-light);
33
- background-image: radial-gradient(var(--grass-dark) 15%, transparent 16%),
34
- radial-gradient(var(--grass-dark) 15%, transparent 16%);
35
- background-size: 30px 30px; background-position: 0 0, 15px 15px;
36
  border: 1px solid var(--tree-trunk); box-sizing: border-box;
37
  overflow: hidden; display: flex; justify-content: center;
38
- align-items: center; font-size: 1.5em; /* Base emoji size */
39
- position: relative; image-rendering: pixelated;
40
- box-shadow: inset 0 0 20px rgba(0,0,0,0.4);
41
  }
42
 
43
  .game-elements {
@@ -65,7 +53,6 @@
65
 
66
  .thronglet {
67
  cursor: default;
68
- /* Slower, smoother transition for movement */
69
  transition: top 1s linear, left 1s linear, transform 0.2s ease, opacity 0.5s ease-out, filter 0.3s ease-out;
70
  z-index: 3;
71
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', Times, Symbola, Aegyptus, Demo;
@@ -100,53 +87,43 @@
100
 
101
  /* Top Left UI Panel */
102
  #topLeftUI { display: flex; flex-direction: column; align-items: center; width: 70px; }
103
- .logo {
104
- background: var(--ui-accent); border: 2px solid var(--ui-border);
105
- color: var(--ui-bg); font-size: 1.8em; font-weight: bold;
106
- width: 40px; height: 40px; display: flex; align-items: center;
107
- justify-content: center; border-radius: 50%; margin-bottom: 5px;
108
  }
109
- .action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; width: 100%; }
110
- .action-grid button {
111
- background: var(--ui-bg); border: 2px solid var(--ui-border);
112
- color: var(--ui-text); font-size: 1.2em; width: 30px; height: 30px;
113
- display: flex; align-items: center; justify-content: center;
114
- border-radius: 4px; cursor: pointer; transition: background-color 0.2s, transform 0.1s;
115
- padding: 0; line-height: 1;
116
  }
117
- .action-grid button.selected { /* Style for selected tool */
118
- border-color: #fff;
119
- box-shadow: inset 0 0 5px rgba(0,0,0,0.5);
 
 
120
  }
 
121
  .action-grid button:hover { background-color: #f5e5c5; }
122
- .action-grid button:active { transform: scale(0.95); border-color: var(--ui-accent); }
123
- /* Updated Button IDs and Icons */
124
- #pointerBtn { grid-column: 1; grid-row: 1; }
125
- #targetBtn { grid-column: 2; grid-row: 1; } /* Target icon */
126
- #feedButton { grid-column: 1; grid-row: 2; }
127
- #cleanButton { grid-column: 2; grid-row: 2; }
128
- #brainBtn { grid-column: 1; grid-row: 3; } /* Brain icon */
129
- #plantBtn { grid-column: 2; grid-row: 3; } /* Seedling icon */
130
- #houseBtn { grid-column: 1; grid-row: 4; } /* House icon */
131
- #gearBtn { grid-column: 2; grid-row: 4; } /* Gear icon */
132
 
133
  /* Top Right UI Panel */
134
  #topRightUI { display: flex; flex-direction: column; align-items: center; width: 80px; }
135
  .thronglet-icon-display {
136
- background: var(--ui-accent); border: 2px solid var(--ui-border);
137
- width: 50px; height: 50px; border-radius: 50%; display: flex;
138
- align-items: center; justify-content: center; font-size: 1.5em;
139
- margin-bottom: 4px; color: var(--ui-bg); position: relative;
 
140
  }
141
- /* Using the new Hamster emoji for the icon */
142
- .thronglet-icon-display span { position: absolute; font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', Times, Symbola, Aegyptus, Demo; }
143
- .thronglet-icon-display .t-icon1 { top: 30%; left: 30%; transform: scale(0.5); }
144
- .thronglet-icon-display .t-icon2 { top: 30%; left: 70%; transform: scale(0.5); }
145
- .thronglet-icon-display .t-icon3 { top: 70%; left: 50%; transform: scale(0.5); }
146
 
147
  #throngletCountDisplayTopRight { color: var(--ui-text); font-weight: bold; font-size: 1.1em; }
148
 
149
- /* Hidden elements (same as before) */
150
  .scanline-overlay, .glitch-overlay, .ominous-elements, .info-panel,
151
  .final-screen, .full-black, .netflix-games-logo {
152
  opacity: 0; pointer-events: none; display: none;
@@ -154,12 +131,10 @@
154
  .visible { opacity: 1 !important; pointer-events: auto !important; display: flex !important; }
155
  .ominous-elements.visible, .info-panel.visible { display: block !important; }
156
 
157
-
158
  @keyframes hatch-pulse { /* Unchanged */
159
  from { transform: translate(-50%, -50%) scale(1); }
160
  to { transform: translate(-50%, -50%) scale(1.05); }
161
  }
162
-
163
  </style>
164
  </head>
165
  <body>
@@ -167,14 +142,13 @@
167
  <div class="game-container" id="gameContainer">
168
  <div class="game-elements" id="gameElements">
169
  <!-- Static Elements -->
170
- <div class="river"></div>
171
- <div class="rock-cluster">πŸͺ¨<br>πŸͺ¨πŸͺ¨</div>
172
  <span class="emoji tree t1">🌳</span> <span class="emoji tree t2">🌳</span>
173
  <span class="emoji tree t3">🌳</span> <span class="emoji tree t4">🌳</span>
174
  <span class="emoji tree t5">🌳</span>
175
  <!-- Initial dynamic elements -->
176
  <span class="emoji egg" id="egg">πŸ₯š</span>
177
- <!-- Elements hidden initially -->
178
  <div class="ominous-elements" id="ominousElements">πŸ’€<br>🦴<br>πŸŒŒπŸ“¦</div>
179
  <div class="info-panel" id="infoPanel"></div>
180
  <span class="emoji blood" id="bloodSplatter">🩸</span>
@@ -184,8 +158,8 @@
184
  <div class="ui-panel-container">
185
  <div class="ui-panel" id="topLeftUI">
186
  <div class="logo">T</div>
187
- <div class="action-grid" id="actionGrid"> <!-- Added ID for easier selection management -->
188
- <button id="pointerBtn" title="Pointer" class="selected">βœ‹</button> <!-- Default selected -->
189
  <button id="targetBtn" title="Target">🎯</button>
190
  <button id="feedButton" title="Feed">🍎</button>
191
  <button id="cleanButton" title="Clean">🧼</button>
@@ -195,13 +169,8 @@
195
  <button id="gearBtn" title="Settings?">βš™οΈ</button>
196
  </div>
197
  </div>
198
-
199
  <div class="ui-panel" id="topRightUI">
200
- <div class="thronglet-icon-display">
201
- <span class="t-icon1">🐹</span>
202
- <span class="t-icon2">🐹</span>
203
- <span class="t-icon3">🐹</span>
204
- </div>
205
  <span id="throngletCountDisplayTopRight">0</span>
206
  </div>
207
  </div>
@@ -214,16 +183,13 @@
214
  <div class="full-black" id="fullBlack"></div>
215
  <div class="netflix-games-logo" id="netflixGamesLogo"><span class="emoji">N</span></div>
216
 
217
-
218
  <script>
219
  // --- DOM Elements ---
220
  const egg = document.getElementById('egg');
221
  const gameContainer = document.getElementById('gameContainer');
222
  const gameElements = document.getElementById('gameElements');
223
- const actionGrid = document.getElementById('actionGrid'); // Get grid container
224
- // UI Display
225
  const throngletCountDisplay = document.getElementById('throngletCountDisplayTopRight');
226
- // Other elements
227
  const infoPanel = document.getElementById('infoPanel');
228
  const bloodSplatter = document.getElementById('bloodSplatter');
229
 
@@ -234,41 +200,52 @@
234
  const MAX_THRONGLETS = 100;
235
  let gameInterval;
236
  let gameActive = true;
237
- let selectedTool = 'pointer'; // Default tool
238
 
239
  // --- Web Audio API Setup ---
240
  let audioContext;
241
  let isAudioContextResumed = false;
242
- // INCLUDE initAudio and playSound functions from the previous response HERE
243
- // ... (Previous Audio Code - initAudio, playSound) ...
244
- function initAudio() { /* ... Same as before ... */
245
  if (!audioContext) {
246
  try {
247
  window.AudioContext = window.AudioContext || window.webkitAudioContext;
248
  audioContext = new AudioContext();
249
- isAudioContextResumed = (audioContext.state === 'running');
250
- if (!isAudioContextResumed) {
251
- const resumeAudio = () => {
252
- if (audioContext && audioContext.state === 'suspended') {
253
- audioContext.resume().then(() => {
254
- isAudioContextResumed = true; console.log("AudioContext Resumed");
255
- document.body.removeEventListener('click', resumeAudio); document.body.removeEventListener('touchend', resumeAudio);
256
- });
257
- } else if (audioContext && audioContext.state === 'running') {
258
- isAudioContextResumed = true;
259
- document.body.removeEventListener('click', resumeAudio); document.body.removeEventListener('touchend', resumeAudio);
260
- }
261
- };
262
- document.body.addEventListener('click', resumeAudio, { once: true }); document.body.addEventListener('touchend', resumeAudio, { once: true });
 
263
  }
264
- } catch (e) { console.error("Web Audio API is not supported", e); }
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  }
266
- }
267
- function playSound(type) { /* ... Same as before ... */
268
- if (!audioContext || !isAudioContextResumed) { if (audioContext && audioContext.state === 'suspended') { audioContext.resume().then(() => { isAudioContextResumed = true; playSound(type); }); } return; }
269
  const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination);
270
  const now = audioContext.currentTime; let freq = 440, duration = 0.1, vol = 0.2; osc.type = 'sine';
271
- switch (type) {
272
  case 'hatch': freq = 660; duration = 0.3; vol = 0.3; osc.type = 'triangle'; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(vol, now + 0.05); gain.gain.linearRampToValueAtTime(0, now + duration); break;
273
  case 'feed': freq = 880; duration = 0.08; vol = 0.15; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration); break;
274
  case 'clean': freq = 1100; duration = 0.1; vol = 0.1; osc.type = 'square'; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration); break;
@@ -280,7 +257,7 @@
280
  default: freq = 900; duration = 0.05; vol = 0.1; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration);
281
  }
282
  osc.frequency.setValueAtTime(freq, now); osc.start(now); osc.stop(now + duration);
283
- }
284
 
285
  // --- Game Logic ---
286
  function updateUI() {
@@ -288,18 +265,19 @@
288
  throngletCountDisplay.textContent = livingCount;
289
  }
290
 
291
- function showInfoPanel(text, duration = 2000) { // Re-add info panel for limits etc.
292
  if (!gameActive || !infoPanel) return;
293
  infoPanel.textContent = text;
294
- infoPanel.style.display = 'block'; // Make visible
295
- infoPanel.style.opacity = 1;
 
 
296
  clearTimeout(infoPanel.timeout);
297
  infoPanel.timeout = setTimeout(() => {
298
  infoPanel.style.opacity = 0;
299
- // Use transitionend event listener for better hiding
300
- infoPanel.addEventListener('transitionend', () => {
301
- infoPanel.style.display = 'none';
302
- }, { once: true });
303
  }, duration);
304
  }
305
 
@@ -307,156 +285,115 @@
307
  const livingCount = thronglets.filter(t => !t.isDead).length;
308
  if (!gameActive || livingCount >= MAX_THRONGLETS) {
309
  if (livingCount >= MAX_THRONGLETS) {
310
- playSound('error');
311
- showInfoPanel("Population Limit!", 1500);
312
- }
313
- return;
314
  }
315
-
316
  const throngletElement = document.createElement('span');
317
  throngletElement.classList.add('emoji', 'thronglet');
318
- throngletElement.textContent = '🐹'; // USE HAMSTER EMOJI
319
  const currentId = nextThrongletId++;
320
  throngletElement.dataset.id = currentId;
321
- const spawnX = Math.max(5, Math.min(95, xPercent)); // Ensure spawn within bounds
322
  const spawnY = Math.max(5, Math.min(95, yPercent));
323
- throngletElement.style.top = `${spawnY}%`;
324
- throngletElement.style.left = `${spawnX}%`;
325
  throngletElement.dataset.bornTime = Date.now();
326
  gameElements.appendChild(throngletElement);
327
 
328
- const newThronglet = { /* ... same as before ... */
329
- id: currentId, element: throngletElement, hunger: 25, cleanliness: 20, happiness: 70,
330
- lastInteraction: Date.now(), lastDuplication: 0, isDead: false, memory: [], feedbackElement: null
331
- };
332
  thronglets.push(newThronglet);
333
 
334
- // Create and append feedback element
335
- const feedbackElement = document.createElement('span');
336
- feedbackElement.classList.add('emoji', 'feedback');
337
- feedbackElement.style.top = throngletElement.style.top;
338
- feedbackElement.style.left = throngletElement.style.left;
339
- gameElements.appendChild(feedbackElement);
340
- newThronglet.feedbackElement = feedbackElement;
341
 
342
  updateUI();
343
  setTimeout(() => { if (!newThronglet.isDead) wander(newThronglet); }, 100);
344
  }
345
 
346
- function feedThronglet(thronglet) { /* ... Same as before ... */
347
  if (!gameActive || thronglet.isDead) return;
348
- thronglet.hunger = Math.max(0, thronglet.hunger - 50);
349
- thronglet.happiness = Math.min(100, thronglet.happiness + 8);
350
- thronglet.lastInteraction = Date.now();
351
- showFeedback(thronglet, 'πŸ˜‹'); playSound('feed'); thronglet.memory.push('Fed');
352
  }
353
-
354
- function cleanThronglet(thronglet) { /* ... Same as before ... */
355
  if (!gameActive || thronglet.isDead) return;
356
- thronglet.cleanliness = Math.max(0, thronglet.cleanliness - 60);
357
- thronglet.happiness = Math.min(100, thronglet.happiness + 4);
358
- thronglet.lastInteraction = Date.now();
359
- showFeedback(thronglet, '✨'); playSound('clean'); thronglet.memory.push('Cleaned');
360
  }
361
-
362
- function showFeedback(thronglet, emoji) { /* ... Same as before ... */
363
- if (!gameActive || !thronglet.feedbackElement || thronglet.isDead) return;
364
- thronglet.feedbackElement.style.top = thronglet.element.style.top;
365
- thronglet.feedbackElement.style.left = thronglet.element.style.left;
366
- thronglet.feedbackElement.textContent = emoji;
367
- thronglet.feedbackElement.classList.add('active');
368
- playSound('feedback');
369
- clearTimeout(thronglet.feedbackElement.timeout);
370
- thronglet.feedbackElement.timeout = setTimeout(() => { if (thronglet.feedbackElement) { thronglet.feedbackElement.classList.remove('active'); } }, 600);
371
  }
372
-
373
- function killThronglet(thronglet, reason = "expiration") { /* ... Same as before ... */
374
- if (!gameActive || thronglet.isDead) return;
375
- thronglet.isDead = true;
376
- thronglet.element.classList.add('dead'); thronglet.element.textContent = 'πŸ’€';
377
- if (thronglet.feedbackElement) { thronglet.feedbackElement.remove(); thronglet.feedbackElement = null; }
378
- bloodSplatter.style.top = thronglet.element.style.top; bloodSplatter.style.left = thronglet.element.style.left;
379
- bloodSplatter.classList.add('splatter'); setTimeout(() => { bloodSplatter.classList.remove('splatter'); }, 800);
380
- deathCount++; updateUI(); playSound('death'); thronglet.memory.push(`Died (${reason})`);
381
- setTimeout(() => { if (thronglet.element) { thronglet.element.style.opacity = 0; setTimeout(() => { if(thronglet.element) thronglet.element.remove(); }, 500); } }, 3000);
382
  }
383
-
384
  function wander(thronglet) {
385
  if (!gameActive || thronglet.isDead) return;
386
- const moveDist = 1.0; // Reduced movement distance further
387
- const currentX = parseFloat(thronglet.element.style.left);
388
- const currentY = parseFloat(thronglet.element.style.top);
389
- const newX = Math.max(5, Math.min(95, currentX + (Math.random() - 0.5) * moveDist * 2)); // Clamp within 5-95%
390
  const newY = Math.max(5, Math.min(95, currentY + (Math.random() - 0.5) * moveDist * 2));
391
- thronglet.element.style.left = `${newX}%`;
392
- thronglet.element.style.top = `${newY}%`;
393
- if (thronglet.feedbackElement) { // Ensure feedback follows smoothly
394
- thronglet.feedbackElement.style.left = `${newX}%`;
395
- thronglet.feedbackElement.style.top = `${newY}%`;
396
- }
397
- // console.log(`Wander #${thronglet.id} to ${newX.toFixed(1)}%, ${newY.toFixed(1)}%`); // Debug log
398
  }
399
 
400
- function updateThronglets() { // Core logic loop
401
  if (!gameActive) return;
402
  const now = Date.now();
403
  const livingThronglets = thronglets.filter(t => !t.isDead);
404
 
405
  livingThronglets.forEach(thronglet => {
406
  const timeSinceLast = now - thronglet.lastInteraction;
407
- const baseNeedRate = 0.15;
408
- const neglectMultiplier = 1 + Math.min(5, timeSinceLast / 10000);
409
-
410
  thronglet.hunger = Math.min(100, thronglet.hunger + baseNeedRate * neglectMultiplier * 1.0);
411
  thronglet.cleanliness = Math.min(100, thronglet.cleanliness + baseNeedRate * neglectMultiplier * 0.8);
412
  thronglet.happiness = Math.max(0, thronglet.happiness - baseNeedRate * neglectMultiplier * 1.1);
413
 
414
  let deathReason = null;
415
- if (thronglet.hunger >= 100) deathReason = "starvation";
416
- else if (thronglet.cleanliness >= 100) deathReason = "filth";
417
- else if (thronglet.happiness <= 0) deathReason = "misery";
418
  if (deathReason) { killThronglet(thronglet, deathReason); return; }
419
 
420
  // Duplication Logic
421
- const canDuplicate = thronglet.happiness > 85 && thronglet.hunger < 15 &&
422
- thronglet.cleanliness < 15 && timeSinceLast < 15000 &&
423
- (now - thronglet.lastDuplication > 20000);
424
  if (canDuplicate && Math.random() < 0.015) {
425
- showFeedback(thronglet, 'πŸ’ž'); playSound('duplicate');
426
- thronglet.lastDuplication = now; thronglet.lastInteraction = now;
427
  setTimeout(() => {
428
- const parentX = parseFloat(thronglet.element.style.left);
429
- const parentY = parseFloat(thronglet.element.style.top);
430
  createThronglet(parentX + (Math.random() - 0.5) * 5, parentY + (Math.random() - 0.5) * 5);
431
  }, 500);
432
  }
433
-
434
- // Wander less often
435
- if (Math.random() < 0.2) { wander(thronglet); }
436
  });
437
- // Update UI count periodically
438
- if (Math.random() < 0.2) updateUI();
439
  }
440
 
441
  // --- Tool Selection Function ---
442
- function selectTool(toolId) {
443
- selectedTool = toolId;
 
444
  playSound('ui_click');
445
- // Remove 'selected' class from all buttons
446
  actionGrid.querySelectorAll('button').forEach(btn => btn.classList.remove('selected'));
447
  // Add 'selected' class to the clicked button
448
- const selectedButton = document.getElementById(toolId + (toolId.endsWith('Btn') || toolId.endsWith('Button') ? '' : 'Btn')); // Handle variations like 'feedButton' vs 'pointerBtn'
449
- if (selectedButton) {
450
- selectedButton.classList.add('selected');
451
- }
452
- console.log("Selected tool:", selectedTool); // Log selected tool
453
  }
454
 
455
-
456
  // --- Event Handlers ---
457
  egg.addEventListener('click', () => {
458
  if (!gameActive || egg.classList.contains('hatching') || !document.contains(egg)) return;
459
- initAudio(); // Important: Ensure audio is ready on first interaction
460
  egg.classList.add('hatching'); playSound('hatch');
461
  setTimeout(() => {
462
  if(document.contains(egg)) egg.remove();
@@ -464,71 +401,63 @@
464
  }, 1000);
465
  });
466
 
467
- // Add event listeners for all buttons in the grid
468
  actionGrid.addEventListener('click', (event) => {
469
  if (event.target.tagName === 'BUTTON') {
470
- const buttonId = event.target.id;
471
- initAudio(); // Ensure audio is ready
472
-
473
- // Handle tool selection or direct action
474
- switch (buttonId) {
475
- case 'pointerBtn':
476
- selectTool('pointer');
477
- break;
478
- case 'targetBtn':
479
- selectTool('target'); // Placeholder functionality
480
- console.log("Target tool selected (placeholder)");
481
- break;
482
- case 'feedButton':
483
- selectTool('feed');
484
- // Auto-feed hungriest for now
485
- const feedTarget = thronglets.filter(t => !t.isDead).reduce((p, c) => (p.hunger > c.hunger) ? p : c, null);
486
- if (feedTarget) feedThronglet(feedTarget); else playSound('error');
487
- break;
488
- case 'cleanButton':
489
- selectTool('clean');
490
- // Auto-clean dirtiest for now
491
- const cleanTarget = thronglets.filter(t => !t.isDead).reduce((p, c) => (p.cleanliness > c.cleanliness) ? p : c, null);
492
- if (cleanTarget) cleanThronglet(cleanTarget); else playSound('error');
493
- break;
494
- // Placeholder actions for other buttons
495
- case 'brainBtn':
496
- selectTool('brain');
497
- console.log("Brain button clicked (placeholder)");
498
- break;
499
- case 'plantBtn':
500
- selectTool('plant');
501
- console.log("Plant button clicked (placeholder)");
502
- break;
503
- case 'houseBtn':
504
- selectTool('house');
505
- console.log("House button clicked (placeholder)");
506
- break;
507
- case 'gearBtn':
508
- selectTool('gear');
509
- console.log("Gear button clicked (placeholder)");
510
- break;
511
- default:
512
- playSound('error'); // Sound for unhandled buttons
513
- break;
514
  }
515
  }
516
  });
517
 
518
-
519
  // --- Initial Setup ---
520
  function initGame() {
521
- gameActive = true;
522
- updateUI();
523
- gameInterval = setInterval(updateThronglets, 400); // Update loop
524
- initAudio(); // Initialize audio context on load (might require user interaction later)
525
- selectTool('pointer'); // Set pointer as default selected tool visually
526
- console.log("Game Initialized. Click the egg.");
 
527
  }
528
-
529
- // Start the game automatically
530
- initGame();
531
-
532
  </script>
533
  </body>
534
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Thronglets Simulation (v3)</title>
7
  <style>
8
  :root {
9
+ --grass-dark: #2a6141; --grass-light: #3a815b; --water: #4171a7;
10
+ --rock-dark: #6b727c; --tree-leaves: #2b602c; --ui-bg: #e0cda9;
11
+ --ui-border: #8b4513; --ui-accent: #6d8c4f; --ui-text: #3a2e20;
 
 
 
 
 
 
 
 
12
  }
13
 
14
  body {
15
  margin: 0; overflow: hidden; background: var(--grass-dark);
16
+ color: #eee; font-family: 'Verdana', sans-serif; display: flex;
17
+ flex-direction: column; align-items: center; justify-content: center;
18
+ min-height: 100vh; position: relative;
19
  }
20
 
21
  .game-container {
22
  position: relative; width: 95vmin; height: 70vmin;
23
  max-width: 1000px; max-height: 700px;
24
+ background-color: var(--grass-light); /* Simple background color */
 
 
 
25
  border: 1px solid var(--tree-trunk); box-sizing: border-box;
26
  overflow: hidden; display: flex; justify-content: center;
27
+ align-items: center; font-size: 1.5em; position: relative;
28
+ image-rendering: pixelated; box-shadow: inset 0 0 20px rgba(0,0,0,0.4);
 
29
  }
30
 
31
  .game-elements {
 
53
 
54
  .thronglet {
55
  cursor: default;
 
56
  transition: top 1s linear, left 1s linear, transform 0.2s ease, opacity 0.5s ease-out, filter 0.3s ease-out;
57
  z-index: 3;
58
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', Times, Symbola, Aegyptus, Demo;
 
87
 
88
  /* Top Left UI Panel */
89
  #topLeftUI { display: flex; flex-direction: column; align-items: center; width: 70px; }
90
+ .logo { /* ... Same style ... */
91
+ background: var(--ui-accent); border: 2px solid var(--ui-border); color: var(--ui-bg);
92
+ font-size: 1.8em; font-weight: bold; width: 40px; height: 40px; display: flex;
93
+ align-items: center; justify-content: center; border-radius: 50%; margin-bottom: 5px;
 
94
  }
95
+ .action-grid { /* ... Same style ... */
96
+ display: grid; grid-template-columns: 1fr 1fr; gap: 4px; width: 100%;
 
 
 
 
 
97
  }
98
+ .action-grid button { /* ... Same style ... */
99
+ background: var(--ui-bg); border: 2px solid var(--ui-border); color: var(--ui-text);
100
+ font-size: 1.2em; width: 30px; height: 30px; display: flex; align-items: center;
101
+ justify-content: center; border-radius: 4px; cursor: pointer;
102
+ transition: background-color 0.2s, transform 0.1s, border-color 0.1s; padding: 0; line-height: 1;
103
  }
104
+ .action-grid button.selected { border-color: #fff; box-shadow: inset 0 0 3px rgba(0,0,0,0.4); }
105
  .action-grid button:hover { background-color: #f5e5c5; }
106
+ .action-grid button:active { transform: scale(0.95); }
107
+ /* Button IDs */
108
+ #pointerBtn { grid-column: 1; grid-row: 1; } #targetBtn { grid-column: 2; grid-row: 1; }
109
+ #feedButton { grid-column: 1; grid-row: 2; } #cleanButton { grid-column: 2; grid-row: 2; }
110
+ #brainBtn { grid-column: 1; grid-row: 3; } #plantBtn { grid-column: 2; grid-row: 3; }
111
+ #houseBtn { grid-column: 1; grid-row: 4; } #gearBtn { grid-column: 2; grid-row: 4; }
 
 
 
 
112
 
113
  /* Top Right UI Panel */
114
  #topRightUI { display: flex; flex-direction: column; align-items: center; width: 80px; }
115
  .thronglet-icon-display {
116
+ background: var(--ui-accent); border: 2px solid var(--ui-border); width: 50px; height: 50px;
117
+ border-radius: 50%; display: flex; align-items: center; justify-content: center;
118
+ font-size: 2em; /* Make single emoji larger */
119
+ margin-bottom: 4px; color: var(--ui-bg);
120
+ font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', Times, Symbola, Aegyptus, Demo; /* Ensure emoji font */
121
  }
122
+ /* Removed inner spans for multiple icons */
 
 
 
 
123
 
124
  #throngletCountDisplayTopRight { color: var(--ui-text); font-weight: bold; font-size: 1.1em; }
125
 
126
+ /* Hidden elements */
127
  .scanline-overlay, .glitch-overlay, .ominous-elements, .info-panel,
128
  .final-screen, .full-black, .netflix-games-logo {
129
  opacity: 0; pointer-events: none; display: none;
 
131
  .visible { opacity: 1 !important; pointer-events: auto !important; display: flex !important; }
132
  .ominous-elements.visible, .info-panel.visible { display: block !important; }
133
 
 
134
  @keyframes hatch-pulse { /* Unchanged */
135
  from { transform: translate(-50%, -50%) scale(1); }
136
  to { transform: translate(-50%, -50%) scale(1.05); }
137
  }
 
138
  </style>
139
  </head>
140
  <body>
 
142
  <div class="game-container" id="gameContainer">
143
  <div class="game-elements" id="gameElements">
144
  <!-- Static Elements -->
145
+ <div class="river"></div> <div class="rock-cluster">πŸͺ¨<br>πŸͺ¨πŸͺ¨</div>
 
146
  <span class="emoji tree t1">🌳</span> <span class="emoji tree t2">🌳</span>
147
  <span class="emoji tree t3">🌳</span> <span class="emoji tree t4">🌳</span>
148
  <span class="emoji tree t5">🌳</span>
149
  <!-- Initial dynamic elements -->
150
  <span class="emoji egg" id="egg">πŸ₯š</span>
151
+ <!-- Hidden Elements -->
152
  <div class="ominous-elements" id="ominousElements">πŸ’€<br>🦴<br>πŸŒŒπŸ“¦</div>
153
  <div class="info-panel" id="infoPanel"></div>
154
  <span class="emoji blood" id="bloodSplatter">🩸</span>
 
158
  <div class="ui-panel-container">
159
  <div class="ui-panel" id="topLeftUI">
160
  <div class="logo">T</div>
161
+ <div class="action-grid" id="actionGrid">
162
+ <button id="pointerBtn" title="Pointer">βœ‹</button>
163
  <button id="targetBtn" title="Target">🎯</button>
164
  <button id="feedButton" title="Feed">🍎</button>
165
  <button id="cleanButton" title="Clean">🧼</button>
 
169
  <button id="gearBtn" title="Settings?">βš™οΈ</button>
170
  </div>
171
  </div>
 
172
  <div class="ui-panel" id="topRightUI">
173
+ <div class="thronglet-icon-display">🐹</div> <!-- Simplified Icon -->
 
 
 
 
174
  <span id="throngletCountDisplayTopRight">0</span>
175
  </div>
176
  </div>
 
183
  <div class="full-black" id="fullBlack"></div>
184
  <div class="netflix-games-logo" id="netflixGamesLogo"><span class="emoji">N</span></div>
185
 
 
186
  <script>
187
  // --- DOM Elements ---
188
  const egg = document.getElementById('egg');
189
  const gameContainer = document.getElementById('gameContainer');
190
  const gameElements = document.getElementById('gameElements');
191
+ const actionGrid = document.getElementById('actionGrid');
 
192
  const throngletCountDisplay = document.getElementById('throngletCountDisplayTopRight');
 
193
  const infoPanel = document.getElementById('infoPanel');
194
  const bloodSplatter = document.getElementById('bloodSplatter');
195
 
 
200
  const MAX_THRONGLETS = 100;
201
  let gameInterval;
202
  let gameActive = true;
203
+ let selectedTool = 'pointer';
204
 
205
  // --- Web Audio API Setup ---
206
  let audioContext;
207
  let isAudioContextResumed = false;
208
+ // initAudio and playSound functions (same as previous version)
209
+ function initAudio() {
210
+ if (isAudioContextResumed) return; // Already resumed
211
  if (!audioContext) {
212
  try {
213
  window.AudioContext = window.AudioContext || window.webkitAudioContext;
214
  audioContext = new AudioContext();
215
+ } catch (e) { console.error("Web Audio API not supported", e); return; }
216
+ }
217
+ // Resume on first user interaction if needed
218
+ if (audioContext.state === 'suspended') {
219
+ const resumeAudio = () => {
220
+ if (audioContext.state === 'suspended') {
221
+ audioContext.resume().then(() => {
222
+ isAudioContextResumed = true; console.log("AudioContext Resumed");
223
+ document.body.removeEventListener('click', resumeAudio);
224
+ document.body.removeEventListener('touchend', resumeAudio);
225
+ }).catch(e => console.error("Audio resume failed", e));
226
+ } else { // Already running or closed?
227
+ isAudioContextResumed = (audioContext.state === 'running');
228
+ document.body.removeEventListener('click', resumeAudio);
229
+ document.body.removeEventListener('touchend', resumeAudio);
230
  }
231
+ };
232
+ // Use capture phase to potentially catch earlier interactions
233
+ document.body.addEventListener('click', resumeAudio, { once: true, capture: true });
234
+ document.body.addEventListener('touchend', resumeAudio, { once: true, capture: true });
235
+ } else {
236
+ isAudioContextResumed = true; // Already running
237
+ }
238
+ }
239
+ function playSound(type) {
240
+ if (!audioContext || !isAudioContextResumed) {
241
+ console.log("Audio not ready, skipping sound:", type);
242
+ // Attempt to resume if called before interaction
243
+ if (audioContext && audioContext.state === 'suspended') initAudio();
244
+ return;
245
  }
 
 
 
246
  const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination);
247
  const now = audioContext.currentTime; let freq = 440, duration = 0.1, vol = 0.2; osc.type = 'sine';
248
+ switch (type) { /* ... cases same as before ... */
249
  case 'hatch': freq = 660; duration = 0.3; vol = 0.3; osc.type = 'triangle'; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(vol, now + 0.05); gain.gain.linearRampToValueAtTime(0, now + duration); break;
250
  case 'feed': freq = 880; duration = 0.08; vol = 0.15; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration); break;
251
  case 'clean': freq = 1100; duration = 0.1; vol = 0.1; osc.type = 'square'; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration); break;
 
257
  default: freq = 900; duration = 0.05; vol = 0.1; gain.gain.setValueAtTime(vol, now); gain.gain.linearRampToValueAtTime(0, now + duration);
258
  }
259
  osc.frequency.setValueAtTime(freq, now); osc.start(now); osc.stop(now + duration);
260
+ }
261
 
262
  // --- Game Logic ---
263
  function updateUI() {
 
265
  throngletCountDisplay.textContent = livingCount;
266
  }
267
 
268
+ function showInfoPanel(text, duration = 2000) {
269
  if (!gameActive || !infoPanel) return;
270
  infoPanel.textContent = text;
271
+ infoPanel.style.display = 'block';
272
+ requestAnimationFrame(() => { // Ensure display:block is applied before opacity transition starts
273
+ infoPanel.style.opacity = 1;
274
+ });
275
  clearTimeout(infoPanel.timeout);
276
  infoPanel.timeout = setTimeout(() => {
277
  infoPanel.style.opacity = 0;
278
+ // Use transitionend event listener for reliable hiding
279
+ const hidePanel = () => { infoPanel.style.display = 'none'; };
280
+ infoPanel.addEventListener('transitionend', hidePanel, { once: true });
 
281
  }, duration);
282
  }
283
 
 
285
  const livingCount = thronglets.filter(t => !t.isDead).length;
286
  if (!gameActive || livingCount >= MAX_THRONGLETS) {
287
  if (livingCount >= MAX_THRONGLETS) {
288
+ playSound('error'); showInfoPanel("Population Limit!", 1500);
289
+ } return;
 
 
290
  }
 
291
  const throngletElement = document.createElement('span');
292
  throngletElement.classList.add('emoji', 'thronglet');
293
+ throngletElement.textContent = '🐹'; // Hamster emoji
294
  const currentId = nextThrongletId++;
295
  throngletElement.dataset.id = currentId;
296
+ const spawnX = Math.max(5, Math.min(95, xPercent));
297
  const spawnY = Math.max(5, Math.min(95, yPercent));
298
+ throngletElement.style.top = `${spawnY}%`; throngletElement.style.left = `${spawnX}%`;
 
299
  throngletElement.dataset.bornTime = Date.now();
300
  gameElements.appendChild(throngletElement);
301
 
302
+ const newThronglet = { id: currentId, element: throngletElement, hunger: 25, cleanliness: 20, happiness: 70, lastInteraction: Date.now(), lastDuplication: 0, isDead: false, memory: [], feedbackElement: null };
 
 
 
303
  thronglets.push(newThronglet);
304
 
305
+ const feedbackElement = document.createElement('span');
306
+ feedbackElement.classList.add('emoji', 'feedback');
307
+ feedbackElement.style.top = throngletElement.style.top; feedbackElement.style.left = throngletElement.style.left;
308
+ gameElements.appendChild(feedbackElement); newThronglet.feedbackElement = feedbackElement;
 
 
 
309
 
310
  updateUI();
311
  setTimeout(() => { if (!newThronglet.isDead) wander(newThronglet); }, 100);
312
  }
313
 
314
+ function feedThronglet(thronglet) {
315
  if (!gameActive || thronglet.isDead) return;
316
+ thronglet.hunger = Math.max(0, thronglet.hunger - 50); thronglet.happiness = Math.min(100, thronglet.happiness + 8);
317
+ thronglet.lastInteraction = Date.now(); showFeedback(thronglet, 'πŸ˜‹'); playSound('feed'); thronglet.memory.push('Fed');
 
 
318
  }
319
+ function cleanThronglet(thronglet) {
 
320
  if (!gameActive || thronglet.isDead) return;
321
+ thronglet.cleanliness = Math.max(0, thronglet.cleanliness - 60); thronglet.happiness = Math.min(100, thronglet.happiness + 4);
322
+ thronglet.lastInteraction = Date.now(); showFeedback(thronglet, '✨'); playSound('clean'); thronglet.memory.push('Cleaned');
 
 
323
  }
324
+ function showFeedback(thronglet, emoji) {
325
+ if (!gameActive || !thronglet.feedbackElement || thronglet.isDead) return;
326
+ thronglet.feedbackElement.style.top = thronglet.element.style.top; thronglet.feedbackElement.style.left = thronglet.element.style.left;
327
+ thronglet.feedbackElement.textContent = emoji; thronglet.feedbackElement.classList.add('active'); playSound('feedback');
328
+ clearTimeout(thronglet.feedbackElement.timeout);
329
+ thronglet.feedbackElement.timeout = setTimeout(() => { if (thronglet.feedbackElement) { thronglet.feedbackElement.classList.remove('active'); } }, 600);
 
 
 
 
330
  }
331
+ function killThronglet(thronglet, reason = "expiration") {
332
+ if (!gameActive || thronglet.isDead) return;
333
+ thronglet.isDead = true; thronglet.element.classList.add('dead'); thronglet.element.textContent = 'πŸ’€';
334
+ if (thronglet.feedbackElement) { thronglet.feedbackElement.remove(); thronglet.feedbackElement = null; }
335
+ bloodSplatter.style.top = thronglet.element.style.top; bloodSplatter.style.left = thronglet.element.style.left;
336
+ bloodSplatter.classList.add('splatter'); setTimeout(() => { bloodSplatter.classList.remove('splatter'); }, 800);
337
+ deathCount++; updateUI(); playSound('death'); thronglet.memory.push(`Died (${reason})`);
338
+ setTimeout(() => { if (thronglet.element) { thronglet.element.style.opacity = 0; setTimeout(() => { if(thronglet.element) thronglet.element.remove(); }, 500); } }, 3000);
 
 
339
  }
 
340
  function wander(thronglet) {
341
  if (!gameActive || thronglet.isDead) return;
342
+ const moveDist = 0.8; // Further reduced movement distance
343
+ const currentX = parseFloat(thronglet.element.style.left); const currentY = parseFloat(thronglet.element.style.top);
344
+ const newX = Math.max(5, Math.min(95, currentX + (Math.random() - 0.5) * moveDist * 2));
 
345
  const newY = Math.max(5, Math.min(95, currentY + (Math.random() - 0.5) * moveDist * 2));
346
+ thronglet.element.style.left = `${newX}%`; thronglet.element.style.top = `${newY}%`;
347
+ if (thronglet.feedbackElement) { thronglet.feedbackElement.style.left = `${newX}%`; thronglet.feedbackElement.style.top = `${newY}%`; }
 
 
 
 
 
348
  }
349
 
350
+ function updateThronglets() {
351
  if (!gameActive) return;
352
  const now = Date.now();
353
  const livingThronglets = thronglets.filter(t => !t.isDead);
354
 
355
  livingThronglets.forEach(thronglet => {
356
  const timeSinceLast = now - thronglet.lastInteraction;
357
+ const baseNeedRate = 0.15; const neglectMultiplier = 1 + Math.min(5, timeSinceLast / 10000);
 
 
358
  thronglet.hunger = Math.min(100, thronglet.hunger + baseNeedRate * neglectMultiplier * 1.0);
359
  thronglet.cleanliness = Math.min(100, thronglet.cleanliness + baseNeedRate * neglectMultiplier * 0.8);
360
  thronglet.happiness = Math.max(0, thronglet.happiness - baseNeedRate * neglectMultiplier * 1.1);
361
 
362
  let deathReason = null;
363
+ if (thronglet.hunger >= 100) deathReason = "starvation"; else if (thronglet.cleanliness >= 100) deathReason = "filth"; else if (thronglet.happiness <= 0) deathReason = "misery";
 
 
364
  if (deathReason) { killThronglet(thronglet, deathReason); return; }
365
 
366
  // Duplication Logic
367
+ const canDuplicate = thronglet.happiness > 85 && thronglet.hunger < 15 && thronglet.cleanliness < 15 && timeSinceLast < 15000 && (now - thronglet.lastDuplication > 20000);
 
 
368
  if (canDuplicate && Math.random() < 0.015) {
369
+ showFeedback(thronglet, 'πŸ’ž'); playSound('duplicate'); thronglet.lastDuplication = now; thronglet.lastInteraction = now;
 
370
  setTimeout(() => {
371
+ const parentX = parseFloat(thronglet.element.style.left); const parentY = parseFloat(thronglet.element.style.top);
 
372
  createThronglet(parentX + (Math.random() - 0.5) * 5, parentY + (Math.random() - 0.5) * 5);
373
  }, 500);
374
  }
375
+ // Wander even less often
376
+ if (Math.random() < 0.15) { wander(thronglet); }
 
377
  });
378
+ if (Math.random() < 0.2) updateUI(); // Update UI count periodically
 
379
  }
380
 
381
  // --- Tool Selection Function ---
382
+ function selectTool(toolButtonElement) {
383
+ // Get the ID to store the tool name
384
+ selectedTool = toolButtonElement.id.replace(/Btn|Button/g, ''); // Get base name like 'pointer', 'feed'
385
  playSound('ui_click');
386
+ // Remove 'selected' class from all buttons in the grid
387
  actionGrid.querySelectorAll('button').forEach(btn => btn.classList.remove('selected'));
388
  // Add 'selected' class to the clicked button
389
+ toolButtonElement.classList.add('selected');
390
+ // console.log("Selected tool:", selectedTool);
 
 
 
391
  }
392
 
 
393
  // --- Event Handlers ---
394
  egg.addEventListener('click', () => {
395
  if (!gameActive || egg.classList.contains('hatching') || !document.contains(egg)) return;
396
+ initAudio(); // IMPORTANT: Call initAudio on first interaction!
397
  egg.classList.add('hatching'); playSound('hatch');
398
  setTimeout(() => {
399
  if(document.contains(egg)) egg.remove();
 
401
  }, 1000);
402
  });
403
 
404
+ // Centralized Action Grid Listener
405
  actionGrid.addEventListener('click', (event) => {
406
  if (event.target.tagName === 'BUTTON') {
407
+ const buttonElement = event.target;
408
+ const buttonId = buttonElement.id;
409
+ initAudio(); // Ensure audio context is active
410
+
411
+ // Select the tool visually first
412
+ selectTool(buttonElement);
413
+
414
+ // Perform immediate action for feed/clean
415
+ if (buttonId === 'feedButton') {
416
+ const living = thronglets.filter(t => !t.isDead);
417
+ if (living.length > 0) {
418
+ // Use reduce with initial value null to handle empty array
419
+ const target = living.reduce((p, c) => (p === null || c.hunger > p.hunger) ? c : p, null);
420
+ if (target) { // Check if a target was found
421
+ feedThronglet(target);
422
+ } else {
423
+ // This case shouldn't happen if living.length > 0, but good practice
424
+ playSound('error');
425
+ }
426
+ } else {
427
+ playSound('error'); // No living thronglets
428
+ }
429
+ } else if (buttonId === 'cleanButton') {
430
+ const living = thronglets.filter(t => !t.isDead);
431
+ if (living.length > 0) {
432
+ // Use reduce with initial value null
433
+ const target = living.reduce((p, c) => (p === null || c.cleanliness > p.cleanliness) ? c : p, null);
434
+ if (target) { // Check if target found
435
+ cleanThronglet(target);
436
+ } else {
437
+ playSound('error');
438
+ }
439
+ } else {
440
+ playSound('error'); // No living thronglets
441
+ }
442
+ } else if (buttonId !== 'pointerBtn') {
443
+ // Placeholder message/sound for other tools for now
444
+ console.log(`${selectedTool} tool selected (no action implemented)`);
445
+ // Optionally play a generic 'select' sound different from 'ui_click'
 
 
 
 
 
446
  }
447
  }
448
  });
449
 
 
450
  // --- Initial Setup ---
451
  function initGame() {
452
+ gameActive = true; updateUI();
453
+ gameInterval = setInterval(updateThronglets, 400);
454
+ // Attempt to initialize audio context, but it requires user interaction first
455
+ // It will be properly initialized/resumed on the first click (e.g., egg click)
456
+ initAudio();
457
+ selectTool(document.getElementById('pointerBtn')); // Set pointer as default selected tool visually
458
+ console.log("Game Initialized. Click the egg.");
459
  }
460
+ initGame(); // Start the game
 
 
 
461
  </script>
462
  </body>
463
  </html>