1""" 2Graphical user interface for PySpaceWar 3""" 4 5import os 6import sys 7import glob 8import time 9import math 10import random 11import itertools 12 13try: 14 from configparser import ConfigParser 15except ImportError: 16 from ConfigParser import RawConfigParser as ConfigParser 17 18try: 19 unicode 20except NameError: 21 unicode = str 22 23import pygame 24from pygame.locals import ( 25 FULLSCREEN, 26 KEYDOWN, 27 KMOD_ALT, 28 K_1, 29 K_2, 30 K_BACKSPACE, 31 K_CAPSLOCK, 32 K_DELETE, 33 K_DOWN, 34 K_EQUALS, 35 K_ESCAPE, 36 K_F1, 37 K_F12, 38 K_KP_ENTER, 39 K_KP_PERIOD, 40 K_LALT, 41 K_LCTRL, 42 K_LEFT, 43 K_LMETA, 44 K_LSHIFT, 45 K_LSUPER, 46 K_MINUS, 47 K_MODE, 48 K_NUMLOCK, 49 K_PAGEDOWN, 50 K_PAGEUP, 51 K_PAUSE, 52 K_RALT, 53 K_RCTRL, 54 K_RETURN, 55 K_RIGHT, 56 K_RMETA, 57 K_RSHIFT, 58 K_RSUPER, 59 K_SCROLLOCK, 60 K_SPACE, 61 K_UP, 62 K_a, 63 K_d, 64 K_f, 65 K_h, 66 K_o, 67 K_q, 68 K_s, 69 K_w, 70 MOUSEBUTTONDOWN, 71 MOUSEBUTTONUP, 72 MOUSEMOTION, 73 QUIT, 74 RESIZABLE, 75 Rect, 76 VIDEORESIZE, 77) 78 79from .world import Vector, Missile 80from .game import Game 81from .ai import AIController 82from .version import version 83 84 85MODIFIER_KEYS = { 86 K_NUMLOCK, K_CAPSLOCK, K_SCROLLOCK, 87 K_RSHIFT, K_LSHIFT, K_RCTRL, K_LCTRL, K_RALT, K_LALT, 88 K_RMETA, K_LMETA, K_LSUPER, K_RSUPER, K_MODE, 89} 90 91 92DEFAULT_CONTROLS = { 93 # Player 1 94 'P1_TOGGLE_AI': K_1, 95 'P1_LEFT': K_LEFT, 96 'P1_RIGHT': K_RIGHT, 97 'P1_FORWARD': K_UP, 98 'P1_BACKWARD': K_DOWN, 99 'P1_BRAKE': K_RALT, 100 'P1_FIRE': K_RCTRL, 101 # Player 2 102 'P2_TOGGLE_AI': K_2, 103 'P2_LEFT': K_a, 104 'P2_RIGHT': K_d, 105 'P2_FORWARD': K_w, 106 'P2_BACKWARD': K_s, 107 'P2_BRAKE': K_LALT, 108 'P2_FIRE': K_LCTRL, 109} 110 111 112HELP_TEXT = u"""\ 113=PySpaceWar= 114 115Two ships duel in a gravity field. Gravity doesn't affect the ships 116themselves (which have spanking new anti-gravity devices), but it affects 117missiles launced by the ships. The law of inertia applies to the ships \u2014 118if you accelerate in one direction, you will continue to move in that direction 119until you accelerate in another direction. 120 121The two player mode is good for target practice, and to get the feel of your 122ship. 123 124=Player 1 Controls= 125 126 P1_LEFT, P1_RIGHT \u2014 rotate 127 P1_FORWARD \u2014 accelerate in the direction you're facing 128 P1_BACKWARD \u2014 accelerate in the opposite direction 129 P1_FIRE \u2014 launch a missile 130 P1_BRAKE \u2014 brake (lose 5% speed) 131 P1_TOGGLE_AI \u2014 enable/disable computer control 132 133=Player 2 Controls= 134 135 P2_LEFT, P2_RIGHT \u2014 rotate 136 P2_FORWARD \u2014 accelerate in the direction you're facing 137 P2_BACKWARD \u2014 accelerate in the opposite direction 138 P2_FIRE \u2014 launch a missile 139 P2_BRAKE \u2014 brake (lose 5% speed) 140 P2_TOGGLE_AI \u2014 enable/disable computer control 141 142=Other Controls= 143 144 F1 \u2014 help 145 ESC \u2014 game menu 146 PAUSE \u2014 pause the game 147 O \u2014 hide/show missile orbits 148 F, ALT+ENTER \u2014 toggle full-screen mode 149 +, - \u2014 zoom in/out 150 mouse wheel \u2014 zoom in/out 151 left click \u2014 game menu 152 right drag \u2014 drag the viewport around 153 154=Credits= 155 156 Developer \u2014 Marius Gedminas 157 AI \u2014 Ignas Mikalaj\u016bnas 158 Graphics \u2014 IGE - Outer Space (planet images) 159 \u2014 Hubble Space Telescope (background) 160 \u2014 Marius Gedminas (everything else) 161 162PySpaceWar is powered by PyGame, Python and SDL. 163 164This program is free software; you can redistribute it and/or modify it under 165the terms of the GNU General Public License as published by the Free Software 166Foundation; either version 2 of the License, or (at your option) any later 167version. 168""" 169 170 171def key_name(key): 172 """Return the name of the key. 173 174 >>> key_name(K_RCTRL) 175 'RIGHT CTRL' 176 >>> key_name(None) 177 '(unset)' 178 179 """ 180 if not key: 181 return '(unset)' 182 return pygame.key.name(key).upper() 183 184 185def fixup_keys_in_text(text, controls): 186 """Replace action names with key names in help text. 187 188 >>> fixup_keys_in_text('Press FIRE to start', {'FIRE': [K_RCTRL]}) 189 'Press RIGHT CTRL to start' 190 191 """ 192 for action, keys in controls.items(): 193 text = text.replace(action, key_name(keys[0])) 194 return text 195 196 197def is_modifier_key(key): 198 """Is this key a modifier?""" 199 return key in MODIFIER_KEYS 200 201 202def find(*filespec): 203 """Construct a pathname relative to the location of this module.""" 204 basedir = os.path.dirname(__file__) 205 return os.path.join(basedir, *filespec) 206 207 208def colorblend(col1, col2, alpha=0.5): 209 """Blend two colors. 210 211 For example, let's blend 25% black and 75% white 212 213 >>> colorblend((0, 0, 0), (255, 255, 255), 0.25) 214 (191, 191, 191) 215 216 """ 217 r1, g1, b1 = col1 218 r2, g2, b2 = col2 219 beta = 1-alpha 220 return ( 221 int(alpha*r1+beta*r2), 222 int(alpha*g1+beta*g2), 223 int(alpha*b1+beta*b2), 224 ) 225 226 227def linear(x, xmax, y1, y2): 228 """Calculate a linear transition from y1 to y2 as x moves from 0 to xmax. 229 230 >>> for x in range(10): 231 ... print('*' * int(linear(x, 9, 1, 10))) 232 * 233 ** 234 *** 235 **** 236 ***** 237 ****** 238 ******* 239 ******** 240 ********* 241 ********** 242 243 """ 244 return y1 + (y2 - y1) * float(x) / xmax 245 246 247def smooth(x, xmax, y1, y2): 248 """Calculate a smooth transition from y1 to y2 as x moves from 0 to xmax. 249 250 >>> for x in range(10): 251 ... print('*' * int(smooth(x, 9, 1, 10))) 252 * 253 * 254 * 255 ** 256 **** 257 ****** 258 ******** 259 ********* 260 ********* 261 ********* 262 263 """ 264 t = -5 + 10 * (float(x) / xmax) 265 value = 1 / (1 + math.exp(-t)) 266 return y1 + (y2 - y1) * value 267 268 269class Viewport(object): 270 """A viewport to the universe. 271 272 The responsibility of this class is to provide a mapping from screen 273 coordinates to world coordinates and back. 274 275 Attributes and properties: 276 277 ``origin`` -- point in the universe corresponding to the center of 278 the screen. 279 280 ``scale`` -- ratio of pixels to world coordinate units. 281 282 """ 283 284 AUTOSCALE_FACTOR = 1.001 285 286 def __init__(self, surface): 287 self.surface = surface 288 self._origin = Vector(0, 0) 289 self._scale = 1.0 290 self._recalc() 291 292 def _set_origin(self, new_origin): 293 self._origin = new_origin 294 self._recalc() 295 296 origin = property(lambda self: self._origin, _set_origin) 297 298 def _set_scale(self, new_scale): 299 self._scale = new_scale 300 self._recalc() 301 302 scale = property(lambda self: self._scale, _set_scale) 303 304 def _recalc(self): 305 """Recalculate everything when origin/scale/screen size changes.""" 306 surface_w, surface_h = self.surface.get_size() 307 # We want self.screen_pos(self.origin) == (surface_w/2, surface_h/2) 308 self._screen_x = surface_w * 0.5 - self.origin.x * self.scale 309 self._screen_y = surface_h * 0.5 + self.origin.y * self.scale 310 # Let's cache world_bounds 311 x1, y1 = self.world_pos((0, 0)) 312 x2, y2 = self.world_pos((surface_w, surface_h)) 313 self.world_bounds = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) 314 315 def surface_size_changed(self): 316 """Notify that surface size has changed.""" 317 self._recalc() 318 319 def screen_len(self, world_len): 320 """Convert a length in world coordinate units to pixels.""" 321 return int(world_len * self.scale) 322 323 def screen_pos(self, world_pos): 324 """Convert world coordinates to screen coordinates.""" 325 return (int(self._screen_x + world_pos[0] * self._scale), 326 int(self._screen_y - world_pos[1] * self._scale)) 327 328 def draw_trail(self, list_of_world_pos, gradient, set_at): 329 """Draw a trail. 330 331 Optimization to avoid function calls and construction of lists. 332 """ 333 sx = self._screen_x 334 sy = self._screen_y 335 scale = self._scale 336 for (x, y), color in zip(list_of_world_pos, gradient): 337 set_at((int(sx + x * scale), int(sy - y * scale)), color) 338 339 def world_pos(self, screen_pos): 340 """Convert screen coordinates into world coordinates.""" 341 x = (screen_pos[0] - self._screen_x) / self._scale 342 y = -(screen_pos[1] - self._screen_y) / self._scale 343 return (x, y) 344 345 def in_screen(self, world_pos): 346 """Is a position visible on screen?""" 347 xmin, ymin, xmax, ymax = self.world_bounds 348 return xmin <= world_pos[0] <= xmax and ymin <= world_pos[1] <= ymax 349 350 def shift_by_pixels(self, delta): 351 """Shift the origin by a given number of screen pixels.""" 352 delta_x, delta_y = delta 353 self.origin += Vector(delta_x / self.scale, -delta_y / self.scale) 354 355 def keep_visible(self, points, margin): 356 """Adjust origin and scale to keep all specified points visible. 357 358 Postcondition: 359 360 margin <= x <= screen_w - margin 361 362 and 363 364 margin <= y <= screen_h - margin 365 366 for x, y in [self.screen_pos(pt) for pt in points] 367 368 """ 369 if len(points) > 1: 370 xs = [pt.x for pt in points] 371 ys = [pt.y for pt in points] 372 w = max(xs) - min(xs) 373 h = max(ys) - min(ys) 374 xmin, ymin, xmax, ymax = self.world_inner_bounds(margin) 375 seen_w = xmax - xmin 376 seen_h = ymax - ymin 377 factor = 1.0 378 while seen_w < w or seen_h < h: 379 factor *= self.AUTOSCALE_FACTOR 380 seen_w *= self.AUTOSCALE_FACTOR 381 seen_h *= self.AUTOSCALE_FACTOR 382 if factor != 1.0: 383 self.scale /= factor 384 385 for pt in points: 386 xmin, ymin, xmax, ymax = self.world_inner_bounds(margin) 387 if pt.x < xmin: 388 self.origin -= Vector(xmin - pt.x, 0) 389 elif pt.x > xmax: 390 self.origin -= Vector(xmax - pt.x, 0) 391 if pt.y < ymin: 392 self.origin -= Vector(0, ymin - pt.y) 393 elif pt.y > ymax: 394 self.origin -= Vector(0, ymax - pt.y) 395 396 def world_inner_bounds(self, margin): 397 """Calculate the rectange in world coordinates that fits inside a 398 given margin in the screen. 399 400 Returns (xmin, ymin, xmax, ymax). 401 402 For all points (x, y) where (xmin <= x <= xmax and ymin <= y <= ymax) 403 it is true, that margin <= sx <= screen_w - margin and 404 margin <= sy <= screen_h - margin; here sx, sy == screen_pos(x, y) 405 """ 406 surface_w, surface_h = self.surface.get_size() 407 x1, y1 = self.world_pos((margin, margin)) 408 x2, y2 = self.world_pos((surface_w - margin, surface_h - margin)) 409 return min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) 410 411 412class FrameRateCounter(object): 413 """Frame rate counter.""" 414 415 avg_last_n_frames = 10 # calculate average FPS for 10 frames 416 417 get_ticks = staticmethod(pygame.time.get_ticks) 418 419 def __init__(self): 420 self.frames = [] 421 422 def frame(self): 423 """Tell the counter that a new frame has just been drawn.""" 424 self.frames.append(self.get_ticks()) 425 if len(self.frames) > self.avg_last_n_frames: 426 del self.frames[0] 427 428 def reset(self): 429 """Tell the counter that we stopped drawing frames for a while. 430 431 Call this method if you pause the game for a time. 432 """ 433 self.frames = [] 434 435 def fps(self): 436 """Calculate the frame rate. 437 438 Returns 0 if not enough frames have been drawn yet. 439 """ 440 if len(self.frames) < 2: 441 return 0 442 ms = self.frames[-1] - self.frames[0] 443 frames = len(self.frames) - 1 444 return frames * 1000.0 / ms 445 446 def notional_fps(self): 447 """Calculate the frame rate assuming that I'm about to draw a frame. 448 449 Returns 0 if not enough frames have been drawn yet. 450 """ 451 if len(self.frames) < 1: 452 return 0.0 453 ms = self.get_ticks() - self.frames[0] 454 frames = len(self.frames) 455 return frames * 1000.0 / ms 456 457 458class HUDCollection(object): 459 """A collection of heads up display widgets.""" 460 461 def __init__(self, widgets=()): 462 self.widgets = list(widgets) 463 464 def draw(self, surface): 465 """Draw all the elements.""" 466 for w in self.widgets: 467 w.draw(surface) 468 469 470class HUDElement(object): 471 """Heads-up status display widget.""" 472 473 def __init__(self, width, height, xalign, yalign): 474 self.width = width 475 self.height = height 476 self.xalign = xalign 477 self.yalign = yalign 478 479 def position(self, surface, margin=10): 480 """Calculate screen position for the widget.""" 481 surface_w, surface_h = surface.get_size() 482 x = margin + self.xalign * (surface_w - self.width - 2 * margin) 483 y = margin + self.yalign * (surface_h - self.height - 2 * margin) 484 return int(x), int(y) 485 486 def draw(self, surface): 487 """Draw the element.""" 488 pass 489 490 491class HUDLabel(HUDElement): 492 """A static text label.""" 493 494 DEFAULT_COLOR = (250, 250, 255) 495 496 def __init__(self, font, text, xalign=0, yalign=0, color=DEFAULT_COLOR): 497 self.font = font 498 self.width, self.height = self.font.size(text) 499 self.xalign = xalign 500 self.yalign = yalign 501 self.color = color 502 self.rendered_text = font.render(text, True, self.color) 503 504 def draw(self, surface): 505 """Draw the element.""" 506 x, y = self.position(surface) 507 surface.blit(self.rendered_text, (x, y)) 508 509 510class HUDFormattedText(HUDElement): 511 """A static text screen.""" 512 513 bgcolor = (0x01, 0x02, 0x08) 514 color = (0xff, 0xff, 0xff) 515 page_number_color = (0x80, 0xcc, 0xff) 516 alpha = int(0.95 * 255) 517 518 xpadding = 40 519 ypadding = 40 520 521 indent = 20 522 tabstop = 140 523 524 def __init__(self, font, bold_font, text, xalign=0.5, yalign=0.5, 525 xsize=1.0, ysize=1.0, small_font=None): 526 self.font = font 527 self.bold_font = bold_font 528 self.small_font = small_font or font 529 self.text = text 530 self.xsize = xsize 531 self.ysize = ysize 532 self.xalign = xalign 533 self.yalign = yalign 534 self.page = 0 535 self.n_pages = -1 536 537 def position(self, surface, margin=30): 538 """Calculate screen position for the widget.""" 539 self.width = int((surface.get_width() - 2 * margin) * self.xsize) 540 self.height = int((surface.get_height() - 2 * margin) * self.ysize) 541 return HUDElement.position(self, surface, margin) 542 543 def draw(self, surface): 544 """Draw the element.""" 545 x, y = self.position(surface) # calculates self.width/height as well 546 rect = Rect(x, y, self.width, self.height) 547 buffer = pygame.Surface(rect.size) 548 buffer.set_alpha(self.alpha) 549 buffer.set_colorkey((1, 1, 1)) 550 buffer.fill(self.bgcolor) 551 for ax in (0, rect.width-1): 552 for ay in (0, rect.height-1): 553 buffer.set_at((ax, ay), (1, 1, 1)) 554 surface.blit(buffer, rect.topleft) 555 rect.inflate_ip(-self.xpadding*2, -self.ypadding*2) 556 self.render_text(surface, rect) 557 558 def split_to_paragraphs(self, text): 559 """Split text into paragraphs.""" 560 paragraphs = [] 561 for paragraph in self.text.split('\n\n'): 562 if not paragraph.startswith(' '): 563 # Intented blocks preserve line breaks, all others are joined 564 paragraph = paragraph.replace('\n', ' ') 565 paragraphs.append(paragraph) 566 return paragraphs 567 568 def split_items_into_groups(self, items, size, spacing): 569 """Split a list of tuples (item_size, item) into groups such that 570 the sum of sizes + spacing * (group size - 1) in each group is <= size. 571 572 Think "word wrapping". 573 """ 574 groups = [] 575 cur_group_size = size + 1 576 for item_size, item in items: 577 if cur_group_size > 0 and cur_group_size + item_size > size: 578 cur_group = [] 579 cur_group_size = 0 580 groups.append(cur_group) 581 cur_group_size += item_size + spacing 582 cur_group.append((item_size, item)) 583 return groups 584 585 def layout_paragraph(self, paragraph, width): 586 """Render and lay out a single paragraph. 587 588 Returns (height, bits, keep_with_next) where bits is a list 589 of images (one for each word) with relative coordinates. 590 """ 591 font = self.font 592 leftindent = 0 593 tabstop = 0 594 keep_with_next = False 595 justify = False 596 if paragraph.startswith('=') and paragraph.endswith('='): 597 # =Title= 598 paragraph = paragraph[1:-1] 599 font = self.bold_font 600 keep_with_next = True 601 elif paragraph.startswith(' '): 602 # Indented block 603 leftindent += self.indent 604 width -= self.indent 605 tabstop = self.tabstop 606 else: 607 # Regular text 608 justify = True 609 word_spacing = font.size(' ')[0] 610 line_spacing = font.get_linesize() 611 bits = [] 612 y = 0 613 for line in paragraph.splitlines(): 614 if tabstop and u'\u2014' in line: 615 prefix, line = line.split(u'\u2014', 1) 616 prefix_img = font.render(prefix.strip(), True, self.color) 617 bits.append((prefix_img, (leftindent, y))) 618 wrapwidth = width - tabstop 619 cur_tabstop = tabstop 620 else: 621 wrapwidth = width 622 cur_tabstop = 0 623 words = [font.render(word, True, self.color) 624 for word in line.split()] 625 items = [(img.get_width(), img) for img in words] 626 groups = self.split_items_into_groups(items, wrapwidth, 627 word_spacing) 628 for group in groups: 629 x = leftindent + cur_tabstop 630 extra_spacing = 0 631 if justify and len(group) > 1 and group is not groups[-1]: 632 extra_spacing = wrapwidth 633 for img_width, img in group: 634 extra_spacing -= img_width 635 extra_spacing -= word_spacing * (len(group) - 1) 636 extra_spacing = float(extra_spacing) / (len(group) - 1) 637 for img_width, img in group: 638 bits.append((img, (int(x), y))) 639 x += img_width + word_spacing + extra_spacing 640 y += line_spacing 641 return y, bits, keep_with_next 642 643 def layout_pages(self, text, page_size): 644 """Render and lay out text into pages. 645 646 Returns a list of pages, where each page is a list of of images (one 647 for each word) with relative coordinates. 648 649 Currently the page layout engine doesn't try to split paragraphs. 650 """ 651 width, height = page_size 652 paragraph_spacing = self.font.get_linesize() 653 last_item_size = 0 654 last_item_bits = [] 655 items = [] 656 for paragraph in self.split_to_paragraphs(self.text): 657 size, bits, keep_with_next = self.layout_paragraph(paragraph, 658 width) 659 if last_item_bits: # join with previous 660 dy = last_item_size + paragraph_spacing 661 bits = last_item_bits + [(img, (x, y+dy)) 662 for (img, (x, y)) in bits] 663 size += dy 664 if keep_with_next: 665 last_item_size = size 666 last_item_bits = bits 667 else: 668 items.append((size, bits)) 669 last_item_size = 0 670 last_item_bits = [] 671 if last_item_bits: # last paragraph had "keep with next" set 672 items.append((last_item_size, last_item_bits)) 673 pages = self.split_items_into_groups(items, height, paragraph_spacing) 674 return pages 675 676 def render_text(self, surface, page_rect): 677 """Render the text onto surface.""" 678 paragraph_spacing = self.font.get_linesize() 679 width, height = page_rect.size 680 height -= self.small_font.get_linesize() * 2 681 pages = self.layout_pages(self.text, (width, height)) 682 self.n_pages = len(pages) 683 if not pages: 684 return 685 self.page = max(0, min(self.page, len(pages)-1)) 686 left = page_rect.left 687 top = page_rect.top 688 for para_size, para in pages[self.page]: 689 for img, (x, y) in para: 690 surface.blit(img, (left + x, top + y)) 691 top += para_size + paragraph_spacing 692 page_text = 'Page %d of %d' % (self.page + 1, self.n_pages) 693 img = self.small_font.render(page_text, True, self.page_number_color) 694 r = img.get_rect() 695 r.bottomright = page_rect.bottomright 696 surface.blit(img, r.topleft) 697 698 699class HUDInfoPanel(HUDElement): 700 """Heads-up status display base class.""" 701 702 STD_COLORS = [(0xff, 0xff, 0xff), (0xcc, 0xff, 0xff)] 703 GREEN_COLORS = [(0x7f, 0xff, 0x00), (0xcc, 0xff, 0xff)] 704 705 def __init__(self, font, ncols, nrows=None, xalign=0, yalign=0, 706 colors=STD_COLORS, content=None): 707 self.font = font 708 self.width = int(self.font.size('x')[0] * ncols) 709 self.row_height = self.font.get_linesize() 710 if nrows is None: 711 nrows = len(content) 712 self.height = int(nrows * self.row_height) 713 self.xalign = xalign 714 self.yalign = yalign 715 self.color1, self.color2 = colors 716 self.surface = pygame.Surface((self.width, self.height)) 717 self.surface.set_alpha(255 * 0.8) 718 self.surface.set_colorkey((1, 1, 1)) 719 self.surface.fill((8, 8, 8)) 720 for x in (0, self.width-1): 721 for y in (0, self.height-1): 722 self.surface.set_at((x, y), (1, 1, 1)) 723 self.content = content or [] 724 725 def draw_rows(self, surface, *rows): 726 """Draw some information. 727 728 ``rows`` is a list of 2-tuples. 729 """ 730 x, y = self.position(surface) 731 surface.blit(self.surface, (x, y)) 732 x += 1 733 y += 1 734 for a, b in rows: 735 img = self.font.render(str(a), True, self.color1) 736 surface.blit(img, (x, y)) 737 img = self.font.render(str(b), True, self.color2) 738 surface.blit(img, (x + self.width - 2 - img.get_width(), y)) 739 y += self.row_height 740 741 def draw(self, surface): 742 """Draw the panel.""" 743 rows = [] 744 for content_row in self.content: 745 row = [] 746 for value in content_row: 747 if callable(value): 748 row.append(value()) 749 else: 750 row.append(unicode(value)) 751 rows.append(row) 752 self.draw_rows(surface, *rows) 753 754 755class HUDShipInfo(HUDInfoPanel): 756 """Heads-up ship status display.""" 757 758 def __init__(self, ship, font, xalign=0, yalign=0, 759 colors=HUDInfoPanel.STD_COLORS): 760 HUDInfoPanel.__init__(self, font, 12, 4.75, xalign, yalign, colors) 761 self.ship = ship 762 763 def draw(self, surface): 764 self.draw_rows( 765 surface, 766 ('direction', '%d' % self.ship.direction), 767 ('heading', '%d' % self.ship.velocity.direction()), 768 ('speed', '%.1f' % self.ship.velocity.length()), 769 ('frags', '%d' % self.ship.frags), 770 ) 771 x, y = self.position(surface) 772 x += 1 773 y += self.height - 5 774 w = max(0, int((self.width - 4) * self.ship.health)) 775 pygame.draw.rect(surface, self.color2, (x, y, self.width-2, 4), 1) 776 surface.fill(self.color1, (x+1, y+1, w, 2)) 777 778 779class HUDCompass(HUDElement): 780 """Heads-up ship compass display. 781 782 Shows two vectors: direction of the ship, and the current velocity. 783 """ 784 785 alpha = int(0.9*255) 786 787 BLUE_COLORS = ( 788 (0x00, 0x11, 0x22), 789 (0x99, 0xaa, 0xff), 790 (0x44, 0x55, 0x66), 791 (0xaa, 0x77, 0x66), 792 ) 793 794 GREEN_COLORS = ( 795 (0x00, 0x22, 0x11), 796 (0x99, 0xff, 0xaa), 797 (0x44, 0x66, 0x55), 798 (0xaa, 0x66, 0x77), 799 ) 800 801 radius = 50 802 radar_scale = 0.05 803 velocity_scale = 50 804 805 def __init__(self, world, ship, viewport, xalign=0, yalign=1, 806 colors=BLUE_COLORS): 807 self.world = world 808 self.ship = ship 809 self.viewport = viewport 810 self.width = self.height = 2*self.radius 811 self.surface = pygame.Surface((self.width, self.height)) 812 self.bgcolor, self.fgcolor1, self.fgcolor2, self.fgcolor3 = colors 813 self.xalign = xalign 814 self.yalign = yalign 815 816 def draw(self, surface): 817 if surface.get_bitsize() >= 24: 818 # Only 24 and 32 bpp modes support aaline 819 draw_line = pygame.draw.aaline 820 else: 821 draw_line = pygame.draw.line 822 x = y = self.radius 823 self.surface.set_colorkey((1, 1, 1)) 824 self.surface.fill((1, 1, 1)) 825 self.surface.set_alpha(self.alpha) 826 827 pygame.draw.circle(self.surface, self.bgcolor, (x, y), self.radius) 828 self.surface.set_at((x, y), self.fgcolor1) 829 830 scale = self.radar_scale * self.viewport.scale 831 for body in self.world.objects: 832 if body.mass == 0: 833 continue 834 pos = (body.position - self.ship.position) * scale 835 if pos.length() > self.radius: 836 continue 837 radius = max(0, int(body.radius * scale)) 838 px = x + int(pos.x) 839 py = y - int(pos.y) 840 if radius < 1: 841 self.surface.set_at((px, py), self.fgcolor3) 842 elif radius == 1: 843 self.surface.fill(self.fgcolor3, (px, py, 2, 2)) 844 else: 845 pygame.draw.circle(self.surface, self.fgcolor3, (px, py), 846 radius) 847 848 d = self.ship.direction_vector 849 d = d.scaled(self.radius * 0.9) 850 x2 = x + int(d.x) 851 y2 = y - int(d.y) 852 draw_line(self.surface, self.fgcolor2, (x, y), (x2, y2)) 853 854 v = self.ship.velocity * self.velocity_scale 855 if v.length() > self.radius * 0.9: 856 v = v.scaled(self.radius * 0.9) 857 x2 = x + int(v.x) 858 y2 = y - int(v.y) 859 draw_line(self.surface, self.fgcolor1, (x, y), (x2, y2)) 860 861 surface.blit(self.surface, self.position(surface)) 862 863 864class FadingImage(object): 865 """An image that can smoothly fade away. 866 867 Uses a color key and surface alpha, as an approximation of a smooth fade 868 out. Drops the alpha information in the source image, so instead of 869 smooth anti-aliased text being faded out the users will see ragged text 870 being faded out. 871 872 This happens quickly enough so that nobody will likely notice -- it took me 873 a good ten minutes to remember why I even had the more advanced fading 874 methods ;) 875 """ 876 877 def __init__(self, image): 878 self.image = image.convert() # drop the alpha channel 879 self.image.set_colorkey((0, 0, 0)) 880 881 def draw(self, surface, x, y, alpha): 882 """Draw the image. 883 884 ``alpha`` is a floating point value between 0 and 255. 885 """ 886 self.image.set_alpha(alpha) 887 surface.blit(self.image, (x, y)) 888 889 890class NumPyFadingImage(object): 891 """An image that can smoothly fade away. 892 893 Implemented using NumPy arrays to scale the alpha channel on the fly. 894 """ 895 896 def __init__(self, image): 897 import numpy # noqa 898 self.image = image 899 self.mask = pygame.surfarray.array_alpha(image) 900 if hasattr(pygame.surfarray, 'use_arraytype'): 901 # This is a global switch, which breaks the abstraction a bit. :( 902 pygame.surfarray.use_arraytype('numpy') 903 904 def draw(self, surface, x, y, alpha): 905 """Draw the image. 906 907 ``alpha`` is a floating point value between 0 and 255. 908 """ 909 import numpy 910 numpy.multiply(self.mask, alpha / 255, 911 pygame.surfarray.pixels_alpha(self.image), 912 casting='unsafe') 913 surface.blit(self.image, (x, y)) 914 915 916class HUDTitle(HUDElement): 917 """Fading out title.""" 918 919 paused = False 920 921 def __init__(self, image, xalign=0.5, yalign=0.25): 922 HUDElement.__init__(self, image.get_width(), image.get_height(), 923 xalign, yalign) 924 self.alpha = 255 925 for cls in NumPyFadingImage, FadingImage: 926 try: 927 self.image = cls(image) 928 except ImportError: 929 pass 930 else: 931 break 932 933 def draw(self, surface): 934 """Draw the element.""" 935 if self.alpha < 1: 936 return 937 x, y = self.position(surface) 938 self.image.draw(surface, x, y, self.alpha) 939 if not self.paused: 940 self.alpha *= 0.95 941 942 943class HUDMenu(HUDElement): 944 """A menu.""" 945 946 normal_fg_color = (220, 255, 64) 947 normal_bg_color = (120, 24, 24) 948 selected_fg_color = (255, 255, 220) 949 selected_bg_color = (210, 48, 48) 950 951 def __init__(self, font, items, xalign=0.5, yalign=0.5, 952 xpadding=32, ypadding=8, yspacing=16): 953 width, item_height = self.itemsize(font, items, xpadding, ypadding) 954 height = max(0, (item_height + yspacing) * len(items) - yspacing) 955 HUDElement.__init__(self, width, height, xalign, yalign) 956 self.full_height = height 957 self.font = font 958 self.items = items 959 self.yspacing = yspacing 960 self.xpadding = xpadding 961 self.ypadding = ypadding 962 self.selected_item = 0 963 self.top = 0 964 self.item_height = item_height 965 self.resize() 966 967 def position(self, surface, margin=10): 968 """Calculate screen position for the widget.""" 969 max_height = surface.get_height() - 2 * margin 970 item_spacing = self.item_height + self.yspacing 971 self.height = self.full_height 972 while self.height > max_height: 973 self.height -= item_spacing 974 if self.selected_item * item_spacing < self.top: 975 self.top = self.selected_item * item_spacing 976 while (self.selected_item * item_spacing + self.item_height > 977 self.top + self.height): 978 self.top += item_spacing 979 return HUDElement.position(self, surface, margin) 980 981 def resize(self): 982 self.surface = pygame.Surface((self.width, self.full_height)) 983 self.surface.set_alpha(255 * 0.9) 984 self.surface.set_colorkey((1, 1, 1)) 985 self.invalidate() 986 987 def invalidate(self): 988 """Indicate that the menu needs to be redrawn.""" 989 self._drawn_with = None 990 991 def itemsize(self, font, items, xpadding, ypadding): 992 """Calculate the size of the largest item.""" 993 width = 0 994 height = 0 995 for item in items: 996 size = font.size(item) 997 if '\t' in item: 998 size = (size[0] + xpadding * 2, size[1]) 999 width = max(width, size[0]) 1000 height = max(height, size[1]) 1001 return width + 2 * xpadding, height + 2 * ypadding 1002 1003 def find(self, surface, pos): 1004 """Find the item at given coordinates.""" 1005 x, y = pos 1006 ix, iy = self.position(surface) 1007 iy -= self.top 1008 for idx, item in enumerate(self.items): 1009 if ix <= x < ix + self.width and iy <= y < iy + self.item_height: 1010 return idx 1011 iy += self.item_height + self.yspacing 1012 return -1 1013 1014 def _draw(self): 1015 """Draw the menu on self.surface.""" 1016 self._drawn_with = self.selected_item 1017 self.surface.fill((1, 1, 1)) 1018 x = 0 1019 y = 0 1020 for idx, item in enumerate(self.items): 1021 if idx == self.selected_item: 1022 fg_color = self.selected_fg_color 1023 bg_color = self.selected_bg_color 1024 else: 1025 fg_color = self.normal_fg_color 1026 bg_color = self.normal_bg_color 1027 self.surface.fill(bg_color, (x, y, self.width, self.item_height)) 1028 if '\t' in item: 1029 # align left and right 1030 parts = item.split('\t', 1) 1031 img = self.font.render(parts[0], True, fg_color) 1032 margin = (self.item_height - img.get_height()) // 2 1033 self.surface.blit(img, (x + self.xpadding, y + margin)) 1034 img = self.font.render(parts[1], True, fg_color) 1035 self.surface.blit( 1036 img, 1037 (x + self.width - img.get_width() - self.xpadding, 1038 y + margin)) 1039 else: 1040 # center 1041 img = self.font.render(item, True, fg_color) 1042 margin = (self.item_height - img.get_height()) // 2 1043 self.surface.blit(img, 1044 (x + (self.width - img.get_width()) // 2, 1045 y + margin)) 1046 for ax in (0, self.width-1): 1047 for ay in (0, self.item_height-1): 1048 self.surface.set_at((x+ax, y+ay), (1, 1, 1)) 1049 y += self.item_height + self.yspacing 1050 1051 def draw(self, surface): 1052 """Draw the element.""" 1053 # NB: self.position() might call self.resize() so we must 1054 # call it before _draw() 1055 x, y = self.position(surface) 1056 if self.selected_item != self._drawn_with: 1057 self._draw() 1058 surface.blit(self.surface, (x, y), 1059 (0, self.top, self.width, self.height)) 1060 1061 1062class HUDControlsMenu(HUDMenu): 1063 """A scrolling menu for keyboard controls.""" 1064 1065 def __init__(self, font, items, xalign=0.5, yalign=0.5, 1066 xpadding=8, ypadding=4, yspacing=2): 1067 HUDMenu.__init__(self, font, items, xalign, yalign, xpadding, 1068 ypadding, yspacing) 1069 1070 def position(self, surface, margin=20): 1071 """Calculate screen position for the widget.""" 1072 width = surface.get_width() - 2 * margin - 2 * self.xpadding 1073 if width != self.width: 1074 self.width = width 1075 self.resize() 1076 return HUDMenu.position(self, surface, margin) 1077 1078 1079class HUDInput(HUDElement): 1080 """An input box.""" 1081 1082 bgcolor = (0x01, 0x02, 0x08) 1083 color1 = (0x80, 0xcc, 0xff) 1084 color2 = (0xee, 0xee, 0xee) 1085 alpha = int(0.8 * 255) 1086 1087 def __init__(self, font, prompt, text='', xmargin=20, ymargin=120, 1088 xpadding=8, ypadding=8): 1089 self.font = font 1090 self.prompt = prompt 1091 self.text = text 1092 self.xmargin = xmargin 1093 self.ymargin = ymargin 1094 self.xpadding = xpadding 1095 self.ypadding = ypadding 1096 1097 def draw(self, surface): 1098 """Draw the element.""" 1099 surface_w, surface_h = surface.get_size() 1100 width = surface_w - 2*self.xmargin 1101 height = self.font.get_linesize() + 2*self.ypadding 1102 buffer = pygame.Surface((width, height)) 1103 buffer.set_alpha(self.alpha) 1104 buffer.set_colorkey((1, 1, 1)) 1105 buffer.fill(self.bgcolor) 1106 img1 = self.font.render(self.prompt, True, self.color1) 1107 buffer.blit(img1, (self.xpadding, self.ypadding)) 1108 img2 = self.font.render(self.text, True, self.color2) 1109 buffer.blit(img2, (self.xpadding + img1.get_width(), self.ypadding)) 1110 for x in (0, width-1): 1111 for y in (0, height-1): 1112 buffer.set_at((x, y), (1, 1, 1)) 1113 surface.blit(buffer, (self.xmargin, 1114 surface_h - self.ymargin - buffer.get_height())) 1115 1116 1117class HUDMessage(HUDElement): 1118 """An message box.""" 1119 1120 fg_color = (220, 255, 255) 1121 bg_color = (24, 120, 14) 1122 alpha = int(255 * 0.9) 1123 1124 def __init__(self, font, text, xpadding=16, ypadding=16, xalign=0.5, 1125 yalign=0.5): 1126 width, height = font.size(text) 1127 width += 2*xpadding 1128 height += 2*ypadding 1129 HUDElement.__init__(self, width, height, xalign, yalign) 1130 self.xpadding = xpadding 1131 self.ypadding = ypadding 1132 self.font = font 1133 self.text = text 1134 self.surface = pygame.Surface((self.width, self.height)) 1135 self.surface.set_colorkey((1, 1, 1)) 1136 self.surface.fill(self.bg_color) 1137 img = self.font.render(text, True, self.fg_color) 1138 x = (self.width - img.get_width()) // 2 1139 y = (self.height - img.get_height()) // 2 1140 self.surface.blit(img, (x, y)) 1141 for dx, dy in (0, 0), (1, 0), (0, 1): 1142 self.surface.set_at((dx, dy), (1, 1, 1)) 1143 self.surface.set_at((self.width-1-dx, dy), (1, 1, 1)) 1144 self.surface.set_at((dx, self.height-1-dy), (1, 1, 1)) 1145 self.surface.set_at((self.width-1-dx, self.height-1-dy), (1, 1, 1)) 1146 1147 def draw(self, surface): 1148 """Draw the element.""" 1149 x, y = self.position(surface) 1150 self.surface.set_alpha(self.alpha) 1151 surface.blit(self.surface, (x, y)) 1152 1153 1154class UIMode(object): 1155 """Mode of user interface. 1156 1157 The mode determines several things: 1158 - what is displayed on screen 1159 - whether the game progresses 1160 - how keystrokes are interpreted 1161 1162 Examples of modes: game play, paused, navigating a menu. 1163 """ 1164 1165 paused = False 1166 mouse_visible = False 1167 keys_repeat = False 1168 music = None 1169 1170 inherit_pause_from_prev_mode = False 1171 1172 def __init__(self, ui): 1173 self.ui = ui 1174 self.prev_mode = None 1175 self.clear_keymap() 1176 self.init() 1177 1178 def init(self): 1179 """Initialize the mode.""" 1180 pass 1181 1182 def enter(self, prev_mode): 1183 """Enter the mode.""" 1184 if self.prev_mode is None: 1185 # Only do this once, otherwise two modes might get in a loop 1186 self.prev_mode = prev_mode 1187 if self.inherit_pause_from_prev_mode and prev_mode is not None: 1188 self.paused = prev_mode.paused 1189 pygame.mouse.set_visible(self.mouse_visible) 1190 if self.keys_repeat: 1191 pygame.key.set_repeat(250, 30) 1192 else: 1193 pygame.key.set_repeat() 1194 if self.music: 1195 self.ui.play_music(self.music) 1196 1197 def leave(self, next_mode=None): 1198 """Leave the mode.""" 1199 pass 1200 1201 def return_to_previous_mode(self): 1202 """Return to the previous game mode.""" 1203 if self.prev_mode is not None: 1204 self.ui.ui_mode = self.prev_mode 1205 1206 def draw(self, screen): 1207 """Draw extra things pertaining to the mode.""" 1208 pass 1209 1210 def clear_keymap(self): 1211 """Clear all key mappings.""" 1212 self._keymap_once = {} 1213 self._keymap_repeat = {} 1214 1215 def on_key(self, key, handler, *args): 1216 """Install a handler to be called once when a key is pressed.""" 1217 self._keymap_once[key] = handler, args 1218 1219 def while_key(self, key, handler, *args): 1220 """Install a handler to be called repeatedly while a key is pressed.""" 1221 self._keymap_repeat[key] = handler, args 1222 1223 def handle_key_press(self, event): 1224 """Handle a KEYDOWN event.""" 1225 key = event.key 1226 if key in self.ui.rev_controls: 1227 action = self.ui.rev_controls[key] 1228 if action in self._keymap_once or action in self._keymap_repeat: 1229 key = action 1230 handler_and_args = self._keymap_once.get(key) 1231 if handler_and_args: 1232 handler, args = handler_and_args 1233 handler(*args) 1234 elif key not in self._keymap_repeat: 1235 self.handle_any_other_key(event) 1236 1237 def handle_any_other_key(self, event): 1238 """Handle a KEYDOWN event for unknown keys.""" 1239 pass 1240 1241 def handle_held_keys(self, pressed): 1242 """Handle any keys that are pressed.""" 1243 for key, (handler, args) in self._keymap_repeat.items(): 1244 for key in self.ui.controls.get(key, [key]): 1245 if key is not None and pressed[key]: 1246 handler(*args) 1247 1248 def handle_mouse_press(self, event): 1249 """Handle a MOUSEBUTTONDOWN event.""" 1250 if event.button == 4: 1251 self.ui.zoom_in() 1252 if event.button == 5: 1253 self.ui.zoom_out() 1254 1255 def handle_mouse_release(self, event): 1256 """Handle a MOUSEBUTTONUP event.""" 1257 pass 1258 1259 def handle_mouse_motion(self, event): 1260 """Handle a MOUSEMOTION event.""" 1261 if event.buttons[1] or event.buttons[2]: 1262 self.ui.viewport.shift_by_pixels(event.rel) 1263 1264 1265class PauseMode(UIMode): 1266 """Mode: paused.""" 1267 1268 paused = True 1269 1270 show_message_after = 1 # seconds 1271 fade_in_time = 5 # seconds 1272 1273 clock = staticmethod(time.time) 1274 1275 def enter(self, prev_mode): 1276 """Enter the mode.""" 1277 UIMode.enter(self, prev_mode) 1278 self.message = None 1279 self.pause_entered = self.clock() 1280 self.animate = self.wait_for_fade 1281 1282 def draw(self, screen): 1283 """Draw extra things pertaining to the mode.""" 1284 self.prev_mode.draw(screen) 1285 if self.animate: 1286 self.animate() 1287 if self.message: 1288 self.message.draw(screen) 1289 1290 def wait_for_fade(self): 1291 if self.clock() >= self.pause_entered + self.show_message_after: 1292 self.message = HUDMessage(self.ui.menu_font, "Paused") 1293 self.message.alpha = 0 1294 self.animate = self.fade_in 1295 1296 def fade_in(self): 1297 t = self.clock() - self.pause_entered - self.show_message_after 1298 if t > self.fade_in_time: 1299 self.message.alpha = int(255 * 0.9) 1300 self.animate = None 1301 else: 1302 self.message.alpha = int(smooth(t, self.fade_in_time, 0, 255*0.9)) 1303 1304 def handle_any_other_key(self, event): 1305 """Handle a KEYDOWN event for unknown keys.""" 1306 if not is_modifier_key(event.key): 1307 self.return_to_previous_mode() 1308 1309 def handle_mouse_release(self, event): 1310 """Handle a MOUSEBUTTONUP event.""" 1311 self.return_to_previous_mode() 1312 1313 1314class DemoMode(UIMode): 1315 """Mode: demo.""" 1316 1317 paused = False 1318 music = 'demo' 1319 1320 def init(self): 1321 """Initialize the mode.""" 1322 self.on_key(K_PAUSE, self.ui.pause) 1323 self.while_key(K_EQUALS, self.ui.zoom_in) 1324 self.while_key(K_MINUS, self.ui.zoom_out) 1325 self.on_key(K_o, self.ui.toggle_missile_orbits) 1326 self.on_key(K_f, self.ui.toggle_fullscreen) 1327 1328 def handle_mouse_release(self, event): 1329 """Handle a MOUSEBUTTONDOWN event.""" 1330 if event.button == 1: 1331 self.ui.main_menu() 1332 else: 1333 UIMode.handle_mouse_press(self, event) 1334 1335 def handle_any_other_key(self, event): 1336 """Handle a KEYDOWN event for unknown keys.""" 1337 if not is_modifier_key(event.key): 1338 self.ui.main_menu() 1339 1340 1341class TitleMode(DemoMode): 1342 """Mode: fading out title.""" 1343 1344 def init(self): 1345 """Initialize the mode.""" 1346 DemoMode.init(self) 1347 title_image = pygame.image.load(find('images', 'title.png')) 1348 self.title = HUDTitle(title_image) 1349 self.version = HUDLabel(self.ui.hud_font, self.ui.version_text, 1350 0.5, 1) 1351 1352 def draw(self, screen): 1353 """Draw extra things pertaining to the mode.""" 1354 self.version.draw(screen) 1355 self.title.paused = self.ui.ui_mode.paused 1356 self.title.draw(screen) 1357 if self.title.alpha < 1: 1358 self.ui.watch_demo() 1359 1360 1361class MenuMode(UIMode): 1362 """Abstract base class for menu modes.""" 1363 1364 mouse_visible = True 1365 keys_repeat = True 1366 inherit_pause_from_prev_mode = True 1367 1368 def init(self): 1369 """Initialize the mode.""" 1370 self.init_menu() 1371 self.menu = self.create_menu() 1372 if self.has_no_action(self.menu.selected_item): 1373 self.select_next_item() 1374 self.on_key(K_UP, self.select_prev_item) 1375 self.on_key(K_DOWN, self.select_next_item) 1376 self.on_key(K_RETURN, self.activate_item) 1377 self.on_key(K_KP_ENTER, self.activate_item) 1378 self.on_key(K_ESCAPE, self.close_menu) 1379 self.menu.invalidate() 1380 # These might be overkill 1381 self.while_key(K_EQUALS, self.ui.zoom_in) 1382 self.while_key(K_MINUS, self.ui.zoom_out) 1383 self.on_key(K_o, self.ui.toggle_missile_orbits) 1384 self.on_key(K_f, self.ui.toggle_fullscreen) 1385 self.version = HUDLabel(self.ui.hud_font, self.ui.version_text, 0.5, 1) 1386 1387 def init_menu(self): 1388 """Initialize the menu.""" 1389 self.menu_items = [ 1390 ('Quit', self.ui.quit), 1391 ] 1392 1393 def create_menu(self): 1394 """Create the menu control for display.""" 1395 return HUDMenu(self.ui.menu_font, 1396 [item[0] for item in self.menu_items]) 1397 1398 def has_no_action(self, item_idx): 1399 """Is this menu item just an unselectable label?""" 1400 return len(self.menu_items[item_idx]) == 1 1401 1402 def reinit_menu(self): 1403 """Reinitialize the menu.""" 1404 self.init_menu() 1405 assert len(self.menu_items) == len(self.menu.items) 1406 self.menu.items = [item[0] for item in self.menu_items] 1407 self.menu.invalidate() 1408 1409 def _select_menu_item(self, pos): 1410 """Select menu item under cursor.""" 1411 which = self.menu.find(self.ui.screen, pos) 1412 if which != -1 and not self.has_no_action(which): 1413 self.menu.selected_item = which 1414 return which 1415 1416 def handle_mouse_press(self, event): 1417 """Handle a MOUSEBUTTONDOWN event.""" 1418 if event.button == 1: 1419 self._select_menu_item(event.pos) 1420 else: 1421 UIMode.handle_mouse_press(self, event) 1422 1423 def handle_mouse_motion(self, event): 1424 """Handle a MOUSEMOTION event.""" 1425 if event.buttons[0]: 1426 self._select_menu_item(event.pos) 1427 UIMode.handle_mouse_motion(self, event) 1428 1429 def handle_mouse_release(self, event): 1430 """Handle a MOUSEBUTTONUP event.""" 1431 if event.button == 1: 1432 which = self._select_menu_item(event.pos) 1433 if which != -1: 1434 self.activate_item() 1435 else: 1436 UIMode.handle_mouse_release(self, event) 1437 1438 def draw(self, screen): 1439 """Draw extra things pertaining to the mode.""" 1440 self.version.draw(screen) 1441 self.menu.draw(screen) 1442 1443 def select_prev_item(self): 1444 """Select the previous menu item.""" 1445 if self.menu.selected_item == 0: 1446 self.menu.selected_item = len(self.menu.items) 1447 self.menu.selected_item -= 1 1448 if self.has_no_action(self.menu.selected_item): 1449 self.select_prev_item() 1450 1451 def select_next_item(self): 1452 """Select the next menu item.""" 1453 self.menu.selected_item += 1 1454 if self.menu.selected_item == len(self.menu.items): 1455 self.menu.selected_item = 0 1456 self.menu.top = 0 1457 if self.has_no_action(self.menu.selected_item): 1458 self.select_next_item() 1459 1460 def activate_item(self): 1461 """Activate the selected menu item.""" 1462 action = self.menu_items[self.menu.selected_item][1:] 1463 if action: 1464 self.ui.play_sound('menu') 1465 handler = action[0] 1466 args = action[1:] 1467 handler(*args) 1468 1469 def close_menu(self): 1470 """Close the menu and return to the previous game mode.""" 1471 self.return_to_previous_mode() 1472 1473 1474class MainMenuMode(MenuMode): 1475 """Mode: main menu.""" 1476 1477 def init_menu(self): 1478 """Initialize the mode.""" 1479 self.menu_items = [ 1480 ('New Game', self.ui.new_game_menu), 1481 ('Options', self.ui.options_menu), 1482 ('Help', self.ui.help), 1483 ('Watch Demo', self.ui.watch_demo), 1484 ('Quit', self.ui.quit), 1485 ] 1486 self.on_key(K_PAUSE, self.ui.pause) 1487 self.on_key(K_q, self.ui.quit) # hidden shortcut 1488 self.on_key(K_h, self.ui.help) # hidden shortcut 1489 self.on_key(K_F1, self.ui.help) # hidden shortcut 1490 1491 1492class NewGameMenuMode(MenuMode): 1493 """Mode: new game menu.""" 1494 1495 def init_menu(self): 1496 """Initialize the mode.""" 1497 self.menu_items = [ 1498 ('One Player Game', self.ui.start_single_player_game), 1499 ('Two Player Game', self.ui.start_two_player_game), 1500 ('Gravity Wars', self.ui.start_gravity_wars), 1501 ('No, thanks', self.close_menu), 1502 ] 1503 1504 1505class OptionsMenuMode(MenuMode): 1506 """Mode: options menu.""" 1507 1508 def init_menu(self): 1509 """Initialize the mode.""" 1510 self.menu_items = [ 1511 ('Video', self.ui.video_options_menu), 1512 ('Sound', self.ui.sound_options_menu), 1513 ('Controls', self.ui.controls_menu), 1514 ('Return to main menu', self.close_menu), 1515 ] 1516 1517 1518class VideoOptionsMenuMode(MenuMode): 1519 """Mode: video options menu.""" 1520 1521 def init_menu(self): 1522 """Initialize the mode.""" 1523 def title(label, on): 1524 return label + '\t' + (on and 'on' or 'off') 1525 self.menu_items = [ 1526 ('Screen size\t%dx%d' % self.ui.fullscreen_mode, 1527 self.ui.screen_resolution_menu), 1528 (title('Full screen mode', self.ui.fullscreen), 1529 self.toggle_fullscreen), 1530 (title('Missile orbits', self.ui.show_missile_trails), 1531 self.toggle_missile_orbits), 1532 ('Return to options menu', self.close_menu), 1533 ] 1534 1535 def enter(self, prev_mode): 1536 """Enter the mode.""" 1537 MenuMode.enter(self, prev_mode) 1538 # If we're coming back from the screen resolution menu, we need 1539 # to update the current resolution 1540 self.reinit_menu() 1541 1542 def toggle_fullscreen(self): 1543 """Toggle full-screen mode and reflect the setting in the menu.""" 1544 self.ui.toggle_fullscreen() 1545 self.reinit_menu() 1546 1547 def toggle_missile_orbits(self): 1548 """Toggle missile orbits and reflect the setting in the menu.""" 1549 self.ui.toggle_missile_orbits() 1550 self.reinit_menu() 1551 1552 1553class ScreenResolutionMenuMode(MenuMode): 1554 """Mode: screen resolution menu.""" 1555 1556 def init_menu(self): 1557 """Initialize the mode.""" 1558 self.menu_items = [ 1559 ('%dx%d' % mode, lambda mode=mode: self.switch_to_mode(mode)) 1560 for mode in pygame.display.list_modes() 1561 ] + [ 1562 ('Return to options menu', self.close_menu), 1563 ] 1564 1565 def switch_to_mode(self, mode): 1566 """Switch to a specified video mode.""" 1567 self.ui.switch_to_mode(mode) 1568 self.reinit_menu() 1569 1570 1571class SoundOptionsMenuMode(MenuMode): 1572 """Mode: sound options menu.""" 1573 1574 def init_menu(self): 1575 """Initialize the mode.""" 1576 def title(label, on): 1577 return label + '\t' + (on and 'on' or 'off') 1578 if self.ui.sound_available: 1579 extra = '' 1580 else: 1581 extra = ' (not available)' 1582 self.menu_items = [ 1583 (title('Music' + extra, self.ui.music), 1584 self.toggle_music), 1585 (title('Sound' + extra, self.ui.sound), 1586 self.toggle_sound), 1587 (title('Sound in vacuum', self.ui.sound_in_vacuum), 1588 self.toggle_sound_in_vacuum), 1589 ('Return to options menu', self.close_menu), 1590 ] 1591 1592 def toggle_music(self): 1593 """Toggle music and reflect the setting in the menu.""" 1594 self.ui.toggle_music() 1595 self.reinit_menu() 1596 1597 def toggle_sound(self): 1598 """Toggle sound effects and reflect the setting in the menu.""" 1599 self.ui.toggle_sound() 1600 self.reinit_menu() 1601 1602 def toggle_sound_in_vacuum(self): 1603 """Toggle sound in vacuum and reflect the setting in the menu.""" 1604 self.ui.toggle_sound_in_vacuum() 1605 self.reinit_menu() 1606 1607 1608class ControlsMenuMode(MenuMode): 1609 """Mode: controls menu.""" 1610 1611 def init(self): 1612 MenuMode.init(self) 1613 self.on_key(K_BACKSPACE, self.clear_item) 1614 self.on_key(K_DELETE, self.clear_item) 1615 self.on_key(K_KP_PERIOD, self.clear_item) 1616 self.version = HUDLabel(self.ui.hud_font, 1617 "Press ENTER to change a binding," 1618 " BACKSPACE to clear it", 1619 0.5, 1) 1620 1621 def items(self, label, items): 1622 return ([(label, )] + 1623 [(title + '\t' + ', '.join(map(key_name, 1624 self.ui.controls[action])), 1625 self.set_control, action) 1626 for title, action in items]) 1627 1628 def init_menu(self): 1629 self.menu_items = self.items('Player 1', [ 1630 ('Turn left', 'P1_LEFT'), 1631 ('Turn right', 'P1_RIGHT'), 1632 ('Accelerate', 'P1_FORWARD'), 1633 ('Decelerate', 'P1_BACKWARD'), 1634 ('Launch missile', 'P1_FIRE'), 1635 ('Brake', 'P1_BRAKE'), 1636 ('Toggle computer control', 'P1_TOGGLE_AI'), 1637 ]) + self.items('Player 2', [ 1638 ('Turn left', 'P2_LEFT'), 1639 ('Turn right', 'P2_RIGHT'), 1640 ('Accelerate', 'P2_FORWARD'), 1641 ('Decelerate', 'P2_BACKWARD'), 1642 ('Launch missile', 'P2_FIRE'), 1643 ('Brake', 'P2_BRAKE'), 1644 ('Toggle computer control', 'P2_TOGGLE_AI'), 1645 ]) + [ 1646 ('Return to options menu', self.close_menu), 1647 ] 1648 1649 def create_menu(self): 1650 """Create the menu control for display.""" 1651 return HUDControlsMenu(self.ui.input_font, 1652 [item[0] for item in self.menu_items]) 1653 1654 def set_control(self, action): 1655 """Change a control""" 1656 self.ui.ui_mode = WaitingForControlMode(self.ui, action) 1657 1658 def clear_item(self): 1659 """Clear the selected menu item.""" 1660 action = self.menu_items[self.menu.selected_item][1:] 1661 if action: 1662 handler = action[0] 1663 args = action[1:] 1664 if handler == self.set_control: 1665 action = args[0] 1666 self.ui.set_control(action, None) 1667 self.reinit_menu() 1668 1669 1670class WaitingForControlMode(UIMode): 1671 """Mode: controls menu, waiting for a key press.""" 1672 1673 inherit_pause_from_prev_mode = True 1674 1675 def __init__(self, ui, action): 1676 self.action = action 1677 UIMode.__init__(self, ui) 1678 1679 def init(self): 1680 self.prompt = HUDMessage(self.ui.menu_font, 1681 "Press a key or ESC to cancel") 1682 self.on_key(K_PAUSE, self.ui.pause) 1683 self.on_key(K_ESCAPE, self.return_to_previous_mode) 1684 1685 def draw(self, screen): 1686 """Draw extra things pertaining to the mode.""" 1687 self.prev_mode.draw(screen) 1688 self.prompt.draw(screen) 1689 1690 def handle_any_other_key(self, event): 1691 """Handle a KEYDOWN event for unknown keys.""" 1692 self.ui.set_control(self.action, event.key) 1693 self.prev_mode.reinit_menu() 1694 self.return_to_previous_mode() 1695 1696 def handle_mouse_release(self, event): 1697 """Handle a MOUSEBUTTONUP event.""" 1698 self.return_to_previous_mode() 1699 1700 1701class GameMenuMode(MenuMode): 1702 """Mode: in-game menu.""" 1703 1704 paused = True 1705 inherit_pause_from_prev_mode = False 1706 1707 def init_menu(self): 1708 """Initialize the mode.""" 1709 self.menu_items = [ 1710 ('Resume game', self.close_menu), 1711 ('Options', self.ui.options_menu), 1712 ('Help', self.ui.help), 1713 ('End Game', self.ui.end_game), 1714 ] 1715 1716 1717class PlayMode(UIMode): 1718 """Mode: play the game.""" 1719 1720 paused = False 1721 music = 'game' 1722 1723 def init(self): 1724 """Initialize the mode.""" 1725 self.on_key(K_PAUSE, self.ui.pause) 1726 self.on_key(K_ESCAPE, self.ui.game_menu) 1727 self.on_key(K_F1, self.ui.help) 1728 self.on_key(K_o, self.ui.toggle_missile_orbits) 1729 self.on_key(K_f, self.ui.toggle_fullscreen) 1730 self.while_key(K_EQUALS, self.ui.zoom_in) 1731 self.while_key(K_MINUS, self.ui.zoom_out) 1732 # Player 1 1733 self.on_key('P1_TOGGLE_AI', self.ui.toggle_ai, 0) 1734 self.while_key('P1_LEFT', self.ui.turn_left, 0) 1735 self.while_key('P1_RIGHT', self.ui.turn_right, 0) 1736 self.while_key('P1_FORWARD', self.ui.accelerate, 0) 1737 self.while_key('P1_BACKWARD', self.ui.backwards, 0) 1738 self.while_key('P1_BRAKE', self.ui.brake, 0) 1739 self.on_key('P1_FIRE', self.ui.launch_missile, 0) 1740 # Player 2 1741 self.on_key('P2_TOGGLE_AI', self.ui.toggle_ai, 1) 1742 self.while_key('P2_LEFT', self.ui.turn_left, 1) 1743 self.while_key('P2_RIGHT', self.ui.turn_right, 1) 1744 self.while_key('P2_FORWARD', self.ui.accelerate, 1) 1745 self.while_key('P2_BACKWARD', self.ui.backwards, 1) 1746 self.while_key('P2_BRAKE', self.ui.brake, 1) 1747 self.on_key('P2_FIRE', self.ui.launch_missile, 1) 1748 1749 def handle_mouse_release(self, event): 1750 """Handle a MOUSEBUTTONUP event.""" 1751 if event.button == 1: 1752 self.ui.game_menu() 1753 else: 1754 UIMode.handle_mouse_release(self, event) 1755 1756 1757class GravityWarsMode(UIMode): 1758 """Mode: play gravity wars.""" 1759 1760 paused = False 1761 music = 'gravitywars' 1762 1763 def init(self): 1764 """Initialize the mode.""" 1765 self.on_key(K_PAUSE, self.ui.pause) 1766 self.on_key(K_ESCAPE, self.ui.game_menu) 1767 self.on_key(K_F1, self.ui.help) 1768 self.on_key(K_o, self.ui.toggle_missile_orbits) 1769 self.on_key(K_f, self.ui.toggle_fullscreen) 1770 self.while_key(K_EQUALS, self.ui.zoom_in) 1771 self.while_key(K_MINUS, self.ui.zoom_out) 1772 self.prompt = None 1773 self.state = self.logic() 1774 next(self.state) 1775 1776 def wait_for_input(self, prompt, value): 1777 """Ask the user to enter a value.""" 1778 self.prompt = HUDInput(self.ui.input_font, 1779 "%s (%s): " % (prompt, value)) 1780 while True: 1781 yield None 1782 if not self.prompt.text: 1783 break 1784 try: 1785 yield float(self.prompt.text) 1786 except (ValueError): 1787 pass 1788 1789 def logic(self): 1790 """Game logic.""" 1791 num_players = len(self.ui.ships) 1792 for ship in self.ui.ships: 1793 ship.missile_recoil = 0 1794 for player in itertools.cycle(range(num_players)): 1795 ship = self.ui.ships[player] 1796 for value in self.wait_for_input("Player %d, launch angle" 1797 % (player + 1), 1798 ship.direction): 1799 if value is None: 1800 yield None 1801 else: 1802 ship.direction = value 1803 break 1804 for value in self.wait_for_input("Player %d, launch speed" 1805 % (player + 1), 1806 ship.launch_speed): 1807 if value is None: 1808 yield None 1809 else: 1810 ship.launch_speed = value 1811 break 1812 ship.launch() 1813 1814 def draw(self, screen): 1815 """Draw extra things pertaining to the mode.""" 1816 if self.prompt is not None: 1817 self.prompt.draw(screen) 1818 1819 def handle_mouse_release(self, event): 1820 """Handle a MOUSEBUTTONUP event.""" 1821 if event.button == 1: 1822 self.ui.game_menu() 1823 else: 1824 UIMode.handle_mouse_press(self, event) 1825 1826 def handle_any_other_key(self, event): 1827 """Handle a KEYDOWN event for unknown keys.""" 1828 if self.prompt is not None: 1829 if event.key == K_RETURN: 1830 next(self.state) 1831 elif event.key == K_BACKSPACE: 1832 self.prompt.text = self.prompt.text[:-1] 1833 elif event.unicode.isdigit() or event.unicode == '.': 1834 self.prompt.text += event.unicode 1835 1836 1837class HelpMode(UIMode): 1838 """Mode: show on-line help.""" 1839 1840 paused = True 1841 mouse_visible = True 1842 1843 def init(self): 1844 """Initialize the mode.""" 1845 self.on_key(K_f, self.ui.toggle_fullscreen) 1846 self.on_key(K_ESCAPE, self.return_to_previous_mode) 1847 self.on_key(K_RETURN, self.next_page) 1848 self.on_key(K_KP_ENTER, self.next_page) 1849 self.on_key(K_SPACE, self.next_page) 1850 self.on_key(K_PAGEDOWN, self.next_page) 1851 self.on_key(K_PAGEUP, self.prev_page) 1852 self.help_text = HUDFormattedText(self.ui.help_font, 1853 self.ui.help_bold_font, 1854 fixup_keys_in_text(HELP_TEXT, 1855 self.ui.controls), 1856 small_font=self.ui.hud_font) 1857 1858 def draw(self, screen): 1859 """Draw extra things pertaining to the mode.""" 1860 self.help_text.draw(screen) 1861 1862 def handle_mouse_release(self, event): 1863 """Handle a MOUSEBUTTONUP event.""" 1864 if self.help_text.page + 1 == self.help_text.n_pages: 1865 self.return_to_previous_mode() 1866 else: 1867 self.next_page() 1868 1869 def prev_page(self): 1870 """Turn to next page""" 1871 self.help_text.page -= 1 1872 1873 def next_page(self): 1874 """Turn to next page""" 1875 self.help_text.page += 1 1876 1877 1878class GameUI(object): 1879 """User interface for the game.""" 1880 1881 ZOOM_FACTOR = 1.25 # Keyboard zoom factor 1882 1883 MAX_TRAIL = 100 # Maximum missile trail length 1884 1885 fullscreen = False # Start in windowed mode 1886 fullscreen_mode = None # Desired video mode (w, h) 1887 show_missile_trails = True # Show missile trails by default 1888 music = True # Do we have background music? 1889 sound = True # Do we have sound effects? 1890 sound_in_vacuum = True # Can you hear what happens to AI ships? 1891 show_debug_info = False # Hide debug info by default 1892 desired_zoom_level = 1.0 # The desired zoom level 1893 1894 min_fps = 10 # Minimum FPS 1895 1896 ship_colors = [ 1897 (255, 255, 255), # Player 1 has a white ship 1898 (127, 255, 0), # Player 2 has a green ship 1899 ] 1900 1901 visibility_margin = 120 # Keep ships >=120px from screen edges 1902 1903 respawn_animation = 100 # Duration (ticks) of respawn animation 1904 1905 _ui_mode = None # Previous user interface mode 1906 1907 now_playing = None # Filename of the current music track 1908 1909 # Some debug information 1910 time_to_draw = 0 # Time to draw everything 1911 time_to_draw_trails = 0 # Time to draw missile trails 1912 flip_time = 0 # Time to draw debug info & flip 1913 total_time = 0 # Time to process a frame 1914 last_time = None # Timestamp of last frame 1915 1916 def __init__(self): 1917 self.rng = random.Random() 1918 self.controls = {} 1919 for action in DEFAULT_CONTROLS: 1920 self.controls[action] = [None] 1921 self.rev_controls = {} 1922 for action, key in DEFAULT_CONTROLS.items(): 1923 self.set_control(action, key) 1924 1925 def load_settings(self, filename=None): 1926 """Load settings from a configuration file.""" 1927 if not filename: 1928 filename = os.path.expanduser('~/.pyspacewarrc') 1929 config = self.get_config_parser() 1930 config.read([filename]) 1931 self.fullscreen = config.getboolean('video', 'fullscreen') 1932 mode = config.get('video', 'mode') 1933 try: 1934 w, h = mode.split('x') 1935 self.fullscreen_mode = int(w), int(h) 1936 except ValueError: 1937 self.fullscreen_mode = None 1938 self.show_missile_trails = config.getboolean('video', 1939 'show_missile_trails') 1940 self.music = config.getboolean('sound', 'music') 1941 self.sound = config.getboolean('sound', 'sound') 1942 self.sound_in_vacuum = config.getboolean('sound', 'sound_in_vacuum') 1943 for action in self.controls: 1944 key = config.get('controls', action) 1945 if key: 1946 # clear all current keys first 1947 self.set_control(action, None) 1948 for key in key.split(): 1949 try: 1950 key = int(key) 1951 except ValueError: 1952 key = None 1953 self.set_control(action, key) 1954 1955 def save_settings(self, filename=None): 1956 """Save settings to a configuration file.""" 1957 if not filename: 1958 filename = os.path.expanduser('~/.pyspacewarrc') 1959 config = self.get_config_parser() 1960 with open(filename, 'w') as f: 1961 config.write(f) 1962 1963 def get_config_parser(self): 1964 """Create a ConfigParser initialized with current settings.""" 1965 config = ConfigParser() 1966 config.add_section('video') 1967 config.set('video', 'fullscreen', str(self.fullscreen)) 1968 if self.fullscreen_mode: 1969 config.set('video', 'mode', '%dx%d' % self.fullscreen_mode) 1970 else: 1971 config.set('video', 'mode', '') 1972 config.set('video', 'show_missile_trails', 1973 str(self.show_missile_trails)) 1974 config.add_section('sound') 1975 config.set('sound', 'music', str(self.music)) 1976 config.set('sound', 'sound', str(self.sound)) 1977 config.set('sound', 'sound_in_vacuum', str(self.sound_in_vacuum)) 1978 config.add_section('controls') 1979 for action, keys in self.controls.items(): 1980 config.set('controls', action, ' '.join(map(str, keys))) 1981 return config 1982 1983 def init(self): 1984 """Initialize the user interface.""" 1985 self.version = version 1986 self.version_text = 'PySpaceWar version %s' % self.version 1987 self._init_pygame() 1988 self._init_trail_colors() 1989 self._load_sounds() 1990 self._load_music() 1991 self._load_planet_images() 1992 self._load_background() 1993 self._init_fonts() 1994 self._set_display_mode() 1995 self._optimize_images() 1996 self.viewport = Viewport(self.screen) 1997 self.frame_counter = FrameRateCounter() 1998 self.framedrop_needed = False 1999 self.ui_mode = TitleMode(self) 2000 self._new_game(0) 2001 2002 def _set_ui_mode(self, new_ui_mode): 2003 prev_mode = self._ui_mode 2004 if prev_mode is not None: 2005 prev_mode.leave(new_ui_mode) 2006 self._ui_mode = new_ui_mode 2007 self._ui_mode.enter(prev_mode) 2008 2009 ui_mode = property(lambda self: self._ui_mode, _set_ui_mode) 2010 2011 def _init_trail_colors(self): 2012 """Precalculate missile trail gradients.""" 2013 self.trail_colors = {} 2014 for appearance, color in enumerate(self.ship_colors): 2015 self.trail_colors[appearance] = [[], ] 2016 r, g, b = color 2017 r1, g1, b1 = r*.1, g*.1, b*.1 2018 r2, g2, b2 = r*.5, g*.5, b*.5 2019 for n in range(1, self.MAX_TRAIL+1): 2020 dr, dg, db = (r2-r1) / n, (g2-g1) / n, (b2-b1) / n 2021 colors_for_length_n = [ 2022 (int(r1+dr*i), int(g1+dg*i), int(b1+db*i)) 2023 for i in range(n)] 2024 self.trail_colors[appearance].append(colors_for_length_n) 2025 2026 def _init_pygame(self): 2027 """Initialize pygame, but don't create an output window just yet.""" 2028 pygame.init() 2029 pygame.display.set_caption('PySpaceWar') 2030 icon = pygame.image.load(find('icons', 'pyspacewar48.png')) 2031 pygame.display.set_icon(icon) 2032 pygame.mouse.set_visible(False) 2033 if not self.fullscreen_mode: 2034 self.fullscreen_mode = self._choose_best_mode() 2035 self.sound_available = bool(pygame.mixer.get_init()) 2036 if not self.sound_available: 2037 # Try again, at least we'll get an error message, maybe? 2038 try: 2039 pygame.mixer.init() 2040 except pygame.error as e: 2041 print("pyspacewar: disabling sound: %s" % e) 2042 else: 2043 self.sound_available = True 2044 2045 def _choose_best_mode(self): 2046 """Choose a suitable display mode.""" 2047 # Previously this function used to pick the largest sane video mode 2048 # Sadly, my laptop is not fast enough to sustain 20 fps at 1024x768 2049 # when there are too many missiles around. 2050 return (800, 600) 2051 2052 def _set_display_mode(self): 2053 """Set display mode.""" 2054 if self.fullscreen: 2055 # Consider using DOUBLEBUF and HWSURFACE flags here 2056 # http://aspn.activestate.com/ASPN/Mail/Message/pygame-users/2793695 2057 # On the other hand, alpha-blended blits are reportedly slow on 2058 # hardware surfaces, and there are other sorts of problems too: 2059 # http://aspn.activestate.com/ASPN/Mail/Message/pygame-users/1825852 2060 # According to my measurements, using HWSURFACE|DOUBLEBUF had no 2061 # impact on pygame.display.flip() time. 2062 self.screen = pygame.display.set_mode(self.fullscreen_mode, 2063 FULLSCREEN) 2064 else: 2065 w, h = self.fullscreen_mode 2066 windowed_mode = (int(w * 0.8), int(h * 0.8)) 2067 self.screen = pygame.display.set_mode(windowed_mode, RESIZABLE) 2068 self._prepare_background() 2069 if self.screen.get_bitsize() >= 24: 2070 # Only 24 and 32 bpp modes support aaline 2071 self.draw_line = pygame.draw.aaline 2072 else: 2073 self.draw_line = pygame.draw.line 2074 2075 def _resize_window(self, size): 2076 """Resize the PyGame window as requested.""" 2077 self.screen = pygame.display.set_mode(size, RESIZABLE) 2078 self._prepare_background() 2079 2080 def _optimize_images(self): 2081 """Convert loaded images to native format for faster blitting. 2082 2083 Must be called after _set_display_mode, and, of course, after 2084 _load_planet_images. 2085 """ 2086 self.planet_images = [img.convert_alpha() 2087 for img in self.planet_images] 2088 2089 def _load_planet_images(self): 2090 """Load bitmaps of planets.""" 2091 self.planet_images = [ 2092 pygame.image.load(img) 2093 for img in glob.glob(find('images', 'planet*.png')) 2094 ] 2095 if not self.planet_images: # pragma: nocover 2096 raise RuntimeError("Could not find planet bitmaps") 2097 2098 def _load_background(self): 2099 """Load background bitmap.""" 2100 self.background = pygame.image.load(find('images', 2101 'background.jpg')) 2102 self.background_surface = None 2103 2104 def _prepare_background(self): 2105 """Prepare a background surface.""" 2106 if self.background_surface is None: 2107 self.background_surface = self.background.convert() 2108 w, h = self.background_surface.get_size() 2109 screen_w, screen_h = self.screen.get_size() 2110 if w != screen_w or h != screen_h: 2111 scaled = pygame.transform.scale(self.background, 2112 (screen_w, screen_h)) 2113 # The call to surface.convert dramatically affects performance 2114 # of subsequent blits 2115 self.background_surface = scaled.convert() 2116 2117 def _load_sounds(self): 2118 """Load sound effects.""" 2119 self.sounds = {} 2120 self.sound_looping = set() 2121 if not self.sound_available: 2122 return 2123 config = ConfigParser() 2124 config.add_section('sounds') 2125 config.read([find('sounds', 'sounds.ini')]) 2126 for name in ['thruster', 'fire', 'bounce', 'hit', 'explode', 'respawn', 2127 'menu']: 2128 if config.has_option('sounds', name): 2129 filename = config.get('sounds', name) 2130 if filename: 2131 try: 2132 sound = pygame.mixer.Sound(find('sounds', filename)) 2133 self.sounds[name] = sound 2134 except pygame.error: 2135 print("pyspacewar: could not load %s" % filename) 2136 if 'thruster' in self.sounds: 2137 self.sounds['thruster'].set_volume(0.5) 2138 2139 def _load_music(self): 2140 """Load music files.""" 2141 self.music_files = {} 2142 if not self.sound_available: 2143 return 2144 config = ConfigParser() 2145 config.add_section('music') 2146 config.read([find('music', 'music.ini')]) 2147 for what in ['demo', 'game', 'gravitywars']: 2148 if config.has_option('music', what): 2149 filename = config.get('music', what) 2150 if filename: 2151 self.music_files[what] = find('music', filename) 2152 2153 def play_music(self, which, restart=False): 2154 """Loop the music file for a certain mode.""" 2155 if not self.sound_available: 2156 return 2157 if which == self.now_playing and not restart: 2158 return 2159 self.now_playing = which 2160 if not self.music: 2161 return 2162 filename = self.music_files.get(which) 2163 if not filename: 2164 pygame.mixer.music.stop() 2165 else: 2166 try: 2167 pygame.mixer.music.load(filename) 2168 pygame.mixer.music.play(-1) 2169 except pygame.error: 2170 print("pyspacewar: could not load %s" % filename) 2171 pygame.mixer.music.stop() 2172 2173 def play_sound(self, which): 2174 """Play a certain sound effect.""" 2175 if which in self.sounds and self.sound: 2176 self.sounds[which].play() 2177 2178 def start_sound(self, which): 2179 """Start looping a certain sound effect.""" 2180 if which not in self.sound_looping and which in self.sounds: 2181 if self.sound: 2182 self.sounds[which].play(-1) 2183 self.sound_looping.add(which) 2184 2185 def stop_sound(self, which): 2186 """Stop playing a certain sound effect.""" 2187 if which in self.sound_looping: 2188 self.sounds[which].stop() 2189 self.sound_looping.remove(which) 2190 2191 def _init_fonts(self): 2192 """Load fonts.""" 2193 # Work around another bug in pygame: 2194 # http://aspn.activestate.com/ASPN/Mail/Message/pygame-users/3415468 2195 verdana = '/usr/share/fonts/truetype/msttcorefonts/verdana.ttf' 2196 verdana_bold = '/usr/share/fonts/truetype/msttcorefonts/verdanab.ttf' 2197 if not os.path.exists(verdana): 2198 verdana = pygame.font.match_font('Verdana') 2199 if not os.path.exists(verdana_bold): 2200 verdana_bold = pygame.font.match_font('Verdana', bold=True) 2201 # Work around a bug in pygame: 2202 # http://aspn.activestate.com/ASPN/Mail/Message/pygame-users/2970161 2203 if verdana and os.path.basename(verdana).lower() == 'verdanaz.ttf': 2204 fontdir = os.path.dirname(verdana) 2205 verdana = os.path.join(fontdir, 'verdana.ttf') 2206 verdana_bold = os.path.join(fontdir, 'verdanab.ttf') 2207 if not os.path.exists(verdana): 2208 verdana = None 2209 if not os.path.exists(verdana_bold): 2210 verdana_bold = verdana 2211 self.hud_font = pygame.font.Font(verdana, 14) 2212 self.help_font = pygame.font.Font(verdana, 16) 2213 self.help_bold_font = pygame.font.Font(verdana_bold, 16) 2214 self.input_font = pygame.font.Font(verdana, 24) 2215 self.menu_font = pygame.font.Font(verdana_bold, 30) 2216 2217 def _new_game(self, players=1): 2218 """Start a new game.""" 2219 self.game = Game.new(ships=2, 2220 planet_kinds=len(self.planet_images), 2221 rng=self.rng) 2222 self.ships = self.game.ships 2223 for ship in self.ships: 2224 ship.launch_effect = self.launch_effect_Ship 2225 ship.bounce_effect = self.bounce_effect_Ship 2226 ship.hit_effect = self.hit_effect_Ship 2227 ship.explode_effect = self.explode_effect_Ship 2228 ship.respawn_effect = self.respawn_effect_Ship 2229 self.ai = [AIController(ship) for ship in self.ships] 2230 self.ai_controlled = [False] * len(self.ships) 2231 self.missile_trails = {} 2232 self.angular_momentum = {} 2233 self.viewport.origin = (self.ships[0].position + 2234 self.ships[1].position) / 2 2235 self.viewport.scale = 1 2236 self.desired_zoom_level = 1 2237 self._init_hud() 2238 if players == 0: # demo mode 2239 self.toggle_ai(0) 2240 self.toggle_ai(1) 2241 elif players == 1: # player vs computer 2242 self.toggle_ai(1) 2243 else: # player vs player 2244 pass 2245 2246 def _count_trails(self): 2247 """Count the number of pixels in missile trails.""" 2248 return sum(len(trail) for trail in self.missile_trails.values()) 2249 2250 def _init_hud(self): 2251 """Initialize the heads-up display.""" 2252 time_format = '%.f ms' 2253 self.fps_hud1 = HUDInfoPanel( 2254 self.hud_font, 16, xalign=0.5, yalign=0, 2255 content=[ 2256 ('objects', lambda: len(self.game.world.objects)), 2257 ('missile trails', self._count_trails), 2258 ('fps', lambda: '%.0f' % self.frame_counter.fps()), 2259 ]) 2260 self.fps_hud2 = HUDCollection([ 2261 HUDInfoPanel( 2262 self.hud_font, 16, xalign=0.25, yalign=0.95, 2263 content=[ 2264 ('update', lambda: time_format % 2265 (self.game.time_to_update * 1000)), 2266 (' gravity', lambda: time_format % 2267 (self.game.world.time_for_gravitation * 1000)), 2268 (' collisions', lambda: time_format % 2269 (self.game.world.time_for_collisions * 1000)), 2270 (' other', lambda: time_format % 2271 ((self.game.time_to_update 2272 - self.game.world.time_for_gravitation 2273 - self.game.world.time_for_collisions) 2274 * 1000)), 2275 ]), 2276 HUDInfoPanel( 2277 self.hud_font, 16, xalign=0.5, yalign=0.95, 2278 content=[ 2279 ('draw', lambda: time_format % 2280 (self.time_to_draw * 1000)), 2281 (' trails', lambda: time_format % 2282 (self.time_to_draw_trails * 1000)), 2283 (' other', lambda: time_format % 2284 ((self.time_to_draw - self.time_to_draw_trails) * 1000)), 2285 (' flip', lambda: time_format % 2286 (self.flip_time * 1000)), 2287 ]), 2288 HUDInfoPanel( 2289 self.hud_font, 16, xalign=0.75, yalign=0.95, 2290 content=[ 2291 ('other', lambda: time_format % 2292 ((self.total_time - self.game.time_to_update 2293 - self.time_to_draw - self.game.time_waiting) 2294 * 1000)), 2295 ('idle', lambda: time_format % 2296 (self.game.time_waiting * 1000)), 2297 ('total', lambda: time_format % 2298 (self.total_time * 1000)), 2299 ]), 2300 ]) 2301 self.hud = HUDCollection([ 2302 HUDShipInfo(self.ships[0], self.hud_font, 1, 0), 2303 HUDShipInfo(self.ships[1], self.hud_font, 0, 0, 2304 HUDShipInfo.GREEN_COLORS), 2305 HUDCompass(self.game.world, self.ships[0], self.viewport, 1, 1, 2306 HUDCompass.BLUE_COLORS), 2307 HUDCompass(self.game.world, self.ships[1], self.viewport, 0, 1, 2308 HUDCompass.GREEN_COLORS), 2309 ]) 2310 2311 def _keep_ships_visible(self): 2312 """Update viewport origin/scale so that all ships are on screen.""" 2313 self.viewport.scale = self.desired_zoom_level 2314 self.viewport.keep_visible([s.position for s in self.ships], 2315 self.visibility_margin) 2316 2317 def interact(self): 2318 """Process pending keyboard/mouse events.""" 2319 for event in pygame.event.get(): 2320 if event.type == QUIT: 2321 self.quit() 2322 elif event.type == VIDEORESIZE: 2323 self._resize_window(event.size) 2324 elif event.type == KEYDOWN: 2325 if event.key == K_F12: 2326 self.toggle_debug_info() 2327 elif (event.key in (K_RETURN, K_KP_ENTER) and 2328 event.mod & KMOD_ALT): 2329 self.toggle_fullscreen() 2330 else: 2331 self.ui_mode.handle_key_press(event) 2332 elif event.type == MOUSEBUTTONDOWN: 2333 self.ui_mode.handle_mouse_press(event) 2334 elif event.type == MOUSEBUTTONUP: 2335 self.ui_mode.handle_mouse_release(event) 2336 elif event.type == MOUSEMOTION: 2337 self.ui_mode.handle_mouse_motion(event) 2338 pressed = pygame.key.get_pressed() 2339 self.ui_mode.handle_held_keys(pressed) 2340 self.update_continuous_sounds() 2341 2342 def quit(self): 2343 """Exit the game.""" 2344 sys.exit(0) 2345 2346 def pause(self): 2347 """Pause whatever is happening (so I can take a screenshot).""" 2348 self.ui_mode = PauseMode(self) 2349 2350 def main_menu(self): 2351 """Enter the main menu.""" 2352 self.play_sound('menu') 2353 self.ui_mode = MainMenuMode(self) 2354 2355 def new_game_menu(self): 2356 """Enter the new game menu.""" 2357 self.ui_mode = NewGameMenuMode(self) 2358 2359 def options_menu(self): 2360 """Enter the options menu.""" 2361 self.ui_mode = OptionsMenuMode(self) 2362 2363 def video_options_menu(self): 2364 """Enter the video options menu.""" 2365 self.ui_mode = VideoOptionsMenuMode(self) 2366 2367 def sound_options_menu(self): 2368 """Enter the sound options menu.""" 2369 self.ui_mode = SoundOptionsMenuMode(self) 2370 2371 def screen_resolution_menu(self): 2372 """Enter the screen resolution menu.""" 2373 self.ui_mode = ScreenResolutionMenuMode(self) 2374 2375 def controls_menu(self): 2376 """Enter the controls menu.""" 2377 self.ui_mode = ControlsMenuMode(self) 2378 2379 def watch_demo(self): 2380 """Go back to demo mode.""" 2381 self.ui_mode = DemoMode(self) 2382 2383 def start_single_player_game(self): 2384 """Start a new single-player game.""" 2385 self._new_game(1) 2386 self.ui_mode = PlayMode(self) 2387 2388 def start_two_player_game(self): 2389 """Start a new two-player game.""" 2390 self._new_game(2) 2391 self.ui_mode = PlayMode(self) 2392 2393 def start_gravity_wars(self): 2394 """Start a new two-player gravity wars game.""" 2395 self._new_game(2) 2396 self.ui_mode = GravityWarsMode(self) 2397 2398 def help(self): 2399 """Show the help screen.""" 2400 self.ui_mode = HelpMode(self) 2401 2402 def game_menu(self): 2403 """Enter the game menu.""" 2404 self.play_sound('menu') 2405 self.ui_mode = GameMenuMode(self) 2406 2407 def resume_game(self): 2408 """Resume a game in progress.""" 2409 self.ui_mode = PlayMode(self) 2410 2411 def end_game(self): 2412 """End the game in progress.""" 2413 self._new_game(0) 2414 self.watch_demo() 2415 self.ui_mode = MainMenuMode(self) 2416 2417 def toggle_fullscreen(self): 2418 """Toggle fullscreen mode.""" 2419 self.fullscreen = not self.fullscreen 2420 self._set_display_mode() 2421 2422 def switch_to_mode(self, mode): 2423 """Toggle fullscreen mode.""" 2424 self.fullscreen_mode = mode 2425 self._set_display_mode() 2426 2427 def set_control(self, action, key): 2428 """Change a key mapping""" 2429 if key in self.rev_controls: 2430 old_action = self.rev_controls[key] 2431 self.controls[old_action].remove(key) 2432 if not self.controls[old_action]: 2433 self.controls[old_action] = [None] 2434 keys = self.controls[action] 2435 if len(keys) > 1 or key is None or keys == [None]: 2436 for old_key in keys: 2437 if old_key is not None: 2438 del self.rev_controls[old_key] 2439 self.controls[action] = [] 2440 self.controls[action].append(key) 2441 if key is not None: 2442 self.rev_controls[key] = action 2443 2444 def zoom_in(self): 2445 """Zoom in.""" 2446 self.desired_zoom_level = self.viewport.scale * self.ZOOM_FACTOR 2447 2448 def zoom_out(self): 2449 """Zoom in.""" 2450 self.desired_zoom_level = self.viewport.scale / self.ZOOM_FACTOR 2451 2452 def toggle_debug_info(self): 2453 """Show/hide debug info.""" 2454 self.show_debug_info = not self.show_debug_info 2455 2456 def toggle_missile_orbits(self): 2457 """Show/hide missile trails.""" 2458 self.show_missile_trails = not self.show_missile_trails 2459 2460 def toggle_music(self): 2461 """Toggle music.""" 2462 self.music = not self.music 2463 if not self.sound_available: 2464 return 2465 if self.music: 2466 self.play_music(self.now_playing, restart=True) 2467 else: 2468 pygame.mixer.music.stop() 2469 2470 def toggle_sound(self): 2471 """Toggle sound effects.""" 2472 self.sound = not self.sound 2473 for sound in self.sound_looping: 2474 if self.sound: 2475 self.sounds[sound].play(-1) 2476 else: 2477 self.sounds[sound].stop() 2478 2479 def toggle_sound_in_vacuum(self): 2480 """Toggle sound in vacuum.""" 2481 self.sound_in_vacuum = not self.sound_in_vacuum 2482 2483 def toggle_ai(self, player_id): 2484 """Toggle AI control for player.""" 2485 self.ai_controlled[player_id] = not self.ai_controlled[player_id] 2486 if self.ai_controlled[player_id]: 2487 self.game.controllers.append(self.ai[player_id]) 2488 else: 2489 self.game.controllers.remove(self.ai[player_id]) 2490 2491 def turn_left(self, player_id): 2492 """Manual ship control: turn left.""" 2493 if not self.ai_controlled[player_id]: 2494 self.ships[player_id].turn_left() 2495 2496 def turn_right(self, player_id): 2497 """Manual ship control: turn right.""" 2498 if not self.ai_controlled[player_id]: 2499 self.ships[player_id].turn_right() 2500 2501 def accelerate(self, player_id): 2502 """Manual ship control: accelerate.""" 2503 if not self.ai_controlled[player_id]: 2504 self.ships[player_id].accelerate() 2505 2506 def backwards(self, player_id): 2507 """Manual ship control: accelerate backwards.""" 2508 if not self.ai_controlled[player_id]: 2509 self.ships[player_id].backwards() 2510 2511 def brake(self, player_id): 2512 """Manual ship control: brake.""" 2513 if not self.ai_controlled[player_id]: 2514 self.ships[player_id].brake() 2515 2516 def launch_missile(self, player_id): 2517 """Manual ship control: launch a missile.""" 2518 if not self.ai_controlled[player_id]: 2519 self.ships[player_id].launch() 2520 2521 def launch_effect_Ship(self, ship, obstacle): 2522 """Play a sound effect when the player's ship bounces off something.""" 2523 player_id = self.ships.index(ship) 2524 if not self.ai_controlled[player_id] or self.sound_in_vacuum: 2525 self.play_sound('fire') 2526 2527 def bounce_effect_Ship(self, ship, obstacle): 2528 """Play a sound effect when the player's ship bounces off something.""" 2529 player_id = self.ships.index(ship) 2530 if not ship.dead: 2531 # It sounds weird to hear that sound when dead ships bounce 2532 if not self.ai_controlled[player_id] or self.sound_in_vacuum: 2533 self.play_sound('bounce') 2534 2535 def hit_effect_Ship(self, ship, missile): 2536 """Play a sound effect when the player's ship is hit.""" 2537 player_id = self.ships.index(ship) 2538 if not self.ai_controlled[player_id] or self.sound_in_vacuum: 2539 self.play_sound('hit') 2540 2541 def explode_effect_Ship(self, ship, killer): 2542 """Play a sound effect when the player's ship explodes.""" 2543 player_id = self.ships.index(ship) 2544 if not self.ai_controlled[player_id] or self.sound_in_vacuum: 2545 self.play_sound('explode') 2546 2547 def respawn_effect_Ship(self, ship): 2548 """Play a sound effect when the player's ship respawns.""" 2549 self.play_sound('respawn') 2550 2551 def update_continuous_sounds(self): 2552 """Loop certain sound effects while certain conditions hold true.""" 2553 makes_noise = False 2554 for player_id, ship in enumerate(self.ships): 2555 if not self.ai_controlled[player_id] or self.sound_in_vacuum: 2556 makes_noise = (ship.forward_thrust or ship.rear_thrust or 2557 ship.left_thrust or ship.right_thrust or 2558 ship.engage_brakes) or makes_noise 2559 if makes_noise: 2560 self.start_sound('thruster') 2561 else: 2562 self.stop_sound('thruster') 2563 2564 def draw(self): 2565 """Draw the state of the game""" 2566 self.time_to_draw = 0 2567 self.time_to_draw_trails = 0 2568 drop_this_frame = (self.framedrop_needed and 2569 self.frame_counter.notional_fps() >= self.min_fps) 2570 if not drop_this_frame: 2571 start = time.time() 2572 self._keep_ships_visible() 2573 self.screen.blit(self.background_surface, (0, 0)) 2574 if self.show_missile_trails: 2575 self.draw_missile_trails() 2576 for obj in self.game.world.objects: 2577 getattr(self, 'draw_' + obj.__class__.__name__)(obj) 2578 self.hud.draw(self.screen) 2579 self.ui_mode.draw(self.screen) 2580 self.time_to_draw = time.time() - start 2581 self.frame_counter.frame() 2582 now = time.time() 2583 if self.last_time is not None: 2584 self.total_time = now - self.last_time 2585 self.last_time = now 2586 if self.show_debug_info: 2587 self.fps_hud1.draw(self.screen) 2588 if not drop_this_frame: 2589 self.fps_hud2.draw(self.screen) 2590 now = time.time() 2591 pygame.display.flip() 2592 self.flip_time = time.time() - now 2593 2594 def draw_Planet(self, planet): 2595 """Draw a planet.""" 2596 pos = self.viewport.screen_pos(planet.position) 2597 size = self.viewport.screen_len(planet.radius * 2) 2598 unscaled_img = self.planet_images[planet.appearance] 2599 img = pygame.transform.scale(unscaled_img, (size, size)) 2600 self.screen.blit(img, (pos[0] - size/2, pos[1] - size/2)) 2601 2602 def draw_Ship(self, ship): 2603 """Draw a ship.""" 2604 color = self.ship_colors[ship.appearance] 2605 if ship.dead: 2606 ratio = self.game.time_to_respawn(ship) / self.game.respawn_time 2607 color = colorblend(color, (0x20, 0x20, 0x20), 0.2) 2608 color = colorblend(color, (0, 0, 0), ratio) 2609 elif self.game.world.time - ship.spawn_time < self.respawn_animation: 2610 self.draw_Ship_spawn_animation(ship) 2611 direction_vector = ship.direction_vector * ship.size 2612 side_vector = direction_vector.perpendicular() 2613 sp = self.viewport.screen_pos 2614 pt1 = sp(ship.position - direction_vector + side_vector * 0.5) 2615 pt2 = sp(ship.position + direction_vector) 2616 pt3 = sp(ship.position - direction_vector - side_vector * 0.5) 2617 self.draw_line(self.screen, color, pt1, pt2) 2618 self.draw_line(self.screen, color, pt2, pt3) 2619 (front, back, left_front, left_back, 2620 right_front, right_back) = self.calc_Ship_thrusters(ship) 2621 thrust_lines = [] 2622 if back: 2623 thrust_lines.append(((-0.1, -0.9), (-0.1, -0.9-back))) 2624 thrust_lines.append(((+0.1, -0.9), (+0.1, -0.9-back))) 2625 if front: 2626 thrust_lines.append(((-0.6, -0.2), (-0.6, -0.2+front))) 2627 thrust_lines.append(((+0.6, -0.2), (+0.6, -0.2+front))) 2628 if left_front: 2629 thrust_lines.append(((-0.2, +0.8), (-0.2-left_front, +0.8))) 2630 if right_front: 2631 thrust_lines.append(((+0.2, +0.8), (+0.2+right_front, +0.8))) 2632 if left_back: 2633 thrust_lines.append(((-0.6, -0.8), (-0.6-left_back, -0.8))) 2634 if right_back: 2635 thrust_lines.append(((+0.6, -0.8), (+0.6+right_back, -0.8))) 2636 for (s1, d1), (s2, d2) in thrust_lines: 2637 pt1 = sp(ship.position + direction_vector * d1 + side_vector * s1) 2638 pt2 = sp(ship.position + direction_vector * d2 + side_vector * s2) 2639 self.draw_line(self.screen, (255, 120, 20), pt1, pt2) 2640 2641 def calc_Ship_thrusters(self, ship): 2642 """Calculate the output of the ship's thrusters. 2643 2644 Returns (front, back, left_front, left_back, right_front, right_back) 2645 where each value is the ratio of world units to the ship size. 2646 2647 Keeps track of the ship's rotation and only shows turn thrusters firing 2648 if there was any change. Updates self.angular_momentum as a side 2649 effect. 2650 """ 2651 front = back = left_front = left_back = right_front = right_back = 0 2652 if ship.forward_thrust: 2653 back += ship.forward_thrust * 0.3 / ship.forward_power 2654 if ship.rear_thrust: 2655 front += ship.rear_thrust * 0.15 / ship.backward_power 2656 rotation = ship.left_thrust - ship.right_thrust 2657 prev_rotation = self.angular_momentum.get(ship, 0) 2658 self.angular_momentum[ship] = rotation 2659 if rotation > prev_rotation: 2660 amount = (rotation - prev_rotation) * 0.15 / ship.rotation_speed 2661 left_back += amount 2662 right_front += amount 2663 elif rotation < prev_rotation: 2664 amount = (prev_rotation - rotation) * 0.15 / ship.rotation_speed 2665 left_front += amount 2666 right_back += amount 2667 if ship.engage_brakes: 2668 delta_v = ship.velocity * (ship.brake_factor - 1) 2669 front_back_proj = delta_v.dot_product(ship.direction_vector) 2670 front_back_proj *= 0.45 / (ship.forward_power+ship.backward_power) 2671 if front_back_proj > 0: 2672 back += front_back_proj 2673 elif front_back_proj < 0: 2674 front -= front_back_proj 2675 left_right_proj = delta_v.dot_product( 2676 ship.direction_vector.perpendicular()) 2677 left_right_proj *= 0.45 / (ship.forward_power+ship.backward_power) 2678 if left_right_proj > 0: 2679 left_front += left_right_proj 2680 left_back += left_right_proj 2681 elif left_right_proj < 0: 2682 right_front -= left_right_proj 2683 right_back -= left_right_proj 2684 # Very high accelerations (caused by braking or the AI code) look 2685 # slightly ridiculous. Clamp all the values 2686 front = min(front, 0.2) 2687 back = min(back, 0.4) 2688 left_front = min(left_front, 0.2) 2689 left_back = min(left_back, 0.2) 2690 right_front = min(right_front, 0.2) 2691 right_back = min(right_back, 0.2) 2692 return (front, back, left_front, left_back, right_front, right_back) 2693 2694 def draw_Ship_spawn_animation(self, ship): 2695 sp = self.viewport.screen_pos(ship.position) 2696 color = self.ship_colors[ship.appearance] 2697 t = math.sqrt((self.game.world.time - ship.spawn_time) 2698 / self.respawn_animation) 2699 radius = linear(t, 1, 1, 100) 2700 color = colorblend((0, 0, 0), color, linear(t, 1, 0.2, 0.9)) 2701 pygame.draw.circle(self.screen, color, sp, int(radius), 1) 2702 2703 def update_missile_trails(self): 2704 """Update missile trails.""" 2705 for missile, trail in list(self.missile_trails.items()): 2706 if missile.world is None: 2707 del trail[:2] 2708 if not trail: 2709 del self.missile_trails[missile] 2710 else: 2711 trail.append(missile.position) 2712 if len(trail) > self.MAX_TRAIL: 2713 del trail[0] 2714 for obj in self.game.world.objects: 2715 if isinstance(obj, Missile) and obj not in self.missile_trails: 2716 self.missile_trails[obj] = [obj.position] 2717 2718 def draw_missile_trails(self): 2719 """Draw missile trails.""" 2720 start = time.time() 2721 for missile, trail in self.missile_trails.items(): 2722 self.draw_missile_trail(missile, trail) 2723 self.time_to_draw_trails = time.time() - start 2724 2725 def draw_missile_trail(self, missile, trail): 2726 """Draw a missile orbit trail.""" 2727 r, g, b = self.ship_colors[missile.appearance] 2728 gradient = self.trail_colors[missile.appearance][len(trail)] 2729 self.viewport.draw_trail(trail, gradient, self.screen.set_at) 2730 2731 def draw_Missile(self, missile): 2732 """Draw a missile.""" 2733 color = self.ship_colors[missile.appearance] 2734 x, y = self.viewport.screen_pos(missile.position) 2735 self.screen.set_at((x, y), color) 2736 if self.viewport.scale > 0.5: 2737 color = colorblend(color, (0, 0, 0), 0.4) 2738 self.screen.set_at((x+1, y), color) 2739 self.screen.set_at((x, y+1), color) 2740 self.screen.set_at((x-1, y), color) 2741 self.screen.set_at((x, y-1), color) 2742 2743 def draw_Debris(self, debris): 2744 """Draw debris.""" 2745 self.screen.set_at(self.viewport.screen_pos(debris.position), 2746 debris.appearance) 2747 2748 def wait_for_tick(self): 2749 """Wait for the next game time tick. World moves during this time.""" 2750 if self.ui_mode.paused: 2751 self.game.skip_a_tick() 2752 self.framedrop_needed = False 2753 else: 2754 self.update_missile_trails() 2755 self.framedrop_needed = not self.game.wait_for_tick() 2756