fade
[hs] / HEAVENSlime.py
1 from pygame import Rect, Surface, Color, mixer, PixelArray
2 from pygame.image import load
3 from pygame.mixer import Sound
4 from pygame.transform import scale
5
6 from lib.pgfw.pgfw.Game import Game
7 from lib.pgfw.pgfw.GameChild import GameChild
8 from lib.pgfw.pgfw.Sprite import Sprite
9 from lib.pgfw.pgfw.Animation import Animation
10 from lib.pgfw.pgfw.extension import get_hsla_color, get_value_in_range
11 from lib.pgfw.pgfw.Note import Note, Chord
12
13 class SoundEffect(GameChild, Sound):
14
15 def __init__(self, parent, path, volume=1.0):
16 GameChild.__init__(self, parent)
17 Sound.__init__(self, path)
18 self.display_surface = self.get_display_surface()
19 self.set_volume(volume)
20
21 def play(self, loops=0, maxtime=0, fade_ms=0, position=None, x=None):
22 channel = Sound.play(self, loops, maxtime, fade_ms)
23 if x is not None:
24 position = float(x) / self.display_surface.get_width()
25 if position is not None and channel is not None:
26 channel.set_volume(*self.get_panning(position))
27 return channel
28
29 def get_panning(self, position):
30 return 1 - max(0, ((position - .5) * 2)), \
31 1 + min(0, ((position - .5) * 2))
32
33
34 class HEAVENSlime(Game):
35
36 CROSSFADE_MS = 3000
37
38 def __init__(self):
39 Game.__init__(self)
40 self.title = Title(self)
41 self.arena = Arena(self)
42 self.title.activate()
43 self.get_input().register_any_press_ignore("left", "right", "pause")
44 self.subscribe(self.respond)
45
46 def reset(self):
47 if not self.title.active:
48 self.arena.deactivate()
49 self.title.activate()
50
51 def respond(self, event):
52 if self.get_delegate().compare(event, "reset-game"):
53 self.reset()
54
55 def update(self):
56 self.title.update()
57 self.arena.update()
58
59
60 class Title(GameChild):
61
62 CHARGE_INCREASE = 1
63 CHARGE_THRESHOLD = 0
64
65 def __init__(self, parent):
66 GameChild.__init__(self, parent)
67 self.music = Sound(self.get_resource("Title.ogg"))
68 self.music.set_volume(.45)
69 self.subscribe(self.respond)
70
71 def respond(self, event):
72 if self.active:
73 delegate = self.get_delegate()
74 if delegate.compare(event, "any"):
75 self.charging = True
76 elif delegate.compare(event, "any", cancel=True):
77 self.charging = False
78 self.spring()
79
80 def spring(self):
81 if self.charge > self.CHARGE_THRESHOLD:
82 self.deactivate()
83 self.parent.arena.activate()
84 self.charge = 0
85
86 def activate(self):
87 self.active = True
88 self.charging = False
89 self.charge = 0
90 self.music.play(-1, 0, HEAVENSlime.CROSSFADE_MS)
91
92 def deactivate(self):
93 self.active = False
94 self.music.fadeout(HEAVENSlime.CROSSFADE_MS)
95
96 def update(self):
97 if self.active:
98 background = load(self.get_resource("Title.png")).convert()
99 self.get_display_surface().blit(background, (0, 0))
100 if self.charging:
101 self.charge += self.CHARGE_INCREASE
102
103
104 class Arena(Animation):
105
106 SKY_COLOR = (25, 168, 255)
107 GRADIENT_SPAN = 100
108 GRADIENT_INTERVAL = 400
109 GRADIENT_HUE_STEP = 1
110 INITIAL_BOOST = 3
111
112 def __init__(self, parent):
113 Animation.__init__(self, parent)
114 self.boost_sound = SoundEffect(self, self.get_resource("use-boost-0.wav"), .7)
115 self.no_gas_sound = SoundEffect(self, self.get_resource("no-gas-1.wav"), .7)
116 self.thud_sound = SoundEffect(self, self.get_resource("thud-1.wav"), .7)
117 self.safe_sound = SoundEffect(self, self.get_resource("safe-2.wav"), .7)
118 ground = self.ground = Sprite(self)
119 ground.load_from_path(self.get_resource("Ground.png"), True)
120 end = self.end = Sprite(self)
121 end.load_from_path(self.get_resource("End.png"), True)
122 end.location.center = self.get_display_surface().get_rect().center
123 self.music = Sound(self.get_resource("Climb.ogg"))
124 self.saved_music_volume = self.music.get_volume()
125 self.meter = Meter(self)
126 self.slime = Slime(self)
127 platforms = self.platforms = []
128 y = 0
129 for difference in (150, 120, 100, 200, 180, 250, 100, 300, 350, 400):
130 y += difference
131 platforms.append(Platform(self, y))
132 platforms[-1].final = True
133 self.register(self.set_gradient, self.restart_game, self.restore_volume)
134 self.subscribe(self.respond)
135 self.reset()
136 self.deactivate()
137 self.play(self.set_gradient, interval=self.GRADIENT_INTERVAL)
138
139 def reset(self):
140 self.halt(self.restart_game)
141 self.suppressing_input = False
142 self.set_active_on_update = False
143 self.boost_count = self.INITIAL_BOOST
144 boosts = self.boosts = Sprite(self, 333)
145 ds = self.get_display_surface()
146 boosts.load_from_path(self.get_resource("boost"), True)
147 boosts.location.top = 10
148 boosts.location.right = ds.get_rect().right - 10
149 boosts.add_location(offset=(-8, 0))
150 boosts.add_location(offset=(-16, 0))
151 self.end.hide()
152 self.ground.location.bottom = ds.get_rect().bottom
153 self.slime.reset()
154 self.meter.reset()
155 for platform in self.platforms:
156 platform.reset()
157
158 def respond(self, event):
159 if self.active and not self.suppressing_input:
160 delegate = self.get_delegate()
161 if delegate.compare(event, "pause"):
162 if self.paused:
163 self.unpause()
164 else:
165 self.pause()
166 elif not self.paused:
167 if delegate.compare(event, "any"):
168 self.meter.hold()
169 elif delegate.compare(event, "any", cancel=True):
170 if not self.slime.travelling or self.boost_count:
171 if self.slime.travelling:
172 self.discard_boost()
173 self.slime.launch(not self.slime.travelling)
174 elif not self.boost_count:
175 self.no_gas_sound.play()
176 self.meter.release()
177
178 def discard_boost(self):
179 self.boost_count -= 1
180 if self.boost_count >= 1:
181 self.boosts.remove_locations(self.boosts.locations[-1])
182 else:
183 self.boosts.hide()
184 self.boost_sound.play()
185
186 def pause(self):
187 self.paused = True
188 mixer.pause()
189
190 def unpause(self):
191 self.paused = False
192 mixer.unpause()
193
194 def set_gradient(self):
195 if not self.paused:
196 self.background = Surface(self.get_screen().get_size())
197 rh = self.background.get_height() / self.GRADIENT_SPAN
198 y = self.background.get_height() - rh
199 for hue in xrange(self.sky_hue, self.sky_hue + self.GRADIENT_SPAN, 1):
200 color = Color(0, 0, 0)
201 color.hsla = hue % 360, 60, 60, 100
202 self.background.fill(get_hsla_color(hue, 60, 60),
203 (0, y, self.background.get_width(), y + rh))
204 y -= rh
205 self.sky_hue += self.GRADIENT_HUE_STEP
206 if self.sky_hue >= 360:
207 self.sky_hue -= 360
208
209 def activate(self):
210 self.paused = False
211 self.sky_hue = 0
212 self.music.play(-1, 0, HEAVENSlime.CROSSFADE_MS)
213 self.meter.reset()
214 self.reset()
215 self.set_active_on_update = True
216
217 def deactivate(self):
218 self.active = False
219 self.music.fadeout(HEAVENSlime.CROSSFADE_MS)
220
221 def shift_world(self, dy):
222 for platform in self.platforms:
223 platform.move(dy=dy)
224 self.ground.move(dy=dy)
225
226 def restore_volume(self):
227 new_volume = self.music.get_volume() + .17
228 if new_volume > self.saved_music_volume:
229 self.halt(self.restore_volume)
230 self.music.set_volume(new_volume)
231 self.get_audio().original_volumes[self.music] = new_volume
232
233 def update(self):
234 if self.active:
235 Animation.update(self)
236 ds = self.get_display_surface()
237 if self.slime.travelling:
238 self.shift_world(self.slime.speed)
239 if self.ground.location.bottom <= ds.get_height():
240 self.shift_world(ds.get_height() - self.ground.location.bottom)
241 self.slime.stop()
242 elif self.slime.speed <= 0:
243 for platform in self.platforms:
244 if platform.passed and platform.collide(self.slime):
245 self.slime.stop()
246 self.shift_world(self.slime.location.bottom - platform.location.top + 1)
247 if abs(self.slime.speed) < Platform.SAFETY_THRESHOLD:
248 platform.set_safe()
249 self.safe_sound.play()
250 if platform.final:
251 self.end.unhide()
252 self.play(self.restart_game, delay=5000, play_once=True)
253 self.suppressing_input = True
254 else:
255 platform.set_miss()
256 self.saved_music_volume = self.music.get_volume()
257 self.music.set_volume(0)
258 self.get_audio().original_volumes[self.music] = 0
259 self.play(self.restore_volume, delay=10000)
260 self.thud_sound.play()
261 self.suppressing_input = True
262 self.meter.reset()
263 break
264 ds.blit(self.background, (0, 0))
265 self.ground.update()
266 for platform in self.platforms:
267 platform.update()
268 self.meter.update()
269 self.boosts.update()
270 self.slime.update()
271 self.end.update()
272 elif self.set_active_on_update:
273 self.active = True
274 self.set_active_on_update = False
275
276 def restart_game(self):
277 self.get_game().reset()
278
279
280 class Meter(Animation):
281
282 HUE_STEP = 60
283 CHARGE_INCREASE = .0125
284 WIDTH_RANGE = 4, 220
285 HEIGHT_RANGE = 1, 30
286 COLORS = ["#FF8080", "#FFC080", "#FFFF80", "#C0FF80", "#80FF80", "#80FFC0",
287 "#80FFFF", "#80C0FF", "#8080FF", "#C080FF", "#FF80FF"]
288
289 def __init__(self, parent):
290 Animation.__init__(self, parent)
291 self.color_index = 0
292
293 def reset(self):
294 self.charge = 0
295 self.charging = False
296
297 def hold(self):
298 self.charging = True
299
300 def release(self):
301 self.reset()
302
303 def update(self):
304 if self.charging:
305 self.charge += self.CHARGE_INCREASE
306 if self.charge > 1:
307 self.charge = 0
308 self.color_index += 1
309 if self.color_index == len(self.COLORS):
310 self.color_index = 0
311 width = get_value_in_range(self.WIDTH_RANGE[0], self.WIDTH_RANGE[1], self.charge)
312 height = get_value_in_range(self.HEIGHT_RANGE[0], self.HEIGHT_RANGE[1], self.charge)
313 rect = Rect(0, 0, width, height)
314 ds = self.get_display_surface()
315 rect.midbottom = ds.get_rect().midbottom
316 surface = Surface(rect.size)
317 surface.fill(Color(self.COLORS[self.color_index]))
318 # ds.blit(surface, rect)
319
320
321 class Slime(Sprite):
322
323 LAUNCH = 7.25
324 SEGMENT_WIDTH = 24
325 HEIGHT = 20
326 COMPRESS = 16
327 BULGE = 40
328 COLORS = Color(28, 187, 183), Color(235, 42, 128), Color(220, 240, 13)
329 GRAVITY = .125
330 MAX_SPEED = 12
331 DROP_SPEED = 8
332 DROP_DELAY = 1000
333
334 def __init__(self, parent):
335 Sprite.__init__(self, parent)
336 self.launch_sound = SoundEffect(self, self.get_resource("jump-0.wav"), .7)
337 self.drop_sound = SoundEffect(self, self.get_resource("end-1.wav"), 1)
338 self.face = load(self.get_resource("Face.png")).convert_alpha()
339 self.register(self.set_dropping)
340
341 def reset(self):
342 self.segments = [self.COLORS[0]]
343 self.travelling = False
344 self.dropping = False
345 self.place()
346 self.halt()
347
348 def place(self):
349 ds = self.get_display_surface()
350 self.location.bottom = ds.get_height() - self.parent.ground.location.h - 1
351 self.location.centerx = ds.get_rect().centerx
352
353 def set_frame(self):
354 self.clear_frames()
355 charge = self.parent.meter.charge
356 width = self.SEGMENT_WIDTH
357 height = self.HEIGHT
358 if charge:
359 width_add = self.BULGE * charge
360 width = int((self.SEGMENT_WIDTH + width_add) * len(self.segments))
361 height_subtract = self.COMPRESS * charge
362 height = int(self.HEIGHT - height_subtract)
363 frame = Surface((width, height))
364 frame.fill((128, 128, 128))
365 frame.set_colorkey((128, 128, 128))
366 x = 1
367 segment_ii = 0
368 step = float(width - 2) / len(self.segments)
369 frame.set_clip((1, 1, width - 2, height - 2))
370 while x <= width - step:
371 frame.fill(self.segments[segment_ii], (int(x), 1, int(step) + 1, height - 2))
372 x += step
373 segment_ii += 1
374 frame.set_clip()
375 border = (0, 0, 0)
376 frame.fill(border, (2, 0, width - 4, 1))
377 frame.set_at((1, 1), border)
378 frame.set_at((width - 2, 1), border)
379 frame.fill(border, (0, 2, 1, height - 4))
380 frame.fill(border, (width - 1, 2, 1, height - 4))
381 frame.set_at((1, height - 2), border)
382 frame.set_at((width - 2, height - 2), border)
383 frame.fill(border, (2, height - 1, width - 4, 1))
384 face = self.face
385 if charge:
386 face = scale(face, (face.get_width() + int(width_add / 4),
387 face.get_height() - int(height_subtract / 4)))
388 fr = face.get_rect()
389 sr = frame.get_rect()
390 fr.centerx = sr.centerx
391 fr.centery = sr.centery + sr.h / 8
392 frame.blit(face, fr)
393 self.add_frame(frame)
394
395 def launch(self, play_sound=True):
396 self.travelling = True
397 self.speed = self.parent.meter.charge * self.MAX_SPEED
398 if play_sound:
399 self.launch_sound.play()
400 if self.parent.music.get_volume() == 0:
401 self.parent.music.set_volume(self.parent.saved_music_volume)
402 self.get_audio().original_volumes[self.parent.music] = self.parent.saved_music_volume
403
404
405 def stop(self):
406 self.travelling = False
407
408 def drop(self):
409 self.drop_sound.play()
410 self.play(self.set_dropping, delay=self.DROP_DELAY, play_once=True)
411
412 def set_dropping(self):
413 self.dropping = True
414
415 def update(self):
416 self.set_frame()
417 if not self.dropping:
418 self.place()
419 else:
420 self.move(dy=self.DROP_SPEED)
421 if self.location.top > self.get_display_surface().get_height() + 300:
422 self.dropping = False
423 self.parent.reset()
424 if self.travelling:
425 self.speed -= self.GRAVITY
426 Sprite.update(self)
427
428 # TYPES
429 # - rising away
430 # - scrolling horizontally
431 # - ping-pong horizontally
432 # - springing up
433 # - arcing swing
434 # - blinking
435 # - defended
436 #
437 # COLORS
438 # - ff1c66 (rose)
439 # - ffff66 (pale yellow)
440 # - 78ff66 (pale green)
441
442 class Platform(Sprite):
443
444 SAFETY_THRESHOLD = 3.75
445
446 def __init__(self, parent, y, final=False):
447 Sprite.__init__(self, parent)
448 self.y = y
449 self.final = final
450 self.register(self.drop, interval=700)
451
452 def reset(self):
453 ds = self.get_display_surface()
454 self.location.centerx = ds.get_rect().centerx
455 self.location.centery = ds.get_height() - self.y
456 self.set_unused()
457 self.unhide()
458 self.halt()
459
460 def set_unused(self):
461 self.clear_frames()
462 self.passed = False
463 if not self.final:
464 self.load_from_path(self.get_resource("Platform.png"), True)
465 else:
466 self.add_frame_with_iris("Platform.png")
467
468 def add_frame_with_iris(self, path):
469 base = load(self.get_resource(path)).convert_alpha()
470 br = base.get_rect()
471 iris = load(self.get_resource("Iris.png")).convert_alpha()
472 pixels = PixelArray(iris)
473 pixels.replace((255, 255, 255), Color("#ff1c66"))
474 del pixels
475 ir = iris.get_rect()
476 ir.center = br.center
477 base.blit(iris, ir)
478 self.add_frame(base)
479
480 def set_safe(self):
481 self.clear_frames()
482 if not self.final:
483 self.load_from_path(self.get_resource("Safe.png"), True)
484 else:
485 self.add_frame_with_iris("Safe.png")
486
487 def set_miss(self):
488 self.clear_frames()
489 self.load_from_path(self.get_resource("miss/miss-0.png"), True)
490 self.blink_index = 0
491 self.play(self.drop, delay=1000)
492
493 def drop(self):
494 self.blink_index += 1
495 if self.blink_index > 2:
496 self.hide()
497 self.halt()
498 self.parent.slime.drop()
499 else:
500 self.clear_frames()
501 self.load_from_path(self.get_resource("miss/miss-" + str(self.blink_index) + ".png"), True)
502
503 def update(self):
504 if self.location.top >= self.parent.slime.location.bottom:
505 self.passed = True
506 Sprite.update(self)