Lukeetah commited on
Commit
04c52be
·
verified ·
1 Parent(s): 5b92433

Upload 9 files

Browse files
Files changed (4) hide show
  1. app.py +23 -2
  2. index.html +170 -1573
  3. index.html_renacer +1677 -0
  4. run_game.bat +28 -0
app.py CHANGED
@@ -1,10 +1,31 @@
1
- from flask import Flask, render_template
 
 
2
 
3
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  @app.route('/')
6
  def index():
7
  return render_template('index.html')
8
 
 
 
 
 
 
 
9
  if __name__ == '__main__':
10
- app.run(host='0.0.0.0', port=7860, debug=False)
 
1
+ import random
2
+ from flask import Flask, render_template, jsonify, request, session
3
+ import os
4
 
5
  app = Flask(__name__)
6
+ app.secret_key = os.urandom(24)
7
+
8
+ # ==========================================
9
+ # SENSOR DATA
10
+ # ==========================================
11
+
12
+ SCAN_RESULTS = [
13
+ {"id": "CITIZEN-492", "status": "NOMINAL", "note": "Carrying architectural blueprints."},
14
+ {"id": "VEHICLE-XK9", "status": "WARNING", "note": "Unregistered plates. Traces of chemicals."},
15
+ {"id": "UNKNOWN-ENTITY", "status": "DANGER", "note": "Biometrics match criminal database (98%)."},
16
+ {"id": "INFRASTRUCTURE", "status": "DECAY", "note": "Structural integrity at 45%. Collapse imminent."},
17
+ {"id": "NETWORK-NODE", "status": "ACTIVE", "note": "Unauthorized data stream detected."},
18
+ ]
19
 
20
  @app.route('/')
21
  def index():
22
  return render_template('index.html')
23
 
24
+ @app.route('/api/scan', methods=['POST'])
25
+ def scan_target():
26
+ # Return a random scan result simulating AI analysis
27
+ result = random.choice(SCAN_RESULTS)
28
+ return jsonify(result)
29
+
30
  if __name__ == '__main__':
31
+ app.run(host='0.0.0.0', port=7860, debug=True)
index.html CHANGED
@@ -4,1674 +4,271 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>RENACER Buenos Aires 2077</title>
8
- <link
9
- href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Space+Mono:wght@400;700&display=swap"
10
- rel="stylesheet">
11
- <style>
12
- :root {
13
- --bg: #0a0a0f;
14
- --surface: rgba(15, 15, 25, 0.95);
15
- --text: #e8e6e3;
16
- --muted: #8a8a9a;
17
- --accent: #ff6b9d;
18
- --accent2: #05d9e8;
19
- --gold: #d4a574;
20
- --danger: #ff4757;
21
- --success: #2ed573;
22
- }
23
 
24
- * {
25
- margin: 0;
26
- padding: 0;
27
- box-sizing: border-box;
 
 
 
28
  }
 
 
 
 
29
 
 
30
  body {
31
- font-family: 'Cormorant Garamond', Georgia, serif;
32
- background: var(--bg);
33
- color: var(--text);
34
- min-height: 100vh;
35
- overflow-x: hidden;
36
- line-height: 1.8;
37
  }
38
 
39
- /* === LOADING SCREEN === */
40
  #loading-screen {
41
  position: fixed;
42
  inset: 0;
43
- background: var(--bg);
 
44
  display: flex;
45
  flex-direction: column;
46
  align-items: center;
47
  justify-content: center;
48
- z-index: 1000;
49
- transition: opacity 1s, visibility 1s;
50
- }
51
-
52
- #loading-screen.hidden {
53
- opacity: 0;
54
- visibility: hidden;
55
  }
56
 
57
- .loading-quote {
58
- font-size: 1.4rem;
59
- font-style: italic;
60
- max-width: 600px;
61
- text-align: center;
62
- padding: 0 30px;
63
- color: var(--muted);
64
- margin-bottom: 30px;
65
- }
66
-
67
- .loading-author {
68
- color: var(--gold);
69
- font-size: 1rem;
70
- margin-top: 15px;
71
- display: block;
72
- }
73
-
74
- .loading-bar {
75
- width: 200px;
76
- height: 2px;
77
- background: rgba(255, 255, 255, 0.1);
78
- border-radius: 2px;
79
  overflow: hidden;
80
  }
81
 
82
- .loading-progress {
83
- height: 100%;
84
- background: linear-gradient(90deg, var(--accent), var(--accent2));
 
 
85
  width: 0%;
86
- animation: loadProgress 2.5s ease-out forwards;
87
- }
88
-
89
- @keyframes loadProgress {
90
- to {
91
- width: 100%;
92
- }
93
  }
94
 
95
- /* === AUDIO CONTROLS === */
96
- #audio-controls {
97
  position: fixed;
98
- top: 20px;
99
- right: 20px;
100
- z-index: 100;
101
- display: flex;
102
- gap: 10px;
103
- }
104
-
105
- .audio-btn {
106
- width: 45px;
107
- height: 45px;
108
- border-radius: 50%;
109
- background: var(--surface);
110
- border: 1px solid rgba(255, 255, 255, 0.1);
111
- color: var(--text);
112
- cursor: pointer;
113
- display: flex;
114
- align-items: center;
115
- justify-content: center;
116
- font-size: 1.2rem;
117
- transition: all 0.3s;
118
- }
119
-
120
- .audio-btn:hover {
121
- background: rgba(255, 107, 157, 0.2);
122
- border-color: var(--accent);
123
- }
124
-
125
- .audio-btn.active {
126
- background: rgba(5, 217, 232, 0.2);
127
- border-color: var(--accent2);
128
- }
129
-
130
- /* === MAIN CONTAINER === */
131
- #game-container {
132
- max-width: 900px;
133
- margin: 0 auto;
134
- padding: 20px;
135
- opacity: 0;
136
- transition: opacity 0.5s;
137
- }
138
-
139
- #game-container.active {
140
- opacity: 1;
141
- }
142
-
143
- /* === TOP STATS BAR === */
144
- #stats-bar {
145
- display: grid;
146
- grid-template-columns: repeat(4, 1fr);
147
- gap: 15px;
148
- margin-bottom: 30px;
149
- padding: 15px;
150
- background: var(--surface);
151
- border-radius: 12px;
152
- border: 1px solid rgba(255, 255, 255, 0.05);
153
- }
154
-
155
- .stat-item {
156
- text-align: center;
157
- }
158
-
159
- .stat-icon {
160
- font-size: 1.5rem;
161
- margin-bottom: 5px;
162
- }
163
-
164
- .stat-value {
165
- font-family: 'Space Mono', monospace;
166
- font-size: 1.2rem;
167
- color: var(--accent2);
168
- }
169
-
170
- .stat-label {
171
- font-size: 0.75rem;
172
- color: var(--muted);
173
- text-transform: uppercase;
174
- letter-spacing: 1px;
175
- }
176
-
177
- /* === SCENE IMAGE === */
178
- #scene-visual {
179
- position: relative;
180
- width: 100%;
181
- aspect-ratio: 16/9;
182
- border-radius: 12px;
183
- overflow: hidden;
184
- margin-bottom: 30px;
185
- background: linear-gradient(135deg, #1a1a2e, #0a0a15);
186
- }
187
-
188
- #scene-image {
189
- width: 100%;
190
- height: 100%;
191
- object-fit: cover;
192
- opacity: 0;
193
- transition: opacity 0.5s;
194
- }
195
-
196
- #scene-image.loaded {
197
- opacity: 1;
198
- }
199
-
200
- #image-loading {
201
- position: absolute;
202
  inset: 0;
203
- display: flex;
204
- flex-direction: column;
205
- align-items: center;
206
- justify-content: center;
207
- background: linear-gradient(135deg, #1a1a2e, #0a0a15);
208
- }
209
-
210
- #image-loading.hidden {
211
- display: none;
212
- }
213
-
214
- .image-spinner {
215
- width: 40px;
216
- height: 40px;
217
- border: 2px solid rgba(255, 255, 255, 0.1);
218
- border-top-color: var(--accent);
219
- border-radius: 50%;
220
- animation: spin 1s linear infinite;
221
- }
222
-
223
- @keyframes spin {
224
- to {
225
- transform: rotate(360deg);
226
- }
227
  }
228
 
229
- .image-loading-text {
230
- margin-top: 15px;
231
- color: var(--muted);
232
- font-size: 0.9rem;
 
 
233
  }
234
 
235
- #scene-location {
 
236
  position: absolute;
237
- top: 15px;
238
- left: 15px;
239
- background: rgba(0, 0, 0, 0.7);
240
- padding: 8px 15px;
241
- border-radius: 20px;
242
- font-family: 'Space Mono', monospace;
243
- font-size: 0.75rem;
244
- color: var(--accent2);
245
- backdrop-filter: blur(10px);
246
  }
247
 
248
- #scene-time {
249
  position: absolute;
250
- top: 15px;
251
- right: 15px;
252
- background: rgba(0, 0, 0, 0.7);
253
- padding: 8px 15px;
254
- border-radius: 20px;
255
- font-family: 'Space Mono', monospace;
256
- font-size: 0.75rem;
257
- color: var(--gold);
258
- backdrop-filter: blur(10px);
259
- }
260
-
261
- /* === NARRATIVE === */
262
- #narrative {
263
- font-size: 1.25rem;
264
- margin-bottom: 30px;
265
- padding: 30px;
266
- background: var(--surface);
267
- border-radius: 12px;
268
- border-left: 3px solid var(--gold);
269
- }
270
-
271
- #narrative p {
272
- margin-bottom: 1.2em;
273
- text-indent: 2em;
274
- }
275
-
276
- #narrative p:first-child::first-letter {
277
- font-size: 3.5rem;
278
- float: left;
279
- line-height: 1;
280
- margin-right: 10px;
281
- color: var(--gold);
282
- font-weight: 600;
283
- }
284
-
285
- .thought {
286
- font-style: italic;
287
- color: var(--accent);
288
- }
289
-
290
- .important {
291
- color: var(--accent2);
292
- font-weight: 600;
293
  }
294
 
295
- .dialogue {
296
- color: var(--gold);
297
  }
298
 
299
- /* === CHARACTER SPEECH === */
300
- .character-speech {
 
301
  display: flex;
302
- gap: 20px;
303
- padding: 25px;
304
- background: linear-gradient(135deg, rgba(212, 165, 116, 0.1), transparent);
305
- border-radius: 12px;
306
- margin-bottom: 30px;
307
- border: 1px solid rgba(212, 165, 116, 0.2);
308
  }
309
 
310
- .character-avatar {
311
- width: 60px;
312
- height: 60px;
313
- border-radius: 50%;
314
- background: var(--surface);
315
  display: flex;
316
  align-items: center;
317
- justify-content: center;
318
- font-size: 2rem;
319
- flex-shrink: 0;
320
- border: 2px solid var(--gold);
321
- }
322
-
323
- .character-name {
324
- font-family: 'Space Mono', monospace;
325
- font-size: 0.85rem;
326
- color: var(--gold);
327
- margin-bottom: 8px;
328
- text-transform: uppercase;
329
- letter-spacing: 1px;
330
- }
331
-
332
- .character-text {
333
- font-size: 1.1rem;
334
- font-style: italic;
335
- }
336
-
337
- /* === CHOICES === */
338
- #choices-container {
339
- margin-bottom: 30px;
340
- }
341
-
342
- .choices-title {
343
- font-family: 'Space Mono', monospace;
344
- font-size: 0.8rem;
345
- color: var(--muted);
346
- margin-bottom: 15px;
347
- text-transform: uppercase;
348
- letter-spacing: 2px;
349
- }
350
-
351
- .choice-btn {
352
- display: block;
353
- width: 100%;
354
- padding: 20px 25px;
355
- margin-bottom: 12px;
356
- background: var(--surface);
357
- border: 1px solid rgba(255, 255, 255, 0.1);
358
- border-radius: 10px;
359
- color: var(--text);
360
- text-align: left;
361
- font-family: 'Cormorant Garamond', serif;
362
- font-size: 1.1rem;
363
- cursor: pointer;
364
- transition: all 0.3s;
365
- position: relative;
366
- }
367
-
368
- .choice-btn:hover {
369
- background: rgba(255, 107, 157, 0.1);
370
- border-color: var(--accent);
371
- transform: translateX(10px);
372
- }
373
-
374
- .choice-consequence {
375
- display: block;
376
- font-size: 0.85rem;
377
- color: var(--muted);
378
- margin-top: 8px;
379
- font-style: italic;
380
- }
381
-
382
- .choice-stats {
383
- position: absolute;
384
- right: 20px;
385
- top: 50%;
386
- transform: translateY(-50%);
387
- display: flex;
388
  gap: 10px;
 
389
  }
390
 
391
- .stat-change {
392
- font-family: 'Space Mono', monospace;
393
- font-size: 0.75rem;
394
- padding: 4px 8px;
395
- border-radius: 4px;
396
- }
397
-
398
- .stat-change.positive {
399
- background: rgba(46, 213, 115, 0.2);
400
- color: var(--success);
401
- }
402
-
403
- .stat-change.negative {
404
- background: rgba(255, 71, 87, 0.2);
405
- color: var(--danger);
406
  }
407
 
408
- /* === DAY COUNTER === */
409
- #day-display {
410
- text-align: center;
411
  padding: 15px;
412
- margin-bottom: 20px;
413
- font-family: 'Space Mono', monospace;
414
- }
415
-
416
- #day-number {
417
- font-size: 2rem;
418
- color: var(--accent2);
419
- }
420
-
421
- #day-label {
422
- color: var(--muted);
423
- font-size: 0.8rem;
424
- }
425
-
426
- /* === MINI GAMES POPUP === */
427
- .minigame-modal {
428
- position: fixed;
429
- inset: 0;
430
- background: rgba(0, 0, 0, 0.9);
431
- display: none;
432
- align-items: center;
433
- justify-content: center;
434
- z-index: 200;
435
- }
436
-
437
- .minigame-modal.active {
438
  display: flex;
 
 
 
 
439
  }
440
 
441
- .minigame-content {
442
- background: var(--surface);
443
- border-radius: 15px;
444
- padding: 40px;
445
- max-width: 500px;
446
- text-align: center;
447
- }
448
-
449
- /* === NOTIFICATION === */
450
- #notification {
451
- position: fixed;
452
- bottom: 30px;
453
- left: 50%;
454
- transform: translateX(-50%) translateY(100px);
455
- background: var(--surface);
456
- padding: 15px 30px;
457
- border-radius: 10px;
458
- border-left: 3px solid var(--accent2);
459
- opacity: 0;
460
- transition: all 0.3s;
461
- z-index: 150;
462
- }
463
-
464
- #notification.show {
465
- transform: translateX(-50%) translateY(0);
466
- opacity: 1;
467
- }
468
-
469
- /* === AMBIENT PARTICLES === */
470
- #particles {
471
- position: fixed;
472
- inset: 0;
473
- pointer-events: none;
474
- z-index: 1;
475
- }
476
-
477
- .particle {
478
- position: absolute;
479
- width: 2px;
480
- height: 2px;
481
- background: var(--accent);
482
- border-radius: 50%;
483
- opacity: 0.3;
484
- animation: floatUp 20s linear infinite;
485
- }
486
-
487
- @keyframes floatUp {
488
- 0% {
489
- transform: translateY(100vh) rotate(0deg);
490
- opacity: 0;
491
- }
492
-
493
- 10% {
494
- opacity: 0.3;
495
- }
496
-
497
- 90% {
498
- opacity: 0.3;
499
- }
500
-
501
- 100% {
502
- transform: translateY(-100vh) rotate(720deg);
503
- opacity: 0;
504
- }
505
- }
506
-
507
- /* === SCANLINES === */
508
- .scanlines {
509
- position: fixed;
510
- inset: 0;
511
- background: repeating-linear-gradient(0deg,
512
- transparent,
513
- transparent 2px,
514
- rgba(0, 0, 0, 0.1) 2px,
515
- rgba(0, 0, 0, 0.1) 4px);
516
- pointer-events: none;
517
- z-index: 1000;
518
- opacity: 0.3;
519
- }
520
-
521
- /* === STREAK REWARD === */
522
- #streak-display {
523
- position: fixed;
524
- top: 80px;
525
- right: 20px;
526
- background: var(--surface);
527
- padding: 10px 15px;
528
  border-radius: 8px;
529
- font-family: 'Space Mono', monospace;
530
- font-size: 0.8rem;
531
- border: 1px solid rgba(255, 255, 255, 0.1);
 
532
  }
533
 
534
- .streak-fire {
535
- color: #ff6b2c;
536
- }
537
-
538
- /* === WEATHER EFFECTS === */
539
- #weather-container {
540
- position: fixed;
541
- inset: 0;
542
- pointer-events: none;
543
- z-index: 2;
544
  }
545
 
546
- .rain {
547
- position: absolute;
548
- width: 2px;
549
- height: 20px;
550
- background: linear-gradient(transparent, rgba(174, 194, 224, 0.6));
551
- animation: rain-fall linear infinite;
552
  }
553
 
554
- @keyframes rain-fall {
555
- 0% {
556
- transform: translateY(-100vh);
557
- }
558
-
559
- 100% {
560
- transform: translateY(100vh);
561
- }
562
  }
563
 
564
- .lightning {
565
- position: fixed;
566
- inset: 0;
567
  background: white;
568
- opacity: 0;
569
- pointer-events: none;
570
- z-index: 999;
 
 
 
 
 
571
  }
572
 
573
- .lightning.flash {
574
- animation: lightning-flash 0.2s;
575
  }
576
 
577
- @keyframes lightning-flash {
578
-
579
- 0%,
580
- 100% {
581
  opacity: 0;
 
582
  }
583
 
584
- 10%,
585
- 30% {
586
- opacity: 0.8;
587
- }
588
-
589
- 20% {
590
- opacity: 0.3;
591
  }
592
  }
593
 
594
- /* === ACHIEVEMENTS === */
595
- #achievement-popup {
596
  position: fixed;
597
- top: -100px;
598
- left: 50%;
599
- transform: translateX(-50%);
600
- background: linear-gradient(135deg, rgba(212, 165, 116, 0.95), rgba(180, 130, 80, 0.95));
601
- padding: 20px 40px;
602
- border-radius: 15px;
603
- color: #1a1a1a;
604
  text-align: center;
605
- z-index: 300;
606
- transition: top 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
607
- box-shadow: 0 10px 40px rgba(212, 165, 116, 0.5);
608
- }
609
-
610
- #achievement-popup.show {
611
- top: 100px;
612
  }
613
 
614
- .achievement-icon {
615
  font-size: 3rem;
 
 
616
  margin-bottom: 10px;
617
  }
618
 
619
- .achievement-title {
620
- font-family: 'Space Mono', monospace;
621
- font-size: 0.8rem;
622
- text-transform: uppercase;
623
- letter-spacing: 2px;
624
- margin-bottom: 5px;
625
- }
626
-
627
- .achievement-name {
628
- font-size: 1.3rem;
629
- font-weight: 600;
630
- }
631
-
632
- /* === RELATIONSHIP BAR === */
633
- #relationships-bar {
634
- display: flex;
635
- gap: 10px;
636
- margin-bottom: 20px;
637
- padding: 10px;
638
- background: var(--surface);
639
- border-radius: 8px;
640
- overflow-x: auto;
641
- }
642
-
643
- .rel-chip {
644
- display: flex;
645
- align-items: center;
646
- gap: 5px;
647
- padding: 5px 12px;
648
- background: rgba(255, 255, 255, 0.05);
649
- border-radius: 20px;
650
- font-size: 0.8rem;
651
- white-space: nowrap;
652
- }
653
-
654
- .rel-chip .avatar {
655
- font-size: 1.2rem;
656
- }
657
-
658
- .rel-chip .level {
659
- font-family: 'Space Mono', monospace;
660
- color: var(--accent2);
661
- }
662
-
663
- /* === WEATHER INDICATOR === */
664
- #weather-indicator {
665
- position: fixed;
666
- top: 140px;
667
- right: 20px;
668
- background: var(--surface);
669
- padding: 8px 12px;
670
- border-radius: 8px;
671
- font-size: 1.5rem;
672
- border: 1px solid rgba(255, 255, 255, 0.1);
673
- }
674
-
675
- /* === RESPONSIVE === */
676
- @media (max-width: 768px) {
677
- #stats-bar {
678
- grid-template-columns: repeat(2, 1fr);
679
- }
680
-
681
- #narrative {
682
- font-size: 1.1rem;
683
- padding: 20px;
684
- }
685
-
686
- .choice-stats {
687
- display: none;
688
- }
689
-
690
- #relationships-bar {
691
- flex-wrap: nowrap;
692
- }
693
- }
694
-
695
- /* === CHOICE ANIMATION === */
696
- @keyframes fadeInUp {
697
- from {
698
- opacity: 0;
699
- transform: translateY(20px);
700
- }
701
-
702
- to {
703
- opacity: 1;
704
- transform: translateY(0);
705
- }
706
  }
707
  </style>
708
  </head>
709
 
710
  <body>
711
- <!-- Ambient Effects -->
712
- <div id="particles"></div>
713
- <div id="weather-container"></div>
714
- <div class="lightning" id="lightning"></div>
715
- <div class="scanlines"></div>
716
-
717
- <!-- Audio Controls -->
718
- <div id="audio-controls">
719
- <button class="audio-btn" id="music-btn" title="Música">🎵</button>
720
- <button class="audio-btn" id="sfx-btn" title="Efectos">🔊</button>
721
- </div>
722
-
723
- <!-- Streak Display -->
724
- <div id="streak-display">
725
- <span class="streak-fire">🔥</span> Racha: <span id="streak-count">0</span> días
726
- </div>
727
 
728
- <!-- Weather Indicator -->
729
- <div id="weather-indicator">☀️</div>
730
 
731
- <!-- Achievement Popup -->
732
- <div id="achievement-popup">
733
- <div class="achievement-icon">🏆</div>
734
- <div class="achievement-title">LOGRO DESBLOQUEADO</div>
735
- <div class="achievement-name" id="achievement-name">Nombre del logro</div>
736
  </div>
737
 
738
- <!-- Loading Screen -->
739
  <div id="loading-screen">
740
- <div class="loading-quote">
741
- <span id="quote-text">"No te rindas, aún estás a tiempo de alcanzar y comenzar de nuevo."</span>
742
- <span class="loading-author" id="quote-author">— Mario Benedetti</span>
743
- </div>
744
- <div class="loading-bar">
745
- <div class="loading-progress"></div>
746
  </div>
747
  </div>
748
 
749
- <!-- Main Game Container -->
750
- <div id="game-container">
751
- <!-- Day Display -->
752
- <div id="day-display">
753
- <div id="day-number">DÍA 1</div>
754
- <div id="day-label">BUENOS AIRES 2077</div>
755
- </div>
756
-
757
- <!-- Relationships Bar -->
758
- <div id="relationships-bar"></div>
759
-
760
- <!-- Stats Bar -->
761
- <div id="stats-bar">
762
- <div class="stat-item">
763
- <div class="stat-icon">✊</div>
764
- <div class="stat-value" id="stat-fuerza">50</div>
765
- <div class="stat-label">Fuerza</div>
766
- </div>
767
- <div class="stat-item">
768
- <div class="stat-icon">🧠</div>
769
- <div class="stat-value" id="stat-mente">50</div>
770
- <div class="stat-label">Mente</div>
771
- </div>
772
- <div class="stat-item">
773
- <div class="stat-icon">💰</div>
774
- <div class="stat-value" id="stat-plata">150</div>
775
- <div class="stat-label">Plata</div>
776
- </div>
777
- <div class="stat-item">
778
- <div class="stat-icon">❤️</div>
779
- <div class="stat-value" id="stat-karma">50</div>
780
- <div class="stat-label">Karma</div>
781
- </div>
782
- </div>
783
-
784
- <!-- Scene Visual -->
785
- <div id="scene-visual">
786
- <div id="image-loading">
787
- <div class="image-spinner"></div>
788
- <div class="image-loading-text">Generando escena...</div>
789
- </div>
790
- <img id="scene-image" alt="Escena">
791
- <div id="scene-location">VILLA 31 · RETIRO</div>
792
- <div id="scene-time">6:47 AM</div>
793
- </div>
794
-
795
- <!-- Narrative -->
796
- <div id="narrative">
797
- <p>Cargando historia...</p>
798
- </div>
799
-
800
- <!-- Character Speech -->
801
- <div id="character-speech" class="character-speech" style="display: none;">
802
- <div class="character-avatar" id="speech-avatar">👤</div>
803
- <div class="character-content">
804
- <div class="character-name" id="speech-name">Personaje</div>
805
- <div class="character-text" id="speech-text">Diálogo</div>
806
  </div>
807
  </div>
808
-
809
- <!-- Choices -->
810
- <div id="choices-container">
811
- <div class="choices-title">¿Qué hacés?</div>
812
- <div id="choices"></div>
813
- </div>
814
  </div>
815
 
816
- <!-- Notification -->
817
- <div id="notification">
818
- <div id="notif-text">Notificación</div>
819
- </div>
820
-
821
- <!-- Minigame Modal -->
822
- <div class="minigame-modal" id="minigame-modal">
823
- <div class="minigame-content" id="minigame-content">
824
- <!-- Minigame content loaded dynamically -->
825
- </div>
826
- </div>
827
-
828
- <!-- Audio Elements -->
829
- <audio id="ambient-music" loop>
830
- <source src="https://assets.mixkit.co/music/preview/mixkit-hip-hop-02-621.mp3" type="audio/mpeg">
831
- </audio>
832
- <audio id="click-sfx">
833
- <source src="https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3" type="audio/mpeg">
834
- </audio>
835
-
836
- <script>
837
- // ========================================
838
- // RENACER - Procedural Narrative Engine
839
- // ========================================
840
-
841
- // Locations with atmosphere
842
- const LOCATIONS = [
843
- { name: "VILLA 31 · RETIRO", mood: "slum", time: "morning" },
844
- { name: "PUERTO MADERO · DÁRSENA", mood: "corporate", time: "day" },
845
- { name: "LA BOCA · CAMINITO", mood: "artistic", time: "sunset" },
846
- { name: "SAN TELMO · MERCADO", mood: "traditional", time: "morning" },
847
- { name: "PALERMO SOHO · PLAZA", mood: "hipster", time: "night" },
848
- { name: "CONSTITUCIÓN · ESTACIÓN", mood: "transit", time: "rush" },
849
- { name: "ONCE · BALVANERA", mood: "commercial", time: "day" },
850
- { name: "MICROCENTRO · FLORIDA", mood: "crowded", time: "afternoon" }
851
- ];
852
-
853
- // Characters with personalities
854
- const CHARACTERS = [
855
- { name: "Mamá Rosa", avatar: "👩", type: "family", traits: ["wise", "worried", "loving"] },
856
- { name: "El Tano", avatar: "👦", type: "friend", traits: ["streetwise", "loyal", "scarred"] },
857
- { name: "Don Carmelo", avatar: "🧔", type: "mentor", traits: ["cryptic", "connected", "fatherly"] },
858
- { name: "Lucía", avatar: "👩‍💻", type: "ally", traits: ["smart", "cynical", "beautiful"] },
859
- { name: "El Rata", avatar: "🐀", type: "danger", traits: ["sneaky", "dangerous", "informant"] },
860
- { name: "La Doctora", avatar: "👩‍⚕️", type: "help", traits: ["caring", "overworked", "kind"] },
861
- { name: "Tío Raúl", avatar: "👷", type: "family", traits: ["honest", "tired", "protective"] },
862
- { name: "Unknown Stranger", avatar: "🕶️", type: "mystery", traits: ["enigmatic", "powerful", "unknown"] }
863
- ];
864
-
865
- // STORY ARCS - Coherent narrative with continuity
866
- const STORY_ARCS = {
867
- // Arc 1: The Beginning
868
- despertar: {
869
- scenes: [
870
- {
871
- id: 'wake_up',
872
- narrative: `<p>El sol entra por las rendijas de la chapa. Son las seis de la mañana y ya hace calor en la villa. Tu madre se levantó antes, como siempre. El olor a mate con leche sube desde la cocina.</p>
873
- <p>Hoy es diferente. Algo en el aire, en el silencio de afuera, te dice que las cosas van a cambiar. <span class="thought">¿Pero para bien o para mal?</span></p>`,
874
- image_prompt: "morning light through corrugated metal roof, Buenos Aires slum interior, young man waking up, cinematic, atmospheric",
875
- choices: [
876
- { text: "Bajar a desayunar con mamá", next: 'mama_breakfast', effect: { karma: 5 }, flag: 'talked_to_mama' },
877
- { text: "Saltear el desayuno y salir temprano", next: 'early_street', effect: { fuerza: 5 }, flag: 'skipped_breakfast' }
878
- ]
879
- },
880
- {
881
- id: 'mama_breakfast',
882
- narrative: `<p>Tu madre te espera con el mate listo. Tiene esa mirada que conocés bien: la de cuando quiere decir algo importante pero no sabe cómo.</p>
883
- <p>"Hijo", dice finalmente. "Don Carmelo preguntó por vos ayer. Dice que tiene algo. Un trabajo."</p>
884
- <p>El silencio que sigue pesa más que las palabras. Los dos saben lo que significa "un trabajo" en boca de Don Carmelo.</p>`,
885
- image_prompt: "humble kitchen in Buenos Aires slum, mother and son having mate, morning light, emotional, intimate",
886
- choices: [
887
- { text: '"¿Qué clase de trabajo?"', next: 'mama_explains', effect: { mente: 5 }, flag: 'asked_about_job' },
888
- { text: "Quedarte callado y escuchar", next: 'mama_continues', effect: { karma: 5 } },
889
- { text: '"No me interesa, ma"', next: 'refuse_early', effect: { karma: 10, plata: -20 }, flag: 'refused_carmelo' }
890
- ]
891
- },
892
- {
893
- id: 'early_street',
894
- narrative: `<p>El pasillo de la villa ya está vivo a esta hora. Pibes yendo a la escuela, madres cargando bidones de agua, el viejo del carrito de verduras gritando los precios del día.</p>
895
- <p>En la esquina ves a El Tano fumando. Te hace una seña con la cabeza. Algo quiere.</p>`,
896
- image_prompt: "Buenos Aires slum alley morning, people walking, street vendor, young man with cigarette in corner, cyberpunk elements",
897
- choices: [
898
- { text: "Acercarte al Tano", next: 'tano_info', effect: { street: 5 }, flag: 'talked_to_tano' },
899
- { text: "Seguir hacia el kiosco de Carmelo", next: 'kiosco_direct', effect: { fuerza: 3 } },
900
- { text: "Volver a casa primero", next: 'mama_breakfast', effect: { karma: 3 } }
901
- ]
902
- }
903
- ]
904
- },
905
- // Arc 2: The Job Offer
906
- trabajo: {
907
- scenes: [
908
- {
909
- id: 'mama_explains',
910
- narrative: `<p>Tu madre baja la voz, aunque estén solos. Vieja costumbre de la villa: las paredes escuchan.</p>
911
- <p>"Es para llevar un sobre a Palermo. Nada más. 500 pesos."</p>
912
- <p>500 pesos. Lo que ella gana en una semana limpiando casas. Por llevar un sobre.</p>
913
- <p><span class="important">Las cosas fáciles nunca son fáciles de verdad.</span></p>`,
914
- image_prompt: "close up of worried mother face, humble kitchen, dramatic lighting, emotional conversation",
915
- choices: [
916
- { text: '"Voy a hablar con Carmelo"', next: 'kiosco_informed', effect: { mente: 5 }, flag: 'knows_about_job' },
917
- { text: '"¿Y si es peligroso?"', next: 'mama_warns', effect: { mente: 10 } },
918
- { text: '"500 pesos es mucha plata..."', next: 'tempted', effect: { plata: 10 }, flag: 'tempted_by_money' }
919
- ]
920
- },
921
- {
922
- id: 'mama_continues',
923
- narrative: `<p>Ella sigue hablando, más para sí misma que para vos. "Tu padre también empezó así. Un trabajo fácil, decían. Llevá esto de acá para allá."</p>
924
- <p>Se le quiebra la voz. No hace falta que termine. Los dos saben cómo terminó tu viejo.</p>
925
- <p>Pero también saben que la medicina de ella se está acabando. Y la plata no alcanza.</p>`,
926
- image_prompt: "elderly woman crying softly, kitchen table, mate cup, morning light through window, emotional",
927
- choices: [
928
- { text: "Abrazarla sin decir nada", next: 'comfort_mama', effect: { karma: 15 }, flag: 'comforted_mama' },
929
- { text: '"Voy a cuidarme, ma. Te lo prometo"', next: 'promise_made', effect: { karma: 10, fuerza: 5 }, flag: 'promised_mama' },
930
- { text: '"Necesitamos esa plata"', next: 'reality_check', effect: { mente: 5, karma: -5 } }
931
- ]
932
- },
933
- {
934
- id: 'tano_info',
935
- narrative: `<p>El Tano te mira de costado mientras tira el pucho. "Che, ¿sabías que Carmelo está moviendo cosas otra vez?"</p>
936
- <p>Se acerca, baja la voz. "Ayer llegaron dos tipos en un auto negro. Hablaron con él una hora. Después se fue a tu casa."</p>
937
- <p>El Tano sabe todo lo que pasa en la villa. Si él te está contando esto, es porque quiere que sepas.</p>`,
938
- image_prompt: "two young men talking secretively in slum alley, morning shadows, cyberpunk Buenos Aires, tense atmosphere",
939
- choices: [
940
- { text: '"¿Qué tipo de cosas?"', next: 'tano_details', effect: { mente: 10, street: 5 }, flag: 'tano_told_truth' },
941
- { text: '"¿Por qué me contás esto?"', next: 'tano_motives', effect: { mente: 15 } },
942
- { text: "Agradecerle e ir a ver a Carmelo", next: 'kiosco_warned', effect: { fuerza: 5 }, flag: 'warned_by_tano' }
943
- ]
944
- }
945
- ]
946
- },
947
- // Arc 3: The Kiosco
948
- kiosco: {
949
- scenes: [
950
- {
951
- id: 'kiosco_informed',
952
- requires: 'knows_about_job',
953
- narrative: `<p>El kiosco de Don Carmelo está donde siempre, entre el pasaje tres y la cancha de fútbol. La persiana a medio abrir, como invitando solo a los que saben.</p>
954
- <p>Cuando entrás, Carmelo levanta la vista. Su ojo mecánico brilla un segundo antes de reconocerte.</p>
955
- <p>"Ah, Martín. Tu madre te contó." No es una pregunta.</p>`,
956
- image_prompt: "small kiosk shop in Buenos Aires slum, old man with cybernetic eye behind counter, morning light, noir atmosphere",
957
- choices: [
958
- { text: '"Quiero saber todo antes de aceptar"', next: 'carmelo_explains_full', effect: { mente: 10 } },
959
- { text: '"Estoy adentro. ¿Qué hay que hacer?"', next: 'accept_job', effect: { fuerza: 10, karma: -10 }, flag: 'accepted_blindly' },
960
- { text: '"Mi vieja está preocupada"', next: 'carmelo_reassures', effect: { karma: 5 } }
961
- ]
962
- },
963
- {
964
- id: 'kiosco_warned',
965
- requires: 'warned_by_tano',
966
- narrative: `<p>Entrás al kiosco con los ojos más abiertos que de costumbre. Lo que te dijo El Tano te da vueltas en la cabeza.</p>
967
- <p>Carmelo levanta la vista. Por un segundo, algo cruza su cara. <span class="thought">¿Sorpresa? ¿Preocupación?</span></p>
968
- <p>"Martín. Llegás temprano hoy."</p>`,
969
- image_prompt: "kiosk shop Buenos Aires slum, old man looking surprised, young man entering, tense atmosphere, cyberpunk",
970
- choices: [
971
- { text: '"¿Quiénes eran los del auto negro?"', next: 'confront_carmelo', effect: { mente: 15, fuerza: 5 }, flag: 'confronted_carmelo' },
972
- { text: "Hacerte el tonto y ver qué dice", next: 'play_dumb', effect: { mente: 10 } },
973
- { text: '"El Tano me contó todo"', next: 'reveal_source', effect: { karma: -5 }, flag: 'betrayed_tano' }
974
- ]
975
- },
976
- {
977
- id: 'kiosco_direct',
978
- narrative: `<p>El kiosco de Don Carmelo parece dormido todavía. La persiana está más baja de lo normal.</p>
979
- <p>Tocás la chapa dos veces, como siempre. Un silencio largo. Después, la voz ronca desde adentro: "Pasá, Martín."</p>
980
- <p>Adentro está más oscuro que de costumbre. Carmelo te espera sentado, con un sobre manila en las manos.</p>`,
981
- image_prompt: "dark interior kiosk shop, old man sitting with manila envelope, mysterious atmosphere, Buenos Aires slum",
982
- choices: [
983
- { text: '"¿Qué es eso?"', next: 'the_envelope', effect: { mente: 5 } },
984
- { text: "Sentarte enfrente sin decir nada", next: 'silent_wait', effect: { fuerza: 5 } },
985
- { text: '"Algo anda mal. Lo noto."', next: 'sense_danger', effect: { mente: 10, fuerza: 5 }, flag: 'sensed_danger' }
986
- ]
987
- }
988
- ]
989
- }
990
- };
991
-
992
- // SCENE CONTINUITY - Track what happened
993
- const FLAGS = new Set();
994
- let currentArc = 'despertar';
995
- let currentSceneId = 'wake_up';
996
- let sceneHistory = [];
997
-
998
- // Find scene by ID across all arcs
999
- function findScene(sceneId) {
1000
- for (const [arcName, arc] of Object.entries(STORY_ARCS)) {
1001
- const scene = arc.scenes.find(s => s.id === sceneId);
1002
- if (scene) return { scene, arcName };
1003
- }
1004
- return null;
1005
- }
1006
-
1007
- // Check if scene requirements are met
1008
- function canAccessScene(scene) {
1009
- if (!scene.requires) return true;
1010
- return FLAGS.has(scene.requires);
1011
- }
1012
-
1013
- // Get current scene
1014
- function getCurrentScene() {
1015
- const result = findScene(currentSceneId);
1016
- if (result && canAccessScene(result.scene)) {
1017
- return result.scene;
1018
- }
1019
- // Fallback to first scene
1020
- return STORY_ARCS.despertar.scenes[0];
1021
- }
1022
-
1023
- // Choice generators now use context
1024
- function generateContextualChoices(scene) {
1025
- // Add context-aware modifications based on flags
1026
- let choices = [...scene.choices];
1027
-
1028
- // Modify choices based on previous decisions
1029
- if (FLAGS.has('warned_by_tano') && !FLAGS.has('confronted_carmelo')) {
1030
- // Add extra caution option
1031
- const cautionChoice = choices.find(c => c.text.includes('cuidado') || c.text.includes('peligro'));
1032
- if (cautionChoice) cautionChoice.effect.mente = (cautionChoice.effect.mente || 0) + 5;
1033
- }
1034
-
1035
- if (FLAGS.has('promised_mama')) {
1036
- // Karma bonus for keeping promise
1037
- choices.forEach(c => {
1038
- if (c.effect.karma && c.effect.karma > 0) c.effect.karma += 5;
1039
- });
1040
- }
1041
-
1042
- return choices;
1043
- }
1044
-
1045
- // Game State
1046
- const STATE = {
1047
- day: 1,
1048
- timeOfDay: 'morning',
1049
- location: LOCATIONS[0],
1050
- stats: { fuerza: 50, mente: 50, plata: 150, karma: 50 },
1051
- streak: 0,
1052
- history: [],
1053
- lastCharacter: null,
1054
- musicEnabled: false,
1055
- sfxEnabled: true,
1056
- weather: 'clear',
1057
- relationships: {
1058
- 'Mamá Rosa': { level: 50, avatar: '👩' },
1059
- 'El Tano': { level: 30, avatar: '👦' },
1060
- 'Don Carmelo': { level: 40, avatar: '🧔' }
1061
- },
1062
- achievements: [],
1063
- totalChoices: 0
1064
- };
1065
-
1066
- // Weather System
1067
- const WEATHER_TYPES = [
1068
- { name: 'clear', icon: '☀️', chance: 0.4 },
1069
- { name: 'cloudy', icon: '☁️', chance: 0.25 },
1070
- { name: 'rain', icon: '🌧️', chance: 0.2 },
1071
- { name: 'storm', icon: '⛈️', chance: 0.1 },
1072
- { name: 'fog', icon: '🌫️', chance: 0.05 }
1073
- ];
1074
-
1075
- // Achievements
1076
- const ACHIEVEMENTS = [
1077
- { id: 'first_choice', name: 'Primer Paso', icon: '��', condition: () => STATE.totalChoices >= 1 },
1078
- { id: 'survivor', name: 'Sobreviviente', icon: '💪', condition: () => STATE.day >= 7 },
1079
- { id: 'wealthy', name: 'Billetera Gorda', icon: '💰', condition: () => STATE.stats.plata >= 500 },
1080
- { id: 'saint', name: 'Santo', icon: '😇', condition: () => STATE.stats.karma >= 90 },
1081
- { id: 'villain', name: 'Villano', icon: '😈', condition: () => STATE.stats.karma <= 10 },
1082
- { id: 'streak_5', name: 'Racha de Fuego', icon: '🔥', condition: () => STATE.streak >= 5 },
1083
- { id: 'streak_10', name: 'Imparable', icon: '⚡', condition: () => STATE.streak >= 10 },
1084
- { id: 'strong', name: 'Fuerza Bruta', icon: '💪', condition: () => STATE.stats.fuerza >= 80 },
1085
- { id: 'genius', name: 'Genio', icon: '🧠', condition: () => STATE.stats.mente >= 80 },
1086
- { id: 'friend', name: 'Buen Amigo', icon: '🤝', condition: () => Object.values(STATE.relationships).some(r => r.level >= 80) },
1087
- { id: 'explorer', name: 'Explorador', icon: '🗺️', condition: () => STATE.history.length >= 20 }
1088
- ];
1089
-
1090
- // More Literary Quotes
1091
- const QUOTES = [
1092
- { text: "No te rindas, aún estás a tiempo de alcanzar y comenzar de nuevo.", author: "Mario Benedetti" },
1093
- { text: "El olvido está lleno de memoria.", author: "Mario Benedetti" },
1094
- { text: "Después de todo, la muerte es sólo un síntoma de que hubo vida.", author: "Mario Benedetti" },
1095
- { text: "Te quiero en mi paraíso, es decir, en mi país de cada día.", author: "Mario Benedetti" },
1096
- { text: "Somos mucho más que dos.", author: "Mario Benedetti" },
1097
- { text: "No te salves, no te llenes de calma, no reserves del mundo sólo un rincón tranquilo.", author: "Mario Benedetti" },
1098
- { text: "Tal vez estamos ciegos. Ciegos de ver.", author: "José Saramago" },
1099
- { text: "Andá a saber si uno es lo que hace o lo que cree que hace.", author: "Julio Cortázar" },
1100
- { text: "Nada se pierde si se tiene el coraje de proclamar que todo está perdido y hay que empezar de nuevo.", author: "Julio Cortázar" },
1101
- { text: "La esperanza no es la convicción de que algo saldrá bien, sino la certeza de que algo tiene sentido.", author: "Václav Havel" },
1102
- { text: "Lo que no nos mata nos hace más fuertes.", author: "Friedrich Nietzsche" },
1103
- { text: "En medio de la dificultad reside la oportunidad.", author: "Albert Einstein" },
1104
- { text: "Hay quienes luchan un día y son buenos, hay quienes luchan muchos días y son muy buenos, pero están los que luchan toda la vida, esos son los imprescindibles.", author: "Bertolt Brecht" },
1105
- { text: "Uno no es lo que es por lo que escribe, sino por lo que ha leído.", author: "Jorge Luis Borges" },
1106
- { text: "El único modo de combatir la peste es la honradez.", author: "Albert Camus" },
1107
- { text: "Si he visto más lejos es porque estoy sentado sobre los hombros de gigantes.", author: "Isaac Newton" },
1108
- { text: "La peor lucha es la que no se hace.", author: "Anónimo" },
1109
- { text: "Donde muere una esperanza, nace otra.", author: "Anónimo argentino" }
1110
- ];
1111
-
1112
- // Create Weather
1113
- function updateWeather() {
1114
- const rand = Math.random();
1115
- let cumulative = 0;
1116
- for (const w of WEATHER_TYPES) {
1117
- cumulative += w.chance;
1118
- if (rand <= cumulative) {
1119
- STATE.weather = w.name;
1120
- document.getElementById('weather-indicator').textContent = w.icon;
1121
- break;
1122
- }
1123
- }
1124
-
1125
- const container = document.getElementById('weather-container');
1126
- container.innerHTML = '';
1127
-
1128
- if (STATE.weather === 'rain' || STATE.weather === 'storm') {
1129
- for (let i = 0; i < 100; i++) {
1130
- const drop = document.createElement('div');
1131
- drop.className = 'rain';
1132
- drop.style.left = Math.random() * 100 + '%';
1133
- drop.style.animationDuration = (0.5 + Math.random() * 0.5) + 's';
1134
- drop.style.animationDelay = Math.random() * 2 + 's';
1135
- container.appendChild(drop);
1136
- }
1137
- }
1138
-
1139
- if (STATE.weather === 'storm') {
1140
- setInterval(() => {
1141
- if (Math.random() > 0.7) {
1142
- const lightning = document.getElementById('lightning');
1143
- lightning.classList.add('flash');
1144
- setTimeout(() => lightning.classList.remove('flash'), 200);
1145
- }
1146
- }, 3000);
1147
- }
1148
- }
1149
-
1150
- // Check Achievements
1151
- function checkAchievements() {
1152
- for (const ach of ACHIEVEMENTS) {
1153
- if (!STATE.achievements.includes(ach.id) && ach.condition()) {
1154
- STATE.achievements.push(ach.id);
1155
- showAchievement(ach);
1156
- }
1157
- }
1158
- }
1159
-
1160
- function showAchievement(ach) {
1161
- const popup = document.getElementById('achievement-popup');
1162
- popup.querySelector('.achievement-icon').textContent = ach.icon;
1163
- document.getElementById('achievement-name').textContent = ach.name;
1164
- popup.classList.add('show');
1165
- setTimeout(() => popup.classList.remove('show'), 4000);
1166
- }
1167
-
1168
- // Update Relationships Display
1169
- function updateRelationships() {
1170
- const bar = document.getElementById('relationships-bar');
1171
- bar.innerHTML = Object.entries(STATE.relationships).map(([name, data]) => `
1172
- <div class="rel-chip">
1173
- <span class="avatar">${data.avatar}</span>
1174
- <span class="name">${name.split(' ')[0]}</span>
1175
- <span class="level">${data.level}</span>
1176
- </div>
1177
- `).join('');
1178
- }
1179
-
1180
- // Initialize random quote
1181
- function setRandomQuote() {
1182
- const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
1183
- document.getElementById('quote-text').textContent = `"${quote.text}"`;
1184
- document.getElementById('quote-author').textContent = `— ${quote.author}`;
1185
- }
1186
-
1187
- // Create particles
1188
- function createParticles() {
1189
- const container = document.getElementById('particles');
1190
- for (let i = 0; i < 25; i++) {
1191
- const p = document.createElement('div');
1192
- p.className = 'particle';
1193
- p.style.left = Math.random() * 100 + '%';
1194
- p.style.animationDuration = (15 + Math.random() * 20) + 's';
1195
- p.style.animationDelay = Math.random() * 20 + 's';
1196
- container.appendChild(p);
1197
- }
1198
- }
1199
-
1200
- // Generate unique narrative
1201
- function generateNarrative() {
1202
- const timeEvents = EVENT_TEMPLATES[STATE.timeOfDay] || EVENT_TEMPLATES.morning;
1203
- const eventType = timeEvents[Math.floor(Math.random() * timeEvents.length)];
1204
- const situation = eventType.situations[Math.floor(Math.random() * eventType.situations.length)];
1205
-
1206
- const styles = Object.keys(NARRATIVE_STYLES);
1207
- const style = styles[Math.floor(Math.random() * styles.length)];
1208
-
1209
- return NARRATIVE_STYLES[style](situation, STATE.location);
1210
- }
1211
-
1212
- // Generate unique choices
1213
- function generateChoices() {
1214
- const types = Object.keys(CHOICE_GENERATORS);
1215
- const type = types[Math.floor(Math.random() * types.length)];
1216
- return CHOICE_GENERATORS[type]();
1217
- }
1218
-
1219
- // Generate character dialogue
1220
- function generateCharacterMoment() {
1221
- // 40% chance of character appearing
1222
- if (Math.random() > 0.6) {
1223
- const availableChars = CHARACTERS.filter(c => c !== STATE.lastCharacter);
1224
- const char = availableChars[Math.floor(Math.random() * availableChars.length)];
1225
- STATE.lastCharacter = char;
1226
-
1227
- const dialogues = {
1228
- family: [
1229
- "Cuidate, ¿me escuchás? Cuidate.",
1230
- "Vos sabés que siempre vas a tener un lugar acá.",
1231
- "Tu viejo estaría orgulloso. O preocupado. Tal vez las dos cosas."
1232
- ],
1233
- friend: [
1234
- "Che, ¿estás bien? Te noto raro.",
1235
- "Mirá, yo no te voy a juzgar. Pero tené cuidado.",
1236
- "La calle habla, ¿viste? Y últimamente habla de vos."
1237
- ],
1238
- mentor: [
1239
- "Las decisiones que tomes hoy van a resonar mañana.",
1240
- "Yo vi mucho, pibe. Más de lo que quisiera. Aprendé de mis errores.",
1241
- "La paciencia es más peligrosa que la fuerza, si sabés usarla."
1242
- ],
1243
- ally: [
1244
- "Tengo información. Pero te va a costar.",
1245
- "No confíes en nadie. Excepto en mí. Tal vez.",
1246
- "Las cosas se están moviendo. Tenés que elegir un lado."
1247
- ],
1248
- danger: [
1249
- "Qué sorpresa verte por acá. ¿O no es sorpresa?",
1250
- "Dicen que sabés cosas. Cosas que valen plata.",
1251
- "Todos tenemos un precio. El tuyo, ¿cuál es?"
1252
- ],
1253
- help: [
1254
- "Vengo viendo mucha gente lastimada. No quiero verte a vos.",
1255
- "La villa necesita gente como vos. Gente que piensa.",
1256
- "¿Estás durmiendo bien? Se te nota en la cara."
1257
- ],
1258
- mystery: [
1259
- "No nos conocemos. Pero yo te conozco a vos.",
1260
- "Hay una propuesta. Pensala bien antes de responder.",
1261
- "El juego cambió. ¿Estás listo para jugar?"
1262
- ]
1263
- };
1264
-
1265
- const typeDialogues = dialogues[char.type] || dialogues.mystery;
1266
- const text = typeDialogues[Math.floor(Math.random() * typeDialogues.length)];
1267
-
1268
- return { ...char, text };
1269
- }
1270
- return null;
1271
- }
1272
-
1273
- // Generate scene image using Pollinations.ai (free, no key needed)
1274
- let currentImagePrompt = '';
1275
-
1276
- async function generateSceneImage(prompt) {
1277
- const imageEl = document.getElementById('scene-image');
1278
- const loadingEl = document.getElementById('image-loading');
1279
-
1280
- imageEl.classList.remove('loaded');
1281
- loadingEl.classList.remove('hidden');
1282
-
1283
- currentImagePrompt = prompt || 'cyberpunk Buenos Aires slum, cinematic, atmospheric';
1284
-
1285
- // Use Pollinations.ai - free AI image generation
1286
- const encodedPrompt = encodeURIComponent(currentImagePrompt);
1287
- const seed = Math.floor(Math.random() * 999999); // Random seed for variety
1288
- const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=800&height=450&seed=${seed}&nologo=true`;
1289
-
1290
- imageEl.src = imageUrl;
1291
-
1292
- imageEl.onload = () => {
1293
- imageEl.classList.add('loaded');
1294
- loadingEl.classList.add('hidden');
1295
- };
1296
-
1297
- imageEl.onerror = () => {
1298
- // Fallback to SVG
1299
- imageEl.src = generateFallbackImage();
1300
- imageEl.classList.add('loaded');
1301
- loadingEl.classList.add('hidden');
1302
- };
1303
- }
1304
-
1305
- // Generate fallback SVG image
1306
- function generateFallbackImage() {
1307
- const colors = ['#2a1a3a', '#ff2a6d', '#05d9e8'];
1308
- const svg = `
1309
- <svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
1310
- <defs>
1311
- <linearGradient id="sky" x1="0%" y1="0%" x2="0%" y2="100%">
1312
- <stop offset="0%" style="stop-color:${colors[0]}"/>
1313
- <stop offset="100%" style="stop-color:#0a0a0f"/>
1314
- </linearGradient>
1315
- </defs>
1316
- <rect width="100%" height="100%" fill="url(#sky)"/>
1317
- ${Array.from({ length: 15 }, (_, i) => {
1318
- const x = i * 55 - 20 + Math.random() * 30;
1319
- const h = 80 + Math.random() * 200;
1320
- const w = 30 + Math.random() * 25;
1321
- return `<rect x="${x}" y="${450 - h}" width="${w}" height="${h}" fill="${colors[0]}" opacity="0.8"/>
1322
- ${Math.random() > 0.5 ? `<rect x="${x + 5}" y="${450 - h + 10}" width="3" height="3" fill="${colors[1]}" opacity="0.8"/>` : ''}`;
1323
- }).join('')}
1324
- <rect x="0" y="445" width="800" height="5" fill="${colors[1]}" opacity="0.4"/>
1325
- </svg>`;
1326
- return 'data:image/svg+xml;base64,' + btoa(svg);
1327
- }
1328
-
1329
- // Update UI
1330
- function updateUI() {
1331
- document.getElementById('day-number').textContent = `DÍA ${STATE.day}`;
1332
- document.getElementById('scene-location').textContent = STATE.location.name;
1333
- document.getElementById('scene-time').textContent = getTimeString();
1334
- document.getElementById('streak-count').textContent = STATE.streak;
1335
-
1336
- document.getElementById('stat-fuerza').textContent = STATE.stats.fuerza;
1337
- document.getElementById('stat-mente').textContent = STATE.stats.mente;
1338
- document.getElementById('stat-plata').textContent = STATE.stats.plata;
1339
- document.getElementById('stat-karma').textContent = STATE.stats.karma;
1340
- }
1341
-
1342
- function getTimeString() {
1343
- const times = {
1344
- morning: `${6 + Math.floor(Math.random() * 3)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} AM`,
1345
- day: `${12 + Math.floor(Math.random() * 5)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1346
- sunset: `${18 + Math.floor(Math.random() * 2)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1347
- night: `${21 + Math.floor(Math.random() * 3)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1348
- rush: `${7 + Math.floor(Math.random() * 2)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} AM`
1349
- };
1350
- return times[STATE.location.time] || times.day;
1351
- }
1352
-
1353
- // Typewriter effect for narrative
1354
- async function typewriterEffect(element, html, speed = 15) {
1355
- const tempDiv = document.createElement('div');
1356
- tempDiv.innerHTML = html;
1357
- const text = tempDiv.textContent;
1358
-
1359
- element.innerHTML = '';
1360
- element.style.opacity = '1';
1361
-
1362
- let i = 0;
1363
- return new Promise(resolve => {
1364
- function type() {
1365
- if (i < text.length) {
1366
- element.innerHTML = html.substring(0, element.innerHTML.length + 1);
1367
- // Actually just show progressively
1368
- const progress = i / text.length;
1369
- element.innerHTML = html;
1370
- element.style.clipPath = `inset(0 ${100 - progress * 100}% 0 0)`;
1371
- i += 3;
1372
- setTimeout(type, speed);
1373
- } else {
1374
- element.style.clipPath = 'none';
1375
- resolve();
1376
- }
1377
- }
1378
- type();
1379
- });
1380
- }
1381
-
1382
- // Generate new scene using coherent story arcs
1383
- async function generateScene() {
1384
- const scene = getCurrentScene();
1385
-
1386
- // Update location based on scene context
1387
- if (scene.id.includes('kiosco')) {
1388
- STATE.location = LOCATIONS.find(l => l.mood === 'slum') || LOCATIONS[0];
1389
- }
1390
-
1391
- updateUI();
1392
-
1393
- // Show narrative with typing effect
1394
- const narrativeEl = document.getElementById('narrative');
1395
- narrativeEl.innerHTML = scene.narrative;
1396
-
1397
- // Add reading time indicator
1398
- const wordCount = scene.narrative.replace(/<[^>]*>/g, '').split(/\s+/).length;
1399
- const readingTime = Math.ceil(wordCount / 200 * 60); // seconds at 200 wpm
1400
-
1401
- // Hide character speech for now (integrated into narrative)
1402
- document.getElementById('character-speech').style.display = 'none';
1403
-
1404
- // Generate choices from scene
1405
- const choices = generateContextualChoices(scene);
1406
- const choicesContainer = document.getElementById('choices');
1407
-
1408
- // Show choices with delay for reading
1409
- choicesContainer.innerHTML = '';
1410
-
1411
- setTimeout(() => {
1412
- choicesContainer.innerHTML = choices.map((choice, i) => `
1413
- <button class="choice-btn" data-index="${i}" data-next="${choice.next || ''}" style="animation: fadeInUp 0.5s ease ${i * 0.15}s both;">
1414
- ${choice.text}
1415
- <div class="choice-stats">
1416
- ${Object.entries(choice.effect).map(([stat, val]) =>
1417
- `<span class="stat-change ${val >= 0 ? 'positive' : 'negative'}">${val >= 0 ? '+' : ''}${val}</span>`
1418
- ).join('')}
1419
- </div>
1420
- </button>
1421
- `).join('');
1422
-
1423
- // Add click handlers
1424
- choicesContainer.querySelectorAll('.choice-btn').forEach((btn, i) => {
1425
- btn.addEventListener('click', () => makeChoice(choices[i]));
1426
- });
1427
- }, Math.min(readingTime * 300, 2000)); // Wait based on reading time, max 2s
1428
-
1429
- // Generate image using scene's specific prompt
1430
- await generateSceneImage(scene.image_prompt);
1431
-
1432
- // Record scene in history
1433
- sceneHistory.push(currentSceneId);
1434
- }
1435
-
1436
- // Make a choice
1437
- async function makeChoice(choice) {
1438
- if (STATE.sfxEnabled) {
1439
- document.getElementById('click-sfx').currentTime = 0;
1440
- document.getElementById('click-sfx').play().catch(() => { });
1441
- }
1442
-
1443
- // Apply effects
1444
- for (const [stat, value] of Object.entries(choice.effect)) {
1445
- if (STATE.stats[stat] !== undefined) {
1446
- STATE.stats[stat] = Math.max(0, Math.min(100, STATE.stats[stat] + value));
1447
- }
1448
- }
1449
-
1450
- // Set flag if choice has one
1451
- if (choice.flag) {
1452
- FLAGS.add(choice.flag);
1453
- }
1454
-
1455
- // Navigate to next scene
1456
- if (choice.next) {
1457
- const nextScene = findScene(choice.next);
1458
- if (nextScene) {
1459
- currentSceneId = choice.next;
1460
- currentArc = nextScene.arcName;
1461
- } else {
1462
- // If scene not found, stay in current arc but generate continuation
1463
- console.log('Scene not found:', choice.next, '- generating continuation');
1464
- // Add the missing scene dynamically or fallback
1465
- }
1466
- }
1467
-
1468
- // Record history
1469
- STATE.history.push({
1470
- day: STATE.day,
1471
- choice: choice.text,
1472
- location: STATE.location.name,
1473
- sceneId: currentSceneId
1474
- });
1475
-
1476
- // Streak and day advancement
1477
- STATE.streak++;
1478
- STATE.totalChoices++;
1479
-
1480
- // Advance time occasionally
1481
- if (STATE.totalChoices % 3 === 0) {
1482
- const times = ['morning', 'day', 'sunset', 'night'];
1483
- const currentIndex = times.indexOf(STATE.timeOfDay);
1484
- STATE.timeOfDay = times[(currentIndex + 1) % times.length];
1485
- STATE.location.time = STATE.timeOfDay;
1486
-
1487
- if (STATE.timeOfDay === 'morning') {
1488
- STATE.day++;
1489
- updateWeather();
1490
- }
1491
- }
1492
-
1493
- // Update relationships based on scene context
1494
- if (currentSceneId.includes('mama') || currentSceneId.includes('comfort')) {
1495
- STATE.relationships['Mamá Rosa'].level = Math.min(100, STATE.relationships['Mamá Rosa'].level + 10);
1496
- }
1497
- if (currentSceneId.includes('tano')) {
1498
- STATE.relationships['El Tano'].level = Math.min(100, STATE.relationships['El Tano'].level + 8);
1499
- }
1500
- if (currentSceneId.includes('carmelo') || currentSceneId.includes('kiosco')) {
1501
- STATE.relationships['Don Carmelo'].level = Math.min(100, STATE.relationships['Don Carmelo'].level + 5);
1502
- }
1503
- updateRelationships();
1504
-
1505
- // Check achievements
1506
- checkAchievements();
1507
-
1508
- // Show contextual notification
1509
- const notifications = {
1510
- 'talked_to_mama': 'Tu madre siempre sabe más de lo que dice.',
1511
- 'skipped_breakfast': 'El hambre aguza los sentidos.',
1512
- 'asked_about_job': 'Las preguntas correctas abren puertas.',
1513
- 'refused_carmelo': 'La dignidad tiene un precio.',
1514
- 'comforted_mama': 'A veces las palabras sobran.',
1515
- 'promised_mama': 'Las promesas pesan.',
1516
- 'talked_to_tano': 'El Tano siempre sabe algo.',
1517
- 'warned_by_tano': 'Ahora sabés más de lo que querías.',
1518
- 'knows_about_job': 'La información es poder.',
1519
- 'confronted_carmelo': 'La verdad incomoda.',
1520
- 'sensed_danger': 'El instinto no miente.'
1521
- };
1522
-
1523
- const notification = choice.flag && notifications[choice.flag]
1524
- ? notifications[choice.flag]
1525
- : '→ Tu decisión dejó una marca.';
1526
- showNotification(notification);
1527
-
1528
- // Generate new scene
1529
- await generateScene();
1530
-
1531
- // Smooth scroll to narrative
1532
- document.getElementById('narrative').scrollIntoView({ behavior: 'smooth', block: 'start' });
1533
-
1534
- // Check for minigame chance (10%)
1535
- if (Math.random() > 0.9 && STATE.totalChoices > 2) {
1536
- setTimeout(() => showMinigame(), 1500);
1537
- }
1538
- }
1539
-
1540
- // Show notification
1541
- function showNotification(text) {
1542
- const notif = document.getElementById('notification');
1543
- document.getElementById('notif-text').textContent = text;
1544
- notif.classList.add('show');
1545
- setTimeout(() => notif.classList.remove('show'), 3000);
1546
- }
1547
-
1548
- // Minigame
1549
- function showMinigame() {
1550
- const modal = document.getElementById('minigame-modal');
1551
- const content = document.getElementById('minigame-content');
1552
-
1553
- const games = [
1554
- {
1555
- name: 'Reacción Rápida',
1556
- html: `
1557
- <h2>⚡ REACCIÓN RÁPIDA</h2>
1558
- <p>Cuando el cuadro se ponga verde, hacé click lo más rápido posible.</p>
1559
- <div id="reaction-box" style="width:200px;height:200px;background:#ff4757;margin:20px auto;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:2rem;">ESPERÁ</div>
1560
- <button onclick="closeMinigame()" style="margin-top:20px;padding:10px 20px;background:var(--accent);border:none;border-radius:5px;color:white;cursor:pointer;">Saltar</button>
1561
- `,
1562
- init: () => {
1563
- const box = document.getElementById('reaction-box');
1564
- let startTime;
1565
- let canClick = false;
1566
-
1567
- setTimeout(() => {
1568
- box.style.background = '#2ed573';
1569
- box.textContent = 'AHORA!';
1570
- startTime = Date.now();
1571
- canClick = true;
1572
- }, 2000 + Math.random() * 3000);
1573
-
1574
- box.onclick = () => {
1575
- if (!canClick) return;
1576
- const time = Date.now() - startTime;
1577
- if (time < 300) {
1578
- STATE.stats.fuerza += 10;
1579
- showNotification('🎯 Reflejos increíbles! +10 Fuerza');
1580
- } else if (time < 500) {
1581
- STATE.stats.fuerza += 5;
1582
- showNotification('✓ Buenos reflejos! +5 Fuerza');
1583
- }
1584
- closeMinigame();
1585
- };
1586
- }
1587
- },
1588
- {
1589
- name: 'Memoria',
1590
- html: `
1591
- <h2>🧠 MEMORIA</h2>
1592
- <p>Recordá la secuencia de números.</p>
1593
- <div id="memory-display" style="font-size:3rem;letter-spacing:10px;margin:20px 0;font-family:'Space Mono',monospace;"></div>
1594
- <input type="text" id="memory-input" style="font-size:1.5rem;padding:10px;width:200px;text-align:center;background:var(--surface);border:1px solid var(--accent2);color:white;border-radius:5px;" placeholder="Tu respuesta">
1595
- <br><button id="memory-submit" style="margin-top:15px;padding:10px 20px;background:var(--accent2);border:none;border-radius:5px;color:black;cursor:pointer;">Verificar</button>
1596
- `,
1597
- init: () => {
1598
- const sequence = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join('');
1599
- const display = document.getElementById('memory-display');
1600
- display.textContent = sequence;
1601
-
1602
- setTimeout(() => {
1603
- display.textContent = '????';
1604
- document.getElementById('memory-input').focus();
1605
- }, 2000);
1606
-
1607
- document.getElementById('memory-submit').onclick = () => {
1608
- const answer = document.getElementById('memory-input').value;
1609
- if (answer === sequence) {
1610
- STATE.stats.mente += 10;
1611
- showNotification('🧠 Memoria perfecta! +10 Mente');
1612
- } else {
1613
- showNotification('Casi... Era ' + sequence);
1614
- }
1615
- closeMinigame();
1616
- };
1617
- }
1618
- }
1619
- ];
1620
-
1621
- const game = games[Math.floor(Math.random() * games.length)];
1622
- content.innerHTML = game.html;
1623
- modal.classList.add('active');
1624
- game.init();
1625
- }
1626
-
1627
- function closeMinigame() {
1628
- document.getElementById('minigame-modal').classList.remove('active');
1629
- }
1630
-
1631
- // Audio controls
1632
- function setupAudio() {
1633
- const musicBtn = document.getElementById('music-btn');
1634
- const sfxBtn = document.getElementById('sfx-btn');
1635
- const music = document.getElementById('ambient-music');
1636
- music.volume = 0.3;
1637
-
1638
- musicBtn.addEventListener('click', () => {
1639
- STATE.musicEnabled = !STATE.musicEnabled;
1640
- musicBtn.classList.toggle('active', STATE.musicEnabled);
1641
- if (STATE.musicEnabled) {
1642
- music.play().catch(() => { });
1643
- } else {
1644
- music.pause();
1645
- }
1646
- });
1647
-
1648
- sfxBtn.addEventListener('click', () => {
1649
- STATE.sfxEnabled = !STATE.sfxEnabled;
1650
- sfxBtn.classList.toggle('active', STATE.sfxEnabled);
1651
- });
1652
-
1653
- sfxBtn.classList.add('active');
1654
- }
1655
-
1656
- // Initialize
1657
- async function init() {
1658
- setRandomQuote();
1659
- createParticles();
1660
- setupAudio();
1661
- updateWeather();
1662
- updateRelationships();
1663
-
1664
- // Wait for loading
1665
- await new Promise(r => setTimeout(r, 3000));
1666
-
1667
- document.getElementById('loading-screen').classList.add('hidden');
1668
- document.getElementById('game-container').classList.add('active');
1669
-
1670
- await generateScene();
1671
- }
1672
-
1673
- document.addEventListener('DOMContentLoaded', init);
1674
  </script>
 
1675
  </body>
1676
 
1677
  </html>
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>EL CANDIDATO: MEMORIA NEURONAL</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ <script type="importmap">
10
+ {
11
+ "imports": {
12
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
13
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
14
+ "cannon-es": "https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js"
15
+ }
16
  }
17
+ </script>
18
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
19
+ <!-- Helper for animations -->
20
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
21
 
22
+ <style>
23
  body {
24
+ margin: 0;
25
+ overflow: hidden;
26
+ background: #050505;
27
+ font-family: 'Inter', sans-serif;
28
+ user-select: none;
 
29
  }
30
 
 
31
  #loading-screen {
32
  position: fixed;
33
  inset: 0;
34
+ background: #000;
35
+ z-index: 9999;
36
  display: flex;
37
  flex-direction: column;
38
  align-items: center;
39
  justify-content: center;
40
+ color: #fff;
41
+ transition: opacity 1s ease-out;
 
 
 
 
 
42
  }
43
 
44
+ .loader-bar {
45
+ width: 300px;
46
+ height: 4px;
47
+ background: #333;
48
+ margin-top: 20px;
49
+ position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  overflow: hidden;
51
  }
52
 
53
+ .loader-progress {
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ bottom: 0;
58
  width: 0%;
59
+ background: #fff;
60
+ box-shadow: 0 0 10px #fff;
61
+ transition: width 0.2s;
 
 
 
 
62
  }
63
 
64
+ /* POST PROCESSING OVERLAY (CSS BASED) */
65
+ #grain {
66
  position: fixed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  inset: 0;
68
+ pointer-events: none;
69
+ z-index: 10;
70
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E");
71
+ opacity: 0.4;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
+ #vignette {
75
+ position: fixed;
76
+ inset: 0;
77
+ pointer-events: none;
78
+ z-index: 11;
79
+ background: radial-gradient(circle, transparent 50%, rgba(0, 0, 0, 0.4) 120%);
80
  }
81
 
82
+ /* UI */
83
+ #ui-layer {
84
  position: absolute;
85
+ inset: 0;
86
+ pointer-events: none;
87
+ z-index: 20;
 
 
 
 
 
 
88
  }
89
 
90
+ #phone {
91
  position: absolute;
92
+ bottom: 30px;
93
+ right: 30px;
94
+ width: 340px;
95
+ height: 680px;
96
+ background: #111;
97
+ border-radius: 40px;
98
+ border: 6px solid #222;
99
+ box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
100
+ transform: translateY(110%);
101
+ transition: transform 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.1);
102
+ overflow: hidden;
103
+ pointer-events: auto;
104
+ display: flex;
105
+ flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
 
108
+ #phone.active {
109
+ transform: translateY(0);
110
  }
111
 
112
+ .screen {
113
+ flex: 1;
114
+ background: #fff;
115
  display: flex;
116
+ flex-direction: column;
117
+ background: #f0f2f5;
 
 
 
 
118
  }
119
 
120
+ .header {
121
+ background: #008069;
122
+ color: white;
123
+ padding: 15px;
 
124
  display: flex;
125
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  gap: 10px;
127
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
128
  }
129
 
130
+ .avatar {
131
+ width: 35px;
132
+ height: 35px;
133
+ background: #ddd;
134
+ border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
+ .chat {
138
+ flex: 1;
 
139
  padding: 15px;
140
+ overflow-y: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  display: flex;
142
+ flex-direction: column;
143
+ gap: 8px;
144
+ background-image: radial-gradient(#ddd 1px, transparent 1px);
145
+ background-size: 20px 20px;
146
  }
147
 
148
+ .msg {
149
+ max-width: 80%;
150
+ padding: 8px 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  border-radius: 8px;
152
+ font-size: 14px;
153
+ line-height: 1.4;
154
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
155
+ animation: pop 0.3s;
156
  }
157
 
158
+ .in {
159
+ align-self: flex-start;
160
+ background: white;
 
 
 
 
 
 
 
161
  }
162
 
163
+ .out {
164
+ align-self: flex-end;
165
+ background: #d9fdd3;
 
 
 
166
  }
167
 
168
+ .opts {
169
+ padding: 10px;
170
+ background: #f0f2f5;
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: 5px;
 
 
174
  }
175
 
176
+ .btn {
 
 
177
  background: white;
178
+ padding: 12px;
179
+ text-align: center;
180
+ border-radius: 20px;
181
+ cursor: pointer;
182
+ color: #008069;
183
+ font-weight: 600;
184
+ border: 1px solid #ddd;
185
+ transition: 0.1s;
186
  }
187
 
188
+ .btn:hover {
189
+ background: #f5f5f5;
190
  }
191
 
192
+ @keyframes pop {
193
+ from {
 
 
194
  opacity: 0;
195
+ transform: scale(0.9);
196
  }
197
 
198
+ to {
199
+ opacity: 1;
200
+ transform: scale(1);
 
 
 
 
201
  }
202
  }
203
 
204
+ /* INTRO */
205
+ #intro {
206
  position: fixed;
207
+ inset: 0;
208
+ background: #111;
209
+ z-index: 100;
210
+ display: grid;
211
+ place-items: center;
 
 
212
  text-align: center;
213
+ color: #fff;
214
+ transition: opacity 1s;
215
+ cursor: pointer;
 
 
 
 
216
  }
217
 
218
+ .title {
219
  font-size: 3rem;
220
+ font-weight: 800;
221
+ letter-spacing: -2px;
222
  margin-bottom: 10px;
223
  }
224
 
225
+ .sub {
226
+ color: #888;
227
+ font-family: monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  }
229
  </style>
230
  </head>
231
 
232
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ <div id="grain"></div>
235
+ <div id="vignette"></div>
236
 
237
+ <div id="intro" onclick="this.style.opacity=0; setTimeout(()=>this.remove(),1000)">
238
+ <div>
239
+ <div class="title">VICENTE LÓPEZ</div>
240
+ <div class="sub">Click para iniciar la simulación</div>
241
+ </div>
242
  </div>
243
 
 
244
  <div id="loading-screen">
245
+ <div>CARGANDO SIMULACIÓN...</div>
246
+ <div class="loader-bar">
247
+ <div class="loader-progress" id="loader-progress"></div>
 
 
 
248
  </div>
249
  </div>
250
 
251
+ <!-- 3D WORLD CONTAINER -->
252
+ <div id="world-container"></div>
253
+
254
+ <!-- UI -->
255
+ <div id="ui-layer">
256
+ <div id="phone">
257
+ <div class="screen">
258
+ <div class="header">
259
+ <div class="avatar"
260
+ style="background:url('https://api.dicebear.com/7.x/micah/svg?seed=Ruso') center/cover"></div>
261
+ <div><b>El Ruso</b><br><small>en línea</small></div>
262
+ </div>
263
+ <div class="chat" id="chat"></div>
264
+ <div class="opts" id="opts"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  </div>
266
  </div>
 
 
 
 
 
 
267
  </div>
268
 
269
+ // Logic moved to main.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  </script>
271
+ <script type="module" src="./static/js/main.js"></script>
272
  </body>
273
 
274
  </html>
index.html_renacer ADDED
@@ -0,0 +1,1677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>RENACER — Buenos Aires 2077</title>
8
+ <link
9
+ href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Space+Mono:wght@400;700&display=swap"
10
+ rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --bg: #0a0a0f;
14
+ --surface: rgba(15, 15, 25, 0.95);
15
+ --text: #e8e6e3;
16
+ --muted: #8a8a9a;
17
+ --accent: #ff6b9d;
18
+ --accent2: #05d9e8;
19
+ --gold: #d4a574;
20
+ --danger: #ff4757;
21
+ --success: #2ed573;
22
+ }
23
+
24
+ * {
25
+ margin: 0;
26
+ padding: 0;
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Cormorant Garamond', Georgia, serif;
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ min-height: 100vh;
35
+ overflow-x: hidden;
36
+ line-height: 1.8;
37
+ }
38
+
39
+ /* === LOADING SCREEN === */
40
+ #loading-screen {
41
+ position: fixed;
42
+ inset: 0;
43
+ background: var(--bg);
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ justify-content: center;
48
+ z-index: 1000;
49
+ transition: opacity 1s, visibility 1s;
50
+ }
51
+
52
+ #loading-screen.hidden {
53
+ opacity: 0;
54
+ visibility: hidden;
55
+ }
56
+
57
+ .loading-quote {
58
+ font-size: 1.4rem;
59
+ font-style: italic;
60
+ max-width: 600px;
61
+ text-align: center;
62
+ padding: 0 30px;
63
+ color: var(--muted);
64
+ margin-bottom: 30px;
65
+ }
66
+
67
+ .loading-author {
68
+ color: var(--gold);
69
+ font-size: 1rem;
70
+ margin-top: 15px;
71
+ display: block;
72
+ }
73
+
74
+ .loading-bar {
75
+ width: 200px;
76
+ height: 2px;
77
+ background: rgba(255, 255, 255, 0.1);
78
+ border-radius: 2px;
79
+ overflow: hidden;
80
+ }
81
+
82
+ .loading-progress {
83
+ height: 100%;
84
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
85
+ width: 0%;
86
+ animation: loadProgress 2.5s ease-out forwards;
87
+ }
88
+
89
+ @keyframes loadProgress {
90
+ to {
91
+ width: 100%;
92
+ }
93
+ }
94
+
95
+ /* === AUDIO CONTROLS === */
96
+ #audio-controls {
97
+ position: fixed;
98
+ top: 20px;
99
+ right: 20px;
100
+ z-index: 100;
101
+ display: flex;
102
+ gap: 10px;
103
+ }
104
+
105
+ .audio-btn {
106
+ width: 45px;
107
+ height: 45px;
108
+ border-radius: 50%;
109
+ background: var(--surface);
110
+ border: 1px solid rgba(255, 255, 255, 0.1);
111
+ color: var(--text);
112
+ cursor: pointer;
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ font-size: 1.2rem;
117
+ transition: all 0.3s;
118
+ }
119
+
120
+ .audio-btn:hover {
121
+ background: rgba(255, 107, 157, 0.2);
122
+ border-color: var(--accent);
123
+ }
124
+
125
+ .audio-btn.active {
126
+ background: rgba(5, 217, 232, 0.2);
127
+ border-color: var(--accent2);
128
+ }
129
+
130
+ /* === MAIN CONTAINER === */
131
+ #game-container {
132
+ max-width: 900px;
133
+ margin: 0 auto;
134
+ padding: 20px;
135
+ opacity: 0;
136
+ transition: opacity 0.5s;
137
+ }
138
+
139
+ #game-container.active {
140
+ opacity: 1;
141
+ }
142
+
143
+ /* === TOP STATS BAR === */
144
+ #stats-bar {
145
+ display: grid;
146
+ grid-template-columns: repeat(4, 1fr);
147
+ gap: 15px;
148
+ margin-bottom: 30px;
149
+ padding: 15px;
150
+ background: var(--surface);
151
+ border-radius: 12px;
152
+ border: 1px solid rgba(255, 255, 255, 0.05);
153
+ }
154
+
155
+ .stat-item {
156
+ text-align: center;
157
+ }
158
+
159
+ .stat-icon {
160
+ font-size: 1.5rem;
161
+ margin-bottom: 5px;
162
+ }
163
+
164
+ .stat-value {
165
+ font-family: 'Space Mono', monospace;
166
+ font-size: 1.2rem;
167
+ color: var(--accent2);
168
+ }
169
+
170
+ .stat-label {
171
+ font-size: 0.75rem;
172
+ color: var(--muted);
173
+ text-transform: uppercase;
174
+ letter-spacing: 1px;
175
+ }
176
+
177
+ /* === SCENE IMAGE === */
178
+ #scene-visual {
179
+ position: relative;
180
+ width: 100%;
181
+ aspect-ratio: 16/9;
182
+ border-radius: 12px;
183
+ overflow: hidden;
184
+ margin-bottom: 30px;
185
+ background: linear-gradient(135deg, #1a1a2e, #0a0a15);
186
+ }
187
+
188
+ #scene-image {
189
+ width: 100%;
190
+ height: 100%;
191
+ object-fit: cover;
192
+ opacity: 0;
193
+ transition: opacity 0.5s;
194
+ }
195
+
196
+ #scene-image.loaded {
197
+ opacity: 1;
198
+ }
199
+
200
+ #image-loading {
201
+ position: absolute;
202
+ inset: 0;
203
+ display: flex;
204
+ flex-direction: column;
205
+ align-items: center;
206
+ justify-content: center;
207
+ background: linear-gradient(135deg, #1a1a2e, #0a0a15);
208
+ }
209
+
210
+ #image-loading.hidden {
211
+ display: none;
212
+ }
213
+
214
+ .image-spinner {
215
+ width: 40px;
216
+ height: 40px;
217
+ border: 2px solid rgba(255, 255, 255, 0.1);
218
+ border-top-color: var(--accent);
219
+ border-radius: 50%;
220
+ animation: spin 1s linear infinite;
221
+ }
222
+
223
+ @keyframes spin {
224
+ to {
225
+ transform: rotate(360deg);
226
+ }
227
+ }
228
+
229
+ .image-loading-text {
230
+ margin-top: 15px;
231
+ color: var(--muted);
232
+ font-size: 0.9rem;
233
+ }
234
+
235
+ #scene-location {
236
+ position: absolute;
237
+ top: 15px;
238
+ left: 15px;
239
+ background: rgba(0, 0, 0, 0.7);
240
+ padding: 8px 15px;
241
+ border-radius: 20px;
242
+ font-family: 'Space Mono', monospace;
243
+ font-size: 0.75rem;
244
+ color: var(--accent2);
245
+ backdrop-filter: blur(10px);
246
+ }
247
+
248
+ #scene-time {
249
+ position: absolute;
250
+ top: 15px;
251
+ right: 15px;
252
+ background: rgba(0, 0, 0, 0.7);
253
+ padding: 8px 15px;
254
+ border-radius: 20px;
255
+ font-family: 'Space Mono', monospace;
256
+ font-size: 0.75rem;
257
+ color: var(--gold);
258
+ backdrop-filter: blur(10px);
259
+ }
260
+
261
+ /* === NARRATIVE === */
262
+ #narrative {
263
+ font-size: 1.25rem;
264
+ margin-bottom: 30px;
265
+ padding: 30px;
266
+ background: var(--surface);
267
+ border-radius: 12px;
268
+ border-left: 3px solid var(--gold);
269
+ }
270
+
271
+ #narrative p {
272
+ margin-bottom: 1.2em;
273
+ text-indent: 2em;
274
+ }
275
+
276
+ #narrative p:first-child::first-letter {
277
+ font-size: 3.5rem;
278
+ float: left;
279
+ line-height: 1;
280
+ margin-right: 10px;
281
+ color: var(--gold);
282
+ font-weight: 600;
283
+ }
284
+
285
+ .thought {
286
+ font-style: italic;
287
+ color: var(--accent);
288
+ }
289
+
290
+ .important {
291
+ color: var(--accent2);
292
+ font-weight: 600;
293
+ }
294
+
295
+ .dialogue {
296
+ color: var(--gold);
297
+ }
298
+
299
+ /* === CHARACTER SPEECH === */
300
+ .character-speech {
301
+ display: flex;
302
+ gap: 20px;
303
+ padding: 25px;
304
+ background: linear-gradient(135deg, rgba(212, 165, 116, 0.1), transparent);
305
+ border-radius: 12px;
306
+ margin-bottom: 30px;
307
+ border: 1px solid rgba(212, 165, 116, 0.2);
308
+ }
309
+
310
+ .character-avatar {
311
+ width: 60px;
312
+ height: 60px;
313
+ border-radius: 50%;
314
+ background: var(--surface);
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ font-size: 2rem;
319
+ flex-shrink: 0;
320
+ border: 2px solid var(--gold);
321
+ }
322
+
323
+ .character-name {
324
+ font-family: 'Space Mono', monospace;
325
+ font-size: 0.85rem;
326
+ color: var(--gold);
327
+ margin-bottom: 8px;
328
+ text-transform: uppercase;
329
+ letter-spacing: 1px;
330
+ }
331
+
332
+ .character-text {
333
+ font-size: 1.1rem;
334
+ font-style: italic;
335
+ }
336
+
337
+ /* === CHOICES === */
338
+ #choices-container {
339
+ margin-bottom: 30px;
340
+ }
341
+
342
+ .choices-title {
343
+ font-family: 'Space Mono', monospace;
344
+ font-size: 0.8rem;
345
+ color: var(--muted);
346
+ margin-bottom: 15px;
347
+ text-transform: uppercase;
348
+ letter-spacing: 2px;
349
+ }
350
+
351
+ .choice-btn {
352
+ display: block;
353
+ width: 100%;
354
+ padding: 20px 25px;
355
+ margin-bottom: 12px;
356
+ background: var(--surface);
357
+ border: 1px solid rgba(255, 255, 255, 0.1);
358
+ border-radius: 10px;
359
+ color: var(--text);
360
+ text-align: left;
361
+ font-family: 'Cormorant Garamond', serif;
362
+ font-size: 1.1rem;
363
+ cursor: pointer;
364
+ transition: all 0.3s;
365
+ position: relative;
366
+ }
367
+
368
+ .choice-btn:hover {
369
+ background: rgba(255, 107, 157, 0.1);
370
+ border-color: var(--accent);
371
+ transform: translateX(10px);
372
+ }
373
+
374
+ .choice-consequence {
375
+ display: block;
376
+ font-size: 0.85rem;
377
+ color: var(--muted);
378
+ margin-top: 8px;
379
+ font-style: italic;
380
+ }
381
+
382
+ .choice-stats {
383
+ position: absolute;
384
+ right: 20px;
385
+ top: 50%;
386
+ transform: translateY(-50%);
387
+ display: flex;
388
+ gap: 10px;
389
+ }
390
+
391
+ .stat-change {
392
+ font-family: 'Space Mono', monospace;
393
+ font-size: 0.75rem;
394
+ padding: 4px 8px;
395
+ border-radius: 4px;
396
+ }
397
+
398
+ .stat-change.positive {
399
+ background: rgba(46, 213, 115, 0.2);
400
+ color: var(--success);
401
+ }
402
+
403
+ .stat-change.negative {
404
+ background: rgba(255, 71, 87, 0.2);
405
+ color: var(--danger);
406
+ }
407
+
408
+ /* === DAY COUNTER === */
409
+ #day-display {
410
+ text-align: center;
411
+ padding: 15px;
412
+ margin-bottom: 20px;
413
+ font-family: 'Space Mono', monospace;
414
+ }
415
+
416
+ #day-number {
417
+ font-size: 2rem;
418
+ color: var(--accent2);
419
+ }
420
+
421
+ #day-label {
422
+ color: var(--muted);
423
+ font-size: 0.8rem;
424
+ }
425
+
426
+ /* === MINI GAMES POPUP === */
427
+ .minigame-modal {
428
+ position: fixed;
429
+ inset: 0;
430
+ background: rgba(0, 0, 0, 0.9);
431
+ display: none;
432
+ align-items: center;
433
+ justify-content: center;
434
+ z-index: 200;
435
+ }
436
+
437
+ .minigame-modal.active {
438
+ display: flex;
439
+ }
440
+
441
+ .minigame-content {
442
+ background: var(--surface);
443
+ border-radius: 15px;
444
+ padding: 40px;
445
+ max-width: 500px;
446
+ text-align: center;
447
+ }
448
+
449
+ /* === NOTIFICATION === */
450
+ #notification {
451
+ position: fixed;
452
+ bottom: 30px;
453
+ left: 50%;
454
+ transform: translateX(-50%) translateY(100px);
455
+ background: var(--surface);
456
+ padding: 15px 30px;
457
+ border-radius: 10px;
458
+ border-left: 3px solid var(--accent2);
459
+ opacity: 0;
460
+ transition: all 0.3s;
461
+ z-index: 150;
462
+ }
463
+
464
+ #notification.show {
465
+ transform: translateX(-50%) translateY(0);
466
+ opacity: 1;
467
+ }
468
+
469
+ /* === AMBIENT PARTICLES === */
470
+ #particles {
471
+ position: fixed;
472
+ inset: 0;
473
+ pointer-events: none;
474
+ z-index: 1;
475
+ }
476
+
477
+ .particle {
478
+ position: absolute;
479
+ width: 2px;
480
+ height: 2px;
481
+ background: var(--accent);
482
+ border-radius: 50%;
483
+ opacity: 0.3;
484
+ animation: floatUp 20s linear infinite;
485
+ }
486
+
487
+ @keyframes floatUp {
488
+ 0% {
489
+ transform: translateY(100vh) rotate(0deg);
490
+ opacity: 0;
491
+ }
492
+
493
+ 10% {
494
+ opacity: 0.3;
495
+ }
496
+
497
+ 90% {
498
+ opacity: 0.3;
499
+ }
500
+
501
+ 100% {
502
+ transform: translateY(-100vh) rotate(720deg);
503
+ opacity: 0;
504
+ }
505
+ }
506
+
507
+ /* === SCANLINES === */
508
+ .scanlines {
509
+ position: fixed;
510
+ inset: 0;
511
+ background: repeating-linear-gradient(0deg,
512
+ transparent,
513
+ transparent 2px,
514
+ rgba(0, 0, 0, 0.1) 2px,
515
+ rgba(0, 0, 0, 0.1) 4px);
516
+ pointer-events: none;
517
+ z-index: 1000;
518
+ opacity: 0.3;
519
+ }
520
+
521
+ /* === STREAK REWARD === */
522
+ #streak-display {
523
+ position: fixed;
524
+ top: 80px;
525
+ right: 20px;
526
+ background: var(--surface);
527
+ padding: 10px 15px;
528
+ border-radius: 8px;
529
+ font-family: 'Space Mono', monospace;
530
+ font-size: 0.8rem;
531
+ border: 1px solid rgba(255, 255, 255, 0.1);
532
+ }
533
+
534
+ .streak-fire {
535
+ color: #ff6b2c;
536
+ }
537
+
538
+ /* === WEATHER EFFECTS === */
539
+ #weather-container {
540
+ position: fixed;
541
+ inset: 0;
542
+ pointer-events: none;
543
+ z-index: 2;
544
+ }
545
+
546
+ .rain {
547
+ position: absolute;
548
+ width: 2px;
549
+ height: 20px;
550
+ background: linear-gradient(transparent, rgba(174, 194, 224, 0.6));
551
+ animation: rain-fall linear infinite;
552
+ }
553
+
554
+ @keyframes rain-fall {
555
+ 0% {
556
+ transform: translateY(-100vh);
557
+ }
558
+
559
+ 100% {
560
+ transform: translateY(100vh);
561
+ }
562
+ }
563
+
564
+ .lightning {
565
+ position: fixed;
566
+ inset: 0;
567
+ background: white;
568
+ opacity: 0;
569
+ pointer-events: none;
570
+ z-index: 999;
571
+ }
572
+
573
+ .lightning.flash {
574
+ animation: lightning-flash 0.2s;
575
+ }
576
+
577
+ @keyframes lightning-flash {
578
+
579
+ 0%,
580
+ 100% {
581
+ opacity: 0;
582
+ }
583
+
584
+ 10%,
585
+ 30% {
586
+ opacity: 0.8;
587
+ }
588
+
589
+ 20% {
590
+ opacity: 0.3;
591
+ }
592
+ }
593
+
594
+ /* === ACHIEVEMENTS === */
595
+ #achievement-popup {
596
+ position: fixed;
597
+ top: -100px;
598
+ left: 50%;
599
+ transform: translateX(-50%);
600
+ background: linear-gradient(135deg, rgba(212, 165, 116, 0.95), rgba(180, 130, 80, 0.95));
601
+ padding: 20px 40px;
602
+ border-radius: 15px;
603
+ color: #1a1a1a;
604
+ text-align: center;
605
+ z-index: 300;
606
+ transition: top 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
607
+ box-shadow: 0 10px 40px rgba(212, 165, 116, 0.5);
608
+ }
609
+
610
+ #achievement-popup.show {
611
+ top: 100px;
612
+ }
613
+
614
+ .achievement-icon {
615
+ font-size: 3rem;
616
+ margin-bottom: 10px;
617
+ }
618
+
619
+ .achievement-title {
620
+ font-family: 'Space Mono', monospace;
621
+ font-size: 0.8rem;
622
+ text-transform: uppercase;
623
+ letter-spacing: 2px;
624
+ margin-bottom: 5px;
625
+ }
626
+
627
+ .achievement-name {
628
+ font-size: 1.3rem;
629
+ font-weight: 600;
630
+ }
631
+
632
+ /* === RELATIONSHIP BAR === */
633
+ #relationships-bar {
634
+ display: flex;
635
+ gap: 10px;
636
+ margin-bottom: 20px;
637
+ padding: 10px;
638
+ background: var(--surface);
639
+ border-radius: 8px;
640
+ overflow-x: auto;
641
+ }
642
+
643
+ .rel-chip {
644
+ display: flex;
645
+ align-items: center;
646
+ gap: 5px;
647
+ padding: 5px 12px;
648
+ background: rgba(255, 255, 255, 0.05);
649
+ border-radius: 20px;
650
+ font-size: 0.8rem;
651
+ white-space: nowrap;
652
+ }
653
+
654
+ .rel-chip .avatar {
655
+ font-size: 1.2rem;
656
+ }
657
+
658
+ .rel-chip .level {
659
+ font-family: 'Space Mono', monospace;
660
+ color: var(--accent2);
661
+ }
662
+
663
+ /* === WEATHER INDICATOR === */
664
+ #weather-indicator {
665
+ position: fixed;
666
+ top: 140px;
667
+ right: 20px;
668
+ background: var(--surface);
669
+ padding: 8px 12px;
670
+ border-radius: 8px;
671
+ font-size: 1.5rem;
672
+ border: 1px solid rgba(255, 255, 255, 0.1);
673
+ }
674
+
675
+ /* === RESPONSIVE === */
676
+ @media (max-width: 768px) {
677
+ #stats-bar {
678
+ grid-template-columns: repeat(2, 1fr);
679
+ }
680
+
681
+ #narrative {
682
+ font-size: 1.1rem;
683
+ padding: 20px;
684
+ }
685
+
686
+ .choice-stats {
687
+ display: none;
688
+ }
689
+
690
+ #relationships-bar {
691
+ flex-wrap: nowrap;
692
+ }
693
+ }
694
+
695
+ /* === CHOICE ANIMATION === */
696
+ @keyframes fadeInUp {
697
+ from {
698
+ opacity: 0;
699
+ transform: translateY(20px);
700
+ }
701
+
702
+ to {
703
+ opacity: 1;
704
+ transform: translateY(0);
705
+ }
706
+ }
707
+ </style>
708
+ </head>
709
+
710
+ <body>
711
+ <!-- Ambient Effects -->
712
+ <div id="particles"></div>
713
+ <div id="weather-container"></div>
714
+ <div class="lightning" id="lightning"></div>
715
+ <div class="scanlines"></div>
716
+
717
+ <!-- Audio Controls -->
718
+ <div id="audio-controls">
719
+ <button class="audio-btn" id="music-btn" title="Música">🎵</button>
720
+ <button class="audio-btn" id="sfx-btn" title="Efectos">🔊</button>
721
+ </div>
722
+
723
+ <!-- Streak Display -->
724
+ <div id="streak-display">
725
+ <span class="streak-fire">🔥</span> Racha: <span id="streak-count">0</span> días
726
+ </div>
727
+
728
+ <!-- Weather Indicator -->
729
+ <div id="weather-indicator">☀️</div>
730
+
731
+ <!-- Achievement Popup -->
732
+ <div id="achievement-popup">
733
+ <div class="achievement-icon">🏆</div>
734
+ <div class="achievement-title">LOGRO DESBLOQUEADO</div>
735
+ <div class="achievement-name" id="achievement-name">Nombre del logro</div>
736
+ </div>
737
+
738
+ <!-- Loading Screen -->
739
+ <div id="loading-screen">
740
+ <div class="loading-quote">
741
+ <span id="quote-text">"No te rindas, aún estás a tiempo de alcanzar y comenzar de nuevo."</span>
742
+ <span class="loading-author" id="quote-author">— Mario Benedetti</span>
743
+ </div>
744
+ <div class="loading-bar">
745
+ <div class="loading-progress"></div>
746
+ </div>
747
+ </div>
748
+
749
+ <!-- Main Game Container -->
750
+ <div id="game-container">
751
+ <!-- Day Display -->
752
+ <div id="day-display">
753
+ <div id="day-number">DÍA 1</div>
754
+ <div id="day-label">BUENOS AIRES 2077</div>
755
+ </div>
756
+
757
+ <!-- Relationships Bar -->
758
+ <div id="relationships-bar"></div>
759
+
760
+ <!-- Stats Bar -->
761
+ <div id="stats-bar">
762
+ <div class="stat-item">
763
+ <div class="stat-icon">✊</div>
764
+ <div class="stat-value" id="stat-fuerza">50</div>
765
+ <div class="stat-label">Fuerza</div>
766
+ </div>
767
+ <div class="stat-item">
768
+ <div class="stat-icon">🧠</div>
769
+ <div class="stat-value" id="stat-mente">50</div>
770
+ <div class="stat-label">Mente</div>
771
+ </div>
772
+ <div class="stat-item">
773
+ <div class="stat-icon">💰</div>
774
+ <div class="stat-value" id="stat-plata">150</div>
775
+ <div class="stat-label">Plata</div>
776
+ </div>
777
+ <div class="stat-item">
778
+ <div class="stat-icon">❤️</div>
779
+ <div class="stat-value" id="stat-karma">50</div>
780
+ <div class="stat-label">Karma</div>
781
+ </div>
782
+ </div>
783
+
784
+ <!-- Scene Visual -->
785
+ <div id="scene-visual">
786
+ <div id="image-loading">
787
+ <div class="image-spinner"></div>
788
+ <div class="image-loading-text">Generando escena...</div>
789
+ </div>
790
+ <img id="scene-image" alt="Escena">
791
+ <div id="scene-location">VILLA 31 · RETIRO</div>
792
+ <div id="scene-time">6:47 AM</div>
793
+ </div>
794
+
795
+ <!-- Narrative -->
796
+ <div id="narrative">
797
+ <p>Cargando historia...</p>
798
+ </div>
799
+
800
+ <!-- Character Speech -->
801
+ <div id="character-speech" class="character-speech" style="display: none;">
802
+ <div class="character-avatar" id="speech-avatar">👤</div>
803
+ <div class="character-content">
804
+ <div class="character-name" id="speech-name">Personaje</div>
805
+ <div class="character-text" id="speech-text">Diálogo</div>
806
+ </div>
807
+ </div>
808
+
809
+ <!-- Choices -->
810
+ <div id="choices-container">
811
+ <div class="choices-title">¿Qué hacés?</div>
812
+ <div id="choices"></div>
813
+ </div>
814
+ </div>
815
+
816
+ <!-- Notification -->
817
+ <div id="notification">
818
+ <div id="notif-text">Notificación</div>
819
+ </div>
820
+
821
+ <!-- Minigame Modal -->
822
+ <div class="minigame-modal" id="minigame-modal">
823
+ <div class="minigame-content" id="minigame-content">
824
+ <!-- Minigame content loaded dynamically -->
825
+ </div>
826
+ </div>
827
+
828
+ <!-- Audio Elements -->
829
+ <audio id="ambient-music" loop>
830
+ <source src="https://assets.mixkit.co/music/preview/mixkit-hip-hop-02-621.mp3" type="audio/mpeg">
831
+ </audio>
832
+ <audio id="click-sfx">
833
+ <source src="https://assets.mixkit.co/active_storage/sfx/2568/2568-preview.mp3" type="audio/mpeg">
834
+ </audio>
835
+
836
+ <script>
837
+ // ========================================
838
+ // RENACER - Procedural Narrative Engine
839
+ // ========================================
840
+
841
+ // Locations with atmosphere
842
+ const LOCATIONS = [
843
+ { name: "VILLA 31 · RETIRO", mood: "slum", time: "morning" },
844
+ { name: "PUERTO MADERO · DÁRSENA", mood: "corporate", time: "day" },
845
+ { name: "LA BOCA · CAMINITO", mood: "artistic", time: "sunset" },
846
+ { name: "SAN TELMO · MERCADO", mood: "traditional", time: "morning" },
847
+ { name: "PALERMO SOHO · PLAZA", mood: "hipster", time: "night" },
848
+ { name: "CONSTITUCIÓN · ESTACIÓN", mood: "transit", time: "rush" },
849
+ { name: "ONCE · BALVANERA", mood: "commercial", time: "day" },
850
+ { name: "MICROCENTRO · FLORIDA", mood: "crowded", time: "afternoon" }
851
+ ];
852
+
853
+ // Characters with personalities
854
+ const CHARACTERS = [
855
+ { name: "Mamá Rosa", avatar: "👩", type: "family", traits: ["wise", "worried", "loving"] },
856
+ { name: "El Tano", avatar: "👦", type: "friend", traits: ["streetwise", "loyal", "scarred"] },
857
+ { name: "Don Carmelo", avatar: "🧔", type: "mentor", traits: ["cryptic", "connected", "fatherly"] },
858
+ { name: "Lucía", avatar: "👩‍💻", type: "ally", traits: ["smart", "cynical", "beautiful"] },
859
+ { name: "El Rata", avatar: "🐀", type: "danger", traits: ["sneaky", "dangerous", "informant"] },
860
+ { name: "La Doctora", avatar: "👩‍⚕️", type: "help", traits: ["caring", "overworked", "kind"] },
861
+ { name: "Tío Raúl", avatar: "👷", type: "family", traits: ["honest", "tired", "protective"] },
862
+ { name: "Unknown Stranger", avatar: "🕶️", type: "mystery", traits: ["enigmatic", "powerful", "unknown"] }
863
+ ];
864
+
865
+ // STORY ARCS - Coherent narrative with continuity
866
+ const STORY_ARCS = {
867
+ // Arc 1: The Beginning
868
+ despertar: {
869
+ scenes: [
870
+ {
871
+ id: 'wake_up',
872
+ narrative: `<p>El sol entra por las rendijas de la chapa. Son las seis de la mañana y ya hace calor en la villa. Tu madre se levantó antes, como siempre. El olor a mate con leche sube desde la cocina.</p>
873
+ <p>Hoy es diferente. Algo en el aire, en el silencio de afuera, te dice que las cosas van a cambiar. <span class="thought">¿Pero para bien o para mal?</span></p>`,
874
+ image_prompt: "morning light through corrugated metal roof, Buenos Aires slum interior, young man waking up, cinematic, atmospheric",
875
+ choices: [
876
+ { text: "Bajar a desayunar con mamá", next: 'mama_breakfast', effect: { karma: 5 }, flag: 'talked_to_mama' },
877
+ { text: "Saltear el desayuno y salir temprano", next: 'early_street', effect: { fuerza: 5 }, flag: 'skipped_breakfast' }
878
+ ]
879
+ },
880
+ {
881
+ id: 'mama_breakfast',
882
+ narrative: `<p>Tu madre te espera con el mate listo. Tiene esa mirada que conocés bien: la de cuando quiere decir algo importante pero no sabe cómo.</p>
883
+ <p>"Hijo", dice finalmente. "Don Carmelo preguntó por vos ayer. Dice que tiene algo. Un trabajo."</p>
884
+ <p>El silencio que sigue pesa más que las palabras. Los dos saben lo que significa "un trabajo" en boca de Don Carmelo.</p>`,
885
+ image_prompt: "humble kitchen in Buenos Aires slum, mother and son having mate, morning light, emotional, intimate",
886
+ choices: [
887
+ { text: '"¿Qué clase de trabajo?"', next: 'mama_explains', effect: { mente: 5 }, flag: 'asked_about_job' },
888
+ { text: "Quedarte callado y escuchar", next: 'mama_continues', effect: { karma: 5 } },
889
+ { text: '"No me interesa, ma"', next: 'refuse_early', effect: { karma: 10, plata: -20 }, flag: 'refused_carmelo' }
890
+ ]
891
+ },
892
+ {
893
+ id: 'early_street',
894
+ narrative: `<p>El pasillo de la villa ya está vivo a esta hora. Pibes yendo a la escuela, madres cargando bidones de agua, el viejo del carrito de verduras gritando los precios del día.</p>
895
+ <p>En la esquina ves a El Tano fumando. Te hace una seña con la cabeza. Algo quiere.</p>`,
896
+ image_prompt: "Buenos Aires slum alley morning, people walking, street vendor, young man with cigarette in corner, cyberpunk elements",
897
+ choices: [
898
+ { text: "Acercarte al Tano", next: 'tano_info', effect: { street: 5 }, flag: 'talked_to_tano' },
899
+ { text: "Seguir hacia el kiosco de Carmelo", next: 'kiosco_direct', effect: { fuerza: 3 } },
900
+ { text: "Volver a casa primero", next: 'mama_breakfast', effect: { karma: 3 } }
901
+ ]
902
+ }
903
+ ]
904
+ },
905
+ // Arc 2: The Job Offer
906
+ trabajo: {
907
+ scenes: [
908
+ {
909
+ id: 'mama_explains',
910
+ narrative: `<p>Tu madre baja la voz, aunque estén solos. Vieja costumbre de la villa: las paredes escuchan.</p>
911
+ <p>"Es para llevar un sobre a Palermo. Nada más. 500 pesos."</p>
912
+ <p>500 pesos. Lo que ella gana en una semana limpiando casas. Por llevar un sobre.</p>
913
+ <p><span class="important">Las cosas fáciles nunca son fáciles de verdad.</span></p>`,
914
+ image_prompt: "close up of worried mother face, humble kitchen, dramatic lighting, emotional conversation",
915
+ choices: [
916
+ { text: '"Voy a hablar con Carmelo"', next: 'kiosco_informed', effect: { mente: 5 }, flag: 'knows_about_job' },
917
+ { text: '"¿Y si es peligroso?"', next: 'mama_warns', effect: { mente: 10 } },
918
+ { text: '"500 pesos es mucha plata..."', next: 'tempted', effect: { plata: 10 }, flag: 'tempted_by_money' }
919
+ ]
920
+ },
921
+ {
922
+ id: 'mama_continues',
923
+ narrative: `<p>Ella sigue hablando, más para sí misma que para vos. "Tu padre también empezó así. Un trabajo fácil, decían. Llevá esto de acá para allá."</p>
924
+ <p>Se le quiebra la voz. No hace falta que termine. Los dos saben cómo terminó tu viejo.</p>
925
+ <p>Pero también saben que la medicina de ella se está acabando. Y la plata no alcanza.</p>`,
926
+ image_prompt: "elderly woman crying softly, kitchen table, mate cup, morning light through window, emotional",
927
+ choices: [
928
+ { text: "Abrazarla sin decir nada", next: 'comfort_mama', effect: { karma: 15 }, flag: 'comforted_mama' },
929
+ { text: '"Voy a cuidarme, ma. Te lo prometo"', next: 'promise_made', effect: { karma: 10, fuerza: 5 }, flag: 'promised_mama' },
930
+ { text: '"Necesitamos esa plata"', next: 'reality_check', effect: { mente: 5, karma: -5 } }
931
+ ]
932
+ },
933
+ {
934
+ id: 'tano_info',
935
+ narrative: `<p>El Tano te mira de costado mientras tira el pucho. "Che, ¿sabías que Carmelo está moviendo cosas otra vez?"</p>
936
+ <p>Se acerca, baja la voz. "Ayer llegaron dos tipos en un auto negro. Hablaron con él una hora. Después se fue a tu casa."</p>
937
+ <p>El Tano sabe todo lo que pasa en la villa. Si él te está contando esto, es porque quiere que sepas.</p>`,
938
+ image_prompt: "two young men talking secretively in slum alley, morning shadows, cyberpunk Buenos Aires, tense atmosphere",
939
+ choices: [
940
+ { text: '"¿Qué tipo de cosas?"', next: 'tano_details', effect: { mente: 10, street: 5 }, flag: 'tano_told_truth' },
941
+ { text: '"¿Por qué me contás esto?"', next: 'tano_motives', effect: { mente: 15 } },
942
+ { text: "Agradecerle e ir a ver a Carmelo", next: 'kiosco_warned', effect: { fuerza: 5 }, flag: 'warned_by_tano' }
943
+ ]
944
+ }
945
+ ]
946
+ },
947
+ // Arc 3: The Kiosco
948
+ kiosco: {
949
+ scenes: [
950
+ {
951
+ id: 'kiosco_informed',
952
+ requires: 'knows_about_job',
953
+ narrative: `<p>El kiosco de Don Carmelo está donde siempre, entre el pasaje tres y la cancha de fútbol. La persiana a medio abrir, como invitando solo a los que saben.</p>
954
+ <p>Cuando entrás, Carmelo levanta la vista. Su ojo mecánico brilla un segundo antes de reconocerte.</p>
955
+ <p>"Ah, Martín. Tu madre te contó." No es una pregunta.</p>`,
956
+ image_prompt: "small kiosk shop in Buenos Aires slum, old man with cybernetic eye behind counter, morning light, noir atmosphere",
957
+ choices: [
958
+ { text: '"Quiero saber todo antes de aceptar"', next: 'carmelo_explains_full', effect: { mente: 10 } },
959
+ { text: '"Estoy adentro. ¿Qué hay que hacer?"', next: 'accept_job', effect: { fuerza: 10, karma: -10 }, flag: 'accepted_blindly' },
960
+ { text: '"Mi vieja está preocupada"', next: 'carmelo_reassures', effect: { karma: 5 } }
961
+ ]
962
+ },
963
+ {
964
+ id: 'kiosco_warned',
965
+ requires: 'warned_by_tano',
966
+ narrative: `<p>Entrás al kiosco con los ojos más abiertos que de costumbre. Lo que te dijo El Tano te da vueltas en la cabeza.</p>
967
+ <p>Carmelo levanta la vista. Por un segundo, algo cruza su cara. <span class="thought">¿Sorpresa? ¿Preocupación?</span></p>
968
+ <p>"Martín. Llegás temprano hoy."</p>`,
969
+ image_prompt: "kiosk shop Buenos Aires slum, old man looking surprised, young man entering, tense atmosphere, cyberpunk",
970
+ choices: [
971
+ { text: '"¿Quiénes eran los del auto negro?"', next: 'confront_carmelo', effect: { mente: 15, fuerza: 5 }, flag: 'confronted_carmelo' },
972
+ { text: "Hacerte el tonto y ver qué dice", next: 'play_dumb', effect: { mente: 10 } },
973
+ { text: '"El Tano me contó todo"', next: 'reveal_source', effect: { karma: -5 }, flag: 'betrayed_tano' }
974
+ ]
975
+ },
976
+ {
977
+ id: 'kiosco_direct',
978
+ narrative: `<p>El kiosco de Don Carmelo parece dormido todavía. La persiana está más baja de lo normal.</p>
979
+ <p>Tocás la chapa dos veces, como siempre. Un silencio largo. Después, la voz ronca desde adentro: "Pasá, Martín."</p>
980
+ <p>Adentro está más oscuro que de costumbre. Carmelo te espera sentado, con un sobre manila en las manos.</p>`,
981
+ image_prompt: "dark interior kiosk shop, old man sitting with manila envelope, mysterious atmosphere, Buenos Aires slum",
982
+ choices: [
983
+ { text: '"¿Qué es eso?"', next: 'the_envelope', effect: { mente: 5 } },
984
+ { text: "Sentarte enfrente sin decir nada", next: 'silent_wait', effect: { fuerza: 5 } },
985
+ { text: '"Algo anda mal. Lo noto."', next: 'sense_danger', effect: { mente: 10, fuerza: 5 }, flag: 'sensed_danger' }
986
+ ]
987
+ }
988
+ ]
989
+ }
990
+ };
991
+
992
+ // SCENE CONTINUITY - Track what happened
993
+ const FLAGS = new Set();
994
+ let currentArc = 'despertar';
995
+ let currentSceneId = 'wake_up';
996
+ let sceneHistory = [];
997
+
998
+ // Find scene by ID across all arcs
999
+ function findScene(sceneId) {
1000
+ for (const [arcName, arc] of Object.entries(STORY_ARCS)) {
1001
+ const scene = arc.scenes.find(s => s.id === sceneId);
1002
+ if (scene) return { scene, arcName };
1003
+ }
1004
+ return null;
1005
+ }
1006
+
1007
+ // Check if scene requirements are met
1008
+ function canAccessScene(scene) {
1009
+ if (!scene.requires) return true;
1010
+ return FLAGS.has(scene.requires);
1011
+ }
1012
+
1013
+ // Get current scene
1014
+ function getCurrentScene() {
1015
+ const result = findScene(currentSceneId);
1016
+ if (result && canAccessScene(result.scene)) {
1017
+ return result.scene;
1018
+ }
1019
+ // Fallback to first scene
1020
+ return STORY_ARCS.despertar.scenes[0];
1021
+ }
1022
+
1023
+ // Choice generators now use context
1024
+ function generateContextualChoices(scene) {
1025
+ // Add context-aware modifications based on flags
1026
+ let choices = [...scene.choices];
1027
+
1028
+ // Modify choices based on previous decisions
1029
+ if (FLAGS.has('warned_by_tano') && !FLAGS.has('confronted_carmelo')) {
1030
+ // Add extra caution option
1031
+ const cautionChoice = choices.find(c => c.text.includes('cuidado') || c.text.includes('peligro'));
1032
+ if (cautionChoice) cautionChoice.effect.mente = (cautionChoice.effect.mente || 0) + 5;
1033
+ }
1034
+
1035
+ if (FLAGS.has('promised_mama')) {
1036
+ // Karma bonus for keeping promise
1037
+ choices.forEach(c => {
1038
+ if (c.effect.karma && c.effect.karma > 0) c.effect.karma += 5;
1039
+ });
1040
+ }
1041
+
1042
+ return choices;
1043
+ }
1044
+
1045
+ // Game State
1046
+ const STATE = {
1047
+ day: 1,
1048
+ timeOfDay: 'morning',
1049
+ location: LOCATIONS[0],
1050
+ stats: { fuerza: 50, mente: 50, plata: 150, karma: 50 },
1051
+ streak: 0,
1052
+ history: [],
1053
+ lastCharacter: null,
1054
+ musicEnabled: false,
1055
+ sfxEnabled: true,
1056
+ weather: 'clear',
1057
+ relationships: {
1058
+ 'Mamá Rosa': { level: 50, avatar: '👩' },
1059
+ 'El Tano': { level: 30, avatar: '👦' },
1060
+ 'Don Carmelo': { level: 40, avatar: '🧔' }
1061
+ },
1062
+ achievements: [],
1063
+ totalChoices: 0
1064
+ };
1065
+
1066
+ // Weather System
1067
+ const WEATHER_TYPES = [
1068
+ { name: 'clear', icon: '☀️', chance: 0.4 },
1069
+ { name: 'cloudy', icon: '☁️', chance: 0.25 },
1070
+ { name: 'rain', icon: '🌧️', chance: 0.2 },
1071
+ { name: 'storm', icon: '⛈️', chance: 0.1 },
1072
+ { name: 'fog', icon: '🌫️', chance: 0.05 }
1073
+ ];
1074
+
1075
+ // Achievements
1076
+ const ACHIEVEMENTS = [
1077
+ { id: 'first_choice', name: 'Primer Paso', icon: '👣', condition: () => STATE.totalChoices >= 1 },
1078
+ { id: 'survivor', name: 'Sobreviviente', icon: '💪', condition: () => STATE.day >= 7 },
1079
+ { id: 'wealthy', name: 'Billetera Gorda', icon: '💰', condition: () => STATE.stats.plata >= 500 },
1080
+ { id: 'saint', name: 'Santo', icon: '😇', condition: () => STATE.stats.karma >= 90 },
1081
+ { id: 'villain', name: 'Villano', icon: '😈', condition: () => STATE.stats.karma <= 10 },
1082
+ { id: 'streak_5', name: 'Racha de Fuego', icon: '🔥', condition: () => STATE.streak >= 5 },
1083
+ { id: 'streak_10', name: 'Imparable', icon: '⚡', condition: () => STATE.streak >= 10 },
1084
+ { id: 'strong', name: 'Fuerza Bruta', icon: '💪', condition: () => STATE.stats.fuerza >= 80 },
1085
+ { id: 'genius', name: 'Genio', icon: '🧠', condition: () => STATE.stats.mente >= 80 },
1086
+ { id: 'friend', name: 'Buen Amigo', icon: '🤝', condition: () => Object.values(STATE.relationships).some(r => r.level >= 80) },
1087
+ { id: 'explorer', name: 'Explorador', icon: '🗺️', condition: () => STATE.history.length >= 20 }
1088
+ ];
1089
+
1090
+ // More Literary Quotes
1091
+ const QUOTES = [
1092
+ { text: "No te rindas, aún estás a tiempo de alcanzar y comenzar de nuevo.", author: "Mario Benedetti" },
1093
+ { text: "El olvido está lleno de memoria.", author: "Mario Benedetti" },
1094
+ { text: "Después de todo, la muerte es sólo un síntoma de que hubo vida.", author: "Mario Benedetti" },
1095
+ { text: "Te quiero en mi paraíso, es decir, en mi país de cada día.", author: "Mario Benedetti" },
1096
+ { text: "Somos mucho más que dos.", author: "Mario Benedetti" },
1097
+ { text: "No te salves, no te llenes de calma, no reserves del mundo sólo un rincón tranquilo.", author: "Mario Benedetti" },
1098
+ { text: "Tal vez estamos ciegos. Ciegos de ver.", author: "José Saramago" },
1099
+ { text: "Andá a saber si uno es lo que hace o lo que cree que hace.", author: "Julio Cortázar" },
1100
+ { text: "Nada se pierde si se tiene el coraje de proclamar que todo está perdido y hay que empezar de nuevo.", author: "Julio Cortázar" },
1101
+ { text: "La esperanza no es la convicción de que algo saldrá bien, sino la certeza de que algo tiene sentido.", author: "Václav Havel" },
1102
+ { text: "Lo que no nos mata nos hace más fuertes.", author: "Friedrich Nietzsche" },
1103
+ { text: "En medio de la dificultad reside la oportunidad.", author: "Albert Einstein" },
1104
+ { text: "Hay quienes luchan un día y son buenos, hay quienes luchan muchos días y son muy buenos, pero están los que luchan toda la vida, esos son los imprescindibles.", author: "Bertolt Brecht" },
1105
+ { text: "Uno no es lo que es por lo que escribe, sino por lo que ha leído.", author: "Jorge Luis Borges" },
1106
+ { text: "El único modo de combatir la peste es la honradez.", author: "Albert Camus" },
1107
+ { text: "Si he visto más lejos es porque estoy sentado sobre los hombros de gigantes.", author: "Isaac Newton" },
1108
+ { text: "La peor lucha es la que no se hace.", author: "Anónimo" },
1109
+ { text: "Donde muere una esperanza, nace otra.", author: "Anónimo argentino" }
1110
+ ];
1111
+
1112
+ // Create Weather
1113
+ function updateWeather() {
1114
+ const rand = Math.random();
1115
+ let cumulative = 0;
1116
+ for (const w of WEATHER_TYPES) {
1117
+ cumulative += w.chance;
1118
+ if (rand <= cumulative) {
1119
+ STATE.weather = w.name;
1120
+ document.getElementById('weather-indicator').textContent = w.icon;
1121
+ break;
1122
+ }
1123
+ }
1124
+
1125
+ const container = document.getElementById('weather-container');
1126
+ container.innerHTML = '';
1127
+
1128
+ if (STATE.weather === 'rain' || STATE.weather === 'storm') {
1129
+ for (let i = 0; i < 100; i++) {
1130
+ const drop = document.createElement('div');
1131
+ drop.className = 'rain';
1132
+ drop.style.left = Math.random() * 100 + '%';
1133
+ drop.style.animationDuration = (0.5 + Math.random() * 0.5) + 's';
1134
+ drop.style.animationDelay = Math.random() * 2 + 's';
1135
+ container.appendChild(drop);
1136
+ }
1137
+ }
1138
+
1139
+ if (STATE.weather === 'storm') {
1140
+ setInterval(() => {
1141
+ if (Math.random() > 0.7) {
1142
+ const lightning = document.getElementById('lightning');
1143
+ lightning.classList.add('flash');
1144
+ setTimeout(() => lightning.classList.remove('flash'), 200);
1145
+ }
1146
+ }, 3000);
1147
+ }
1148
+ }
1149
+
1150
+ // Check Achievements
1151
+ function checkAchievements() {
1152
+ for (const ach of ACHIEVEMENTS) {
1153
+ if (!STATE.achievements.includes(ach.id) && ach.condition()) {
1154
+ STATE.achievements.push(ach.id);
1155
+ showAchievement(ach);
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ function showAchievement(ach) {
1161
+ const popup = document.getElementById('achievement-popup');
1162
+ popup.querySelector('.achievement-icon').textContent = ach.icon;
1163
+ document.getElementById('achievement-name').textContent = ach.name;
1164
+ popup.classList.add('show');
1165
+ setTimeout(() => popup.classList.remove('show'), 4000);
1166
+ }
1167
+
1168
+ // Update Relationships Display
1169
+ function updateRelationships() {
1170
+ const bar = document.getElementById('relationships-bar');
1171
+ bar.innerHTML = Object.entries(STATE.relationships).map(([name, data]) => `
1172
+ <div class="rel-chip">
1173
+ <span class="avatar">${data.avatar}</span>
1174
+ <span class="name">${name.split(' ')[0]}</span>
1175
+ <span class="level">${data.level}</span>
1176
+ </div>
1177
+ `).join('');
1178
+ }
1179
+
1180
+ // Initialize random quote
1181
+ function setRandomQuote() {
1182
+ const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
1183
+ document.getElementById('quote-text').textContent = `"${quote.text}"`;
1184
+ document.getElementById('quote-author').textContent = `— ${quote.author}`;
1185
+ }
1186
+
1187
+ // Create particles
1188
+ function createParticles() {
1189
+ const container = document.getElementById('particles');
1190
+ for (let i = 0; i < 25; i++) {
1191
+ const p = document.createElement('div');
1192
+ p.className = 'particle';
1193
+ p.style.left = Math.random() * 100 + '%';
1194
+ p.style.animationDuration = (15 + Math.random() * 20) + 's';
1195
+ p.style.animationDelay = Math.random() * 20 + 's';
1196
+ container.appendChild(p);
1197
+ }
1198
+ }
1199
+
1200
+ // Generate unique narrative
1201
+ function generateNarrative() {
1202
+ const timeEvents = EVENT_TEMPLATES[STATE.timeOfDay] || EVENT_TEMPLATES.morning;
1203
+ const eventType = timeEvents[Math.floor(Math.random() * timeEvents.length)];
1204
+ const situation = eventType.situations[Math.floor(Math.random() * eventType.situations.length)];
1205
+
1206
+ const styles = Object.keys(NARRATIVE_STYLES);
1207
+ const style = styles[Math.floor(Math.random() * styles.length)];
1208
+
1209
+ return NARRATIVE_STYLES[style](situation, STATE.location);
1210
+ }
1211
+
1212
+ // Generate unique choices
1213
+ function generateChoices() {
1214
+ const types = Object.keys(CHOICE_GENERATORS);
1215
+ const type = types[Math.floor(Math.random() * types.length)];
1216
+ return CHOICE_GENERATORS[type]();
1217
+ }
1218
+
1219
+ // Generate character dialogue
1220
+ function generateCharacterMoment() {
1221
+ // 40% chance of character appearing
1222
+ if (Math.random() > 0.6) {
1223
+ const availableChars = CHARACTERS.filter(c => c !== STATE.lastCharacter);
1224
+ const char = availableChars[Math.floor(Math.random() * availableChars.length)];
1225
+ STATE.lastCharacter = char;
1226
+
1227
+ const dialogues = {
1228
+ family: [
1229
+ "Cuidate, ¿me escuchás? Cuidate.",
1230
+ "Vos sabés que siempre vas a tener un lugar acá.",
1231
+ "Tu viejo estaría orgulloso. O preocupado. Tal vez las dos cosas."
1232
+ ],
1233
+ friend: [
1234
+ "Che, ¿estás bien? Te noto raro.",
1235
+ "Mirá, yo no te voy a juzgar. Pero tené cuidado.",
1236
+ "La calle habla, ¿viste? Y últimamente habla de vos."
1237
+ ],
1238
+ mentor: [
1239
+ "Las decisiones que tomes hoy van a resonar mañana.",
1240
+ "Yo vi mucho, pibe. Más de lo que quisiera. Aprendé de mis errores.",
1241
+ "La paciencia es más peligrosa que la fuerza, si sabés usarla."
1242
+ ],
1243
+ ally: [
1244
+ "Tengo información. Pero te va a costar.",
1245
+ "No confíes en nadie. Excepto en mí. Tal vez.",
1246
+ "Las cosas se están moviendo. Tenés que elegir un lado."
1247
+ ],
1248
+ danger: [
1249
+ "Qué sorpresa verte por acá. ¿O no es sorpresa?",
1250
+ "Dicen que sabés cosas. Cosas que valen plata.",
1251
+ "Todos tenemos un precio. El tuyo, ¿cuál es?"
1252
+ ],
1253
+ help: [
1254
+ "Vengo viendo mucha gente lastimada. No quiero verte a vos.",
1255
+ "La villa necesita gente como vos. Gente que piensa.",
1256
+ "¿Estás durmiendo bien? Se te nota en la cara."
1257
+ ],
1258
+ mystery: [
1259
+ "No nos conocemos. Pero yo te conozco a vos.",
1260
+ "Hay una propuesta. Pensala bien antes de responder.",
1261
+ "El juego cambió. ¿Estás listo para jugar?"
1262
+ ]
1263
+ };
1264
+
1265
+ const typeDialogues = dialogues[char.type] || dialogues.mystery;
1266
+ const text = typeDialogues[Math.floor(Math.random() * typeDialogues.length)];
1267
+
1268
+ return { ...char, text };
1269
+ }
1270
+ return null;
1271
+ }
1272
+
1273
+ // Generate scene image using Pollinations.ai (free, no key needed)
1274
+ let currentImagePrompt = '';
1275
+
1276
+ async function generateSceneImage(prompt) {
1277
+ const imageEl = document.getElementById('scene-image');
1278
+ const loadingEl = document.getElementById('image-loading');
1279
+
1280
+ imageEl.classList.remove('loaded');
1281
+ loadingEl.classList.remove('hidden');
1282
+
1283
+ currentImagePrompt = prompt || 'cyberpunk Buenos Aires slum, cinematic, atmospheric';
1284
+
1285
+ // Use Pollinations.ai - free AI image generation
1286
+ const encodedPrompt = encodeURIComponent(currentImagePrompt);
1287
+ const seed = Math.floor(Math.random() * 999999); // Random seed for variety
1288
+ const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=800&height=450&seed=${seed}&nologo=true`;
1289
+
1290
+ imageEl.src = imageUrl;
1291
+
1292
+ imageEl.onload = () => {
1293
+ imageEl.classList.add('loaded');
1294
+ loadingEl.classList.add('hidden');
1295
+ };
1296
+
1297
+ imageEl.onerror = () => {
1298
+ // Fallback to SVG
1299
+ imageEl.src = generateFallbackImage();
1300
+ imageEl.classList.add('loaded');
1301
+ loadingEl.classList.add('hidden');
1302
+ };
1303
+ }
1304
+
1305
+ // Generate fallback SVG image
1306
+ function generateFallbackImage() {
1307
+ const colors = ['#2a1a3a', '#ff2a6d', '#05d9e8'];
1308
+ const svg = `
1309
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
1310
+ <defs>
1311
+ <linearGradient id="sky" x1="0%" y1="0%" x2="0%" y2="100%">
1312
+ <stop offset="0%" style="stop-color:${colors[0]}"/>
1313
+ <stop offset="100%" style="stop-color:#0a0a0f"/>
1314
+ </linearGradient>
1315
+ </defs>
1316
+ <rect width="100%" height="100%" fill="url(#sky)"/>
1317
+ ${Array.from({ length: 15 }, (_, i) => {
1318
+ const x = i * 55 - 20 + Math.random() * 30;
1319
+ const h = 80 + Math.random() * 200;
1320
+ const w = 30 + Math.random() * 25;
1321
+ return `<rect x="${x}" y="${450 - h}" width="${w}" height="${h}" fill="${colors[0]}" opacity="0.8"/>
1322
+ ${Math.random() > 0.5 ? `<rect x="${x + 5}" y="${450 - h + 10}" width="3" height="3" fill="${colors[1]}" opacity="0.8"/>` : ''}`;
1323
+ }).join('')}
1324
+ <rect x="0" y="445" width="800" height="5" fill="${colors[1]}" opacity="0.4"/>
1325
+ </svg>`;
1326
+ return 'data:image/svg+xml;base64,' + btoa(svg);
1327
+ }
1328
+
1329
+ // Update UI
1330
+ function updateUI() {
1331
+ document.getElementById('day-number').textContent = `DÍA ${STATE.day}`;
1332
+ document.getElementById('scene-location').textContent = STATE.location.name;
1333
+ document.getElementById('scene-time').textContent = getTimeString();
1334
+ document.getElementById('streak-count').textContent = STATE.streak;
1335
+
1336
+ document.getElementById('stat-fuerza').textContent = STATE.stats.fuerza;
1337
+ document.getElementById('stat-mente').textContent = STATE.stats.mente;
1338
+ document.getElementById('stat-plata').textContent = STATE.stats.plata;
1339
+ document.getElementById('stat-karma').textContent = STATE.stats.karma;
1340
+ }
1341
+
1342
+ function getTimeString() {
1343
+ const times = {
1344
+ morning: `${6 + Math.floor(Math.random() * 3)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} AM`,
1345
+ day: `${12 + Math.floor(Math.random() * 5)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1346
+ sunset: `${18 + Math.floor(Math.random() * 2)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1347
+ night: `${21 + Math.floor(Math.random() * 3)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} PM`,
1348
+ rush: `${7 + Math.floor(Math.random() * 2)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')} AM`
1349
+ };
1350
+ return times[STATE.location.time] || times.day;
1351
+ }
1352
+
1353
+ // Typewriter effect for narrative
1354
+ async function typewriterEffect(element, html, speed = 15) {
1355
+ const tempDiv = document.createElement('div');
1356
+ tempDiv.innerHTML = html;
1357
+ const text = tempDiv.textContent;
1358
+
1359
+ element.innerHTML = '';
1360
+ element.style.opacity = '1';
1361
+
1362
+ let i = 0;
1363
+ return new Promise(resolve => {
1364
+ function type() {
1365
+ if (i < text.length) {
1366
+ element.innerHTML = html.substring(0, element.innerHTML.length + 1);
1367
+ // Actually just show progressively
1368
+ const progress = i / text.length;
1369
+ element.innerHTML = html;
1370
+ element.style.clipPath = `inset(0 ${100 - progress * 100}% 0 0)`;
1371
+ i += 3;
1372
+ setTimeout(type, speed);
1373
+ } else {
1374
+ element.style.clipPath = 'none';
1375
+ resolve();
1376
+ }
1377
+ }
1378
+ type();
1379
+ });
1380
+ }
1381
+
1382
+ // Generate new scene using coherent story arcs
1383
+ async function generateScene() {
1384
+ const scene = getCurrentScene();
1385
+
1386
+ // Update location based on scene context
1387
+ if (scene.id.includes('kiosco')) {
1388
+ STATE.location = LOCATIONS.find(l => l.mood === 'slum') || LOCATIONS[0];
1389
+ }
1390
+
1391
+ updateUI();
1392
+
1393
+ // Show narrative with typing effect
1394
+ const narrativeEl = document.getElementById('narrative');
1395
+ narrativeEl.innerHTML = scene.narrative;
1396
+
1397
+ // Add reading time indicator
1398
+ const wordCount = scene.narrative.replace(/<[^>]*>/g, '').split(/\s+/).length;
1399
+ const readingTime = Math.ceil(wordCount / 200 * 60); // seconds at 200 wpm
1400
+
1401
+ // Hide character speech for now (integrated into narrative)
1402
+ document.getElementById('character-speech').style.display = 'none';
1403
+
1404
+ // Generate choices from scene
1405
+ const choices = generateContextualChoices(scene);
1406
+ const choicesContainer = document.getElementById('choices');
1407
+
1408
+ // Show choices with delay for reading
1409
+ choicesContainer.innerHTML = '';
1410
+
1411
+ setTimeout(() => {
1412
+ choicesContainer.innerHTML = choices.map((choice, i) => `
1413
+ <button class="choice-btn" data-index="${i}" data-next="${choice.next || ''}" style="animation: fadeInUp 0.5s ease ${i * 0.15}s both;">
1414
+ ${choice.text}
1415
+ <div class="choice-stats">
1416
+ ${Object.entries(choice.effect).map(([stat, val]) =>
1417
+ `<span class="stat-change ${val >= 0 ? 'positive' : 'negative'}">${val >= 0 ? '+' : ''}${val}</span>`
1418
+ ).join('')}
1419
+ </div>
1420
+ </button>
1421
+ `).join('');
1422
+
1423
+ // Add click handlers
1424
+ choicesContainer.querySelectorAll('.choice-btn').forEach((btn, i) => {
1425
+ btn.addEventListener('click', () => makeChoice(choices[i]));
1426
+ });
1427
+ }, Math.min(readingTime * 300, 2000)); // Wait based on reading time, max 2s
1428
+
1429
+ // Generate image using scene's specific prompt
1430
+ await generateSceneImage(scene.image_prompt);
1431
+
1432
+ // Record scene in history
1433
+ sceneHistory.push(currentSceneId);
1434
+ }
1435
+
1436
+ // Make a choice
1437
+ async function makeChoice(choice) {
1438
+ if (STATE.sfxEnabled) {
1439
+ document.getElementById('click-sfx').currentTime = 0;
1440
+ document.getElementById('click-sfx').play().catch(() => { });
1441
+ }
1442
+
1443
+ // Apply effects
1444
+ for (const [stat, value] of Object.entries(choice.effect)) {
1445
+ if (STATE.stats[stat] !== undefined) {
1446
+ STATE.stats[stat] = Math.max(0, Math.min(100, STATE.stats[stat] + value));
1447
+ }
1448
+ }
1449
+
1450
+ // Set flag if choice has one
1451
+ if (choice.flag) {
1452
+ FLAGS.add(choice.flag);
1453
+ }
1454
+
1455
+ // Navigate to next scene
1456
+ if (choice.next) {
1457
+ const nextScene = findScene(choice.next);
1458
+ if (nextScene) {
1459
+ currentSceneId = choice.next;
1460
+ currentArc = nextScene.arcName;
1461
+ } else {
1462
+ // If scene not found, stay in current arc but generate continuation
1463
+ console.log('Scene not found:', choice.next, '- generating continuation');
1464
+ // Add the missing scene dynamically or fallback
1465
+ }
1466
+ }
1467
+
1468
+ // Record history
1469
+ STATE.history.push({
1470
+ day: STATE.day,
1471
+ choice: choice.text,
1472
+ location: STATE.location.name,
1473
+ sceneId: currentSceneId
1474
+ });
1475
+
1476
+ // Streak and day advancement
1477
+ STATE.streak++;
1478
+ STATE.totalChoices++;
1479
+
1480
+ // Advance time occasionally
1481
+ if (STATE.totalChoices % 3 === 0) {
1482
+ const times = ['morning', 'day', 'sunset', 'night'];
1483
+ const currentIndex = times.indexOf(STATE.timeOfDay);
1484
+ STATE.timeOfDay = times[(currentIndex + 1) % times.length];
1485
+ STATE.location.time = STATE.timeOfDay;
1486
+
1487
+ if (STATE.timeOfDay === 'morning') {
1488
+ STATE.day++;
1489
+ updateWeather();
1490
+ }
1491
+ }
1492
+
1493
+ // Update relationships based on scene context
1494
+ if (currentSceneId.includes('mama') || currentSceneId.includes('comfort')) {
1495
+ STATE.relationships['Mamá Rosa'].level = Math.min(100, STATE.relationships['Mamá Rosa'].level + 10);
1496
+ }
1497
+ if (currentSceneId.includes('tano')) {
1498
+ STATE.relationships['El Tano'].level = Math.min(100, STATE.relationships['El Tano'].level + 8);
1499
+ }
1500
+ if (currentSceneId.includes('carmelo') || currentSceneId.includes('kiosco')) {
1501
+ STATE.relationships['Don Carmelo'].level = Math.min(100, STATE.relationships['Don Carmelo'].level + 5);
1502
+ }
1503
+ updateRelationships();
1504
+
1505
+ // Check achievements
1506
+ checkAchievements();
1507
+
1508
+ // Show contextual notification
1509
+ const notifications = {
1510
+ 'talked_to_mama': 'Tu madre siempre sabe más de lo que dice.',
1511
+ 'skipped_breakfast': 'El hambre aguza los sentidos.',
1512
+ 'asked_about_job': 'Las preguntas correctas abren puertas.',
1513
+ 'refused_carmelo': 'La dignidad tiene un precio.',
1514
+ 'comforted_mama': 'A veces las palabras sobran.',
1515
+ 'promised_mama': 'Las promesas pesan.',
1516
+ 'talked_to_tano': 'El Tano siempre sabe algo.',
1517
+ 'warned_by_tano': 'Ahora sabés más de lo que querías.',
1518
+ 'knows_about_job': 'La información es poder.',
1519
+ 'confronted_carmelo': 'La verdad incomoda.',
1520
+ 'sensed_danger': 'El instinto no miente.'
1521
+ };
1522
+
1523
+ const notification = choice.flag && notifications[choice.flag]
1524
+ ? notifications[choice.flag]
1525
+ : '→ Tu decisión dejó una marca.';
1526
+ showNotification(notification);
1527
+
1528
+ // Generate new scene
1529
+ await generateScene();
1530
+
1531
+ // Smooth scroll to narrative
1532
+ document.getElementById('narrative').scrollIntoView({ behavior: 'smooth', block: 'start' });
1533
+
1534
+ // Check for minigame chance (10%)
1535
+ if (Math.random() > 0.9 && STATE.totalChoices > 2) {
1536
+ setTimeout(() => showMinigame(), 1500);
1537
+ }
1538
+ }
1539
+
1540
+ // Show notification
1541
+ function showNotification(text) {
1542
+ const notif = document.getElementById('notification');
1543
+ document.getElementById('notif-text').textContent = text;
1544
+ notif.classList.add('show');
1545
+ setTimeout(() => notif.classList.remove('show'), 3000);
1546
+ }
1547
+
1548
+ // Minigame
1549
+ function showMinigame() {
1550
+ const modal = document.getElementById('minigame-modal');
1551
+ const content = document.getElementById('minigame-content');
1552
+
1553
+ const games = [
1554
+ {
1555
+ name: 'Reacción Rápida',
1556
+ html: `
1557
+ <h2>⚡ REACCIÓN RÁPIDA</h2>
1558
+ <p>Cuando el cuadro se ponga verde, hacé click lo más rápido posible.</p>
1559
+ <div id="reaction-box" style="width:200px;height:200px;background:#ff4757;margin:20px auto;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:2rem;">ESPERÁ</div>
1560
+ <button onclick="closeMinigame()" style="margin-top:20px;padding:10px 20px;background:var(--accent);border:none;border-radius:5px;color:white;cursor:pointer;">Saltar</button>
1561
+ `,
1562
+ init: () => {
1563
+ const box = document.getElementById('reaction-box');
1564
+ let startTime;
1565
+ let canClick = false;
1566
+
1567
+ setTimeout(() => {
1568
+ box.style.background = '#2ed573';
1569
+ box.textContent = 'AHORA!';
1570
+ startTime = Date.now();
1571
+ canClick = true;
1572
+ }, 2000 + Math.random() * 3000);
1573
+
1574
+ box.onclick = () => {
1575
+ if (!canClick) return;
1576
+ const time = Date.now() - startTime;
1577
+ if (time < 300) {
1578
+ STATE.stats.fuerza += 10;
1579
+ showNotification('🎯 Reflejos increíbles! +10 Fuerza');
1580
+ } else if (time < 500) {
1581
+ STATE.stats.fuerza += 5;
1582
+ showNotification('✓ Buenos reflejos! +5 Fuerza');
1583
+ }
1584
+ closeMinigame();
1585
+ };
1586
+ }
1587
+ },
1588
+ {
1589
+ name: 'Memoria',
1590
+ html: `
1591
+ <h2>🧠 MEMORIA</h2>
1592
+ <p>Recordá la secuencia de números.</p>
1593
+ <div id="memory-display" style="font-size:3rem;letter-spacing:10px;margin:20px 0;font-family:'Space Mono',monospace;"></div>
1594
+ <input type="text" id="memory-input" style="font-size:1.5rem;padding:10px;width:200px;text-align:center;background:var(--surface);border:1px solid var(--accent2);color:white;border-radius:5px;" placeholder="Tu respuesta">
1595
+ <br><button id="memory-submit" style="margin-top:15px;padding:10px 20px;background:var(--accent2);border:none;border-radius:5px;color:black;cursor:pointer;">Verificar</button>
1596
+ `,
1597
+ init: () => {
1598
+ const sequence = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10)).join('');
1599
+ const display = document.getElementById('memory-display');
1600
+ display.textContent = sequence;
1601
+
1602
+ setTimeout(() => {
1603
+ display.textContent = '????';
1604
+ document.getElementById('memory-input').focus();
1605
+ }, 2000);
1606
+
1607
+ document.getElementById('memory-submit').onclick = () => {
1608
+ const answer = document.getElementById('memory-input').value;
1609
+ if (answer === sequence) {
1610
+ STATE.stats.mente += 10;
1611
+ showNotification('🧠 Memoria perfecta! +10 Mente');
1612
+ } else {
1613
+ showNotification('Casi... Era ' + sequence);
1614
+ }
1615
+ closeMinigame();
1616
+ };
1617
+ }
1618
+ }
1619
+ ];
1620
+
1621
+ const game = games[Math.floor(Math.random() * games.length)];
1622
+ content.innerHTML = game.html;
1623
+ modal.classList.add('active');
1624
+ game.init();
1625
+ }
1626
+
1627
+ function closeMinigame() {
1628
+ document.getElementById('minigame-modal').classList.remove('active');
1629
+ }
1630
+
1631
+ // Audio controls
1632
+ function setupAudio() {
1633
+ const musicBtn = document.getElementById('music-btn');
1634
+ const sfxBtn = document.getElementById('sfx-btn');
1635
+ const music = document.getElementById('ambient-music');
1636
+ music.volume = 0.3;
1637
+
1638
+ musicBtn.addEventListener('click', () => {
1639
+ STATE.musicEnabled = !STATE.musicEnabled;
1640
+ musicBtn.classList.toggle('active', STATE.musicEnabled);
1641
+ if (STATE.musicEnabled) {
1642
+ music.play().catch(() => { });
1643
+ } else {
1644
+ music.pause();
1645
+ }
1646
+ });
1647
+
1648
+ sfxBtn.addEventListener('click', () => {
1649
+ STATE.sfxEnabled = !STATE.sfxEnabled;
1650
+ sfxBtn.classList.toggle('active', STATE.sfxEnabled);
1651
+ });
1652
+
1653
+ sfxBtn.classList.add('active');
1654
+ }
1655
+
1656
+ // Initialize
1657
+ async function init() {
1658
+ setRandomQuote();
1659
+ createParticles();
1660
+ setupAudio();
1661
+ updateWeather();
1662
+ updateRelationships();
1663
+
1664
+ // Wait for loading
1665
+ await new Promise(r => setTimeout(r, 3000));
1666
+
1667
+ document.getElementById('loading-screen').classList.add('hidden');
1668
+ document.getElementById('game-container').classList.add('active');
1669
+
1670
+ await generateScene();
1671
+ }
1672
+
1673
+ document.addEventListener('DOMContentLoaded', init);
1674
+ </script>
1675
+ </body>
1676
+
1677
+ </html>
run_game.bat ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ==========================================
3
+ echo INICIANDO RENACER: VILLA CITY STORIES
4
+ echo ==========================================
5
+ echo.
6
+
7
+ :: Try 'python' first
8
+ python app.py
9
+ if %ERRORLEVEL% EQU 0 goto :end
10
+
11
+ echo.
12
+ echo 'python' command not found or failed. Trying 'py' launcher...
13
+ echo.
14
+
15
+ :: Try 'py' launcher
16
+ py app.py
17
+ if %ERRORLEVEL% EQU 0 goto :end
18
+
19
+ echo.
20
+ echo ==========================================
21
+ echo ERROR: No se pudo encontrar Python.
22
+ echo Por favor asegurate de tener Python instalado.
23
+ echo Podes descargarlo en https://www.python.org/downloads/
24
+ echo ==========================================
25
+ pause
26
+
27
+ :end
28
+ pause