-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
1640 lines (1373 loc) · 71.5 KB
/
main.py
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 pymunk
import pymunk.pygame_util
import os
import math
import random
import json
import time
import sys
# Initialize pygame mixer for audio
pygame.mixer.init()
class Button:
def __init__(self, x, y, width, height, text, callback):
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.callback = callback
self.font = pygame.font.Font(None, 24)
self.enabled = True
self.visible = True
self.is_golden = False # Add flag for golden border
self.font_size = 24 # Default font size
def draw(self, screen):
if not self.visible:
return
# Draw golden border if it's the golden button
if self.is_golden:
# Use smaller font for golden button
self.font = pygame.font.Font(None, 23) # Reduced by 1pt
border_rect = self.rect.inflate(6, 6) # Slightly larger rect for border
pygame.draw.rect(screen, (255, 215, 0), border_rect) # Gold color
# Draw inner golden border
inner_border = self.rect.inflate(2, 2)
pygame.draw.rect(screen, (218, 165, 32), inner_border) # Darker gold
else:
# Use default font size for other buttons
self.font = pygame.font.Font(None, 24)
color = (150, 150, 150) if self.enabled else (100, 100, 100)
pygame.draw.rect(screen, color, self.rect)
# Draw text
text_color = (0, 0, 0) if self.enabled else (155, 155, 155)
text_surface = self.font.render(self.text, True, text_color)
text_rect = text_surface.get_rect(center=self.rect.center)
screen.blit(text_surface, text_rect)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if self.rect.collidepoint(event.pos) and self.enabled and self.visible: # Check visibility
self.callback()
class Game:
def __init__(self):
pygame.init()
# Initialize mixer first, before any sound loading
pygame.mixer.init()
pygame.mixer.music.set_volume(0.0) # Start at zero volume
# Load and set up background music first - before anything else
assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
try:
self.music_tracks = [
os.path.join(assets_dir, 'Endless-Journey.mp3'),
os.path.join(assets_dir, 'Endless-Ascent.mp3')
]
# Initialize music system
self.music_enabled = True
self.music_volume = 0.0
self.target_volume = 0.7
self.initial_fade_frames = 300 # 5 seconds at 60fps
self.toggle_fade_frames = 60 # 1 second at 60fps
self.current_fade_frame = 0
self.is_fading = True
self.is_initial_fade = True
# Set up initial track
self.current_track = random.randint(0, len(self.music_tracks) - 1)
pygame.mixer.music.load(self.music_tracks[self.current_track])
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1)
except pygame.error as e:
print(f"Failed to load music: {e}")
# Remove all other music initialization code...
# (delete the duplicate music setup sections later in __init__)
# Rest of initialization code...
self.width = 3400 # Always have full width for both hills
self.height = 600 # Updated width to 1740px
# Initialize timer variables BEFORE loading save
self.start_time = None # Initialize start time for the speedrun timer
self.elapsed_time = 0 # Initialize elapsed time
self.total_elapsed_time = 0 # Total elapsed time including previous sessions
self.timer_visible = True # Add visibility flag for timer
self.screen = pygame.display.set_mode(
(800, 600),
pygame.DOUBLEBUF | pygame.HWSURFACE,
depth=0,
display=0,
vsync=1
)
# Create DrawOptions and disable collision points
self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)
self.draw_options.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES # Only draw shapes, not collision points
self.space = pymunk.Space()
self.space.gravity = (0, 900)
pygame.display.set_caption("Squaresyphus")
self.space = pymunk.Space()
self.space.gravity = (0, 900)
# **Set collision_slop to zero to prevent penetration**
self.space.collision_slop = 0.0
# **Load Boulder Sprites**
try:
self.boulder_sprite_gray = pygame.image.load(os.path.join(assets_dir, 'boulder_gray.png')).convert_alpha()
except pygame.error as e:
print(f"Failed to load boulder_gray.png: {e}")
pygame.quit()
exit()
# Optional: Load a separate sprite for the crushing state
# If you don't have one, we'll tint the gray sprite dynamically
try:
self.boulder_sprite_orange = pygame.image.load(os.path.join(assets_dir, 'boulder_orange.png')).convert_alpha()
self.has_orange_sprite = True
except pygame.error:
self.has_orange_sprite = False
# Create an orange tint surface if separate sprite isn't available
self.boulder_sprite_orange = self.boulder_sprite_gray.copy()
orange_surface = pygame.Surface(self.boulder_sprite_gray.get_size(), pygame.SRCALPHA)
orange_surface.fill((255, 165, 0, 100)) # Semi-transparent orange
self.boulder_sprite_orange.blit(orange_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
# Set default values that were previously in sliders
self.jump_force = 3000
self.strength = 36
self.strength_xp = 0 # Start with 0 XP
self.strength_level = 1
self.friction = 0.6
# Track unlocked boulder sizes
self.unlocked_sizes = {
40: True, # Small boulder always unlocked
50: False, # Medium boulder starts locked
80: False, # Large boulder size increased to 80
120: False, # Huge boulder starts locked
150: False # Golden boulder starts locked
}
self.particles = [] # List to store particles
self.cloud_sprite_sheet = pygame.image.load(os.path.join(assets_dir, 'Clouds-Sheet.png')).convert_alpha() # Load cloud sprite sheet
self.clouds = self.create_clouds() # Create clouds
self.money_particles = [] # List to store money particles
self.money_texts = [] # List to store money text effects
grass_raw = pygame.image.load(os.path.join(assets_dir, 'grass.png')).convert_alpha()
# Scale grass sprite up by 2x
grass_width = grass_raw.get_width() * 2
grass_height = grass_raw.get_height() * 2
self.grass_sprite = pygame.transform.scale(grass_raw, (grass_width, grass_height))
# Debug position controls for grass
self.grass_x = 0 # Initial X offset
self.grass_y = 540 # Adjust initial Y position by raising 20 pixels
self.offset = 20
self.sisyphus = self.create_sisyphus()
self.current_boulder = None
self.crushing_boulders = []
self.ground = self.create_ground_poly() # Use the new ground creation method
self.walls = self.create_walls()
self.hill = self.create_hill()
self.hill_light_color = (255, 255, 0) # Bright yellow
self.hill_dark_color = (200, 200, 0) # Darker yellow
self.current_hill_color = self.hill_dark_color
self.bottom_sensor_color = (255, 200, 200) # Light red for bottom sensors
self.clock = pygame.time.Clock()
self.jump_cooldown = 0
self.camera_x = 0
self.is_grounded = False # Track if player is touching ground
self.font = pygame.font.Font(None, 24) # Font for debug text
# Add counter for hill passes and money
self.hill_passes = 0
self.money = 0 # Start with 0 money
self.last_boulder_detected = False # Track previous detection state
self.boulder_at_bottom = False # Track if boulder has reached bottom
# Add boulder spawn cooldown
self.spawn_cooldown = 0
# **Add Collision Handlers to Ignore Specific Collisions**
# Crushing Boulders (4) vs Player (1) - Ignore
handler_crushing_player = self.space.add_collision_handler(4, 1)
handler_crushing_player.begin = self.ignore_collision
# Crushing Boulders (4) vs Normal Boulders (3) - Ignore
handler_crushing_boulders = self.space.add_collision_handler(4, 3)
handler_crushing_boulders.begin = self.ignore_collision
# Crushing Boulders (4) vs Crushing Boulders (4) - Ignore
handler_crushing_crushing = self.space.add_collision_handler(4, 4)
handler_crushing_crushing.begin = self.ignore_collision
# Create fonts - add money font
self.font = pygame.font.Font(None, 24) # Regular font for debug text
self.money_font = pygame.font.Font(None, 48) # Bigger font for money display
# Move buttons to right side - calculate x position
button_width = 180
button_x = 800 - button_width - 10 # Right side with 10px padding
self.small_boulder_button = Button(button_x, 60, button_width, 30, "Small Boulder", lambda: self.spawn_boulder(40, 1))
self.medium_boulder_button = Button(button_x, 100, button_width, 30, "Medium Boulder (10$)", lambda: self.unlock_and_spawn(50))
self.large_boulder_button = Button(button_x, 140, button_width, 30, "Large Boulder (50$)", lambda: self.unlock_and_spawn(80))
self.huge_boulder_button = Button(button_x, 180, button_width, 30, "Huge Boulder (200$)", lambda: self.unlock_and_spawn(120))
self.golden_boulder_button = Button(button_x, 220, button_width, 30, self.get_golden_boulder_text(), self.unlock_and_spawn_golden_boulder)
self.golden_boulder_button.is_golden = True # Set the golden border flag
# Add music state tracking
self.music_enabled = True
self.current_track = 1
# Load music icons
assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
try:
self.music_icon = pygame.image.load(os.path.join(assets_dir, 'music-icon.png')).convert_alpha()
self.next_icon = pygame.image.load(os.path.join(assets_dir, 'next-icon.png')).convert_alpha()
# Scale icons
self.music_icon = pygame.transform.scale(self.music_icon, (32, 32))
self.next_icon = pygame.transform.scale(self.next_icon, (32, 32))
except pygame.error as e:
print(f"Failed to load music icons: {e}")
self.music_icon = None
self.next_icon = None
# Create music buttons
self.music_button = Button(20, 65, 32, 32, "", self.toggle_music)
self.next_button = Button(60, 65, 32, 32, "", self.next_track) # Position it right after music button
# Load sound effects with adjusted volume
try:
self.level_up_sound = pygame.mixer.Sound(os.path.join(assets_dir, 'level-up.mp3'))
self.money_pickup_sound = pygame.mixer.Sound(os.path.join(assets_dir, 'money-pickup.mp3'))
self.jump_sound = pygame.mixer.Sound(os.path.join(assets_dir, 'jump.mp3')) # Add jump sound
# Adjust volumes
self.level_up_sound.set_volume(0.2) # Lowered from 0.7 to 0.2
self.money_pickup_sound.set_volume(0.4)
self.jump_sound.set_volume(0.5) # Set jump sound volume
except pygame.error as e:
print(f"Failed to load sound effects: {e}")
self.level_up_sound = None
self.money_pickup_sound = None
self.jump_sound = None
# Load splash screen
try:
self.splash_screen = pygame.image.load(os.path.join(assets_dir, 'splash.png')).convert_alpha()
# Scale splash screen to 800x600
self.splash_screen = pygame.transform.scale(self.splash_screen, (800, 600))
except pygame.error as e:
print(f"Failed to load splash screen: {e}")
self.splash_screen = None
# Music fade-in variables
self.music_volume = 0.7 # Start at full volume
self.target_volume = 0.7 # Target volume for music
self.fade_steps = 60 # Number of steps for fade (30 frames = 0.5 seconds at 60fps)
self.current_fade_step = 0
self.is_fading = False
pygame.mixer.music.set_volume(self.music_volume)
# Add hill texture with fixed position
assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
try:
self.hill_texture = pygame.image.load(os.path.join(assets_dir, 'hill_1.png')).convert_alpha()
# Scale the hill texture by 2x
self.hill_texture = pygame.transform.scale(self.hill_texture, (self.hill_texture.get_width() * 2, self.hill_texture.get_height() * 2))
except pygame.error as e:
print(f"Failed to load hill texture: {e}")
self.hill_texture = None
# Fixed hill position
self.hill_x_offset = 200
self.hill_y_offset = 125
# Music fade variables
self.music_volume = 0.0
self.target_volume = 0.7
self.fade_steps = 120 # 2 seconds at 60fps
self.current_fade_step = 0
self.is_fading = False
self.current_track = random.randint(0, len(self.music_tracks) - 1)
self.music_enabled = True
pygame.mixer.music.set_volume(0.0)
# Load and start music
try:
pygame.mixer.music.load(self.music_tracks[self.current_track])
pygame.mixer.music.play(-1) # Start playing immediately but at volume 0
self.is_fading = True # Start fading in
except pygame.error as e:
print(f"Failed to load music: {e}")
# Music system variables
self.music_enabled = True
self.music_volume = 0.0
self.target_volume = 0.7
self.initial_fade_frames = 300 # 5 seconds at 60fps
self.toggle_fade_frames = 60 # 1 second at 60fps
self.current_fade_frame = 0
self.is_fading = True # Start with initial fade-in
self.is_initial_fade = True # Track if this is the first fade
# Start playing music immediately (at volume 0)
try:
pygame.mixer.music.load(self.music_tracks[self.current_track])
pygame.mixer.music.set_volume(0.0)
pygame.mixer.music.play(-1)
except pygame.error as e:
print(f"Failed to load music: {e}")
# Add button press tracking
self.next_button_pressed = False
self.next_button_timer = 0
self.next_button_press_duration = 5 # 60 frames = 1 second at 60fps
# Update save file path to work with both development and exe
if getattr(sys, 'frozen', False):
# If running as exe
application_path = os.path.dirname(sys.executable)
else:
# If running in development
application_path = os.path.dirname(__file__)
self.save_file = os.path.join(application_path, 'save_data.json')
# Load saved data first
saved_data = self.load_save()
# Start music immediately
try:
pygame.mixer.music.load(self.music_tracks[self.current_track])
pygame.mixer.music.set_volume(0.0) # Start at 0 volume
pygame.mixer.music.play(-1) # Start playing immediately
except pygame.error as e:
print(f"Failed to load music: {e}")
# Load completion state and final time
self.game_completed = saved_data.get('game_completed', False)
self.final_time = saved_data.get('final_time', 0)
if self.game_completed:
self.elapsed_time = self.final_time # Use final time if game was completed
# Define default unlocked sizes
default_unlocked_sizes = {
40: True, # Small boulder always unlocked
50: False, # Medium boulder starts locked
80: False, # Large boulder
120: False, # Huge boulder starts locked
150: False # Golden boulder starts locked
}
# Initialize values with saved data or defaults
self.money = saved_data.get('money', 0)
self.strength_xp = saved_data.get('strength_xp', 0)
# Calculate initial strength based on loaded XP
current_level = self.calculate_strength_level()
self.strength = 36 + (current_level - 1) * 20 # Base strength + level bonus
self.jump_force = 3000 + (current_level - 1) * 200 # Base jump + level bonus
# Properly merge saved unlocked sizes with defaults
self.unlocked_sizes = default_unlocked_sizes.copy()
if 'unlocked_sizes' in saved_data:
self.unlocked_sizes.update(saved_data['unlocked_sizes'])
# Update button text based on unlocked status
if self.unlocked_sizes[50]:
self.medium_boulder_button.text = "Medium Boulder"
if self.unlocked_sizes[80]:
self.large_boulder_button.text = "Large Boulder"
if self.unlocked_sizes[120]:
self.huge_boulder_button.text = "Huge Boulder"
if self.unlocked_sizes[150]:
self.golden_boulder_button.text = "Golden Boulder"
# Set up music end event
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1)
# Define reward mapping
self.boulder_rewards = {
40: (1, 1), # (money, xp) for small boulder
50: (2, 2), # medium boulder
80: (5, 5), # large boulder
120: (20, 20), # huge boulder
150: (50, 50) # golden boulder
}
# Instead of spawning default boulder, spawn the last used boulder size
last_boulder_size = saved_data.get('last_boulder_size', 40) # Default to 40 if not found or None
if last_boulder_size is None: # Additional check to ensure we always have a valid size
last_boulder_size = 40
# Get the correct rewards for the loaded boulder size
self.boulder_reward, self.boulder_xp_gain = self.boulder_rewards[last_boulder_size]
self.spawn_boulder(last_boulder_size, self.boulder_reward, self.boulder_xp_gain)
# Load hill_2 texture
try:
self.hill_2_texture = pygame.image.load(os.path.join(assets_dir, 'hill_2.png')).convert_alpha()
except pygame.error as e:
print(f"Failed to load hill_2 texture: {e}")
self.hill_2_texture = None
# Load Golden Boulder Sprite
try:
self.golden_boulder_sprite = pygame.image.load(os.path.join(assets_dir, 'golden_boulder.png')).convert_alpha()
except pygame.error as e:
print(f"Failed to load golden_boulder.png: {e}")
self.golden_boulder_sprite = None
self.golden_boulder_unlocked = False # Track if the golden boulder is unlocked
# Initialize floating texts
self.floating_texts = []
# Add congratulations screen state
self.showing_congrats = False
self.congrats_particles = []
self.final_time = 0
self.continue_button = Button(300, 400, 200, 40, "Continue Playing", self.close_congrats)
self.new_game_button = Button(300, 450, 200, 40, "Start New Game", self.start_new_game)
# Add main menu buttons
self.menu_continue_button = Button(300, 400, 200, 40, "Continue Game", self.continue_game)
self.menu_new_game_button = Button(300, 450, 200, 40, "New Game", self.start_new_game)
self.in_main_menu = True # Track if we're in the main menu
def ignore_collision(self, arbiter, space, data):
"""Collision handler that ignores the collision."""
return False # Returning False tells Pymunk to ignore the collision
def calculate_xp_required(self, level):
# Fixed XP requirements per level
requirements = {
1: 5, # Level 1->2: 10 XP
2: 20, # Level 2->3: 20 XP
3: 50, # Level 3->4: 50 XP
4: 100, # Level 4->5: 100 XP
5: 200, # And so on...
6: 500,
7: 1000,
8: 2000,
}
return requirements.get(level, 5000) # Default to 5000 XP for very high levels
def level_up(self):
# Apply level up effects
current_level = self.calculate_strength_level()
self.strength = 36 + (current_level - 1) * 20 # Base strength + level bonus
self.jump_force = 3000 + (current_level - 1) * 200 # Base jump + level bonus
print(f"Level Up! Now level {current_level}")
self.create_level_up_particles()
# Play level up sound
if self.level_up_sound:
self.level_up_sound.play()
def create_walls(self):
walls = []
wall_body = pymunk.Body(body_type=pymunk.Body.STATIC)
wall_thickness = 5
# Add the wall body to the space first
self.space.add(wall_body)
# Left wall
left_wall_shape = pymunk.Segment(wall_body, (0, 0), (0, self.height), wall_thickness)
# Right wall - ensure it's at exactly self.width
right_wall_shape = pymunk.Segment(wall_body, (self.width, 0), (self.width, self.height), wall_thickness)
# Top wall
top_wall_shape = pymunk.Segment(wall_body, (0, 0), (self.width, 0), wall_thickness)
for wall in [left_wall_shape, right_wall_shape, top_wall_shape]:
wall.friction = self.friction
wall.collision_type = 2 # Set collision type for walls
self.space.add(wall)
walls.append(wall)
return walls
def create_ground_poly(self):
# Create a ground as a static polygon with thickness
ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
ground_shape = pymunk.Poly(ground_body, [
(0, self.height - self.offset), # Raise by offset
(self.width, self.height - self.offset), # Extend to new width
(self.width, self.height - self.offset - 10), # Extend to new width
(0, self.height - self.offset - 10) # Raise by offset
])
ground_shape.friction = self.friction
ground_shape.collision_type = 2 # Set collision type for ground
ground_shape.color = pygame.Color(139, 69, 19) # Change ground color to match mountain fill color
self.space.add(ground_body, ground_shape)
return ground_body
def create_level_up_particles(self):
# Create particles for visual effect
for _ in range(200): # Increased number of particles
pos = self.sisyphus.position
vel = [random.uniform(-4, 4), random.uniform(-4, 4)] # Increased velocity
self.particles.append([pos, vel, random.randint(4, 10)]) # Increased size
def update_particles(self):
# Update particle positions and remove old particles
for particle in self.particles[:]:
particle[0] += particle[1] # Update position by velocity
particle[2] -= 0.1 # Decrease size
if particle[2] <= 0:
self.particles.remove(particle)
# Update money texts
for text in self.money_texts[:]:
text['pos'][1] -= 1 # Move text up
text['life'] -= 0.02 # Decrease life
if text['life'] <= 0:
self.money_texts.remove(text)
def draw_particles(self):
# Draw particles on the screen
for particle in self.particles:
pygame.draw.circle(self.screen, (255, 215, 0),
(int(particle[0][0] - self.camera_x), int(particle[0][1])),
int(particle[2]))
# Draw money texts with camera offset
for text in self.money_texts:
font = pygame.font.Font(None, text['size'])
text_surface = font.render(text['text'], True, (0, 100, 0)) # Darker green color
text_surface.set_alpha(int(255 * text['life'])) # Fade out
# Apply camera offset to x position
screen_x = text['pos'][0] - self.camera_x
self.screen.blit(text_surface, (int(screen_x), int(text['pos'][1])))
def spawn_money_particles(self, amount, hill2=False):
# Create money text effect 90 pixels higher (increased from 40)
if hill2:
# Position for Hill 2 (centered above the hill)
x_pos = 1980 # Center of Hill 2
y_pos = self.height - 470 # Raised by 50px (from 420)
else:
# Position for Hill 1 (centered above the hill)
x_pos = 870 # Center of Hill 1
y_pos = self.height - 390 # Raised by 50px (from 340)
self.money_texts.append({
'text': f"+${amount}",
'pos': [x_pos, y_pos],
'life': 1.0,
'size': 48
})
def calculate_strength_level(self):
# Calculate level based on total XP instead of strength
level = 1
xp = self.strength_xp
while True:
required = self.calculate_xp_required(level)
if xp < required:
break
xp -= required
level += 1
return level
def calculate_xp_progress(self):
current_level = self.calculate_strength_level()
total_xp = self.calculate_xp_required(current_level)
# Calculate XP in current level
xp_in_prev_levels = sum(self.calculate_xp_required(l) for l in range(1, current_level))
current_level_xp = self.strength_xp - xp_in_prev_levels
return current_level_xp / total_xp
def draw_strength_stats(self):
# Draw level text
current_level = self.calculate_strength_level()
level_text = self.font.render(f"STR Level {current_level}", True, (0, 0, 0))
self.screen.blit(level_text, (10, 10))
# Draw XP bar
bar_width = 200
bar_height = 20
border = 2
# Draw border
pygame.draw.rect(self.screen, (0, 0, 0), (10, 30, bar_width, bar_height))
# Draw background
pygame.draw.rect(self.screen, (200, 200, 200), (10 + border, 30 + border,
bar_width - 2*border, bar_height - 2*border))
# Draw progress
progress = self.calculate_xp_progress()
if progress > 0:
pygame.draw.rect(self.screen, (0, 255, 0), (10 + border, 30 + border,
(bar_width - 2*border) * progress, bar_height - 2*border))
# Draw XP numbers
total_xp_required = self.calculate_xp_required(current_level)
xp_in_prev_levels = sum(self.calculate_xp_required(l) for l in range(1, current_level))
current_level_xp = self.strength_xp - xp_in_prev_levels
xp_text = self.font.render(f"{current_level_xp}/{total_xp_required}xp", True, (0, 0, 0))
xp_text_rect = xp_text.get_rect(center=(10 + bar_width // 2, 30 + bar_height // 2))
self.screen.blit(xp_text, xp_text_rect)
def create_sisyphus(self):
sisyphus_size = 50
sisyphus_mass = 10
sisyphus_moment = pymunk.moment_for_box(sisyphus_mass, (sisyphus_size, sisyphus_size))
sisyphus_body = pymunk.Body(sisyphus_mass, sisyphus_moment)
sisyphus_body.position = 400, self.height - sisyphus_size/2 - self.offset # Raise by offset
sisyphus_shape = pymunk.Poly.create_box(sisyphus_body, (sisyphus_size, sisyphus_size))
sisyphus_shape.friction = self.friction
sisyphus_shape.color = pygame.Color('red') # Change color to red
# Add collision handler to detect ground contact
def begin_collision(arbiter, space, data):
self.is_grounded = True
return True
def separate_collision(arbiter, space, data):
self.is_grounded = False
return True
handler = self.space.add_collision_handler(1, 2) # 1 for sisyphus, 2 for ground/platforms
handler.begin = begin_collision
handler.separate = separate_collision
sisyphus_shape.collision_type = 1 # Set collision type for sisyphus
self.space.add(sisyphus_body, sisyphus_shape)
return sisyphus_body
def create_boulder(self, radius=40, position=(480, 0)):
boulder_mass = radius * 0.5
boulder_moment = pymunk.moment_for_circle(boulder_mass, 0, radius)
boulder_body = pymunk.Body(boulder_mass, boulder_moment)
# Use the provided position for spawning
boulder_body.position = position
boulder_shape = pymunk.Circle(boulder_body, radius)
boulder_shape.friction = self.friction
boulder_shape.color = pygame.Color('gray') # Set default color
boulder_shape.collision_type = 3 # Collision type for normal boulders
self.space.add(boulder_body, boulder_shape)
return boulder_body, boulder_shape
def unlock_and_spawn(self, size):
costs = {50: 10, 80: 50, 120: 200} # Costs for boulders
rewards = {50: (2, 2), 80: (5, 5), 120: (20, 20)} # Updated rewards for boulders
if not self.unlocked_sizes[size] and self.money >= costs[size]:
self.money -= costs[size]
self.unlocked_sizes[size] = True
# Update button text
if size == 50:
self.medium_boulder_button.text = "Medium Boulder"
elif size == 80:
self.large_boulder_button.text = "Large Boulder"
else:
self.huge_boulder_button.text = "Huge Boulder"
if self.unlocked_sizes[size]:
reward, xp_gain = rewards.get(size, (1, 1)) # Default to (1$, 1 XP) if not found
self.spawn_boulder(size, reward, xp_gain)
def spawn_boulder(self, size=40, reward=None, xp_gain=None):
# Check cooldown
if self.spawn_cooldown > 0:
return
# Only check unlocks, no cost per spawn
if not self.unlocked_sizes[size]:
return
if self.current_boulder is not None:
self.space.remove(self.current_boulder['body'], self.current_boulder['shape'])
self.current_boulder = None
# Get rewards from mapping if not specified
if reward is None or xp_gain is None:
reward, xp_gain = self.boulder_rewards[size] # Changed from .get() to direct access
# Determine spawn position based on Sisyphus's position
if self.sisyphus.position.x < 900: # Before/at hill 1 peak
# Spawn in front of the first hill
boulder_position = (480, self.height - 250 - self.offset)
elif self.sisyphus.position.x < 2040: # Before/at hill 2 peak
# Spawn in front of the second hill
boulder_position = (1500, self.height - 250 - self.offset)
else:
# Spawn after the second hill, beyond its right base (2380 + some padding)
boulder_position = (2800, self.height - 250 - self.offset)
boulder_body, boulder_shape = self.create_boulder(size, boulder_position)
new_boulder = {'body': boulder_body, 'shape': boulder_shape, 'state': 'normal'}
self.current_boulder = new_boulder
self.boulder_reward = reward
self.boulder_xp_gain = xp_gain
# Set spawn cooldown
self.spawn_cooldown = 10
def clear_boulders(self):
if self.current_boulder:
self.space.remove(self.current_boulder['body'], self.current_boulder['shape'])
self.current_boulder = None
for boulder in self.crushing_boulders:
self.space.remove(boulder['body'], boulder['shape'])
self.crushing_boulders.clear()
def create_hill(self):
hill_body = pymunk.Body(body_type=pymunk.Body.STATIC)
# Create both hills' shapes
hill1_points = [
(600, self.height - self.offset), # Left base
(840, self.height - 140 - self.offset), # Left peak
(900, self.height - 140 - self.offset), # Right peak
(1140, self.height - self.offset) # Right base
]
hill2_points = [
(1600, self.height - self.offset), # Left base
(1940, self.height - 240 - self.offset), # Left peak, taller
(2040, self.height - 240 - self.offset), # Right peak, taller
(2380, self.height - self.offset) # Right base
]
hill_shapes = []
# Create segments for both hills
for points in [hill1_points, hill2_points]:
for i in range(len(points) - 1):
segment = pymunk.Segment(hill_body, points[i], points[i+1], 5)
segment.friction = self.friction
segment.collision_type = 2 # Set collision type for hill
segment.color = pygame.Color(139, 69, 19) # Brown color
hill_shapes.append(segment)
self.space.add(hill_body, *hill_shapes)
return hill_body
def draw_hill(self):
# Draw the first hill shape
hill1_points = [
(600, self.height - self.offset), # Left base
(840, self.height - 140 - self.offset), # Left peak
(900, self.height - 140 - self.offset), # Right peak
(1140, self.height - self.offset) # Right base
]
pygame.draw.polygon(self.screen, (139, 69, 19), [(x - self.camera_x, y) for x, y in hill1_points])
pygame.draw.lines(self.screen, (139, 69, 19), False, [(x - self.camera_x, y) for x, y in hill1_points], 5)
# Draw the second hill shape
hill2_points = [
(1600, self.height - self.offset), # Left base
(1940, self.height - 240 - self.offset), # Left peak
(2040, self.height - 240 - self.offset), # Right peak
(2380, self.height - self.offset) # Right base
]
pygame.draw.polygon(self.screen, (139, 69, 19), [(x - self.camera_x, y) for x, y in hill2_points])
pygame.draw.lines(self.screen, (139, 69, 19), False, [(x - self.camera_x, y) for x, y in hill2_points], 5)
def create_clouds(self):
clouds = []
for _ in range(15):
x = random.randint(0, self.width)
y = random.randint(0, 200) # Clouds in the upper part of the screen
width = 96 # Base cloud width
height = 96 # Base cloud height
scale = random.uniform(0.8, 1.2) # Random scale between 0.8 and 1.2
speed = random.uniform(0.1, 0.4)
opacity = int(255 * (1 - speed))
cloud_type = random.choice([0, 1, 2, 3])
clouds.append([x, y, width, height, speed, opacity, cloud_type, scale]) # Added scale
return clouds
def draw_clouds(self):
for cloud in self.clouds:
x, y, width, height, speed, opacity, cloud_type, scale = cloud # Unpack scale
# Calculate scaled dimensions
scaled_width = int(width * scale)
scaled_height = int(height * scale)
cloud_surface = pygame.Surface((scaled_width, scaled_height), pygame.SRCALPHA)
# Create subsurface for the cloud type
cloud_sprite = self.cloud_sprite_sheet.subsurface((cloud_type * 32, 0, 32, 32))
# Scale the sprite using the random scale
scaled_sprite = pygame.transform.scale(cloud_sprite, (scaled_width, scaled_height))
# Blit the scaled sprite
cloud_surface.blit(scaled_sprite, (0, 0))
cloud_surface.set_alpha(opacity)
self.screen.blit(cloud_surface, (x, y))
cloud[0] += speed # Move cloud right
if cloud[0] > self.width: # Reset cloud position if it goes off screen
cloud[0] = -scaled_width # Use scaled width for reset position
def draw_grass(self):
# Calculate how many times we need to tile the grass horizontally
grass_width = self.grass_sprite.get_width()
num_tiles = (self.width // grass_width) + 2 # +2 to ensure coverage during scrolling
# Draw grass tiles
for i in range(num_tiles):
x = i * grass_width + (self.grass_x % grass_width) - self.camera_x
self.screen.blit(self.grass_sprite, (x, self.grass_y))
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.save_progress()
return False
# Handle congratulations screen buttons if showing
if self.showing_congrats:
self.continue_button.handle_event(event)
self.new_game_button.handle_event(event)
continue # Skip other input handling while showing congratulations
# Handle music end event
if event.type == pygame.USEREVENT + 1: # Music ended
self.next_track()
# Handle UI buttons
self.music_button.handle_event(event)
self.next_button.handle_event(event)
self.small_boulder_button.handle_event(event)
self.medium_boulder_button.handle_event(event)
self.large_boulder_button.handle_event(event)
self.huge_boulder_button.handle_event(event)
self.golden_boulder_button.handle_event(event)
# Handle timer click
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# Check if click is in timer area
timer_rect = pygame.Rect(10, self.height - 62, 200, 30) # Approximate timer area
if timer_rect.collidepoint(event.pos):
self.timer_visible = not self.timer_visible
self.save_progress() # Save timer visibility state
# ... rest of existing handle_events code ...
# Handle continuous jumping when key is held
keys = pygame.key.get_pressed()
if (keys[pygame.K_SPACE] or keys[pygame.K_w] or keys[pygame.K_UP]) and self.jump_cooldown <= 0 and self.is_grounded:
self.jump()
self.jump_cooldown = 30 # Set cooldown after jumping
self.is_grounded = False # Immediately set grounded to false when jumping
return True
def move_sisyphus(self):
keys = pygame.key.get_pressed()
base_move_force = 100 # Base movement force
strength = self.strength
# Scale sisyphus based on strength directly
for shape in self.space.shapes:
if shape.body == self.sisyphus:
current_size = shape.get_vertices()[2][0] - shape.get_vertices()[0][0]
target_size = 40 + (self.calculate_strength_level() - 1) * 5 # Adjust size progression
if abs(current_size - target_size) > 1:
self.space.remove(shape)
new_shape = pymunk.Poly.create_box(self.sisyphus, (target_size, target_size))
new_shape.friction = self.friction
new_shape.collision_type = 1 # Set collision type for resized sisyphus
self.space.add(new_shape)
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
move_force = -base_move_force
# Apply additional force based on strength when pushing boulders
if self.current_boulder and self.current_boulder['state'] == 'normal':
boulder = self.current_boulder['body']
if self.sisyphus.position.x > boulder.position.x:
move_force -= strength
self.sisyphus.apply_impulse_at_world_point((move_force, 0), self.sisyphus.position)
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
move_force = base_move_force
# Apply additional force based on strength when pushing boulders
if self.current_boulder and self.current_boulder['state'] == 'normal':
boulder = self.current_boulder['body']
if self.sisyphus.position.x < boulder.position.x:
move_force += strength
self.sisyphus.apply_impulse_at_world_point((move_force, 0), self.sisyphus.position)
def jump(self):
# Play jump sound
if self.jump_sound:
self.jump_sound.play()
# Apply jump force in world coordinates (always upwards)
jump_force = (0, -self.jump_force)
self.sisyphus.apply_impulse_at_world_point(jump_force, self.sisyphus.position)
def update_camera(self):
# Update camera position based on Sisyphus's position
target_x = self.sisyphus.position.x - 400 # Center Sisyphus horizontally
self.camera_x += (target_x - self.camera_x) * 0.1 # Smooth camera movement
self.camera_x = max(0, min(self.camera_x, self.width - 800)) # Clamp camera position
def show_splash_screen(self):
if not self.splash_screen:
return
fade_duration = 2000 # 2 seconds for splash screen fade out
start_time = pygame.time.get_ticks()
running = True
while running:
current_time = pygame.time.get_ticks()
elapsed = current_time - start_time
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN:
running = False
# Fill screen with black
self.screen.fill((0, 0, 0))
# Calculate alpha for fade out
if elapsed < fade_duration:
alpha = 255
else:
alpha = max(0, 255 - ((elapsed - fade_duration) * 255 // 1000))
if alpha == 0:
running = False
# Draw splash screen with fade, centered in window
splash_surface = self.splash_screen.copy()
splash_surface.set_alpha(alpha)
splash_rect = splash_surface.get_rect(center=(400, 300)) # Center in 800x600 window
self.screen.blit(splash_surface, splash_rect)
pygame.display.flip()
self.clock.tick(60)
return True
def toggle_music(self):
self.music_enabled = not self.music_enabled
self.is_fading = True
self.is_initial_fade = False # This is a toggle fade
self.current_fade_frame = 0
if not self.music_enabled:
self.target_volume = 0.0
else:
self.target_volume = 0.7
if not pygame.mixer.music.get_busy():
pygame.mixer.music.unpause()
def update_music_fade(self):
if self.is_fading:
self.current_fade_frame += 1
fade_frames = self.initial_fade_frames if self.is_initial_fade else self.toggle_fade_frames
progress = min(self.current_fade_frame / fade_frames, 1.0)
if self.music_enabled:
# Fading in
self.music_volume = min(progress * self.target_volume, self.target_volume)
else:
# Fading out - start from current volume instead of max volume
start_volume = self.music_volume
self.music_volume = max(0, start_volume * (1.0 - progress))
pygame.mixer.music.set_volume(self.music_volume)
if self.current_fade_frame >= fade_frames:
self.is_fading = False
self.is_initial_fade = False
if not self.music_enabled:
pygame.mixer.music.pause()
def next_track(self):
# Visual feedback for button
self.next_button_pressed = True