-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVisionBreaker.py
More file actions
1097 lines (888 loc) · 34.2 KB
/
VisionBreaker.py
File metadata and controls
1097 lines (888 loc) · 34.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import pygame
import random
import os
import sys
import math
# ================== INIT ==================
pygame.init()
pygame.mixer.init()
def resource_path(relative_path: str) -> str:
"""
Get absolute path to resource, works in dev and when bundled with PyInstaller.
"""
if hasattr(sys, "_MEIPASS"):
# When running from a PyInstaller bundle
base_path = sys._MEIPASS # type: ignore[attr-defined]
else:
# When running from source
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
ASSET_DIR = resource_path("assets")
MUSIC_FILE = os.path.join(ASSET_DIR, "codefall_ambience.ogg")
SFX_HACK_FILE = os.path.join(ASSET_DIR, "hack_confirm.wav")
SFX_BINARY_FILE = os.path.join(ASSET_DIR, "binary_toggle.wav")
SFX_ERROR_FILE = os.path.join(ASSET_DIR, "critical_error.wav")
music_loaded = False
sfx_hack = None
sfx_binary = None
sfx_error = None
def init_audio():
"""Try to load music and SFX; fail gracefully if files aren't there."""
global music_loaded, sfx_hack, sfx_binary, sfx_error
# Background music
try:
pygame.mixer.music.load(MUSIC_FILE)
pygame.mixer.music.set_volume(0.35)
pygame.mixer.music.play(-1) # loop forever
music_loaded = True
except Exception:
music_loaded = False
# Hack confirm SFX
try:
sfx_hack = pygame.mixer.Sound(SFX_HACK_FILE)
sfx_hack.set_volume(0.6)
except Exception:
sfx_hack = None
# Binary toggle SFX
try:
sfx_binary = pygame.mixer.Sound(SFX_BINARY_FILE)
sfx_binary.set_volume(0.5)
except Exception:
sfx_binary = None
# Critical error SFX
try:
sfx_error = pygame.mixer.Sound(SFX_ERROR_FILE)
sfx_error.set_volume(0.25)
except Exception:
sfx_error = None
# ---- Dynamic screen + fullscreen handling ----
DEFAULT_WINDOW_SIZE = (1300, 600)
info = pygame.display.Info()
FULLSCREEN_SIZE = (info.current_w, info.current_h)
# Start in fullscreen
fullscreen = True
WIDTH, HEIGHT = FULLSCREEN_SIZE
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
hack_input_mode = False
hack_buffer = ""
game_mode = "free" # "free" or "puzzle"
# Typewriter and glow state for puzzle line
puzzle_full_line = "" # full text including "PUZZLE x/y: ..."
puzzle_visible_chars = 0 # how many characters are currently revealed
puzzle_type_accum = 0.0 # accumulator for typewriter timing
PUZZLE_CHARS_PER_SEC = 40.0 # speed of typewriter (chars per second)
# Last frame delta time in ms for UI effects
last_dt_ms = 0
pygame.display.set_caption(
f"VisionBreaker: Neurogrid Terminal | mode={game_mode} hack={hack_input_mode}"
)
# Font Settings
FONT_SIZE = 28
font = pygame.font.Font(pygame.font.match_font("monospace"), FONT_SIZE)
big_font = pygame.font.Font(pygame.font.match_font("monospace"), 48)
# Character Pool (English + Katakana)
char_pool = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*カタカナ"
# Special words that occasionally rain down as vertical strings
SPECIAL_WORDS = [
# Core identity
"VISIONBREAKER",
"NEUROGRID TERMINAL",
"GRID ONLINE",
"GRID BREACH",
"NEURAL LINK",
# Theme-flavored
"EMERALD STREAM",
"RED ALERT",
"SYNTHWAVE CASCADE",
# Trace / security vibe
"TRACE RISING",
"TRACE SUPPRESSED",
"TRACE NEUTRALIZED",
"INTRUSION DETECTED",
"ACCESS GRANTED",
"ACCESS DENIED",
"OVERRIDE ACCEPTED",
# Signal / data flavor
"SIGNAL BREACH",
"GHOST SIGNAL",
"PACKET STORM",
"LINK ESTABLISHED",
"CONNECTION LOST",
"NODE COMPROMISED",
# Glitchy system stuff
"CORE GLITCH",
"SYSTEM FAULT",
"KERNEL PANIC",
]
# Color themes
COLOR_THEMES = [
{
"name": "Emerald Stream",
"bg": (0, 0, 0),
"main": (0, 255, 0),
"trail": (0, 100, 0),
"bright": (180, 255, 180),
"flash": (255, 255, 255),
},
{
"name": "Red Alert",
"bg": (5, 0, 0),
"main": (255, 40, 40),
"trail": (120, 0, 0),
"bright": (255, 150, 150),
"flash": (255, 255, 255),
},
{
"name": "Synthwave",
"bg": (5, 0, 20),
"main": (255, 0, 255),
"trail": (80, 0, 120),
"bright": (255, 150, 255),
"flash": (255, 255, 255),
},
# 4) Neon cyan grid
{
"name": "Neon Circuit",
"bg": (0, 4, 12),
"main": (0, 255, 200),
"trail": (0, 120, 100),
"bright": (180, 255, 240),
"flash": (255, 255, 255),
},
# 5) Amber warning tones
{
"name": "Amber Warning",
"bg": (10, 3, 0),
"main": (255, 180, 0),
"trail": (150, 80, 0),
"bright": (255, 230, 150),
"flash": (255, 255, 255),
},
# 6) Icy scan blue
{
"name": "Glacial Scan",
"bg": (0, 8, 20),
"main": (120, 200, 255),
"trail": (40, 90, 140),
"bright": (200, 240, 255),
"flash": (255, 255, 255),
},
# 7) Purple glitch
{
"name": "Violet Static",
"bg": (8, 0, 16),
"main": (200, 120, 255),
"trail": (90, 40, 140),
"bright": (235, 190, 255),
"flash": (255, 255, 255),
},
# 8) Soft ghostly green
{
"name": "Ghost Signal",
"bg": (0, 0, 0),
"main": (120, 255, 200),
"trail": (40, 120, 90),
"bright": (210, 255, 230),
"flash": (255, 255, 255),
},
# 9) Solar orange blast
{
"name": "Solar Flare",
"bg": (10, 0, 0),
"main": (255, 120, 0),
"trail": (140, 40, 0),
"bright": (255, 210, 150),
"flash": (255, 255, 255),
},
]
theme_index = 0
current_theme = COLOR_THEMES[theme_index]
unlocked_themes = 1
# These will be initialized in init_surfaces()
columns = 0
raindrops = []
x_positions = []
speeds = []
trail_length = 10
trail_surface = None
scene_surface = None
error_overlay = None
# Camera shake
shake_intensity = 0
shake_timer = 0
# Global speed multiplier
base_speed_factor = 0.5
slow_mo = False # bullet-time toggle
binary_mode = False # all 0/1 mode
# ==== PUZZLE MODE STATE ====
puzzles = [
{
"prompt": "ACCESS PHRASE: SEE BEYOND THE SURFACE (ONE WORD, THINK 'DEEP UNDERSTANDING')",
"answer": "INSIGHT",
"hint": "A single word for deep understanding or intuition.",
},
{
"prompt": "ACCESS CODE SEQUENCE: 2 4 8 16 ? (PATTERN: EACH VALUE DOUBLES)",
"answer": "32",
"hint": "Each number is 2x the previous one.",
},
{
"prompt": "DECRYPT: OPPOSITE OF 'NOISE' (ONE WORD, THINK SIGNAL PROCESSING)",
"answer": "SIGNAL",
"hint": "In signal processing, you want more ____ and less noise.",
},
{
"prompt": "CORE OVERRIDE KEYWORD (THE MAIN TITLE WORD IN ALL CAPS)",
"answer": "VISIONBREAKER",
"hint": "It's the first word in the game's title screen.",
},
{
"prompt": "LOGIC SEQUENCE: 1 3 6 10 15 ? (ADD 2,3,4,5,6...)",
"answer": "21",
"hint": "Differences between numbers go 2,3,4,5,...",
},
{
"prompt": "SYSTEM MODEL: INPUT -> PROCESS -> ______",
"answer": "OUTPUT",
"hint": "Classic three-step flow in computing.",
},
{
"prompt": "CONVERT: BINARY 1010 = ? (DECIMAL)",
"answer": "10",
"hint": "1*8 + 0*4 + 1*2 + 0*1.",
},
{
"prompt": "IDENT VERIFY: NAME THE TERMINAL (ONE WORD FROM TITLE)",
"answer": "NEUROGRID",
"hint": "It's the last word in the game window title.",
},
]
current_puzzle_index = 0
puzzle_message = "" # text shown for current puzzle / status
# TRACE system
trace_level = 0.0
trace_max = 100.0
trace_active = False
# Game state
running = True
paused = False
clock = pygame.time.Clock()
# Smaller control overlay font so it does not crowd the screen
ui_font = pygame.font.SysFont("consolas", 12)
hack_font = pygame.font.SysFont("consolas", 14)
puzzle_font = pygame.font.SysFont("consolas", 13)
# Special vertical word rain effects
# list of dicts: {"x", "y", "letters", "speed"}
word_rains = []
# Critical error state
critical_error_timer = 0
critical_glitch_intensity = 0
def init_surfaces():
"""Initialize / rebuild surfaces and rain columns when screen size changes."""
global trail_surface, scene_surface, error_overlay
global columns, raindrops, x_positions, speeds, word_rains
trail_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
scene_surface = pygame.Surface((WIDTH, HEIGHT))
error_overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
# Rebuild rain columns to fill the new resolution
columns = (WIDTH // FONT_SIZE) * 2
raindrops = [random.randint(-HEIGHT // FONT_SIZE, 0) for _ in range(columns)]
x_positions = [i * (FONT_SIZE // 2) for i in range(columns)]
speeds = [random.uniform(0.4, 1.2) for _ in range(columns)]
# Clear word rains when resizing so they do not get weird positions
word_rains.clear()
def trigger_shake(intensity=3, duration=20):
"""Start a camera shake."""
global shake_intensity, shake_timer
shake_intensity = intensity
shake_timer = duration
def get_shake_offset():
"""Return current shake offset."""
global shake_timer
if shake_timer > 0:
shake_timer -= 1
return (
random.randint(-shake_intensity, shake_intensity),
random.randint(-shake_intensity, shake_intensity),
)
return 0, 0
def next_theme():
"""Cycle to the next unlocked color theme."""
global theme_index, current_theme
if unlocked_themes <= 0:
return
theme_index = (theme_index + 1) % unlocked_themes
current_theme = COLOR_THEMES[theme_index]
def toggle_fullscreen():
"""Toggle between fullscreen and windowed 800x600."""
global fullscreen, screen, WIDTH, HEIGHT
fullscreen = not fullscreen
if fullscreen:
WIDTH, HEIGHT = FULLSCREEN_SIZE
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
else:
WIDTH, HEIGHT = DEFAULT_WINDOW_SIZE
screen = pygame.display.set_mode((WIDTH, HEIGHT))
init_surfaces()
def spawn_word_rain():
"""Spawn a vertical word cascade in a random column from SPECIAL_WORDS."""
word = random.choice(SPECIAL_WORDS)
spawn_word_rain_from_text(word)
def spawn_word_rain_from_text(text):
"""Spawn a vertical word cascade using the given text."""
text = text.strip()
if not text:
return
letters = list(text.upper())
col_index = random.randrange(len(x_positions))
x = x_positions[col_index]
start_y = -len(letters) * FONT_SIZE
word_rains.append(
{
"x": x,
"y": start_y,
"letters": letters,
"speed": random.uniform(1.5, 3.0),
}
)
def draw_word_rains(surface, effective_speed):
"""Draw and update special vertical word rains."""
to_remove = []
for i, wr in enumerate(word_rains):
x = wr["x"]
y_top = wr["y"]
letters = wr["letters"]
# Draw each letter top to bottom
for idx, ch in enumerate(letters):
y = y_top + idx * FONT_SIZE
if y > HEIGHT:
continue
# skip rendering spaces but keep vertical spacing
if ch == " ":
continue
# Make word rains pop: bright or flash color
if random.random() > 0.9:
color = current_theme["flash"]
else:
color = current_theme["bright"]
text = font.render(ch, True, color)
surface.blit(text, (x, y))
# Move the whole word rain down
wr["y"] += wr["speed"] * effective_speed
# Remove once it fully moved past bottom
if wr["y"] - len(letters) * FONT_SIZE > HEIGHT:
to_remove.append(i)
# Remove in reverse so indices stay valid
for i in reversed(to_remove):
del word_rains[i]
def trigger_critical_error():
"""Start a critical error event with red glitch and system failure text."""
global critical_error_timer, critical_glitch_intensity
global game_mode, puzzle_message, hack_input_mode, hack_buffer
global trace_level, trace_active
critical_error_timer = 180 # ~6 seconds at 30fps
critical_glitch_intensity = 6
trigger_shake(intensity=critical_glitch_intensity, duration=critical_error_timer)
# Leave puzzle mode on failure
if game_mode == "puzzle":
game_mode = "free"
puzzle_message = "TRACE FAILED"
hack_input_mode = False
hack_buffer = ""
# Reset trace when error hits
trace_level = 0.0
trace_active = False
# Play critical error SFX if available
if sfx_error is not None:
sfx_error.play()
def apply_critical_error_overlay(surface):
"""Apply red tint plus glitch lines plus SYSTEM FAILURE while timer is active."""
global critical_error_timer
if critical_error_timer <= 0:
return
critical_error_timer -= 1
# Red tint overlay
error_overlay.fill((255, 0, 0, 80))
surface.blit(error_overlay, (0, 0))
# Random horizontal glitch lines
for _ in range(8):
y = random.randint(0, HEIGHT)
width = random.randint(WIDTH // 4, WIDTH)
x = random.randint(-WIDTH // 4, WIDTH)
pygame.draw.rect(surface, (255, 0, 0), (x, y, width, 2))
# Big SYSTEM FAILURE text
text = big_font.render("SYSTEM FAILURE", True, current_theme["flash"])
rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
surface.blit(text, rect)
# ==== PUZZLE MODE LOGIC ====
def reset_puzzle_line():
"""Reset typewriter state for the current puzzle_message."""
global puzzle_full_line, puzzle_visible_chars, puzzle_type_accum
if game_mode == "puzzle" and puzzle_message:
puzzle_full_line = f"PUZZLE {current_puzzle_index + 1}/{len(puzzles)}: {puzzle_message}"
else:
puzzle_full_line = ""
puzzle_visible_chars = 0
puzzle_type_accum = 0.0
def start_puzzle_mode():
"""Enter puzzle mode and prepare the first puzzle."""
global game_mode, current_puzzle_index, puzzle_message, hack_input_mode, hack_buffer
global trace_level, trace_active
game_mode = "puzzle"
current_puzzle_index = 0
puzzle_message = puzzles[current_puzzle_index]["prompt"]
hack_input_mode = True
hack_buffer = ""
reset_puzzle_line()
# start trace bar
trace_level = 0.0
trace_active = True
def give_puzzle_hint():
"""Show a hint for the current puzzle and increase trace."""
global puzzle_message, trace_level
global puzzle_full_line, puzzle_visible_chars, puzzle_type_accum
hint = puzzles[current_puzzle_index].get("hint")
if not hint:
return
HINT_COST = 15.0 # cost in TRACE units
# update message with hint
puzzle_message = (
f"{puzzles[current_puzzle_index]['prompt']} // HINT: {hint} "
f"// TRACE +{int(HINT_COST)}/{int(trace_max)}"
)
# spend trace
trace_level = min(trace_max, trace_level + HINT_COST)
# 🔥 Instead of reset_puzzle_line(), directly update the typewriter state
puzzle_full_line = f"PUZZLE {current_puzzle_index + 1}/{len(puzzles)}: {puzzle_message}"
# Show the full line instantly (no re-type)
puzzle_visible_chars = len(puzzle_full_line)
puzzle_type_accum = float(puzzle_visible_chars)
def handle_puzzle_answer(answer_str):
"""Check the player's answer for the current puzzle."""
global current_puzzle_index, game_mode, puzzle_message, hack_input_mode, hack_buffer
global trace_level, trace_active, unlocked_themes, theme_index, current_theme
answer = answer_str.strip().upper()
current = puzzles[current_puzzle_index]
correct = current["answer"].upper()
if answer == correct:
# Correct answer
spawn_word_rain_from_text("ACCESS GRANTED")
if sfx_hack is not None:
sfx_hack.play()
# Move to next puzzle index (number solved so far)
current_puzzle_index += 1
# ---------- THEME UNLOCK LOGIC ----------
# You start with 1 theme unlocked (index 0).
# After solving N puzzles, you should have 1 + N themes unlocked,
# up to the total number of COLOR_THEMES.
solved = current_puzzle_index
desired_unlocked = min(1 + solved, len(COLOR_THEMES))
if desired_unlocked > unlocked_themes:
# Unlock each new theme between old and new counts
for theme_idx in range(unlocked_themes, desired_unlocked):
theme_name = COLOR_THEMES[theme_idx]["name"].upper()
spawn_word_rain_from_text(f"THEME UNLOCKED: {theme_name}")
# Update unlocked count
unlocked_themes = desired_unlocked
# 🔥 Auto-apply the newest unlocked theme
theme_index = unlocked_themes - 1
current_theme = COLOR_THEMES[theme_index]
# ---------- END THEME UNLOCK LOGIC ----------
# All puzzles solved?
if current_puzzle_index >= len(puzzles):
spawn_word_rain_from_text("TRACE NEUTRALIZED")
puzzle_message = "TRACE NEUTRALIZED"
reset_puzzle_line()
game_mode = "free"
hack_input_mode = False
hack_buffer = ""
trace_level = 0.0
trace_active = False
else:
# Next puzzle: reset trace for new round
puzzle_message = puzzles[current_puzzle_index]["prompt"]
reset_puzzle_line()
hack_input_mode = True
hack_buffer = ""
trace_level = 0.0
trace_active = True
else:
# Wrong answer -> increase trace instead of instant failure
spawn_word_rain_from_text("ACCESS DENIED")
# Each wrong answer spikes the trace bar
penalty = 10.0 # tweak this if needed
trace_level = min(trace_max, trace_level + penalty)
# Always keep the original riddle visible
base_prompt = puzzles[current_puzzle_index]["prompt"]
if trace_level >= trace_max:
# You burned through all your "lives"
trigger_critical_error()
else:
# Still alive: stay in puzzle mode, let them try again
global puzzle_full_line, puzzle_visible_chars, puzzle_type_accum
puzzle_message = (
f"{base_prompt} // ACCESS DENIED - TRACE +{int(penalty)} "
f"({int(trace_level)}/{int(trace_max)})"
)
# Build the full line and **show it instantly** (no re-type)
puzzle_full_line = (
f"PUZZLE {current_puzzle_index + 1}/{len(puzzles)}: {puzzle_message}"
)
puzzle_visible_chars = len(puzzle_full_line)
puzzle_type_accum = float(puzzle_visible_chars)
# Keep console active so they can retype
hack_input_mode = True
hack_buffer = ""
def exit_puzzle_mode():
"""Exit puzzle mode cleanly and clear puzzle text."""
global game_mode, puzzle_message, hack_input_mode, hack_buffer
global trace_level, trace_active
global puzzle_full_line, puzzle_visible_chars, puzzle_type_accum
game_mode = "free"
puzzle_message = ""
hack_input_mode = False
hack_buffer = ""
# Reset trace bar
trace_level = 0.0
trace_active = False
# Clear typewriter line so no text lingers
puzzle_full_line = ""
puzzle_visible_chars = 0
puzzle_type_accum = 0.0
def draw_ui_overlay(surface):
"""Draw small text with controls and theme name plus hack console."""
speed_label = f"{base_speed_factor:.1f}x"
mode_flags = []
if slow_mo:
mode_flags.append("Slow-mo")
if binary_mode:
mode_flags.append("Binary")
if game_mode == "puzzle":
mode_flags.append("Puzzle")
if hack_input_mode:
mode_flags.append("Hack")
flag_str = f" ({' / '.join(mode_flags)})" if mode_flags else ""
# Window caption reflects mode
pygame.display.set_caption(
f"VisionBreaker: Neurogrid | mode={game_mode} hack={hack_input_mode}"
)
lines = [
f"Theme: {current_theme['name']} (Unlocked {unlocked_themes}/{len(COLOR_THEMES)})",
f"Speed: {speed_label}{flag_str}",
"C - Theme Space - Pause",
"Up/Down - Speed B - Slow-mo N - Binary",
"H - Hack console P - Puzzle mode",
"E - Critical error S - Shake F11 - Fullscreen",
"Esc - Quit / Exit hack",
]
y = 6
for line in lines:
text_surf = ui_font.render(line, True, (200, 200, 200))
surface.blit(text_surf, (8, y))
y += 14
# Optional extra line about hints while in puzzle mode
if game_mode == "puzzle":
hint_line = "Puzzle mode: type HINT in the console for a clue (costs trace)"
text_surf = ui_font.render(hint_line, True, (200, 200, 200))
surface.blit(text_surf, (8, y))
y += 14
# Show puzzle prompt if in puzzle mode - typewriter + glow/glitch
if game_mode == "puzzle" and puzzle_full_line:
global puzzle_type_accum, puzzle_visible_chars
# Advance typewriter based on elapsed time
dt_sec = last_dt_ms / 1000.0
puzzle_type_accum += dt_sec * PUZZLE_CHARS_PER_SEC
target_chars = int(puzzle_type_accum)
if target_chars > puzzle_visible_chars:
puzzle_visible_chars = min(target_chars, len(puzzle_full_line))
visible_text = puzzle_full_line[:puzzle_visible_chars]
if visible_text:
# Glow effect using a sine wave between bright and flash
t = pygame.time.get_ticks() / 1000.0
glow = (math.sin(t * 3.0) + 1.0) * 0.5 # 0..1
br = current_theme["bright"]
fl = current_theme["flash"]
r = int(br[0] + (fl[0] - br[0]) * glow * 0.5)
g = int(br[1] + (fl[1] - br[1]) * glow * 0.5)
b = int(br[2] + (fl[2] - br[2]) * glow * 0.5)
color = (r, g, b)
# Render with the bigger puzzle font
puzzle_surf = puzzle_font.render(visible_text, True, color)
rect = puzzle_surf.get_rect()
rect.midbottom = (WIDTH // 2, HEIGHT - 60)
# Little random glitch duplicate
if random.random() < 0.06:
gx = random.randint(-2, 2)
gy = random.randint(-1, 1)
glitch_surf = puzzle_font.render(
visible_text, True, current_theme["flash"]
)
surface.blit(glitch_surf, (rect.x + gx, rect.y + gy))
surface.blit(puzzle_surf, rect)
# TRACE bar (top right) only in puzzle mode
if game_mode == "puzzle":
bar_width = WIDTH // 4
bar_height = 12
margin = 10
bar_x = WIDTH - bar_width - margin
bar_y = margin + 8
# background
pygame.draw.rect(surface, (30, 30, 30), (bar_x, bar_y, bar_width, bar_height))
# filled portion
fill_ratio = max(0.0, min(1.0, trace_level / trace_max))
fill_w = int(bar_width * fill_ratio)
if fill_w > 0:
pygame.draw.rect(surface, (255, 80, 40), (bar_x, bar_y, fill_w, bar_height))
# border
pygame.draw.rect(
surface, (200, 200, 200), (bar_x, bar_y, bar_width, bar_height), 1
)
# label just above bar
label = ui_font.render(
f"TRACE {int(trace_level):03d}/{int(trace_max):03d}",
True,
(200, 200, 200),
)
label_rect = label.get_rect()
label_rect.bottomright = (bar_x + bar_width, bar_y - 2)
surface.blit(label, label_rect)
# Hack console input shown at bottom when active
if hack_input_mode:
prompt = f"HACK> {hack_buffer}_"
text_surf = hack_font.render(prompt, True, current_theme["bright"])
rect = text_surf.get_rect()
rect.topleft = (8, HEIGHT - rect.height - 12)
# Semi-transparent bar behind the console so it stands out
console_bg = pygame.Surface((rect.width + 16, rect.height + 8), pygame.SRCALPHA)
console_bg.fill((0, 0, 0, 160))
surface.blit(console_bg, (rect.x - 8, rect.y - 4))
surface.blit(text_surf, rect)
# ==== BOOT SCREEN WITH RED / BLUE PILL ====
def show_boot_screen():
boot_font = pygame.font.SysFont("consolas", 24)
small_font = pygame.font.SysFont("consolas", 18)
boot_lines = [
"BOOT SEQUENCE - NEUROGRID TERMINAL",
"SCANNING UPLINK...",
"HANDSHAKE ESTABLISHED",
"LOADING CODEFALL ENGINE...",
"TRACE SUPPRESSION ONLINE",
]
boot_buffer = ""
revealed_lines = 0
last_reveal_time = 0
reveal_interval = 550 # ms per line
boot_running = True
local_clock = pygame.time.Clock()
while boot_running:
dt = local_clock.tick(60)
last_reveal_time += dt
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
elif event.key == pygame.K_RETURN:
norm = " ".join(boot_buffer.strip().lower().split())
if norm in ("awaken",):
boot_running = False # enter the game
elif norm in ("sleep",):
pygame.quit()
sys.exit()
elif event.key == pygame.K_BACKSPACE:
boot_buffer = boot_buffer[:-1]
else:
if event.unicode and event.unicode.isprintable():
boot_buffer += event.unicode
if revealed_lines < len(boot_lines) and last_reveal_time >= reveal_interval:
revealed_lines += 1
last_reveal_time = 0
screen.fill((0, 0, 0))
# Draw boot lines
y = HEIGHT // 3
for i in range(revealed_lines):
text_surf = boot_font.render(boot_lines[i], True, (0, 255, 0))
rect = text_surf.get_rect(center=(WIDTH // 2, y))
screen.blit(text_surf, rect)
y += 32
if revealed_lines == len(boot_lines):
# Choice text
prompt1 = "TYPE 'AWAKEN' TO JACK INTO THE NEUROGRID"
prompt2 = "TYPE 'SLEEP' TO ABORT CONNECTION"
p1_surf = small_font.render(prompt1, True, (0, 255, 0))
p2_surf = small_font.render(prompt2, True, (0, 255, 0))
p1_rect = p1_surf.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 60))
p2_rect = p2_surf.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 90))
screen.blit(p1_surf, p1_rect)
screen.blit(p2_surf, p2_rect)
# Input line
label_surf = small_font.render("CHOICE>", True, (0, 255, 0))
screen.blit(label_surf, (WIDTH // 2 - 220, HEIGHT // 2 + 130))
choice_surf = small_font.render(boot_buffer + "_", True, (0, 255, 0))
screen.blit(choice_surf, (WIDTH // 2 - 120, HEIGHT // 2 + 130))
pygame.display.flip()
# ============= BOOTSTRAP =============
init_surfaces()
init_audio()
show_boot_screen()
while running:
# One tick per frame
dt_ms = clock.tick(30)
last_dt_ms = dt_ms # store for UI effects like typewriter
# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
# While in hack console mode, intercept most keys for typing
if hack_input_mode and event.key not in (pygame.K_ESCAPE,):
if event.key == pygame.K_RETURN:
cleaned = hack_buffer.strip()
if game_mode == "puzzle":
upper = cleaned.upper()
if upper == "HINT":
give_puzzle_hint()
hack_buffer = ""
else:
handle_puzzle_answer(cleaned)
hack_buffer = ""
else:
if cleaned:
spawn_word_rain_from_text(cleaned)
if sfx_hack is not None:
sfx_hack.play()
hack_buffer = ""
hack_input_mode = False
elif event.key == pygame.K_BACKSPACE:
hack_buffer = hack_buffer[:-1]
else:
if event.unicode and event.unicode.isprintable():
hack_buffer += event.unicode
continue # skip normal controls while typing
if event.key == pygame.K_ESCAPE:
if hack_input_mode:
hack_input_mode = False
hack_buffer = ""
if game_mode == "puzzle":
exit_puzzle_mode()
else:
running = False
elif event.key == pygame.K_SPACE:
paused = not paused
elif event.key == pygame.K_c:
next_theme()
elif event.key == pygame.K_UP:
base_speed_factor = min(base_speed_factor + 0.1, 3.0)
elif event.key == pygame.K_DOWN:
base_speed_factor = max(base_speed_factor - 0.1, 0.2)
elif event.key == pygame.K_b:
slow_mo = not slow_mo
elif event.key == pygame.K_n:
binary_mode = not binary_mode
if sfx_binary is not None:
sfx_binary.play()
elif event.key == pygame.K_h:
hack_input_mode = True
hack_buffer = ""
elif event.key == pygame.K_p:
if game_mode == "free":
start_puzzle_mode()
elif event.key == pygame.K_e:
trigger_critical_error()
elif event.key == pygame.K_s:
trigger_shake(
intensity=random.randint(2, 5),
duration=random.randint(15, 30),
)
elif event.key == pygame.K_F11:
toggle_fullscreen()
# Effective speed (base * slow-mo multiplier)
effective_speed = base_speed_factor * (0.3 if slow_mo else 1.0)
# TRACE progression while in puzzle mode
if (
game_mode == "puzzle"
and not paused
and trace_active
and critical_error_timer <= 0
):