The right flipper's arm was computed with the same +cos(angle) direction as the left one, so it pointed into the right wall, off the 480px canvas - only the left paddle was ever visible/playable. - add physics.flipper_tip() which mirrors the right flipper (and use it in both collision and rendering) - derive the flipper's surface velocity from actual tip displacement (scaled by contact radius), instead of a hand-signed angular formula - advance flipper angles inside the collision substeps (6/frame while pressed) so a fast sweep can't jump past a resting ball; resting balls now get a real 'scoop' from either paddle - verify: pixel probe shows both paddles on screen (L=1007, R=999 blue px); physics probe confirms each paddle launches a resting ball
938 lines
38 KiB
Python
938 lines
38 KiB
Python
"""Cosmic Candy Pinball - game logic + procedural pixel-art rendering."""
|
|
|
|
import math
|
|
import os
|
|
import random
|
|
|
|
import pygame
|
|
|
|
from physics import (CV_W, CV_H, PLAY_L, PLAY_R, PLAY_T, PLAY_B, BALL_R,
|
|
CENTER_HOLE, OUTLANE_L, OUTLANE_R, DRAIN_Y, LAUNCH_DIR,
|
|
LAUNCH_MIN, LAUNCH_MAX, Table, FLIP_PIVOT_L, FLIP_PIVOT_R,
|
|
FLIP_RADIUS, FLIP_LEN, flipper_tip)
|
|
import font as pix
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# palette
|
|
# ---------------------------------------------------------------------------
|
|
C = dict(
|
|
bg_top=(42, 26, 96), bg_bot=(14, 9, 42),
|
|
hud=(22, 15, 56), hud_line=(255, 120, 190),
|
|
field=(64, 50, 140), field2=(70, 54, 150),
|
|
rail_dark=(22, 14, 52), rail=(255, 150, 200), rail_hi=(255, 210, 235),
|
|
floor=(176, 140, 255), floor_hi=(226, 205, 255), floor_dark=(70, 50, 150),
|
|
pit=(8, 5, 26), pit_glint=(60, 40, 120),
|
|
white=(255, 255, 255), ink=(26, 18, 60),
|
|
pink=(255, 138, 196), pink_d=(200, 82, 150), pink_l=(255, 200, 230),
|
|
mint=(110, 240, 200), mint_d=(40, 170, 140), mint_l=(200, 255, 240),
|
|
lemon=(255, 214, 92), lemon_d=(214, 150, 40), lemon_l=(255, 244, 190),
|
|
candy=(200, 120, 255), candy_d=(140, 70, 210),
|
|
flipper=(120, 205, 255), flipper_d=(50, 120, 210), flipper_l=(215, 245, 255),
|
|
gold=(255, 214, 92), gold_d=(200, 130, 40),
|
|
heart=(255, 90, 130), dim=(180, 170, 220),
|
|
fur=(255, 230, 225), fur_d=(150, 110, 130), ear=(255, 130, 180),
|
|
blush=(255, 140, 170), eye=(40, 30, 70),
|
|
bubble=(30, 22, 78), bubble_b=(255, 150, 200),
|
|
)
|
|
|
|
PASTELS = [(255, 158, 205), (255, 226, 140), (140, 245, 215),
|
|
(165, 190, 255), (240, 165, 255), (255, 255, 255)]
|
|
|
|
|
|
def star_pts(cx, cy, ro, ri, n=5, rot=-90.0):
|
|
pts = []
|
|
for i in range(n * 2):
|
|
r = ro if i % 2 == 0 else ri
|
|
a = math.radians(rot + i * 180.0 / n)
|
|
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
|
return pts
|
|
|
|
|
|
def draw_star(surf, cx, cy, ro, color, ri=None, outline=None):
|
|
ri = ri if ri is not None else ro * 0.45
|
|
if outline:
|
|
pygame.draw.polygon(surf, outline, star_pts(cx, cy, ro + 1, ri + 1))
|
|
pygame.draw.polygon(surf, color, star_pts(cx, cy, ro, ri))
|
|
pygame.draw.polygon(surf, (255, 255, 255), star_pts(cx, cy, ro * 0.4, ri * 0.5))
|
|
|
|
|
|
def draw_heart(surf, x, y, s, color):
|
|
m = [" .##. ", "##### ", "######", "##### ", " .##. "]
|
|
for row, line in enumerate(m):
|
|
for col, ch in enumerate(line):
|
|
if ch == "#":
|
|
surf.fill(color, (x + col * s, y + row * s, s, s))
|
|
|
|
|
|
CAT_HEAD = [
|
|
"..##....##..",
|
|
".##.#..#.##.",
|
|
"##..####..##",
|
|
"##....##..##",
|
|
"##..####..##",
|
|
"###########.",
|
|
".#########..",
|
|
"..#######...",
|
|
"..###.###...",
|
|
"..##...##...",
|
|
]
|
|
|
|
BODY = [
|
|
"..#####...",
|
|
".#######..",
|
|
".#######..",
|
|
"##########",
|
|
".#######..",
|
|
"...##.##..",
|
|
]
|
|
|
|
|
|
def draw_cat(surf, x, y, s, expr="happy", t=0.0):
|
|
"""Pixel cat: x,y = head center (top-ish). s = pixel scale."""
|
|
head_w = 12 * s
|
|
hx = int(x - head_w // 2)
|
|
hy = int(y - head_w // 2)
|
|
for row, line in enumerate(CAT_HEAD):
|
|
for col, ch in enumerate(line):
|
|
if ch == "#":
|
|
surf.fill(C["fur"], (hx + col * s, hy + row * s, s, s))
|
|
# eyes (blink on a cycle), mouth, blush
|
|
blink = (int(t * 2.2) % 70) > 63
|
|
eye_y = hy + 4 * s
|
|
ex_l = hx + 3 * s
|
|
ex_r = hx + 8 * s
|
|
if expr == "happy":
|
|
pygame.draw.rect(surf, C["eye"], (ex_l, eye_y + s, s, s))
|
|
pygame.draw.rect(surf, C["eye"], (ex_r, eye_y + s, s, s))
|
|
elif expr == "sad":
|
|
pygame.draw.rect(surf, C["eye"], (ex_l, eye_y + 2 * s, s, s))
|
|
pygame.draw.rect(surf, C["eye"], (ex_r, eye_y + 2 * s, s, s))
|
|
elif expr == "star": # wide amazed eyes
|
|
pygame.draw.rect(surf, C["eye"], (ex_l - s, eye_y, 2 * s, 2 * s))
|
|
pygame.draw.rect(surf, C["eye"], (ex_r, eye_y, 2 * s, 2 * s))
|
|
else:
|
|
if blink:
|
|
pygame.draw.rect(surf, C["eye"], (ex_l, eye_y + s, 2 * s, s))
|
|
pygame.draw.rect(surf, C["eye"], (ex_r, eye_y + s, 2 * s, s))
|
|
else:
|
|
pygame.draw.rect(surf, C["eye"], (ex_l, eye_y + s, s, s))
|
|
pygame.draw.rect(surf, C["eye"], (ex_r, eye_y + s, s, s))
|
|
# mouth
|
|
my = hy + 6 * s
|
|
pygame.draw.rect(surf, C["eye"], (hx + 5 * s, my, s, s))
|
|
# blush
|
|
surf.fill(C["blush"], (hx + s, hy + 5 * s, 2 * s, s))
|
|
surf.fill(C["blush"], (hx + 9 * s, hy + 5 * s, 2 * s, s))
|
|
|
|
|
|
def draw_body(surf, x, y, s):
|
|
w = 10 * s
|
|
bx = int(x - w // 2)
|
|
by = int(y)
|
|
for row, line in enumerate(BODY):
|
|
for col, ch in enumerate(line):
|
|
if ch == "#":
|
|
surf.fill(C["fur"], (bx + col * s, by + row * s, s, s))
|
|
pygame.draw.rect(surf, C["fur_d"], (bx + 2 * s, by + 2 * s, w - 4 * s, s))
|
|
# little tail curl
|
|
tx, ty = bx + w + 1, by + 3 * s
|
|
for i in range(4):
|
|
pygame.draw.rect(surf, C["fur"], (tx + i, ty - i, s, s))
|
|
pygame.draw.rect(surf, C["ear"], (tx + 4, ty - 4, s, s))
|
|
|
|
|
|
def draw_bumper(surf, b, face_t, blink_t):
|
|
cx, cy, r = b["x"], b["y"], b["r"]
|
|
color_map = {"pink": (C["pink"], C["pink_d"], C["pink_l"]),
|
|
"mint": (C["mint"], C["mint_d"], C["mint_l"]),
|
|
"lemon": (C["lemon"], C["lemon_d"], C["lemon_l"])}
|
|
base, dark, light = color_map[b["color"]]
|
|
squash = max(0, b["squash"])
|
|
rr = int(r - (squash * 0.6))
|
|
pygame.draw.circle(surf, (30, 20, 66), (int(cx) + 2, int(cy) + 3), rr) # shadow
|
|
pygame.draw.circle(surf, dark, (int(cx), int(cy)), rr)
|
|
pygame.draw.circle(surf, base, (int(cx), int(cy)), rr - 2)
|
|
pygame.draw.circle(surf, light, (int(cx), int(cy)), rr - 6)
|
|
# gloss pixels
|
|
for dx in (-3, -2, -1):
|
|
for dy in (-4, -3):
|
|
surf.fill((255, 255, 255), (int(cx) + dx, int(cy) + dy, 1, 1))
|
|
# candy dots around rim
|
|
for i in range(8):
|
|
a = i * math.pi / 4 + 0.35
|
|
surf.fill(dark, (int(cx + (rr - 3) * math.cos(a)), int(cy + (rr - 3) * math.sin(a)), 2, 2))
|
|
# face
|
|
surprised = face_t < 9
|
|
blink = blink_t > 0
|
|
ex = 6
|
|
ey = -2
|
|
pygame.draw.circle(surf, C["eye"], (int(cx - ex), int(cy + ey)), 2 if surprised else 1)
|
|
pygame.draw.circle(surf, C["eye"], (int(cx + ex), int(cy + ey)), 2 if surprised else 1)
|
|
if blink and not surprised:
|
|
surf.fill(C["eye"], (int(cx - ex) - 1, int(cy + ey), 3, 1))
|
|
surf.fill(C["eye"], (int(cx + ex) - 1, int(cy + ey), 3, 1))
|
|
# mouth
|
|
if surprised:
|
|
pygame.draw.circle(surf, C["eye"], (int(cx), int(cy + 6)), 2)
|
|
else:
|
|
surf.fill(C["eye"], (int(cx) - 2, int(cy + 5), 5, 1))
|
|
# blush
|
|
surf.fill(C["blush"], (int(cx - ex) - 4, int(cy + 3), 2, 1))
|
|
surf.fill(C["blush"], (int(cx + ex) + 2, int(cy + 3), 2, 1))
|
|
|
|
|
|
def draw_sling(surf, s):
|
|
x, y = int(s["x"]), int(s["y"])
|
|
flash = s["flash"] > 0
|
|
pygame.draw.circle(surf, (40, 30, 80), (x + 2, y + 3), 11)
|
|
pygame.draw.circle(surf, C["candy_d"], (x, y), 11)
|
|
body = (255, 255, 255) if flash else C["candy"]
|
|
pygame.draw.circle(surf, body, (x, y), 8)
|
|
pygame.draw.circle(surf, (255, 255, 255), (x, y), 3)
|
|
# swirl stripe
|
|
pygame.draw.arc(surf, C["candy_d"], (x - 8, y - 8, 16, 16), 0.4, 2.0, 2)
|
|
|
|
|
|
def draw_flipper(surf, f, side):
|
|
px, py = f["pivot"]
|
|
tipx, tipy = flipper_tip(f)
|
|
p0 = (int(px), int(py))
|
|
p1 = (int(tipx), int(tipy))
|
|
# shadow
|
|
pygame.draw.line(surf, (24, 16, 50), (int(px), int(py) + 4), (int(tipx), int(tipy) + 4), 12)
|
|
# outline, body, highlight
|
|
pygame.draw.line(surf, C["flipper_d"], p0, p1, 12)
|
|
pygame.draw.line(surf, C["flipper"], p0, p1, 8)
|
|
# rounded ends
|
|
for (ex, ey) in (p0, p1):
|
|
pygame.draw.circle(surf, C["flipper_d"], (ex, ey), 6)
|
|
pygame.draw.circle(surf, C["flipper"], (ex, ey), 4)
|
|
# cute stripe near the top edge
|
|
a = f["angle"]
|
|
side_sign = -1.0 if side == "R" else 1.0
|
|
nx, ny = -math.sin(a) * side_sign, math.cos(a)
|
|
hx, hy = int(px + nx * 3), int(py + ny * 3)
|
|
tx, ty = int(tipx + nx * 3), int(tipy + ny * 3)
|
|
pygame.draw.line(surf, C["flipper_l"], (hx, hy), (tx, ty), 2)
|
|
|
|
|
|
def draw_cannon(surf, cx, cy, charge, charging, firing_flash):
|
|
x, y = int(cx), int(cy)
|
|
pygame.draw.circle(surf, (26, 18, 58), (x + 2, y + 2), 20) # shadow
|
|
pygame.draw.circle(surf, (90, 60, 160), (x, y), 19) # rim
|
|
pygame.draw.circle(surf, (150, 100, 220), (x, y), 15) # body
|
|
pygame.draw.circle(surf, (210, 170, 255), (x, y), 15, 2)
|
|
# barrel mouth pointing up-left along LAUNCH_DIR
|
|
mx, my = LAUNCH_DIR
|
|
px, py = x + mx * 8, y + my * 8
|
|
pygame.draw.line(surf, (70, 45, 130), (x + mx * 2, y + my * 2), (px, py), 12)
|
|
pygame.draw.line(surf, (190, 150, 250), (x + mx * 2, y + my * 2), (px, py), 6)
|
|
pygame.draw.circle(surf, (60, 38, 110), (int(px), int(py)), 7)
|
|
pygame.draw.circle(surf, (30, 18, 60), (int(px), int(py)), 4)
|
|
# charge glow / sparkle
|
|
if charging:
|
|
glow = 1.0
|
|
elif firing_flash > 0:
|
|
glow = min(1.0, firing_flash / 5.0)
|
|
else:
|
|
glow = 0.0
|
|
if glow > 0:
|
|
rr = int(4 + (1 - charge) * 3 + (1 - glow) * 0)
|
|
for i in range(3):
|
|
a = 0.9 + i * 2.2 + charge * 0.5
|
|
sx = px + math.cos(a) * (7 + charge * 9)
|
|
sy = py + math.sin(a) * (7 + charge * 9)
|
|
surf.fill((255, 240, 170), (int(sx), int(sy), 2, 2))
|
|
|
|
|
|
def draw_ball(surf, bx, by, vx, vy, waiting):
|
|
x, y = int(bx), int(by)
|
|
pygame.draw.circle(surf, (120, 130, 170), (x, y), BALL_R) # base shade
|
|
pygame.draw.circle(surf, (245, 250, 255), (x, y), BALL_R - 1)
|
|
# look direction
|
|
sp = math.hypot(vx, vy)
|
|
if sp > 0.4 and not waiting:
|
|
lx, ly = vx / sp, vy / sp
|
|
else:
|
|
lx, ly = -0.4, -0.92 # looking up
|
|
px, py = -ly, lx # perpendicular
|
|
ex = x + lx * 3
|
|
ey = y + ly * 3
|
|
s1 = 4
|
|
pygame.draw.circle(surf, C["eye"], (int(ex - px * s1), int(ey - py * s1)), 1)
|
|
pygame.draw.circle(surf, C["eye"], (int(ex + px * s1), int(ey + py * s1)), 1)
|
|
pygame.draw.circle(surf, C["blush"], (int(x + px * (s1 + 1) - lx), int(y + py * (s1 + 1) - ly + 2)), 1)
|
|
pygame.draw.circle(surf, C["blush"], (int(x - px * (s1 + 1) - lx), int(y - py * (s1 + 1) - ly + 2)), 1)
|
|
pygame.draw.circle(surf, (255, 255, 255), (x - 2, y - 3), 2) # shine
|
|
|
|
|
|
def draw_ball_shadow(surf, bx, by, on_floor_y=PLAY_B):
|
|
x, y = int(bx), int(by)
|
|
k = min(1.0, max(0.15, 1.0 - (y - (on_floor_y - 40)) / 60.0))
|
|
pygame.draw.circle(surf, (20, 14, 48), (x + 2, int(on_floor_y) + 4), int(6 * k))
|
|
|
|
|
|
class Game:
|
|
def __init__(self, screen_size=None):
|
|
pygame.init()
|
|
pygame.display.set_caption("Cosmic Candy Pinball")
|
|
icon = pygame.Surface((16, 16))
|
|
icon.fill(C["bg_top"])
|
|
for row, line in enumerate(CAT_HEAD):
|
|
for col, ch in enumerate(line):
|
|
if ch == "#":
|
|
icon.fill(C["fur"], (int(col * 16 / 12), int(row * 16 / 10), 2, 2))
|
|
pygame.display.set_icon(icon)
|
|
if screen_size is None:
|
|
info = pygame.display.Info()
|
|
scale = max(1, min(4, info.current_w // CV_W, info.current_h // CV_H))
|
|
screen_size = (CV_W * scale, CV_H * scale)
|
|
self.scale = screen_size[0] // CV_W
|
|
self.win = pygame.display.set_mode(screen_size)
|
|
self.canvas = pygame.Surface((CV_W, CV_H))
|
|
self.overlay = pygame.Surface((CV_W, CV_H), pygame.SRCALPHA)
|
|
|
|
from sfx import Synth
|
|
self.sfx = Synth()
|
|
self.muted = False
|
|
|
|
self.rng = random.Random()
|
|
self.table = Table(seed=self.rng.randrange(1 << 30))
|
|
self.keys = {"L": False, "R": False, "fire": False}
|
|
self.was_charging = False
|
|
self.state = "title" # title | play | over
|
|
self.frame = 0
|
|
self.stars_bg = []
|
|
for _ in range(120):
|
|
self.stars_bg.append([self.rng.randrange(CV_W), self.rng.randrange(CV_H),
|
|
self.rng.random(), self.rng.randrange(40)])
|
|
self._build_static()
|
|
|
|
# run state
|
|
self.score = 0
|
|
self.hi = self._load_hi()
|
|
self.lives = 3
|
|
self.ball_no = 1
|
|
self.new_hi = False
|
|
self.combo = 0
|
|
self.combo_t = 0
|
|
self.fever = 0.0
|
|
self.next_extra = 50000
|
|
self.messages = [] # (text,color,scale,x,y,life)
|
|
self.particles = []
|
|
self.bubble = None # (text, life)
|
|
self.shake = 0.0
|
|
self.charge = 0.0
|
|
self.charging = False
|
|
self.pending = "serve" # serve | drain_pause | ball_banner | over_banner
|
|
self.pending_t = 0
|
|
self.over_t = 0
|
|
self.confetti_t = 0
|
|
self.wall_cool = 0
|
|
self.enter_was_down = False
|
|
self.paused = False
|
|
self.firing_flash = 0
|
|
self.sfx.start_menu_music()
|
|
|
|
# ------------------------------------------------------------------ files
|
|
def _hi_path(self):
|
|
d = os.path.dirname(os.path.abspath(__file__))
|
|
return os.path.join(d, "highscore.json")
|
|
|
|
def _load_hi(self):
|
|
try:
|
|
import json
|
|
with open(self._hi_path()) as fh:
|
|
return int(json.load(fh).get("hi", 0))
|
|
except Exception:
|
|
return 0
|
|
|
|
def _save_hi(self):
|
|
try:
|
|
import json
|
|
with open(self._hi_path(), "w") as fh:
|
|
json.dump({"hi": self.hi}, fh)
|
|
except Exception:
|
|
pass
|
|
|
|
# ------------------------------------------------------------------ static
|
|
def _build_static(self):
|
|
bg = pygame.Surface((CV_W, CV_H))
|
|
for y in range(CV_H):
|
|
t = y / CV_H
|
|
col = tuple(int(a + (b - a) * t) for a, b in zip(C["bg_top"], C["bg_bot"]))
|
|
pygame.draw.line(bg, col, (0, y), (CV_W, y))
|
|
# twinkle stars & planets for the background
|
|
for sx, sy, tw, ph in self.stars_bg:
|
|
if 0 < sx < 20 or 460 < sx < CV_W or sy < 46:
|
|
pygame.draw.rect(bg, (230, 225, 255), (sx, sy, 1, 1))
|
|
self.bg = bg
|
|
|
|
field = pygame.Surface((CV_W, CV_H), pygame.SRCALPHA)
|
|
# wall frame (thick rounded frame forming the rails)
|
|
field.fill((0, 0, 0, 0))
|
|
rect = (PLAY_L - 3, PLAY_T - 3, PLAY_R - PLAY_L + 6, PLAY_B - PLAY_T + 6)
|
|
pygame.draw.rect(field, C["rail_dark"], rect, border_radius=10)
|
|
pygame.draw.rect(field, C["rail"], rect, width=3, border_radius=10)
|
|
pygame.draw.rect(field, C["rail_hi"], rect, width=1, border_radius=10)
|
|
# playfield interior
|
|
inner = (PLAY_L, PLAY_T, PLAY_R - PLAY_L, PLAY_B - PLAY_T)
|
|
pygame.draw.rect(field, C["field"], inner, border_radius=7)
|
|
pygame.draw.rect(field, C["field2"], (PLAY_L + 4, PLAY_T + 4,
|
|
PLAY_R - PLAY_L - 8, PLAY_B - PLAY_T - 8), border_radius=5)
|
|
# candy dot sparkle inside the field
|
|
r = self.rng
|
|
for _ in range(46):
|
|
dx = r.randrange(int(PLAY_L) + 12, int(PLAY_R) - 12)
|
|
dy = r.randrange(int(PLAY_T) + 14, int(PLAY_B) - 12)
|
|
if any((dx - b["pos"][0]) ** 2 + (dy - b["pos"][1]) ** 2 < (b["r"] + 4) ** 2
|
|
for b in ({"pos": (150, 180), "r": 23}, {"pos": (330, 180), "r": 23},
|
|
{"pos": (240, 240), "r": 23})):
|
|
continue
|
|
col = PASTELS[r.randrange(len(PASTELS))]
|
|
field.fill(tuple(c // 3 for c in col), (dx, dy, 2, 2))
|
|
# floor ledges
|
|
for x0, x1 in ((OUTLANE_L, CENTER_HOLE[0]), (CENTER_HOLE[1], OUTLANE_R)):
|
|
pygame.draw.line(field, C["floor_dark"], (x0, PLAY_B + 1), (x1, PLAY_B + 1), 5)
|
|
pygame.draw.line(field, C["floor"], (x0, PLAY_B), (x1, PLAY_B), 3)
|
|
pygame.draw.line(field, C["floor_hi"], (x0, PLAY_B - 1), (x1, PLAY_B - 1), 1)
|
|
# pits (drains)
|
|
pits = [(PLAY_L, OUTLANE_L), (CENTER_HOLE[0], CENTER_HOLE[1]), (OUTLANE_R, PLAY_R)]
|
|
for x0, x1 in pits:
|
|
pygame.draw.rect(field, C["pit"], (x0, PLAY_B, x1 - x0, DRAIN_Y - PLAY_B + 6), border_radius=4)
|
|
pygame.draw.line(field, C["pit_glint"], (x0 + 3, PLAY_B + 3), (x0 + 3, PLAY_B + 9))
|
|
# little post feet under the flipper pivots
|
|
for (px, py) in (FLIP_PIVOT_L, FLIP_PIVOT_R):
|
|
pygame.draw.circle(field, C["floor_dark"], (int(px), int(py) - 2), 7)
|
|
pygame.draw.circle(field, (150, 120, 255), (int(px), int(py) - 2), 5)
|
|
pygame.draw.circle(field, (220, 205, 255), (int(px), int(py) - 3), 2)
|
|
self.field = field
|
|
|
|
hud = pygame.Surface((CV_W, 52))
|
|
hud.fill(C["hud"])
|
|
pygame.draw.line(hud, C["rail_dark"], (0, 50), (CV_W, 50), 2)
|
|
pygame.draw.line(hud, C["hud_line"], (0, 47), (CV_W, 47), 1)
|
|
pygame.draw.rect(hud, (255, 255, 255), (0, 0, 8, 52))
|
|
pygame.draw.rect(hud, (255, 255, 255), (CV_W - 8, 0, 8, 52))
|
|
self.hud = hud
|
|
|
|
# ------------------------------------------------------------------ game
|
|
def new_game(self):
|
|
self.table = Table(seed=self.rng.randrange(1 << 30))
|
|
self.score = 0
|
|
self.lives = 3
|
|
self.ball_no = 1
|
|
self.new_hi = False
|
|
self.combo = 0
|
|
self.combo_t = 0
|
|
self.fever = 0.0
|
|
self.next_extra = 50000
|
|
self.messages = []
|
|
self.particles = []
|
|
self.bubble = None
|
|
self.shake = 0.0
|
|
self.charge = 0.0
|
|
self.charging = False
|
|
self.firing_flash = 0
|
|
self.pending = "ball_banner"
|
|
self.pending_t = 90
|
|
self.sfx.start_play_music()
|
|
self.sfx.play("start")
|
|
|
|
def _ball_lost(self, x, y):
|
|
self.sfx.play("drain")
|
|
self.particles += [dict(x=x, y=y, vx=r.uniform(-1.5, 1.5), vy=r.uniform(-2, 0.5),
|
|
life=30, kind="spark", color=(255, 255, 255))
|
|
for r in (self.rng,) for _ in range(8)]
|
|
self.shake = 4
|
|
self.combo = 0
|
|
self.cat_say("OOF!", 60, "sad")
|
|
self.lives -= 1
|
|
if self.lives > 0:
|
|
self.pending = "drain_pause"
|
|
self.pending_t = 55
|
|
else:
|
|
self.pending = "over_banner"
|
|
self.pending_t = 10
|
|
if self.score > self.hi:
|
|
self.hi = self.score
|
|
self.new_hi = True
|
|
self._save_hi()
|
|
self.sfx.play("hi")
|
|
|
|
def _serve_next(self):
|
|
self.ball_no += 1
|
|
self.table.reset_ball_in_cannon()
|
|
self.fever = 0.0
|
|
self.pending = "serve"
|
|
|
|
def _start_fever(self):
|
|
self.fever = 60 * 20
|
|
self.sfx.play("fever")
|
|
self.cat_say("CANDY FEVER!", 90, "star")
|
|
self.shake = 3
|
|
self.add_msg("CANDY FEVER!", CV_W // 2, 150, C["gold"], scale=2)
|
|
self.add_msg("DOUBLE SCORE x2", CV_W // 2, 166, C["pink"], scale=1)
|
|
for i in range(30):
|
|
self.particles.append(dict(
|
|
x=self.rng.uniform(PLAY_L + 10, PLAY_R - 10), y=self.rng.uniform(60, 120),
|
|
vx=self.rng.uniform(-0.4, 0.4), vy=self.rng.uniform(0.4, 1.2),
|
|
life=80, kind="confetti",
|
|
color=PASTELS[self.rng.randrange(len(PASTELS))]))
|
|
|
|
# ------------------------------------------------------------------ cat
|
|
def cat_say(self, text, frames, expr):
|
|
self.bubble = [text, frames, expr]
|
|
|
|
# ------------------------------------------------------------------ update
|
|
def update(self):
|
|
self.frame += 1
|
|
if self.paused:
|
|
return
|
|
if self.state == "title":
|
|
self._title_update()
|
|
return
|
|
if self.state == "over":
|
|
self._update_particles()
|
|
self._update_messages()
|
|
self.pending_t -= 1
|
|
return
|
|
|
|
# --- PLAY ---
|
|
for f in self.table.flippers:
|
|
self.table.press("L", self.keys["L"])
|
|
self.table.press("R", self.keys["R"])
|
|
|
|
if self.pending == "ball_banner":
|
|
self.pending_t -= 1
|
|
if self.pending_t <= 30:
|
|
self.pending = "serve"
|
|
elif self.pending == "drain_pause":
|
|
self.pending_t -= 1
|
|
if self.pending_t <= 0:
|
|
self._serve_next()
|
|
elif self.pending == "over_banner":
|
|
self.pending_t -= 1
|
|
if self.pending_t <= 0:
|
|
self.state = "over"
|
|
self.sfx.start_menu_music()
|
|
return
|
|
elif self.pending == "serve":
|
|
if self.table.ball_live:
|
|
self.pending = "live"
|
|
else:
|
|
self.charging = self.keys["fire"]
|
|
if self.charging:
|
|
self.was_charging = True
|
|
self.charge = min(1.0, self.charge + 0.022)
|
|
if self.frame % 5 == 0:
|
|
self.sfx.play("tick", vol=0.3 + self.charge * 0.5)
|
|
if self.charge >= 1.0:
|
|
self._fire()
|
|
else:
|
|
if self.was_charging and self.charge >= 0.06:
|
|
self._fire()
|
|
self.charge = 0.0
|
|
self.was_charging = False
|
|
elif self.pending == "live":
|
|
events = self.table.update()
|
|
self._handle_events(events)
|
|
if self.pending != "live":
|
|
return # a drain just happened
|
|
|
|
# timers
|
|
if self.combo > 0:
|
|
self.combo_t -= 1
|
|
if self.combo_t <= 0:
|
|
self.combo = 0
|
|
if self.fever > 0:
|
|
self.fever -= 1
|
|
if self.confetti_t <= 0:
|
|
self.confetti_t = 4
|
|
self.particles.append(dict(
|
|
x=self.rng.uniform(PLAY_L + 20, PLAY_R - 20), y=PLAY_T + 2,
|
|
vx=self.rng.uniform(-0.5, 0.5), vy=self.rng.uniform(0.3, 1.0),
|
|
life=70, kind="confetti",
|
|
color=PASTELS[self.rng.randrange(len(PASTELS))]))
|
|
if self.confetti_t > 0:
|
|
self.confetti_t -= 1
|
|
if self.wall_cool > 0:
|
|
self.wall_cool -= 1
|
|
if self.shake > 0:
|
|
self.shake = max(0.0, self.shake - 0.25)
|
|
if self.bubble:
|
|
self.bubble[1] -= 1
|
|
if self.bubble[1] <= 0:
|
|
self.bubble = None
|
|
if self.firing_flash > 0:
|
|
self.firing_flash -= 1
|
|
|
|
self._update_particles()
|
|
self._update_messages()
|
|
|
|
def _fire(self):
|
|
power = LAUNCH_MIN + (LAUNCH_MAX - LAUNCH_MIN) * self.charge
|
|
self.sfx.play("launch", vol=0.6 + 0.4 * self.charge)
|
|
ev = self.table.launch(power)
|
|
self.firing_flash = 6
|
|
self.pending = "live"
|
|
self.was_charging = False
|
|
self.charge = 0.0
|
|
self.add_msg("FIRE!", ev.x, ev.y, C["gold"], scale=1)
|
|
|
|
# ------------------------------------------------------------------ events
|
|
def _handle_events(self, events):
|
|
mult = 2 if self.fever > 0 else 1
|
|
for e in events:
|
|
if e.kind == "bumper":
|
|
self.combo = min(self.combo + 1, 9)
|
|
self.combo_t = 150
|
|
pts = 250 * self.combo * mult
|
|
self._add_score(pts, e.x, e.y)
|
|
self.sfx.play("bump", pitch_var=min(9, self.combo), vol=0.8)
|
|
b = e.data
|
|
self.particles += [
|
|
dict(x=e.x, y=e.y, vx=self.rng.uniform(-1.2, 1.2),
|
|
vy=self.rng.uniform(-1.8, -0.2), life=22, kind="spark",
|
|
color=(255, 255, 255)),
|
|
dict(x=e.x, y=e.y, life=16, kind="ring", color=(255, 255, 255)),
|
|
]
|
|
self.shake = max(self.shake, 1.6)
|
|
if self.combo == 5:
|
|
self.sfx.play("combo")
|
|
self.cat_say("COMBO x5!", 60, "happy")
|
|
elif e.kind == "sling":
|
|
self.combo = min(self.combo + 1, 9)
|
|
self.combo_t = 150
|
|
pts = 150 * self.combo * mult
|
|
self._add_score(pts, e.x, e.y)
|
|
self.sfx.play("sling", vol=0.8)
|
|
self.particles.append(dict(x=e.x, y=e.y, life=12, kind="ring", color=(255, 255, 255)))
|
|
self.shake = max(self.shake, 1.0)
|
|
elif e.kind == "star_lit":
|
|
lit = sum(1 for s in self.table.stars if s["lit"])
|
|
pts = 100 * mult
|
|
self._add_score(pts, e.x, e.y)
|
|
self.sfx.play("starLit" if lit == len(self.table.stars) else "star")
|
|
if lit == len(self.table.stars):
|
|
self._start_fever()
|
|
else:
|
|
self.cat_say(["MEOW!", "NYA!", "PURR!"][lit - 1], 45, "happy")
|
|
elif e.kind == "star_again":
|
|
pts = (100 if self.fever > 0 else 25) * mult
|
|
self._add_score(pts, e.x, e.y, small=True)
|
|
if self.fever > 0:
|
|
self.fever = min(60 * 40, self.fever + 60 * 3)
|
|
elif e.kind == "drain":
|
|
self._ball_lost(e.x, e.y)
|
|
elif e.kind == "nudge":
|
|
self.sfx.play("tick", vol=0.2)
|
|
elif e.kind == "wall_hit":
|
|
if self.wall_cool <= 0 and e.data > 9.5:
|
|
self.sfx.play("wall", vol=min(0.5, (e.data - 9) / 5))
|
|
self.wall_cool = 5
|
|
elif e.kind == "flipper_hit":
|
|
if self.rng.random() < 0.3:
|
|
self.sfx.play("tick", vol=0.25)
|
|
elif e.kind == "launch":
|
|
pass
|
|
|
|
def _add_score(self, pts, x, y, small=False):
|
|
self.score += pts
|
|
self.add_msg("+%d" % pts, x, y, C["gold"] if pts >= 500 else C["white"], scale=1)
|
|
# milestone extra lives
|
|
while self.score >= self.next_extra:
|
|
self.lives += 1
|
|
self.sfx.play("extra")
|
|
self.cat_say("EXTRA BALL!", 90, "star")
|
|
self.add_msg("EXTRA BALL!", CV_W // 2, 120, C["pink"], scale=2)
|
|
self.next_extra += 30000
|
|
|
|
def add_msg(self, text, x, y, color, scale=1):
|
|
if len(self.messages) < 26:
|
|
self.messages.append([text, color, scale, float(x), float(y), 60])
|
|
|
|
# ------------------------------------------------------------------ fx
|
|
def _update_messages(self):
|
|
for m in self.messages[:]:
|
|
m[5] -= 1
|
|
m[3] += self.rng.uniform(-0.05, 0.05)
|
|
m[4] -= 0.5
|
|
if m[5] <= 0:
|
|
self.messages.remove(m)
|
|
|
|
def _update_particles(self):
|
|
for p in self.particles[:]:
|
|
p["life"] -= 1
|
|
if p["life"] <= 0:
|
|
self.particles.remove(p)
|
|
continue
|
|
if p["kind"] == "confetti":
|
|
p["vy"] += 0.02
|
|
p["x"] += p["vx"]
|
|
p["y"] += p["vy"]
|
|
elif p["kind"] == "spark":
|
|
p["vy"] += 0.06
|
|
p["x"] += p["vx"]
|
|
p["y"] += p["vy"]
|
|
elif p["kind"] == "ring":
|
|
pass
|
|
|
|
# ------------------------------------------------------------------ render
|
|
def render(self):
|
|
c = self.canvas
|
|
if self.state == "title":
|
|
self._render_title(c)
|
|
elif self.state == "over":
|
|
self._render_over(c)
|
|
else:
|
|
self._render_play(c)
|
|
if self.shake > 0.5:
|
|
tmp = c.copy()
|
|
c.fill((0, 0, 0))
|
|
sx = self.rng.randint(-2, 2)
|
|
sy = self.rng.randint(-2, 2)
|
|
c.blit(tmp, (int(sx * self.shake * 0.5), int(sy * self.shake * 0.5)))
|
|
if self.paused and self.state == "play":
|
|
self.overlay.fill((10, 8, 40, 170))
|
|
c.blit(self.overlay, (0, 0))
|
|
pix.draw_text(c, CV_W // 2, CV_H // 2 - 20, "PAUSED", C["white"], 3, align="center")
|
|
pix.draw_text(c, CV_W // 2, CV_H // 2 + 16, "PRESS P TO RESUME", C["dim"], 1, align="center")
|
|
pygame.transform.scale(c, self.win.get_size(), self.win)
|
|
pygame.display.flip()
|
|
|
|
# ------------------------------------------------------------------ play render
|
|
def _render_play(self, c):
|
|
c.blit(self.bg, (0, 0))
|
|
c.blit(self.field, (0, 0))
|
|
t = self.table
|
|
|
|
# slings
|
|
for s in t.slings:
|
|
draw_sling(c, s)
|
|
# flippers
|
|
for f in t.flippers:
|
|
draw_flipper(c, f, f["side"])
|
|
# star lanes
|
|
for i, st in enumerate(t.stars):
|
|
lit = st["lit"]
|
|
pulse = st["pulse"] > 0
|
|
ro = 8 + (2 if pulse else 0)
|
|
color = C["gold"] if lit else (110, 95, 175)
|
|
o = (200, 130, 40) if lit else (70, 60, 120)
|
|
draw_star(c, st["x"], st["y"], ro, color, outline=o)
|
|
# cannon
|
|
draw_cannon(c, t.cannon["x"], t.cannon["y"], self.charge, self.charging, self.firing_flash)
|
|
# bumpers
|
|
for b in t.bumpers:
|
|
draw_bumper(c, b, b["face_t"], b["blink"])
|
|
# ball
|
|
ball = t.ball
|
|
if not t.ball_live:
|
|
draw_ball(c, ball.x, ball.y - 4, 0, 0, True)
|
|
else:
|
|
if ball.speed > 7:
|
|
for i, (px, py) in enumerate(t.trail[:-1]):
|
|
r = max(1, 4 - i)
|
|
c.fill((220, 210, 255), (int(px) - r // 2, int(py) - r // 2, r, r))
|
|
draw_ball_shadow(c, ball.x, ball.y)
|
|
draw_ball(c, ball.x, ball.y, ball.vx, ball.vy, False)
|
|
|
|
# combo indicator
|
|
if self.combo >= 2 and self.pending == "live":
|
|
col = (255, 140, 90) if self.combo >= 5 else C["gold"]
|
|
pix.draw_text(c, CV_W // 2, 56, "COMBO x%d!" % self.combo, col, 1, align="center")
|
|
|
|
# particles
|
|
for p in self.particles:
|
|
x, y = int(p["x"]), int(p["y"])
|
|
if p["kind"] == "confetti":
|
|
c.fill(p["color"], (x, y, 2, 2))
|
|
elif p["kind"] == "spark":
|
|
c.fill(p["color"], (x, y, 2, 2))
|
|
elif p["kind"] == "ring":
|
|
age = 16 - p["life"]
|
|
pygame.draw.circle(c, p["color"], (x, y), min(14, 2 + age), 1)
|
|
|
|
# fever edge glow
|
|
if self.fever > 0:
|
|
col = C["pink"] if self.fever < 120 else C["gold"]
|
|
for i in range(0, 8):
|
|
pygame.draw.rect(c, col, (PLAY_L - 3 + i * 3, PLAY_T - 6, 1, 3))
|
|
|
|
# floating score text
|
|
for m in self.messages:
|
|
if m[5] > 10:
|
|
pix.draw_text(c, m[3], m[4], m[0], m[1], m[2], align="center")
|
|
|
|
# cat bubble
|
|
if self.bubble:
|
|
self._draw_bubble(c)
|
|
|
|
# pending banners
|
|
if self.pending == "ball_banner" and self.pending_t > 40:
|
|
self._dim_center(c)
|
|
pix.draw_text(c, CV_W // 2, 140, "BALL %d" % self.ball_no, C["white"], 3, align="center")
|
|
pix.draw_text(c, CV_W // 2, 170, "GET READY...", C["pink"], 1, align="center")
|
|
if self.pending == "drain_pause":
|
|
self._dim_center(c)
|
|
pix.draw_text(c, CV_W // 2, 150, "LOST THE BALL!", C["pink"], 2, align="center")
|
|
if self.pending == "serve" and not t.ball_live:
|
|
pix.draw_text(c, CV_W // 2, 300, "HOLD SPACE / UP TO CHARGE", C["dim"], 1, align="center")
|
|
|
|
# HUD
|
|
self._render_hud(c)
|
|
|
|
def _dim_center(self, c):
|
|
self.overlay.fill((0, 0, 0, 0))
|
|
pygame.draw.rect(self.overlay, (12, 8, 40, 160),
|
|
(CV_W // 2 - 100, 110, 200, 90), border_radius=8)
|
|
c.blit(self.overlay, (0, 0))
|
|
|
|
def _draw_bubble(self, c):
|
|
text, life, expr = self.bubble
|
|
x0, y0 = 30, 70
|
|
w = pix.text_width(text, 1) + 14
|
|
pygame.draw.rect(c, C["bubble"], (x0, y0, w, 14), border_radius=4)
|
|
pygame.draw.rect(c, C["bubble_b"], (x0, y0, w, 14), 1, border_radius=4)
|
|
pix.draw_text(c, x0 + 7, y0 + 5, text, C["white"], 1)
|
|
# little cat reacting inside a corner
|
|
draw_cat(c, x0 + w + 14, y0 + 2, 1, expr, self.frame)
|
|
|
|
def _render_hud(self, c):
|
|
c.blit(self.hud, (0, 0))
|
|
pix.draw_text(c, 14, 4, "SCORE", C["dim"], 1)
|
|
pix.draw_text(c, 14, 13, "{:,}".format(self.score), C["white"], 2)
|
|
pix.draw_text(c, 170, 4, "BEST", C["dim"], 1)
|
|
best = max(self.hi, self.score)
|
|
pix.draw_text(c, 170, 13, "{:,}".format(best), C["gold"], 2)
|
|
# lives hearts
|
|
for i in range(self.lives):
|
|
draw_heart(c, 336 + i * 14, 14, 2, C["heart"])
|
|
pix.draw_text(c, 336, 30, "LIVES", C["dim"], 1)
|
|
# stars progress
|
|
pix.draw_text(c, 14, 32, "STARS", C["dim"], 1)
|
|
for i, st in enumerate(self.table.stars):
|
|
x = 52 + i * 18
|
|
draw_star(c, x, 37, 5 if st["lit"] else 4,
|
|
C["gold"] if st["lit"] else (90, 78, 150))
|
|
# fever indicator + bar
|
|
if self.fever > 0:
|
|
frac = max(0.0, self.fever / (60 * 20))
|
|
pygame.draw.rect(c, (60, 46, 110), (260, 30, 70, 6))
|
|
pygame.draw.rect(c, (255, 150, 200), (260, 30, int(70 * frac), 6))
|
|
pix.draw_text(c, 260, 38, "FEVER x2", C["gold"], 1)
|
|
if self.paused:
|
|
pix.draw_text(c, CV_W - 40, 40, "PAUSED", C["dim"], 1, align="right")
|
|
|
|
# ------------------------------------------------------------------ title
|
|
def _title_update(self):
|
|
self._update_particles()
|
|
if self.frame % 30 == 0 and self.state == "title":
|
|
self.particles.append(dict(
|
|
x=self.rng.uniform(30, CV_W - 30), y=CV_H + 4,
|
|
vx=self.rng.uniform(-0.3, 0.3), vy=self.rng.uniform(-0.7, -0.3),
|
|
life=300, kind="spark", color=(255, 240, 200)))
|
|
self.sfx if False else None
|
|
|
|
def _render_title(self, c):
|
|
c.blit(self.bg, (0, 0))
|
|
# ringed planet
|
|
pygame.draw.circle(c, (150, 110, 220), (90, 70), 24)
|
|
pygame.draw.circle(c, (190, 160, 245), (90, 70), 24, 2)
|
|
pygame.draw.ellipse(c, (255, 200, 220), (58, 64, 64, 18), 2)
|
|
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10)
|
|
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10, 1)
|
|
# twinkling stars
|
|
for sx, sy, tw, ph in self.stars_bg:
|
|
if (self.frame + ph) % 70 < 6:
|
|
pygame.draw.rect(c, (255, 255, 255), (sx, sy, 1, 1))
|
|
# particles
|
|
for p in self.particles:
|
|
c.fill(p["color"], (int(p["x"]), int(p["y"]), 2, 2))
|
|
|
|
# logo
|
|
words = [("COSMIC", 3, 36, C["pink"]), ("CANDY", 3, 62, C["gold"]),
|
|
("PINBALL", 2, 108, C["mint"])]
|
|
y = 40
|
|
for txt, sc, _, col in words:
|
|
tw = pix.text_width(txt, sc)
|
|
x = CV_W // 2 - tw // 2
|
|
for i, ch in enumerate(txt):
|
|
col2 = col
|
|
pix.draw_text(c, x, y, ch, col2, sc, shadow=(20, 12, 50))
|
|
x += (5 + 1) * sc
|
|
y += (5 + 2) * sc
|
|
# little shooting star occasionally
|
|
if self.frame % 90 < 24:
|
|
t0 = self.frame % 90
|
|
sx = 330 + t0 * 3
|
|
sy = 40 + t0 * 1.5
|
|
pygame.draw.line(c, (255, 255, 255), (int(sx), int(sy)), (int(sx - 8), int(sy - 4)), 1)
|
|
|
|
# cat
|
|
draw_body(c, CV_W // 2 - 26, 180, 2)
|
|
draw_cat(c, CV_W // 2 - 26, 168, 2, "happy", self.frame)
|
|
# instructions panel
|
|
pygame.draw.rect(c, (22, 15, 56), (70, 240, CV_W - 140, 74), border_radius=8)
|
|
pygame.draw.rect(c, C["rail"], (70, 240, CV_W - 140, 74), 2, border_radius=8)
|
|
lines = [
|
|
("LEFT / RIGHT ARROWS = FLIPPERS", C["flipper"]),
|
|
("HOLD SPACE OR UP = CHARGE CANNON", C["gold"]),
|
|
("M MUTE R RESTART P PAUSE", C["dim"]),
|
|
]
|
|
for i, (txt, col) in enumerate(lines):
|
|
pix.draw_text(c, CV_W // 2, 250 + i * 20, txt, col, 1, align="center")
|
|
|
|
if (self.frame // 30) % 2 == 0:
|
|
pix.draw_text(c, CV_W // 2, 325, "PRESS ENTER TO START", C["white"], 2, align="center")
|
|
|
|
# ------------------------------------------------------------------ game over
|
|
def _render_over(self, c):
|
|
self._render_play(c)
|
|
self.overlay.fill((14, 9, 40, 215))
|
|
c.blit(self.overlay, (0, 0))
|
|
pix.draw_text(c, CV_W // 2, 60, "GAME OVER", C["pink"], 3, align="center")
|
|
pix.draw_text(c, CV_W // 2, 105, "SCORE", C["dim"], 1, align="center")
|
|
pix.draw_text(c, CV_W // 2, 115, "{:,}".format(self.score), C["white"], 3, align="center")
|
|
pix.draw_text(c, CV_W // 2, 160, "BEST", C["dim"], 1, align="center")
|
|
pix.draw_text(c, CV_W // 2, 170, "{:,}".format(self.hi), C["gold"], 2, align="center")
|
|
if self.new_hi and (self.frame // 20) % 2 == 0:
|
|
pix.draw_text(c, CV_W // 2, 205, "NEW HI-SCORE!", C["gold"], 1, align="center")
|
|
draw_body(c, CV_W // 2 - 20, 240, 2)
|
|
draw_cat(c, CV_W // 2 - 20, 226, 2, "sad", self.frame)
|
|
if (self.frame // 30) % 2 == 0:
|
|
pix.draw_text(c, CV_W // 2, 320, "PRESS ENTER TO PLAY AGAIN", C["white"], 1, align="center")
|
|
|
|
# ------------------------------------------------------------------ input
|
|
def event(self, ev):
|
|
if ev.type == pygame.KEYDOWN:
|
|
if ev.key == pygame.K_ESCAPE:
|
|
return False
|
|
if ev.key == pygame.K_m:
|
|
self.muted = not self.muted
|
|
self.sfx.set_muted(self.muted)
|
|
self.sfx.set_sfx_muted(self.muted)
|
|
if ev.key == pygame.K_p and self.state == "play":
|
|
self.paused = not self.paused
|
|
if ev.key == pygame.K_r and self.state == "play":
|
|
self.new_game()
|
|
if ev.key in (pygame.K_RETURN, pygame.K_KP_ENTER, pygame.K_SPACE):
|
|
if ev.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
|
|
self.enter_was_down = True
|
|
if self.state == "title":
|
|
self.state = "play"
|
|
self.new_game()
|
|
elif self.state == "over":
|
|
self.state = "play"
|
|
self.new_game()
|
|
if ev.type == pygame.QUIT:
|
|
return False
|
|
return True
|
|
|
|
def set_keys(self):
|
|
k = pygame.key.get_pressed()
|
|
self.keys["L"] = k[pygame.K_LEFT] or k[pygame.K_z] or k[pygame.K_j]
|
|
self.keys["R"] = k[pygame.K_RIGHT] or k[pygame.K_SLASH] or k[pygame.K_LCTRL]
|
|
self.keys["fire"] = k[pygame.K_SPACE] or k[pygame.K_UP] or k[pygame.K_DOWN]
|