mirror of
https://git.davidovski.xyz/tkinter-space-game.git
synced 2024-08-15 00:43:41 +00:00
Initial Commit
This commit is contained in:
commit
b99dbb396c
19 changed files with 3912 additions and 0 deletions
203
boss.py
Normal file
203
boss.py
Normal file
|
@ -0,0 +1,203 @@
|
|||
from dataclasses import replace
|
||||
import math
|
||||
|
||||
from enemy import EnemyAttributes
|
||||
from formation import (
|
||||
CircleFormation,
|
||||
CircleFormationAttributes,
|
||||
EnemyFormation,
|
||||
FormationAttributes,
|
||||
FormationEnemy,
|
||||
)
|
||||
from game import Game
|
||||
|
||||
|
||||
class CircleBossFormation(EnemyFormation):
|
||||
"""Enemy Formation for the circular game boss"""
|
||||
|
||||
RADIUS = 10
|
||||
CYCLE_PEROID = 500
|
||||
COUNT = 6
|
||||
|
||||
def __init__(self, game: Game, attributes: EnemyAttributes):
|
||||
"""Initialise the circle boss
|
||||
|
||||
:param game: The game which this boss belongs to
|
||||
:type game: Game
|
||||
:param attributes: The attributes on which to base spawned enemies
|
||||
:type attributes: EnemyAttributes
|
||||
"""
|
||||
self.image_name = "enemy0"
|
||||
self.minion_image_name = "smallenemy0"
|
||||
|
||||
self.alpha = 0
|
||||
|
||||
attributes = CircleFormationAttributes(
|
||||
radius=40,
|
||||
period=300,
|
||||
count=CircleBossFormation.COUNT,
|
||||
velocity=attributes.velocity,
|
||||
cooldown=attributes.cooldown,
|
||||
hp=attributes.hp//2,
|
||||
reward=attributes.reward*10
|
||||
)
|
||||
self.circle_formation = CircleFormation(
|
||||
game, self.minion_image_name, attributes)
|
||||
|
||||
super().__init__(game, self.image_name, attributes)
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn the boss"""
|
||||
self.spawn_enemy((-4, -4, 0))
|
||||
|
||||
def tick(self, player):
|
||||
"""Update the boss's position
|
||||
|
||||
:param player: The player which to check collision with
|
||||
:type player: Player
|
||||
"""
|
||||
super().tick(player)
|
||||
self.circle_formation.x = self.x
|
||||
self.circle_formation.y = self.y
|
||||
a = (self.alpha/CircleBossFormation.CYCLE_PEROID)*2*math.pi - math.pi
|
||||
|
||||
r = 50*math.sin(a) - 25
|
||||
|
||||
p = math.sin(a*2)*100
|
||||
|
||||
self.circle_formation.attributes.radius = math.floor(
|
||||
CircleBossFormation.RADIUS + (r if r > 0 else 0)
|
||||
)
|
||||
|
||||
self.circle_formation.attributes.period = math.floor(
|
||||
400 + (p if p < 100 else 100)
|
||||
)
|
||||
|
||||
self.circle_formation.tick(player)
|
||||
|
||||
# When the boss is dead, the minions will all die
|
||||
if len(self.sprites) == 0:
|
||||
if len(self.circle_formation.sprites) > 0:
|
||||
self.circle_formation.sprites[0].damage()
|
||||
else:
|
||||
self.destroy()
|
||||
|
||||
def destroy(self):
|
||||
"""Remove the circle boss"""
|
||||
super().destroy()
|
||||
self.circle_formation.destroy()
|
||||
|
||||
def hide(self):
|
||||
"""Hide the circle boss"""
|
||||
self.circle_formation.hide()
|
||||
return super().hide()
|
||||
|
||||
def show(self):
|
||||
"""Show the circle boss"""
|
||||
self.circle_formation.show()
|
||||
return super().show()
|
||||
|
||||
|
||||
class SnakeBossFormation(EnemyFormation):
|
||||
"""Enemy formation for the snake boss"""
|
||||
|
||||
LENGTH = 32
|
||||
|
||||
def __init__(self, game: Game, attributes: FormationAttributes):
|
||||
"""Initialise the snake boss
|
||||
|
||||
:param game: The game which the boss belongs to
|
||||
:type game: Game
|
||||
:param attributes: The attributes of which to base spawned enemies on
|
||||
:type attributes: FormationAttributes
|
||||
"""
|
||||
self.minion_name = "smallenemy1"
|
||||
self.tail_name = "smallenemy1_evil"
|
||||
self.head_name = "enemy2"
|
||||
|
||||
self.phase = 1
|
||||
self.phase_timer = 0
|
||||
|
||||
super().__init__(game, self.minion_name, attributes)
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn the snake"""
|
||||
head_attributes = replace(self.attributes)
|
||||
head_attributes.hp *= 100
|
||||
self.head = FormationEnemy(self.game, self.head_name,
|
||||
(0, 0, 0), head_attributes)
|
||||
|
||||
self.sprites.append(self.head)
|
||||
|
||||
for i in range(SnakeBossFormation.LENGTH):
|
||||
self.spawn_enemy((0, 0, i+1))
|
||||
|
||||
tail_attributes = replace(self.attributes)
|
||||
head_attributes.hp //= 5
|
||||
self.tail = FormationEnemy(self.game, self.tail_name,
|
||||
(0, 0, SnakeBossFormation.LENGTH+1),
|
||||
tail_attributes)
|
||||
|
||||
self.sprites.append(self.tail)
|
||||
|
||||
def spawn_enemy(self, offset):
|
||||
"""Spawn one enemy unit of the snake
|
||||
|
||||
:param offset: The offset of the enemy
|
||||
"""
|
||||
attributes = replace(self.attributes)
|
||||
if offset[2] % 6 == 0:
|
||||
attributes.cooldown = 40
|
||||
else:
|
||||
attributes.cooldown = -1
|
||||
|
||||
enemy = FormationEnemy(self.game, self.image_name, offset, attributes)
|
||||
self.sprites.append(enemy)
|
||||
return enemy
|
||||
|
||||
def position_enemy(self, enemy: FormationEnemy):
|
||||
"""Position the enemy on the game screen
|
||||
|
||||
:param enemy: The enemy to position
|
||||
:type enemy: FormationEnemy
|
||||
"""
|
||||
if self.phase == 2:
|
||||
p = 120 / (100 + math.cos(self.phase_timer / 400)*20) * 120
|
||||
else:
|
||||
p = 120
|
||||
|
||||
m = 4
|
||||
t = ((-enemy.offset_a*m) + self.game.alpha) / p + math.pi
|
||||
a = self.game.w // 2
|
||||
b = self.game.h // 3
|
||||
c = 0
|
||||
|
||||
if self.phase == 2:
|
||||
n = 10 - (2000 / (self.phase_timer+2000))*5
|
||||
else:
|
||||
n = 5
|
||||
|
||||
enemy.set_pos((
|
||||
int(self.x + a*math.sin(n*t+c)),
|
||||
int(self.y + b*math.sin(t))
|
||||
))
|
||||
|
||||
def tick(self, player):
|
||||
"""Update the position of the enemies
|
||||
|
||||
:param player: The player which to check collision with
|
||||
"""
|
||||
super().tick(player)
|
||||
|
||||
if self.phase == 1:
|
||||
self.head.hp = self.attributes.hp*100
|
||||
if self.tail.destroyed:
|
||||
if len(self.sprites) > 1:
|
||||
self.sprites[-1].damage(amount=(self.attributes.hp//4))
|
||||
else:
|
||||
self.head.hp = self.attributes.hp * 3
|
||||
self.phase = 2
|
||||
elif self.phase == 2:
|
||||
self.phase_timer += 1
|
||||
self.head.attributes.cooldown = int(
|
||||
20 + math.sin(self.phase_timer / 50)*10)
|
127
boss_key.py
Normal file
127
boss_key.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
from config import Config
|
||||
from game import Game
|
||||
|
||||
|
||||
class BossKey():
|
||||
"""Object which manages the 'boss key' feature
|
||||
When a key is pressed, then the screen switches to a "work"
|
||||
related image
|
||||
"""
|
||||
|
||||
FG = "#ffaa00"
|
||||
BG = "#aaaaaa"
|
||||
BG2 = "#ffffff"
|
||||
FG2 = "#555555"
|
||||
TEXT_SIZE = 30
|
||||
|
||||
def __init__(self, game: Game, pause_callback) -> None:
|
||||
"""Initialises the boss key feature
|
||||
|
||||
:param game: The game which to use
|
||||
:type game: Game
|
||||
:param pause_callback: The function to call to pause the game
|
||||
:rtype: None
|
||||
"""
|
||||
self.game = game
|
||||
self.canvas = game.canvas
|
||||
self.width, self.height = game.w * Config.SCALE, game.h * Config.SCALE
|
||||
self.shapes = []
|
||||
self.game.inputs.add_keypress_handler(self.on_key)
|
||||
self.hidden = True
|
||||
self.pause_callback = pause_callback
|
||||
|
||||
def on_key(self, event):
|
||||
"""Handle key press events
|
||||
|
||||
:param event: The key press event
|
||||
"""
|
||||
if event.keysym == self.game.inputs.settings.boss \
|
||||
and self.hidden:
|
||||
self.pause_callback()
|
||||
self.create_shapes()
|
||||
self.hidden = False
|
||||
return True
|
||||
|
||||
if not self.hidden:
|
||||
self.delete_shapes()
|
||||
self.hidden = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def create_rectangle(self, x, y, w, h, color):
|
||||
"""Create a rectangle object
|
||||
|
||||
:param x: x coordinate
|
||||
:param y: y coordinate
|
||||
:param w: width
|
||||
:param h: height
|
||||
:param color: The colour of the rectangle
|
||||
"""
|
||||
self.shapes.append(self.canvas.create_rectangle(
|
||||
x, y, x+w, y+h, fill=color, state="disabled"))
|
||||
|
||||
def write_text(self, x, y, text):
|
||||
"""Create a text object
|
||||
|
||||
:param x: x coordiante
|
||||
:param y: y coordinate
|
||||
:param text: The text used for this label
|
||||
"""
|
||||
self.shapes.append(self.canvas.create_text(
|
||||
x, y, text=text, fill=BossKey.BG2,
|
||||
font=(f"Helvetica {BossKey.TEXT_SIZE} bold"), state="disabled"))
|
||||
|
||||
def create_shapes(self):
|
||||
"""Create all the shapes needed for the calculator"""
|
||||
width = self.width
|
||||
height = self.height
|
||||
padding = width // 50
|
||||
|
||||
num_rows = 5
|
||||
num_cols = 4
|
||||
|
||||
grid_width = width // num_cols
|
||||
grid_height = height // (num_rows+1)
|
||||
|
||||
self.create_rectangle(0, 0, width, height, BossKey.BG)
|
||||
self.create_rectangle(padding,
|
||||
padding,
|
||||
width - padding*2,
|
||||
grid_height-padding*2,
|
||||
BossKey.FG2)
|
||||
|
||||
symbols = [
|
||||
"(", ")", "%", "AC",
|
||||
"7", "8", "9", "/",
|
||||
"4", "5", "6", "x",
|
||||
"1", "2", "3", "-",
|
||||
"0", ".", "=", "+"
|
||||
]
|
||||
for row in range(num_rows):
|
||||
for col in range(num_cols):
|
||||
color = BossKey.FG2
|
||||
if row == 0 or col == num_cols - 1:
|
||||
color = BossKey.FG
|
||||
x = col*grid_width+padding
|
||||
y = row*grid_height+padding+grid_height
|
||||
w = grid_width-padding*2
|
||||
h = grid_height-padding*2
|
||||
self.create_rectangle(x, y, w, h, color)
|
||||
|
||||
offset_x = x + padding + (
|
||||
grid_width
|
||||
- padding*2
|
||||
- BossKey.TEXT_SIZE) // 2
|
||||
|
||||
offset_y = y + padding + (
|
||||
grid_height-padding * 2
|
||||
- BossKey.TEXT_SIZE) // 2
|
||||
|
||||
symbol = symbols[col + row*num_cols]
|
||||
self.write_text(offset_x, offset_y, symbol)
|
||||
|
||||
def delete_shapes(self):
|
||||
"""Remove all the shapes used for the calculator"""
|
||||
for shape in self.shapes:
|
||||
self.canvas.delete(shape)
|
162
cheat_engine.py
Normal file
162
cheat_engine.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
from typing import Callable, List
|
||||
|
||||
from config import Config
|
||||
from menu import Menu
|
||||
|
||||
|
||||
class Cheat:
|
||||
"""A single cheat, to be assigned to a cheat engine"""
|
||||
|
||||
def __init__(self, game, code: List[str], callback: Callable):
|
||||
"""Initialise the cheat
|
||||
|
||||
:param game: The game which the cheat belongs to
|
||||
:param code: A string of key codes to be pressed to activate the cheat
|
||||
:type code: List[str]
|
||||
:param callback: The function to be called when this cheat is activated
|
||||
:type callback: Callable
|
||||
"""
|
||||
self.game = game
|
||||
self.code = code
|
||||
self.callback = callback
|
||||
self.position = 0
|
||||
|
||||
def on_key(self, event):
|
||||
"""Handle a key press event
|
||||
|
||||
:param event: The event that is being handled
|
||||
"""
|
||||
if self.position < len(self.code):
|
||||
next_key = self.code[self.position]
|
||||
if event.keysym == next_key:
|
||||
self.position += 1
|
||||
if self.position == len(self.code):
|
||||
self.callback()
|
||||
self.position = 0
|
||||
else:
|
||||
self.position = 0
|
||||
return False
|
||||
|
||||
|
||||
class InvincibilityCheat(Cheat):
|
||||
"""Cheat that makes the player invincible"""
|
||||
|
||||
def __init__(self, game, code: List[str]):
|
||||
"""Initialise the cheat
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:param code: The combination of characters for which to
|
||||
activate this cheat
|
||||
:type code: List[str]
|
||||
"""
|
||||
super().__init__(game, code, self.toggle)
|
||||
self.enabled = False
|
||||
self.damage_function = None
|
||||
|
||||
def toggle(self):
|
||||
"""Enable or disable this cheat"""
|
||||
self.enabled = not self.enabled
|
||||
if self.enabled:
|
||||
self.game.effect_player.splash_text("Godmode on")
|
||||
self.game.player.set_image(self.game.player.white_image)
|
||||
self.damage_function = self.game.player.damage
|
||||
self.game.player.damage = (lambda: None)
|
||||
else:
|
||||
self.game.effect_player.splash_text("Godmode off")
|
||||
self.game.player.set_image(
|
||||
self.game.texture_factory.get_image("ship"))
|
||||
self.game.player.damage = self.damage_function
|
||||
|
||||
|
||||
class DevModeCheat(Cheat):
|
||||
"""Cheat that enables 'dev mode' which:
|
||||
- enables spawning menu
|
||||
- key to remove all enemies
|
||||
- key to stop spawning outright
|
||||
"""
|
||||
|
||||
def __init__(self, game, code: List[str]):
|
||||
"""Initialise the cheat
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:param code: The combination of characters which activate this cheat
|
||||
:type code: List[str]
|
||||
"""
|
||||
super().__init__(game, code, self.toggle)
|
||||
self.enabled = Config.DEVMODE
|
||||
|
||||
self.spawning_disabled = False
|
||||
self.spawn_menu = Menu(self.game, "Spawn Menu")
|
||||
for i in ("circle_boss",
|
||||
"snake_boss",
|
||||
"loop",
|
||||
"orbital",
|
||||
"rectangle",
|
||||
"fleet"):
|
||||
self.spawn_menu.add_item(i, self.spawn_item(i))
|
||||
|
||||
def toggle(self):
|
||||
"""Toggle if this mode is enabled"""
|
||||
self.enabled = not self.enabled
|
||||
if self.enabled:
|
||||
self.game.effect_player.splash_text("devmode on")
|
||||
else:
|
||||
self.game.effect_player.splash_text("devmode off")
|
||||
|
||||
def spawn_item(self, name):
|
||||
"""Spawn a named item from the menu
|
||||
|
||||
:param name: The name of the formation to spawn
|
||||
"""
|
||||
return lambda: (
|
||||
self.spawn_menu.hide(),
|
||||
getattr(self.game.formation_spawner, f"spawn_{name}")()
|
||||
)
|
||||
|
||||
def on_key(self, event):
|
||||
"""Handle Key press events
|
||||
|
||||
:param event: The key press event to handle
|
||||
"""
|
||||
if self.enabled:
|
||||
if event.keysym == "n":
|
||||
self.game.formation_spawner.clear_all()
|
||||
self.game.formation_spawner.next_phase()
|
||||
|
||||
if event.keysym == "c":
|
||||
self.game.formation_spawner.clear_all()
|
||||
|
||||
if event.keysym == "k":
|
||||
if self.spawning_disabled:
|
||||
self.game.effect_player.splash_text("spawning on")
|
||||
self.spawning_disabled = False
|
||||
self.game.formation_spawner.next_formation = 0
|
||||
else:
|
||||
self.game.effect_player.splash_text("spawning off")
|
||||
self.game.formation_spawner.clear_all()
|
||||
self.game.formation_spawner.next_formation = -1
|
||||
|
||||
if event.keysym == "m":
|
||||
self.spawn_menu.show()
|
||||
|
||||
return super().on_key(event)
|
||||
|
||||
|
||||
class CheatEngine:
|
||||
"""Object which manages cheats"""
|
||||
|
||||
def __init__(self, game):
|
||||
"""Initialise the cheat engine
|
||||
|
||||
:param game: The game which this belongs to
|
||||
"""
|
||||
self.game = game
|
||||
self.cheats = []
|
||||
|
||||
def add_cheat(self, cheat):
|
||||
"""Register a cheat to the engine
|
||||
|
||||
:param cheat: The cheat to be registered
|
||||
"""
|
||||
self.game.inputs.add_keypress_handler(cheat.on_key)
|
||||
self.cheats.append(cheat)
|
20
config.py
Normal file
20
config.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Various constants and configuration for the game"""
|
||||
|
||||
WIDTH = 100
|
||||
HEIGHT = 200
|
||||
# Number of window pixels used for each game "pixel"
|
||||
SCALE = 6
|
||||
|
||||
FPS = 30
|
||||
|
||||
NICK_LEN = 3
|
||||
DEVMODE = False
|
||||
|
||||
LEADERBOARD_FILE = "leaderboard"
|
||||
SAVE_FILE = "save"
|
||||
SETTINGS_FILE = "settings"
|
63
enemy.py
Normal file
63
enemy.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from game import Game
|
||||
from shooter import Shooter, ShooterAttributes
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnemyAttributes(ShooterAttributes):
|
||||
"""Attributes of an enemy object"""
|
||||
|
||||
reward: int = 100
|
||||
lazer_color: str = "red"
|
||||
cooldown: int = 20
|
||||
|
||||
|
||||
class Enemy(Shooter):
|
||||
"""An enemy in the game"""
|
||||
|
||||
def __init__(self, game: Game, image_name: str,
|
||||
attributes: EnemyAttributes):
|
||||
"""Initialise the enemy
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use
|
||||
:type image_name: str
|
||||
:param attributes: The attributes of this
|
||||
:type attributes: EnemyAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, attributes)
|
||||
self.attributes = attributes
|
||||
|
||||
def tick(self, player):
|
||||
"""Check for collisions and shoot
|
||||
|
||||
:param player: The player which to check collisions with
|
||||
"""
|
||||
super().tick()
|
||||
if self.attributes.cooldown != -1:
|
||||
self.shoot()
|
||||
|
||||
lazer_collisions = self.collide_all(player.lazers)
|
||||
if lazer_collisions != -1:
|
||||
self.damage()
|
||||
player.lazers[lazer_collisions].destroy()
|
||||
|
||||
player_collisions = player.collide_all(self.lazers)
|
||||
if player_collisions != -1:
|
||||
player.damage()
|
||||
self.lazers[player_collisions].destroy()
|
||||
|
||||
if self.collides(player):
|
||||
player.damage()
|
||||
self.damage()
|
||||
|
||||
def damage(self, amount=1):
|
||||
"""Reduce the object's health
|
||||
|
||||
:param amount:
|
||||
"""
|
||||
super().damage(amount)
|
||||
if self.destroyed:
|
||||
self.game.score += self.attributes.reward
|
342
font.py
Normal file
342
font.py
Normal file
|
@ -0,0 +1,342 @@
|
|||
class Font:
|
||||
"""Convert a pixel font into photoimages"""
|
||||
|
||||
FONT_SIZE = 5
|
||||
|
||||
# effective width of each character, including letter spacing
|
||||
FONT_WIDTH = 6
|
||||
|
||||
CHARS = {
|
||||
"\0": [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
"0": [
|
||||
" xxx ",
|
||||
"x xx",
|
||||
"x x x",
|
||||
"xx x",
|
||||
" xxx ",
|
||||
],
|
||||
"1": [
|
||||
" x ",
|
||||
" xx ",
|
||||
" x ",
|
||||
" x ",
|
||||
"xxxxx",
|
||||
],
|
||||
"2": [
|
||||
"xxxx ",
|
||||
" x",
|
||||
" xxx ",
|
||||
"x ",
|
||||
"xxxxx",
|
||||
],
|
||||
"3": [
|
||||
"xxxx ",
|
||||
" x",
|
||||
"xxxx ",
|
||||
" x",
|
||||
"xxxx ",
|
||||
],
|
||||
"4": [
|
||||
"x x",
|
||||
"x x",
|
||||
"xxxxx",
|
||||
" x",
|
||||
" x",
|
||||
],
|
||||
"5": [
|
||||
"xxxxx",
|
||||
"x ",
|
||||
" xxx ",
|
||||
" x",
|
||||
"xxxx ",
|
||||
],
|
||||
"6": [
|
||||
" xxx ",
|
||||
"x ",
|
||||
"xxxx ",
|
||||
"x x",
|
||||
" xxx ",
|
||||
],
|
||||
"7": [
|
||||
"xxxxx",
|
||||
" x",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
],
|
||||
"8": [
|
||||
" xxx ",
|
||||
"x x",
|
||||
" xxx ",
|
||||
"x x",
|
||||
" xxx ",
|
||||
],
|
||||
"9": [
|
||||
" xxx ",
|
||||
"x x",
|
||||
" xxxx",
|
||||
" x",
|
||||
" x",
|
||||
],
|
||||
|
||||
"a": [
|
||||
" xxx ",
|
||||
"x x",
|
||||
"xxxxx",
|
||||
"x x",
|
||||
"x x",
|
||||
],
|
||||
"b": [
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"xxxx ",
|
||||
],
|
||||
"c": [
|
||||
" xxxx",
|
||||
"x ",
|
||||
"x ",
|
||||
"x ",
|
||||
" xxxx",
|
||||
],
|
||||
"d": [
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
"xxxx ",
|
||||
],
|
||||
"e": [
|
||||
"xxxxx",
|
||||
"x ",
|
||||
"xxxxx",
|
||||
"x ",
|
||||
"xxxxx",
|
||||
],
|
||||
"f": [
|
||||
"xxxxx",
|
||||
"x ",
|
||||
"xxxxx",
|
||||
"x ",
|
||||
"x ",
|
||||
],
|
||||
"g": [
|
||||
" xxxx",
|
||||
"x ",
|
||||
"x xx",
|
||||
"x x",
|
||||
" xxxx",
|
||||
],
|
||||
"h": [
|
||||
"x x",
|
||||
"x x",
|
||||
"xxxxx",
|
||||
"x x",
|
||||
"x x",
|
||||
],
|
||||
"i": [
|
||||
"xxxxx",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
"xxxxx",
|
||||
],
|
||||
"j": [
|
||||
"xxxxx",
|
||||
" x",
|
||||
" x",
|
||||
" x",
|
||||
"xxxx ",
|
||||
],
|
||||
"k": [
|
||||
"x x",
|
||||
"x x ",
|
||||
"xxx ",
|
||||
"x x ",
|
||||
"x x",
|
||||
],
|
||||
"l": [
|
||||
"x ",
|
||||
"x ",
|
||||
"x ",
|
||||
"x ",
|
||||
"xxxxx",
|
||||
],
|
||||
"m": [
|
||||
"x x",
|
||||
"xx xx",
|
||||
"x x x",
|
||||
"x x",
|
||||
"x x",
|
||||
],
|
||||
"n": [
|
||||
"x x",
|
||||
"xx x",
|
||||
"x x x",
|
||||
"x xx",
|
||||
"x x",
|
||||
],
|
||||
"o": [
|
||||
" xxx ",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
" xxx ",
|
||||
],
|
||||
"p": [
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"xxxx ",
|
||||
"x ",
|
||||
"x ",
|
||||
],
|
||||
"q": [
|
||||
" xxx ",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x ",
|
||||
" xx x",
|
||||
],
|
||||
"r": [
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"xxxx ",
|
||||
"x x",
|
||||
"x x",
|
||||
],
|
||||
"s": [
|
||||
" xxxx",
|
||||
"x ",
|
||||
" xxx ",
|
||||
" x",
|
||||
"xxxx ",
|
||||
],
|
||||
"t": [
|
||||
"xxxxx",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
],
|
||||
"u": [
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
" xxx ",
|
||||
],
|
||||
"v": [
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
" x x ",
|
||||
" x ",
|
||||
],
|
||||
"w": [
|
||||
"x x",
|
||||
"x x",
|
||||
"x x x",
|
||||
"x x x",
|
||||
" x x ",
|
||||
],
|
||||
"x": [
|
||||
"x x",
|
||||
" x x ",
|
||||
" x ",
|
||||
" x x ",
|
||||
"x x",
|
||||
],
|
||||
"y": [
|
||||
"x x",
|
||||
" x x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
],
|
||||
"z": [
|
||||
"xxxxx",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
"xxxxx",
|
||||
],
|
||||
" ": [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
">": [
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
],
|
||||
"<": [
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
" x ",
|
||||
],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _create_font_texture(text, color="#fff", letter_space=1):
|
||||
"""Convert a font array into a game texture
|
||||
|
||||
:param text: the characters used within the font
|
||||
:param color: The colour to use
|
||||
:param letter_space: The spacing between each letter to use
|
||||
"""
|
||||
string = Font._create_characters(text, letter_space)
|
||||
return [
|
||||
[
|
||||
None if character == " " else color
|
||||
for character in row
|
||||
] for row in string
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _create_characters(text, letter_space):
|
||||
"""Concatenate font symbols
|
||||
|
||||
:param text: The text of the font
|
||||
:param letter_space: The spacing between each letter
|
||||
"""
|
||||
# create a list of all characters in the string
|
||||
characters = [
|
||||
Font.CHARS[c] if c in Font.CHARS else Font.CHARS["\0"]
|
||||
for c in text.lower()
|
||||
]
|
||||
|
||||
# join each row of each character into one "character"
|
||||
return [
|
||||
(" "*letter_space).join([c[row] for c in characters])
|
||||
for row in range(Font.FONT_SIZE)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def load_text(texture_factory, text, color="#fff", letter_space=1):
|
||||
"""Create and load text into a photo image
|
||||
|
||||
:param texture_factory: The texture factory used for processing
|
||||
:param text: The text to convert
|
||||
:param color: Color of the text
|
||||
:param letter_space: Spacing between letters
|
||||
"""
|
||||
return texture_factory.load_texture(f"text:{text}",
|
||||
Font._create_font_texture(
|
||||
text,
|
||||
color=color,
|
||||
letter_space=letter_space)
|
||||
)
|
296
formation.py
Normal file
296
formation.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
from dataclasses import dataclass
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from enemy import Enemy, EnemyAttributes
|
||||
from game import Game
|
||||
from sprite import Sprite
|
||||
|
||||
|
||||
@dataclass
|
||||
class FormationAttributes(EnemyAttributes):
|
||||
"""FormationAttributes."""
|
||||
|
||||
count: int = 1
|
||||
|
||||
|
||||
class FormationEnemy(Enemy):
|
||||
"""An enemy that belongs to a formation"""
|
||||
|
||||
def __init__(self, game: Game, image_name, offset,
|
||||
attributes: EnemyAttributes):
|
||||
"""Initialise the enemy
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:param offset: The offset from the other enemies
|
||||
:param attributes: The attributes given to this enemy
|
||||
:type attributes: EnemyAttributes
|
||||
"""
|
||||
self.offset_x, self.offset_y, self.offset_a = offset
|
||||
super().__init__(game, image_name, attributes)
|
||||
|
||||
|
||||
class EnemyFormation:
|
||||
"""Cluster of enemies that move in a particular way"""
|
||||
|
||||
def __init__(self, game: Game, image_name: str,
|
||||
enemy_attributes: FormationAttributes):
|
||||
"""Initialise the formation
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:type image_name: str
|
||||
:param enemy_attributes: The attributes to use for spawned enemies
|
||||
:type enemy_attributes: FormationAttributes
|
||||
"""
|
||||
self.game = game
|
||||
self.sprites: List[FormationEnemy] = []
|
||||
self.image_name = image_name
|
||||
|
||||
self.alpha = 0
|
||||
self.attributes = enemy_attributes
|
||||
|
||||
self.x, self.y = 0, 0
|
||||
self.destroyed = False
|
||||
|
||||
self.create_enemies()
|
||||
self.hidden = True
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn enemies"""
|
||||
pass
|
||||
|
||||
def position_enemy(self, enemy: FormationEnemy):
|
||||
"""Position a single enemy
|
||||
|
||||
:param enemy: The enemy to position
|
||||
:type enemy: FormationEnemy
|
||||
"""
|
||||
enemy.set_pos(
|
||||
(
|
||||
int(self.x + enemy.offset_x),
|
||||
int(self.y + enemy.offset_y)
|
||||
)
|
||||
)
|
||||
|
||||
def spawn_enemy(self, offset):
|
||||
"""Spawn a single enemy
|
||||
|
||||
:param offset: The offset which to apply to the enemy
|
||||
"""
|
||||
enemy = FormationEnemy(self.game, self.image_name,
|
||||
offset, self.attributes)
|
||||
self.sprites.append(enemy)
|
||||
return enemy
|
||||
|
||||
def tick(self, player):
|
||||
"""Update the positions of all enemies
|
||||
|
||||
:param player: The player to check if the enemies collide with
|
||||
"""
|
||||
self.alpha += 1
|
||||
for enemy in self.sprites:
|
||||
enemy.tick(player)
|
||||
self.position_enemy(enemy)
|
||||
self.sprites = Sprite.remove_destroyed(self.sprites)
|
||||
if len(self.sprites) == 0:
|
||||
self.destroy()
|
||||
|
||||
def destroy(self):
|
||||
"""Delete all enemies in this formation"""
|
||||
for enemy in self.sprites:
|
||||
enemy.destroy()
|
||||
self.sprites = []
|
||||
self.destroyed = True
|
||||
|
||||
def set_pos(self, pos):
|
||||
"""Set the position of this formation
|
||||
|
||||
:param pos: position to move to
|
||||
"""
|
||||
self.x, self.y = pos
|
||||
|
||||
def show(self):
|
||||
"""Make this formation visible"""
|
||||
if self.hidden:
|
||||
for enemy in self.sprites:
|
||||
enemy.show()
|
||||
self.hidden = False
|
||||
|
||||
def hide(self):
|
||||
"""Make this formation hidden"""
|
||||
if not self.hidden:
|
||||
for enemy in self.sprites:
|
||||
enemy.hide()
|
||||
self.hidden = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class CircleFormationAttributes(FormationAttributes):
|
||||
"""Attributes for a circle formation"""
|
||||
|
||||
radius: int = 40
|
||||
period: int = 300
|
||||
|
||||
|
||||
class CircleFormation(EnemyFormation):
|
||||
"""A circular formation of enemies, rotating in a ring"""
|
||||
|
||||
def __init__(self, game: Game, image_name,
|
||||
attributes: CircleFormationAttributes):
|
||||
"""Initialise the formation
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:param attributes: The attributes to use for spawned enemies
|
||||
:type attributes: CircleFormationAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, attributes)
|
||||
self.attributes: CircleFormationAttributes
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn all the enemies"""
|
||||
for i in range(self.attributes.count):
|
||||
self.spawn_enemy((0, 0, i))
|
||||
|
||||
def position_enemy(self, enemy: FormationEnemy):
|
||||
"""Position a single enemy
|
||||
|
||||
:param enemy:
|
||||
:type enemy: FormationEnemy
|
||||
"""
|
||||
a = (enemy.offset_a / self.attributes.count) * \
|
||||
self.attributes.period + self.game.alpha
|
||||
enemy.set_pos(
|
||||
(
|
||||
int(
|
||||
self.x+math.sin((-a/self.attributes.period)
|
||||
* 2*math.pi) * self.attributes.radius
|
||||
),
|
||||
int(
|
||||
self.y+math.cos((-a/self.attributes.period)
|
||||
* 2*math.pi) * self.attributes.radius
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LemniscateFormation(EnemyFormation):
|
||||
"""An 'infinity' shape enemy formation"""
|
||||
|
||||
def __init__(self, game: Game, image_name,
|
||||
attributes: CircleFormationAttributes):
|
||||
"""Initialise the formation
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:param attributes: The attributes to use for spawned enemies
|
||||
:type attributes: CircleFormationAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, attributes)
|
||||
self.attributes: CircleFormationAttributes
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn all enemies"""
|
||||
for i in range(self.attributes.count):
|
||||
self.spawn_enemy((0, 0, (i / self.attributes.count)
|
||||
* self.attributes.period * 0.25))
|
||||
|
||||
def position_enemy(self, enemy: FormationEnemy):
|
||||
"""Position an enemy
|
||||
|
||||
:param enemy:
|
||||
:type enemy: FormationEnemy
|
||||
"""
|
||||
a = enemy.offset_a + self.game.alpha
|
||||
|
||||
t = (-a/self.attributes.period)*2*math.pi
|
||||
x = self.x+(self.attributes.radius * math.cos(t)) / \
|
||||
(1 + math.sin(t)**2)
|
||||
y = self.y+(self.attributes.radius * math.sin(t) * math.cos(t)) / \
|
||||
(1 + math.sin(t)**2)
|
||||
|
||||
enemy.set_pos(
|
||||
(
|
||||
int(x),
|
||||
int(y)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TriangleFormationAttributes(FormationAttributes):
|
||||
"""Attributes for a triangular formation"""
|
||||
|
||||
spacing: int = 16
|
||||
|
||||
|
||||
class TriangleFormation(EnemyFormation):
|
||||
"""A v-shaped formation of enemies"""
|
||||
|
||||
def __init__(self, game: Game, image_name: str,
|
||||
attributes: TriangleFormationAttributes):
|
||||
"""Initialise the formation
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:type image_name: str
|
||||
:param attributes: The attributes to use for spawned enemies
|
||||
:type attributes: TriangleFormationAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, attributes)
|
||||
self.attributes: TriangleFormationAttributes
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn all enemies in this formation"""
|
||||
for i in range(self.attributes.count):
|
||||
y = -((i+1) // 2)*self.attributes.spacing//2
|
||||
|
||||
# first part is multiply by 1 or -1 to determine the side
|
||||
# then just have an offset for how far
|
||||
x = 2*((i % 2)-0.5) * ((i+1)//2)*self.attributes.spacing
|
||||
|
||||
self.spawn_enemy((x, y, 1))
|
||||
|
||||
|
||||
@dataclass
|
||||
class RectangleFormationAttributes(TriangleFormationAttributes):
|
||||
"""Attributes for a rectangle formation"""
|
||||
|
||||
width: int = 5
|
||||
height: int = 2
|
||||
|
||||
|
||||
class RectangleFormation(EnemyFormation):
|
||||
"""A grid-like formation of enemies"""
|
||||
|
||||
def __init__(self, game: Game, image_name,
|
||||
attributes: RectangleFormationAttributes):
|
||||
"""Initialise the formation
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for the enemy
|
||||
:param attributes: The attributes to use for spawned enemies
|
||||
:type attributes: RectangleFormationAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, attributes)
|
||||
self.attributes: RectangleFormationAttributes
|
||||
|
||||
def create_enemies(self):
|
||||
"""Spawn all enemies"""
|
||||
full_width = self.attributes.width * self.attributes.spacing
|
||||
full_height = self.attributes.height * self.attributes.spacing
|
||||
|
||||
for y in range(self.attributes.height):
|
||||
offset_y = ((y+0.5)*self.attributes.spacing)-(full_height/2)
|
||||
|
||||
for x in range(self.attributes.width):
|
||||
offset_x = ((x+0.5)*self.attributes.spacing)-(full_width/2)
|
||||
self.spawn_enemy((offset_x, offset_y, 1))
|
341
formation_spawner.py
Normal file
341
formation_spawner.py
Normal file
|
@ -0,0 +1,341 @@
|
|||
import math
|
||||
from random import choice, randint, random
|
||||
|
||||
from boss import CircleBossFormation, SnakeBossFormation
|
||||
from enemy import EnemyAttributes
|
||||
from formation import (
|
||||
CircleFormation,
|
||||
CircleFormationAttributes,
|
||||
EnemyFormation,
|
||||
FormationAttributes,
|
||||
LemniscateFormation,
|
||||
RectangleFormation,
|
||||
RectangleFormationAttributes,
|
||||
TriangleFormation,
|
||||
TriangleFormationAttributes,
|
||||
)
|
||||
|
||||
|
||||
def wobble_pattern(formation):
|
||||
"""A sinusoidal movement pattern
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
x = (1+math.sin(formation.alpha/80)) * formation.game.w/2
|
||||
y = formation.y + (1 if formation.alpha % 4 == 0 else 0)
|
||||
formation.set_pos((x, y))
|
||||
|
||||
|
||||
def speed_pattern(formation):
|
||||
"""Quickly move the formation downwards
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
x = formation.x
|
||||
y = formation.y + 2
|
||||
formation.set_pos((x, y))
|
||||
|
||||
|
||||
def slow_pattern(formation):
|
||||
"""Slowly move the formation downwards
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
x = formation.x
|
||||
y = formation.y + (1 if formation.alpha % 8 == 0 else 0)
|
||||
formation.set_pos((x, y))
|
||||
|
||||
|
||||
def slide_in_pattern(formation):
|
||||
"""Slowly move into the center of the screen and then remain there
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
cy = formation.game.h//3
|
||||
|
||||
if formation.alpha < 400:
|
||||
x = formation.x
|
||||
y = (formation.alpha/400) * (cy*1.5) - (cy*0.5)
|
||||
else:
|
||||
x = formation.x
|
||||
y = formation.y
|
||||
|
||||
formation.set_pos((int(x), int(y)))
|
||||
|
||||
|
||||
def no_pattern(formation):
|
||||
"""No movement, stay in the center
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
formation.set_pos((
|
||||
formation.game.w // 2,
|
||||
formation.game.h // 3
|
||||
))
|
||||
|
||||
|
||||
def figure_of_eight_pattern(formation):
|
||||
"""Move the formation in a figure of eight
|
||||
|
||||
:param formation: Formation to move
|
||||
"""
|
||||
period = 600
|
||||
edge = 8
|
||||
radius = formation.game.h//3 - edge
|
||||
cx, cy = formation.game.w//2, formation.game.h//3
|
||||
|
||||
if formation.alpha < 200:
|
||||
x = formation.x
|
||||
y = (formation.alpha/200) * (cy*1.5) - (cy*0.5)
|
||||
else:
|
||||
a = formation.alpha - 200
|
||||
t = (a/period)*2*math.pi - math.pi/2
|
||||
|
||||
y = cy + (radius * math.cos(t)) / (1 + math.sin(t)**2)
|
||||
x = cx + (radius * math.sin(t) * math.cos(t)) / (1 + math.sin(t)**2)
|
||||
|
||||
formation.set_pos((int(x), int(y)))
|
||||
|
||||
|
||||
class FormationSpawner():
|
||||
"""Object to manage spawning of enemies and phases"""
|
||||
|
||||
def __init__(self, game):
|
||||
"""Initialise the formation spawner
|
||||
|
||||
:param game: The game which this belongs to
|
||||
"""
|
||||
self.game = game
|
||||
self.formations = []
|
||||
self.difficulty_multiplier = 0.5
|
||||
|
||||
self.next_formation = 0
|
||||
|
||||
self.phase = -1
|
||||
self.phases = [
|
||||
Phase("Phase:1", [
|
||||
self.spawn_fleet,
|
||||
self.spawn_loop,
|
||||
self.spawn_orbital], 10),
|
||||
Phase("Boss:1", [self.spawn_circle_boss], 1),
|
||||
Phase("Phase:2", [
|
||||
self.spawn_fleet,
|
||||
self.spawn_loop,
|
||||
self.spawn_orbital], 10, max_wave=3),
|
||||
Phase("Boss:2", [self.spawn_snake_boss], 1),
|
||||
]
|
||||
|
||||
self.to_spawn = 0
|
||||
self.current_reward = 1
|
||||
|
||||
def tick(self):
|
||||
"""Update all formations"""
|
||||
for formation, update in self.formations:
|
||||
formation.tick(self.game.player)
|
||||
update(formation)
|
||||
if formation.y > self.game.h:
|
||||
formation.destroy()
|
||||
self.formations = list(
|
||||
filter(lambda s: not s[0].destroyed, self.formations))
|
||||
|
||||
self.spawn_next()
|
||||
|
||||
def spawn_random(self):
|
||||
"""Spawn a random formation"""
|
||||
options = [
|
||||
self.spawn_fleet,
|
||||
self.spawn_loop,
|
||||
self.spawn_orbital,
|
||||
self.spawn_rectangle
|
||||
]
|
||||
choice(options)()
|
||||
|
||||
def spawn_formation(self, formation: EnemyFormation, update):
|
||||
"""Add a formation to the list of formations
|
||||
|
||||
:param formation: Formation to add
|
||||
:type formation: EnemyFormation
|
||||
:param update: movement function to use for this formation
|
||||
"""
|
||||
update(formation)
|
||||
formation.show()
|
||||
self.formations.append((formation, update))
|
||||
|
||||
def spawn_circle_boss(self):
|
||||
"""Spawn the circle boss"""
|
||||
attributes = EnemyAttributes(
|
||||
hp=int(15*self.difficulty_multiplier),
|
||||
reward=self.current_reward,
|
||||
cooldown=50
|
||||
)
|
||||
formation = CircleBossFormation(self.game, attributes)
|
||||
formation.set_pos((self.game.w//2, 0))
|
||||
update = figure_of_eight_pattern
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_snake_boss(self):
|
||||
"""Spawn the snake boss"""
|
||||
attributes = FormationAttributes(
|
||||
hp=int(10*self.difficulty_multiplier),
|
||||
reward=self.current_reward,
|
||||
cooldown=160
|
||||
)
|
||||
|
||||
formation = SnakeBossFormation(self.game, attributes)
|
||||
formation.set_pos((self.game.w//2, 0))
|
||||
update = slide_in_pattern
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_fleet(self):
|
||||
"""Spawn the fleet formation"""
|
||||
sprite = randint(6, 7)
|
||||
|
||||
position = (random()*self.game.w, -32)
|
||||
attributes = TriangleFormationAttributes(
|
||||
hp=int(self.difficulty_multiplier),
|
||||
cooldown=-1,
|
||||
reward=self.current_reward,
|
||||
count=randint(1, 3)*2 + 1,
|
||||
spacing=8
|
||||
)
|
||||
formation = TriangleFormation(
|
||||
self.game, f"smallenemy{sprite}", attributes)
|
||||
formation.set_pos(position)
|
||||
|
||||
update = speed_pattern
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_orbital(self):
|
||||
"""Spawn the orbital formation"""
|
||||
position = (random()*self.game.w, -32)
|
||||
sprite = choice((1, 3))
|
||||
|
||||
attributes = CircleFormationAttributes(
|
||||
hp=int(self.difficulty_multiplier * 2),
|
||||
count=randint(3, 4)*2,
|
||||
radius=randint(10, 20),
|
||||
period=randint(100//int(self.difficulty_multiplier), 400),
|
||||
cooldown=80,
|
||||
reward=self.current_reward
|
||||
|
||||
)
|
||||
|
||||
formation = CircleFormation(
|
||||
self.game, f"smallenemy{sprite}", attributes)
|
||||
formation.set_pos(position)
|
||||
|
||||
update = wobble_pattern
|
||||
formation.alpha = randint(1, 1000)
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_rectangle(self):
|
||||
"""Spawn the rectangle formation"""
|
||||
sprite = choice((0, 2))
|
||||
position = (random() * self.game.w, -32)
|
||||
|
||||
attributes = RectangleFormationAttributes(
|
||||
hp=int(self.difficulty_multiplier * 2),
|
||||
width=randint(4, 6),
|
||||
height=randint(2, 3),
|
||||
cooldown=80,
|
||||
reward=self.current_reward,
|
||||
)
|
||||
|
||||
formation = RectangleFormation(
|
||||
self.game, f"smallenemy{sprite}", attributes
|
||||
)
|
||||
formation.set_pos(position)
|
||||
|
||||
update = wobble_pattern
|
||||
formation.alpha = randint(1, 1000)
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_loop(self):
|
||||
"""Spawn the loop formation"""
|
||||
sprite = choice((4, 5))
|
||||
position = (random()*self.game.w, -32)
|
||||
attributes = CircleFormationAttributes(
|
||||
count=randint(4, 8),
|
||||
radius=randint(self.game.w//2, self.game.w),
|
||||
period=randint(200, 300),
|
||||
hp=int(self.difficulty_multiplier),
|
||||
reward=self.current_reward,
|
||||
cooldown=160,
|
||||
)
|
||||
|
||||
formation = LemniscateFormation(
|
||||
self.game, f"smallenemy{sprite}", attributes)
|
||||
formation.set_pos(position)
|
||||
|
||||
update = slow_pattern
|
||||
self.spawn_formation(formation, update)
|
||||
|
||||
def spawn_next(self):
|
||||
"""Spawn the next formation to be spawned"""
|
||||
if self.to_spawn > 0:
|
||||
if len(self.formations) < self.current_phase().max_wave:
|
||||
if self.game.alpha > self.next_formation \
|
||||
and self.next_formation != -1:
|
||||
self.next_formation = self.game.alpha \
|
||||
+ 100 / self.difficulty_multiplier
|
||||
|
||||
self.current_phase().get_spawn_function()()
|
||||
self.to_spawn -= 1
|
||||
else:
|
||||
if len(self.formations) == 0:
|
||||
self.next_phase()
|
||||
|
||||
def next_phase(self):
|
||||
"""Increment the phase by 1 and start the next phase"""
|
||||
self.phase += 1
|
||||
self.game.save_game()
|
||||
self.start_phase()
|
||||
|
||||
def start_phase(self):
|
||||
"""Start the next phase"""
|
||||
self.to_spawn = self.current_phase().duration
|
||||
|
||||
self.difficulty_multiplier = (self.phase+2) * 0.5
|
||||
self.current_reward = int(2**self.difficulty_multiplier)
|
||||
|
||||
self.next_formation = self.game.alpha + 100
|
||||
if self.current_phase().name:
|
||||
self.game.effect_player.splash_text(self.current_phase().name)
|
||||
|
||||
def current_phase(self):
|
||||
"""Return the current phase"""
|
||||
if self.phase < len(self.phases):
|
||||
return self.phases[self.phase]
|
||||
|
||||
return Phase(f"Phase:{self.phase-1}", [
|
||||
self.spawn_random
|
||||
], 10 * self.difficulty_multiplier,
|
||||
max_wave=int(self.difficulty_multiplier)
|
||||
)
|
||||
|
||||
def clear_all(self):
|
||||
"""Remove all formation objects"""
|
||||
for f, _ in self.formations:
|
||||
f.destroy()
|
||||
|
||||
|
||||
class Phase:
|
||||
"""Rules for which formation will be spawned"""
|
||||
|
||||
def __init__(self, name, spawn_functions, duration, max_wave=2):
|
||||
"""__init__.
|
||||
|
||||
:param name: The name of the phase
|
||||
:param spawn_functions: A list of functions to use to spawn enemies
|
||||
:param duration: The number of formations to spawn
|
||||
before the phase is over
|
||||
:param max_wave: The maximum number of formations to spawn at a time
|
||||
"""
|
||||
self.spawn_functions = spawn_functions
|
||||
self.duration = duration
|
||||
self.name = name
|
||||
self.max_wave = max_wave
|
||||
|
||||
def get_spawn_function(self):
|
||||
"""Return a random spawn function"""
|
||||
return choice(self.spawn_functions)
|
41
frame_counter.py
Normal file
41
frame_counter.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from sys import stderr
|
||||
from time import time
|
||||
|
||||
|
||||
class FrameCounter:
|
||||
"""Creates a main loop and ensures that the framerate is static"""
|
||||
|
||||
def __init__(self, canvas, target_fps):
|
||||
"""Initialise the frame counter
|
||||
|
||||
:param canvas: The canvas which to call after on
|
||||
:param target_fps: The fps to aim to achieve
|
||||
"""
|
||||
self.canvas = canvas
|
||||
self.fps = target_fps
|
||||
self.frame_time = 1 / target_fps
|
||||
self.last_frame = time()
|
||||
|
||||
self.current_fps = 1
|
||||
|
||||
def next_frame(self, callback):
|
||||
"""Calculate when the next frame should be called
|
||||
|
||||
:param callback: function to call for the next frame
|
||||
"""
|
||||
t = time()
|
||||
ft = t - self.last_frame
|
||||
|
||||
delay = 0
|
||||
|
||||
if ft > self.frame_time:
|
||||
if ft - self.frame_time > self.frame_time / 5:
|
||||
print(
|
||||
f"Help! Running {ft - self.frame_time} seconds behind!",
|
||||
file=stderr)
|
||||
else:
|
||||
delay = self.frame_time - ft
|
||||
|
||||
self.canvas.after(int(delay*1000), callback)
|
||||
self.current_fps = 1 / (delay+ft)
|
||||
self.last_frame = t
|
310
game.py
Normal file
310
game.py
Normal file
|
@ -0,0 +1,310 @@
|
|||
from random import randint, random
|
||||
from tkinter import Canvas, PhotoImage, Tk
|
||||
from typing import List
|
||||
|
||||
from config import Config
|
||||
from font import Font
|
||||
from frame_counter import FrameCounter
|
||||
from inputs import InputController
|
||||
from sprite import Sprite
|
||||
from textures import TextureFactory
|
||||
|
||||
|
||||
class Game:
|
||||
"""A generic game object"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialise the game
|
||||
"""
|
||||
self.win = Tk()
|
||||
game_width, game_height = (
|
||||
Config.WIDTH*Config.SCALE, Config.HEIGHT*Config.SCALE
|
||||
)
|
||||
self.w, self.h = Config.WIDTH, Config.HEIGHT
|
||||
|
||||
self.win.geometry(f"{game_width}x{game_height}")
|
||||
self.canvas = Canvas(self.win, width=game_width,
|
||||
height=game_height, bg="#000")
|
||||
self.canvas.pack()
|
||||
|
||||
self.texture_factory = TextureFactory(scale=Config.SCALE)
|
||||
self.effect_player = EffectPlayer(self)
|
||||
self.frame_counter = FrameCounter(self.canvas, Config.FPS)
|
||||
|
||||
self.inputs = InputController(self)
|
||||
self.sprites = []
|
||||
|
||||
self.score = 0
|
||||
|
||||
self.alpha = 0
|
||||
|
||||
def start(self):
|
||||
"""Start the game"""
|
||||
self.loop()
|
||||
self.win.mainloop()
|
||||
|
||||
def tick(self):
|
||||
"""Update the game's sprites"""
|
||||
for sprite in self.sprites:
|
||||
sprite.tick()
|
||||
self.effect_player.tick()
|
||||
|
||||
def loop(self):
|
||||
"""Loop the game at a set framerate"""
|
||||
self.alpha += 1
|
||||
self.tick()
|
||||
self.frame_counter.next_frame(self.loop)
|
||||
|
||||
def clear_all(self):
|
||||
"""Remove all game sprites"""
|
||||
for sprite in self.sprites:
|
||||
sprite.destroy()
|
||||
self.sprites = []
|
||||
|
||||
|
||||
class GameSprite(Sprite):
|
||||
"""A sprite which belongs to a game"""
|
||||
|
||||
def __init__(self, game: Game, image: PhotoImage):
|
||||
"""Initialise the sprite
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image: The image to use for the sprite
|
||||
:type image: PhotoImage
|
||||
"""
|
||||
self.game = game
|
||||
super().__init__(game.canvas, image, (0, 0))
|
||||
|
||||
def move(self, x, y):
|
||||
"""Move the sprite by a certain amount
|
||||
|
||||
:param x: Amount of pixels to move right
|
||||
:param y: Amount of pixels to move down
|
||||
"""
|
||||
# if the object needs to move less than a pixel
|
||||
# only move it every few frames to create this effect
|
||||
if abs(x) >= 1:
|
||||
self.x += x
|
||||
elif x != 0:
|
||||
if self.game.alpha % (1/x) == 0:
|
||||
self.x += 1
|
||||
|
||||
if abs(y) >= 1:
|
||||
self.y += y
|
||||
elif y != 0:
|
||||
if self.game.alpha % (1/y) == 0:
|
||||
self.y += 1
|
||||
|
||||
self.update_position()
|
||||
|
||||
|
||||
class GameEffect(GameSprite):
|
||||
"""An effect that can be played within game"""
|
||||
|
||||
def __init__(self, game: Game, image: PhotoImage,
|
||||
duration=10, momentum=(0, 0)):
|
||||
"""Initialise the game effect
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image: The image to use for this
|
||||
:type image: PhotoImage
|
||||
:param duration: How long this effect should last for
|
||||
:param momentum: Which direction to move this effect
|
||||
"""
|
||||
self.start_time = game.alpha
|
||||
self.duration = duration
|
||||
self.velocity_x, self.velocity_y = momentum
|
||||
super().__init__(game, image)
|
||||
|
||||
def tick(self):
|
||||
"""Move the effect by its momentum and remove it if its over"""
|
||||
super().tick()
|
||||
self.move(self.velocity_x, self.velocity_y)
|
||||
|
||||
alpha = self.game.alpha - self.start_time
|
||||
if self.duration != -1 and alpha > self.duration:
|
||||
self.destroy()
|
||||
|
||||
|
||||
class AnimatedEffect(GameEffect):
|
||||
"""An effect which involves animating an image"""
|
||||
|
||||
def __init__(self, game: Game, images: List[PhotoImage],
|
||||
frame_time=1, momentum=(0, 0)):
|
||||
"""Initialise the effect
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param images: The images to use for this animation
|
||||
:type images: List[PhotoImage]
|
||||
:param frame_time: Length of each frame of the animation
|
||||
:param momentum: Direction to move this effect
|
||||
"""
|
||||
self.start_time = game.alpha
|
||||
self.frame_time = frame_time
|
||||
self.images = images
|
||||
self.frame_start = game.alpha
|
||||
super().__init__(game, images[0], duration=len(
|
||||
images)*frame_time, momentum=momentum)
|
||||
|
||||
def tick(self):
|
||||
"""Animate the effect"""
|
||||
super().tick()
|
||||
|
||||
alpha = self.game.alpha - self.start_time
|
||||
|
||||
i = int(alpha // self.frame_time)
|
||||
if i < len(self.images):
|
||||
self.set_image(self.images[i])
|
||||
else:
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EffectPlayer:
|
||||
"""An object which concerns itself with managing the effects"""
|
||||
|
||||
def __init__(self, game: Game) -> None:
|
||||
"""Initialise the
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
"""
|
||||
self.sprites = []
|
||||
self.game = game
|
||||
self.explosion_frames = []
|
||||
self.star_image: PhotoImage
|
||||
|
||||
def load_textures(self):
|
||||
"""Load effect textures"""
|
||||
|
||||
self.explosion_frames = [
|
||||
self.game.texture_factory.get_image(f"explosion{i+1}")
|
||||
for i in range(3)
|
||||
]
|
||||
self.star_image = self.game.texture_factory.get_image("star")
|
||||
|
||||
def tick(self):
|
||||
"""Update all effects"""
|
||||
for sprite in self.sprites:
|
||||
sprite.tick()
|
||||
|
||||
self.sprites = Sprite.remove_destroyed(self.sprites)
|
||||
|
||||
def create_stars(self):
|
||||
"""Initialise the stars in the background"""
|
||||
for _ in range(100):
|
||||
self.create_star(True)
|
||||
|
||||
def create_star(self, new=False):
|
||||
"""Add a star to the background
|
||||
|
||||
:param new: Whether this star should be added at
|
||||
the top of the screen or anywhere
|
||||
"""
|
||||
x = randint(0, self.game.w)
|
||||
if new:
|
||||
y = randint(0, self.game.h)
|
||||
else:
|
||||
y = -1
|
||||
|
||||
speed = randint(1, 4) * 0.1
|
||||
duration = 2*self.game.h / speed
|
||||
|
||||
star = GameEffect(
|
||||
self.game,
|
||||
self.star_image,
|
||||
duration=int(duration),
|
||||
momentum=(0, speed)
|
||||
)
|
||||
star.set_pos((x, y))
|
||||
|
||||
star.send_to_back()
|
||||
star.show()
|
||||
|
||||
self.sprites.append(star)
|
||||
|
||||
def create_explosion(self, position=(0, 0)):
|
||||
"""Create an explosion effect
|
||||
|
||||
:param position: location of the explosion
|
||||
"""
|
||||
for _ in range(randint(1, 3)):
|
||||
m = ((random()*2)-1, (random()*2)-1)
|
||||
explosion_sprite = AnimatedEffect(
|
||||
self.game, self.explosion_frames, frame_time=5, momentum=m)
|
||||
explosion_sprite.set_pos(position)
|
||||
explosion_sprite.show()
|
||||
|
||||
self.sprites.append(explosion_sprite)
|
||||
|
||||
def splash_text(self, text, duration=50):
|
||||
"""splash_text.
|
||||
|
||||
:param text:
|
||||
:param duration:
|
||||
"""
|
||||
text_img = Font.load_text(self.game.texture_factory, text)
|
||||
position = (
|
||||
(self.game.w-Font.FONT_WIDTH*len(text)) // 2,
|
||||
(self.game.h-Font.FONT_SIZE) // 3
|
||||
)
|
||||
|
||||
text_sprite = GameEffect(
|
||||
self.game, text_img, duration=duration)
|
||||
text_sprite.set_pos(position)
|
||||
text_sprite.show()
|
||||
self.sprites.append(text_sprite)
|
||||
|
||||
|
||||
class DamageableSprite(GameSprite):
|
||||
"""Sprite with health points """
|
||||
|
||||
def __init__(self, game: Game, image_name: str, hp=3):
|
||||
"""Initialise the sprite
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for this sprite
|
||||
:type image_name: str
|
||||
:param hp: The number of hit points this sprite spawns with
|
||||
"""
|
||||
self.image = game.texture_factory.get_image(image_name)
|
||||
self.white_image = game.texture_factory.get_image(
|
||||
f"{image_name}:white")
|
||||
|
||||
self.hp = hp
|
||||
self.animation_frame = 0
|
||||
|
||||
super().__init__(game, self.image)
|
||||
|
||||
def damage(self, amount=1):
|
||||
"""Decrease number of hit points by an amount
|
||||
|
||||
:param amount:
|
||||
"""
|
||||
if not self.destroyed:
|
||||
self.hp -= amount
|
||||
self.animation_frame = 5
|
||||
if self.hp <= 0:
|
||||
self.hp = 0
|
||||
self.destroy()
|
||||
self.game.effect_player.create_explosion(self.get_pos())
|
||||
|
||||
def tick(self):
|
||||
"""Update the sprite"""
|
||||
super().tick()
|
||||
if self.animation_frame > 0:
|
||||
self.animation_frame -= 1
|
||||
if self.animation_frame % 2 == 0:
|
||||
self.set_image(self.image)
|
||||
else:
|
||||
self.set_image(self.white_image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("!!!")
|
||||
print("This is not the main file!")
|
||||
print("Pleae run\n\tpython main.py\ninstead!")
|
||||
print("!!!")
|
152
hud.py
Normal file
152
hud.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
from game import Game, GameSprite
|
||||
from font import Font
|
||||
|
||||
|
||||
class ScoreCounterSprite(GameSprite):
|
||||
"""Single digit for a score counter"""
|
||||
|
||||
def __init__(self, game: Game):
|
||||
"""Initialise the score counter
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
"""
|
||||
self.number_images = []
|
||||
|
||||
self.x = 0
|
||||
|
||||
for i in range(10):
|
||||
self.number_images.append(
|
||||
Font.load_text(game.texture_factory, str(i)))
|
||||
|
||||
super().__init__(game, self.number_images[0])
|
||||
|
||||
def update_image(self):
|
||||
"""Update the digit"""
|
||||
self.set_image(self.number_images[int(self.x % 10)])
|
||||
|
||||
def set(self, x):
|
||||
"""Set the image
|
||||
|
||||
:param x: number to set this digit to
|
||||
"""
|
||||
self.x = x
|
||||
self.update_image()
|
||||
|
||||
|
||||
class ScoreCounter:
|
||||
"""Sprite to display a number"""
|
||||
|
||||
def __init__(self, game: Game, num_digits, position=(0, 0)) -> None:
|
||||
"""__init__.
|
||||
|
||||
:param game:
|
||||
:type game: Game
|
||||
:param num_digits:
|
||||
:param position:
|
||||
:rtype: None
|
||||
"""
|
||||
self.digits = []
|
||||
x, y = position
|
||||
|
||||
self.number = 0
|
||||
|
||||
for i in range(num_digits):
|
||||
sprite = ScoreCounterSprite(game)
|
||||
sprite.set_pos((x+(Font.FONT_SIZE + 1)*i, y))
|
||||
self.digits.append(sprite)
|
||||
|
||||
def set(self, number):
|
||||
"""Set the score to be displayed
|
||||
|
||||
:param number:
|
||||
"""
|
||||
if number != self.number:
|
||||
self.number = number
|
||||
power = 10**len(self.digits)
|
||||
for digit in self.digits:
|
||||
power /= 10
|
||||
digit.set((number // power) % 10)
|
||||
|
||||
def destroy(self):
|
||||
"""Remove this counter"""
|
||||
for n in self.digits:
|
||||
n.destroy()
|
||||
|
||||
def send_to_front(self):
|
||||
"""Move this counter to the foreground"""
|
||||
for d in self.digits:
|
||||
d.send_to_front()
|
||||
|
||||
def show(self):
|
||||
"""Make this counter visible"""
|
||||
for d in self.digits:
|
||||
d.show()
|
||||
|
||||
def hide(self):
|
||||
"""Make this counter invisible"""
|
||||
for d in self.digits:
|
||||
d.hide()
|
||||
|
||||
|
||||
class GameHud:
|
||||
"""Object to manage the items visible in the game's heads up display"""
|
||||
|
||||
SCORE_DIGITS = 8
|
||||
HP_DIGITS = 2
|
||||
|
||||
def __init__(self, game) -> None:
|
||||
"""Initialise the HUD
|
||||
|
||||
:param game: The game which this belongs to
|
||||
"""
|
||||
|
||||
self.game = game
|
||||
self.score_counter = ScoreCounter(game, GameHud.SCORE_DIGITS,
|
||||
position=(
|
||||
game.w
|
||||
- GameHud.SCORE_DIGITS
|
||||
* (Font.FONT_SIZE+1),
|
||||
1)
|
||||
)
|
||||
|
||||
self.hp_symbol = GameSprite(game, game.player.image)
|
||||
self.hp_symbol.set_pos((1, 1))
|
||||
|
||||
x_image = Font.load_text(game.texture_factory, "x")
|
||||
self.x_symbol = GameSprite(game, x_image)
|
||||
self.x_symbol.set_pos((self.hp_symbol.x+self.hp_symbol.w+1, 1))
|
||||
|
||||
self.hp_counter = ScoreCounter(game, GameHud.HP_DIGITS,
|
||||
position=(self.x_symbol.x+1 +
|
||||
self.x_symbol.w, 1)
|
||||
)
|
||||
|
||||
self.items = (self.score_counter,
|
||||
self.hp_symbol,
|
||||
self.x_symbol,
|
||||
self.hp_counter)
|
||||
|
||||
def tick(self):
|
||||
"""Update the hud"""
|
||||
self.score_counter.set(self.game.score)
|
||||
self.hp_counter.set(
|
||||
self.game.player.hp if self.game.player.hp > 0 else 0)
|
||||
|
||||
for x in self.items:
|
||||
x.send_to_front()
|
||||
|
||||
def destroy(self):
|
||||
"""Remove all the associated objects"""
|
||||
for x in self.items:
|
||||
x.destroy()
|
||||
|
||||
def hide(self):
|
||||
"""Make this object invisible"""
|
||||
for x in self.items:
|
||||
x.hide()
|
||||
|
||||
def show(self):
|
||||
"""Make this object visible"""
|
||||
for x in self.items:
|
||||
x.show()
|
133
inputs.py
Normal file
133
inputs.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
from dataclasses import dataclass
|
||||
from os import path
|
||||
from sys import stderr
|
||||
|
||||
from config import Config
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputSettings:
|
||||
"""Settings for keybinds"""
|
||||
|
||||
down: str = "Down"
|
||||
left: str = "Left"
|
||||
up: str = "Up"
|
||||
right: str = "Right"
|
||||
action: str = "space"
|
||||
pause: str = "Escape"
|
||||
boss: str = "F9"
|
||||
|
||||
def save_inputs(self):
|
||||
"""Save keybinds to a file"""
|
||||
with open(Config.SETTINGS_FILE, "w", encoding="utf-8") as file:
|
||||
for key, value in vars(self).items():
|
||||
file.write(f"{key}: {value}\n")
|
||||
|
||||
def load_inputs(self):
|
||||
"""Load keybinds from a file"""
|
||||
if path.exists(Config.SETTINGS_FILE):
|
||||
with open(Config.SETTINGS_FILE, "r", encoding="utf-8") as file:
|
||||
for line in file.readlines():
|
||||
split = line.strip().split(": ")
|
||||
if len(split) == 2:
|
||||
setattr(self, split[0], split[1])
|
||||
else:
|
||||
print(
|
||||
f"Settings file corrupted? Invalid line {line}",
|
||||
file=stderr
|
||||
)
|
||||
|
||||
|
||||
class InputController:
|
||||
"""Object which listens to key inputs"""
|
||||
|
||||
def __init__(self, game) -> None:
|
||||
"""Initialise the input controller
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:rtype: None
|
||||
"""
|
||||
game.win.bind('<KeyPress>', self.on_key_press)
|
||||
game.win.bind('<KeyRelease>', self.on_key_release)
|
||||
|
||||
self.handlers = []
|
||||
|
||||
self.settings = InputSettings()
|
||||
self.settings.load_inputs()
|
||||
|
||||
self.k_down = False
|
||||
self.k_left = False
|
||||
self.k_up = False
|
||||
self.k_right = False
|
||||
self.k_action = False
|
||||
self.k_pause = False
|
||||
self.k_boss = False
|
||||
|
||||
def on_key_press(self, e):
|
||||
"""Handle Key press events
|
||||
|
||||
:param e: The key press event to handle
|
||||
"""
|
||||
if e.keysym == self.settings.left:
|
||||
self.k_left = True
|
||||
|
||||
if e.keysym == self.settings.right:
|
||||
self.k_right = True
|
||||
|
||||
if e.keysym == self.settings.up:
|
||||
self.k_up = True
|
||||
|
||||
if e.keysym == self.settings.down:
|
||||
self.k_down = True
|
||||
|
||||
if e.keysym == self.settings.action:
|
||||
self.k_action = True
|
||||
|
||||
if e.keysym == self.settings.pause:
|
||||
self.k_pause = True
|
||||
|
||||
for t, h in self.handlers:
|
||||
if t == "press" and h(e):
|
||||
break
|
||||
|
||||
def on_key_release(self, e):
|
||||
"""Handle Key release events
|
||||
|
||||
|
||||
:param e: The key press event to handle
|
||||
"""
|
||||
if e.keysym == self.settings.left:
|
||||
self.k_left = False
|
||||
|
||||
if e.keysym == self.settings.right:
|
||||
self.k_right = False
|
||||
|
||||
if e.keysym == self.settings.up:
|
||||
self.k_up = False
|
||||
|
||||
if e.keysym == self.settings.down:
|
||||
self.k_down = False
|
||||
|
||||
if e.keysym == self.settings.action:
|
||||
self.k_action = False
|
||||
|
||||
if e.keysym == self.settings.pause:
|
||||
self.k_pause = False
|
||||
|
||||
for t, h in self.handlers:
|
||||
if t == "release" and h(e):
|
||||
break
|
||||
|
||||
def add_keypress_handler(self, callback):
|
||||
"""Register a key press listener
|
||||
|
||||
:param callback:
|
||||
"""
|
||||
self.handlers.insert(0, ("press", callback))
|
||||
|
||||
def add_keyrelease_handler(self, callback):
|
||||
"""Register a key release listener
|
||||
|
||||
:param callback:
|
||||
"""
|
||||
self.handlers.insert(0, ("release", callback))
|
443
leaderboard.py
Normal file
443
leaderboard.py
Normal file
|
@ -0,0 +1,443 @@
|
|||
from os import path
|
||||
from random import randint
|
||||
from typing import List
|
||||
|
||||
from config import Config
|
||||
from font import Font
|
||||
from game import Game, GameSprite
|
||||
|
||||
|
||||
class LeaderboardFile:
|
||||
"""Object to manage saving and loading the leaderboard"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise the leaderboard file"""
|
||||
self.entries = []
|
||||
|
||||
def load_entries(self):
|
||||
"""Load leaderboard entries from a file"""
|
||||
self.entries = []
|
||||
if path.exists(Config.LEADERBOARD_FILE):
|
||||
with open(Config.LEADERBOARD_FILE, "rb") as file:
|
||||
while (entry := file.read(8 + Config.NICK_LEN)):
|
||||
name = entry[0:Config.NICK_LEN]
|
||||
score = entry[Config.NICK_LEN:11]
|
||||
score_entry = (name.decode("ascii"),
|
||||
int.from_bytes(score, byteorder="little"))
|
||||
self.entries.append(score_entry)
|
||||
self.sort_entries()
|
||||
|
||||
def sort_entries(self):
|
||||
"""Sort leaderboard entries"""
|
||||
self.entries.sort(key=(lambda e: e[1]))
|
||||
self.entries.reverse()
|
||||
|
||||
def save_entries(self):
|
||||
"""Save leaderboard entries"""
|
||||
with open(Config.LEADERBOARD_FILE, "wb") as file:
|
||||
for name, score in self.entries:
|
||||
file.write(bytes(name[0:Config.NICK_LEN], "ascii"))
|
||||
file.write(int(score).to_bytes(8, "little"))
|
||||
|
||||
def add_entry(self, name, score):
|
||||
"""Add a leaderboard entry
|
||||
|
||||
:param name: Initials of the player
|
||||
:param score: The sore that was achieved
|
||||
"""
|
||||
self.entries.append((name, score))
|
||||
self.sort_entries()
|
||||
|
||||
|
||||
class NameEntryLetter(GameSprite):
|
||||
"""A single sprite used in a initial entry"""
|
||||
|
||||
def __init__(self, game: Game, image, letter):
|
||||
"""Initialise the letter
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image: The image to use for this
|
||||
:param letter: the letter to use
|
||||
"""
|
||||
self.letter = letter
|
||||
super().__init__(game, image)
|
||||
|
||||
|
||||
class NameEntry():
|
||||
"""An initial entry element, allowing the user to enter their initials"""
|
||||
|
||||
def __init__(self, game: Game, callback, num_letters=3, position=(0, 0)):
|
||||
"""Initialise the name entry
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param callback: callback to call when the entry is complete
|
||||
:param num_letters: Number of letters to use for the initials
|
||||
:param position: Position of this element
|
||||
"""
|
||||
self.game = game
|
||||
self.callback = callback
|
||||
|
||||
self.alphabet = [
|
||||
Font.load_text(game.texture_factory, c)
|
||||
for c in list(map(chr, range(97, 123)))
|
||||
]
|
||||
|
||||
self.letters: List[NameEntryLetter] = []
|
||||
self.selection = 0
|
||||
|
||||
self.hidden = True
|
||||
|
||||
self.populate_letters(num_letters)
|
||||
self.game.inputs.add_keypress_handler(self.on_key)
|
||||
self.set_pos(position)
|
||||
|
||||
def populate_letters(self, num_letters):
|
||||
"""Create sprites for each of the letters
|
||||
|
||||
:param num_letters: Number of letters to use for initials
|
||||
"""
|
||||
for _ in range(num_letters):
|
||||
sprite = NameEntryLetter(
|
||||
self.game, self.alphabet[0], 0
|
||||
)
|
||||
self.letters.append(sprite)
|
||||
|
||||
enter_image = Font.load_text(self.game.texture_factory, "enter")
|
||||
self.button = GameSprite(self.game, enter_image)
|
||||
self.w = self.button.w + (self.letters[0].w+1)*len(self.letters)
|
||||
self.h = Font.FONT_SIZE
|
||||
|
||||
def on_key(self, _):
|
||||
"""Handle Key press events
|
||||
|
||||
|
||||
:param _: The key press event to handle
|
||||
"""
|
||||
inp = self.game.inputs
|
||||
if not self.hidden:
|
||||
if inp.k_action and self.selection == len(self.letters):
|
||||
self.callback(self.get_string())
|
||||
return True
|
||||
|
||||
if inp.k_left:
|
||||
self.selection -= 1
|
||||
self.get_selected_letter().show()
|
||||
|
||||
if inp.k_right or inp.k_action:
|
||||
self.get_selected_letter().show()
|
||||
self.selection += 1
|
||||
self.get_selected_letter().hide()
|
||||
|
||||
self.selection %= len(self.letters) + 1
|
||||
|
||||
if inp.k_up:
|
||||
self.modify_letter(-1)
|
||||
|
||||
if inp.k_down:
|
||||
self.modify_letter(1)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_string(self):
|
||||
"""Get the initials entered"""
|
||||
return "".join(map(lambda l: chr(97 + l.letter), self.letters))
|
||||
|
||||
def modify_letter(self, amount):
|
||||
"""Increase or decrease a single character
|
||||
|
||||
:param amount: number of letters to increment by
|
||||
"""
|
||||
letter = self.get_selected_letter()
|
||||
if letter in self.letters:
|
||||
letter.letter += amount
|
||||
letter.letter %= len(self.alphabet)
|
||||
self.update_letter(letter)
|
||||
letter.show()
|
||||
|
||||
def update_letter(self, letter):
|
||||
"""Upare the image of a single letter
|
||||
|
||||
:param letter: letter to update
|
||||
"""
|
||||
letter.set_image(self.alphabet[letter.letter])
|
||||
|
||||
def get_selected_letter(self):
|
||||
"""Get the letter that has been selected"""
|
||||
if self.selection < len(self.letters):
|
||||
return self.letters[self.selection]
|
||||
|
||||
return self.button
|
||||
|
||||
def set_pos(self, pos):
|
||||
"""set the element's position
|
||||
|
||||
:param pos: position to move to
|
||||
"""
|
||||
pos_x, pos_y = pos
|
||||
offset_x = 0
|
||||
for letter in self.letters:
|
||||
letter.set_pos((pos_x + offset_x, pos_y))
|
||||
offset_x += letter.w + 1
|
||||
offset_x += Font.FONT_SIZE
|
||||
|
||||
self.button.set_pos((pos_x + offset_x, pos_y))
|
||||
|
||||
def update_position(self):
|
||||
"""Update the position of all the letters"""
|
||||
for letter in self.letters:
|
||||
letter.update_position()
|
||||
|
||||
def show(self):
|
||||
"""Make this object visible"""
|
||||
if self.hidden:
|
||||
for letter in self.letters:
|
||||
letter.show()
|
||||
letter.send_to_front()
|
||||
self.button.show()
|
||||
self.button.send_to_front()
|
||||
self.hidden = False
|
||||
|
||||
def hide(self):
|
||||
"""Make this object invisible"""
|
||||
if not self.hidden:
|
||||
for letter in self.letters:
|
||||
letter.hide()
|
||||
self.button.hide()
|
||||
self.hidden = True
|
||||
|
||||
def tick(self):
|
||||
"""Update the state of this object"""
|
||||
if not self.hidden:
|
||||
selected = self.get_selected_letter()
|
||||
for letter in self.letters + [self.button]:
|
||||
if self.game.alpha//15 == self.game.alpha / 15:
|
||||
if letter == selected:
|
||||
if (self.game.alpha//15) % 2 == 0:
|
||||
self.get_selected_letter().show()
|
||||
else:
|
||||
self.get_selected_letter().hide()
|
||||
else:
|
||||
letter.show()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
|
||||
class Leaderboard:
|
||||
"""Leaderboard object to display previous scores"""
|
||||
|
||||
ANIMATION_TIME = 5
|
||||
ANIMATION_DELAY = 5
|
||||
|
||||
def __init__(self, game: Game):
|
||||
"""Initialise the leaderboard
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
"""
|
||||
self.game = game
|
||||
self.file = LeaderboardFile()
|
||||
self.entries = []
|
||||
self.editing = True
|
||||
self.padding = 5
|
||||
|
||||
self.callback = (lambda: None)
|
||||
|
||||
self.hidden = True
|
||||
|
||||
self.game.inputs.add_keypress_handler(self.on_key)
|
||||
self.name_entry = NameEntry(self.game, self.submit_name)
|
||||
|
||||
self.blinking_sprite = None
|
||||
self.animation_start = -1
|
||||
|
||||
def populate_entries(self, blink_entry=("", 0)):
|
||||
"""Populate entries.
|
||||
|
||||
:param blink_entry:
|
||||
"""
|
||||
self.clear_entries()
|
||||
self.file.load_entries()
|
||||
|
||||
editing_area = 0
|
||||
if self.editing:
|
||||
editing_area = Font.FONT_SIZE + self.padding*2
|
||||
remaining_area = self.game.h - self.padding*2 - editing_area
|
||||
|
||||
to_fit = remaining_area // (Font.FONT_SIZE+self.padding) - 1
|
||||
|
||||
to_draw = self.file.entries[0:to_fit]
|
||||
|
||||
# create a row variable that is incremented for each entry
|
||||
y = self.padding
|
||||
|
||||
# create the title sprite and increment the row
|
||||
image = Font.load_text(self.game.texture_factory, "leaderboard")
|
||||
sprite = GameSprite(self.game, image)
|
||||
sprite.set_pos((0, y))
|
||||
self.entries.append(sprite)
|
||||
x = (self.game.w - sprite.w) // 2
|
||||
sprite.set_pos((x, y))
|
||||
|
||||
y += sprite.h + self.padding
|
||||
|
||||
# calculate the number of zeros to pad the score by
|
||||
zfill = ((self.game.w-self.padding*2) //
|
||||
(Font.FONT_SIZE+1)) - Config.NICK_LEN - 5
|
||||
for name, score in to_draw:
|
||||
text = f"{name} {str(score).zfill(zfill)}"
|
||||
x = self.padding
|
||||
image = Font.load_text(self.game.texture_factory, text)
|
||||
sprite = GameSprite(self.game, image)
|
||||
sprite.set_pos((x, y))
|
||||
|
||||
if (name, score) == blink_entry:
|
||||
self.blinking_sprite = sprite
|
||||
self.entries.append(sprite)
|
||||
|
||||
y += sprite.h + self.padding
|
||||
|
||||
if self.editing:
|
||||
self.name_entry.set_pos((self.padding, y+self.padding))
|
||||
self.name_entry.show()
|
||||
else:
|
||||
self.name_entry.hide()
|
||||
|
||||
def start_animation(self):
|
||||
"""Start the animation."""
|
||||
for e in self.entries:
|
||||
e.set_pos((-self.game.w, e.y))
|
||||
self.name_entry.hide()
|
||||
self.animation_start = self.game.alpha
|
||||
|
||||
def on_key(self, _):
|
||||
"""Handle Key press events
|
||||
|
||||
:param _: The key press event to handle
|
||||
"""
|
||||
inp = self.game.inputs
|
||||
if not self.hidden and inp.k_action and not self.editing:
|
||||
self.callback()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def submit_name(self, name):
|
||||
"""Submit a name to the leaderboard
|
||||
|
||||
:param name:
|
||||
"""
|
||||
score = self.game.score
|
||||
self.file.add_entry(name, score)
|
||||
self.file.save_entries()
|
||||
|
||||
self.editing = False
|
||||
self.name_entry.hide()
|
||||
|
||||
self.populate_entries(blink_entry=(name, score))
|
||||
self.start_animation()
|
||||
for e in self.entries:
|
||||
e.show()
|
||||
|
||||
def animate_sprite(self, sprite, i):
|
||||
"""Animate a single sprite.
|
||||
|
||||
:param sprite:
|
||||
:param i:
|
||||
"""
|
||||
alpha = self.game.alpha \
|
||||
- self.animation_start \
|
||||
- i*Leaderboard.ANIMATION_DELAY
|
||||
|
||||
if alpha <= Leaderboard.ANIMATION_TIME:
|
||||
if i == 0:
|
||||
# only title should be h aligned
|
||||
cx = (self.game.w - sprite.w) // 2
|
||||
else:
|
||||
cx = self.padding
|
||||
|
||||
x = (alpha/Leaderboard.ANIMATION_TIME) * \
|
||||
(cx+self.game.w//2) - self.game.w//2
|
||||
sprite.set_pos((x, sprite.y))
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
def tick(self):
|
||||
"""Update the leaderboard"""
|
||||
animation_complete = True
|
||||
for i, sprite in enumerate(self.entries):
|
||||
sprite.send_to_front()
|
||||
if not self.animate_sprite(sprite, i):
|
||||
animation_complete = False
|
||||
|
||||
if self.editing:
|
||||
animation_time = self.game.alpha \
|
||||
- self.animation_start \
|
||||
- len(self.entries)*Leaderboard.ANIMATION_DELAY
|
||||
|
||||
if animation_complete \
|
||||
and animation_time > Leaderboard.ANIMATION_TIME:
|
||||
self.name_entry.show()
|
||||
self.name_entry.tick()
|
||||
else:
|
||||
if self.blinking_sprite is not None:
|
||||
if (self.game.alpha//15) % 2 == 0:
|
||||
self.blinking_sprite.show()
|
||||
else:
|
||||
self.blinking_sprite.hide()
|
||||
|
||||
def show(self):
|
||||
"""Make this object visible"""
|
||||
if self.hidden:
|
||||
for m in self.entries:
|
||||
m.show()
|
||||
m.send_to_front()
|
||||
|
||||
self.hidden = False
|
||||
|
||||
def hide(self):
|
||||
"""Make this object invisible"""
|
||||
if not self.hidden:
|
||||
for m in self.entries:
|
||||
m.hide()
|
||||
self.name_entry.hide()
|
||||
self.hidden = True
|
||||
|
||||
def start_editing(self):
|
||||
"""Allow the user to input a name"""
|
||||
self.editing = True
|
||||
self.blinking_sprite = None
|
||||
|
||||
def clear_entries(self):
|
||||
"""Remove all the associated objects"""
|
||||
for entry in self.entries:
|
||||
entry.destroy()
|
||||
self.entries = []
|
||||
|
||||
|
||||
# test to add entries to game leaderboard
|
||||
if __name__ == "__main__":
|
||||
lb = LeaderboardFile()
|
||||
lb.load_entries()
|
||||
|
||||
for input_name, input_score in lb.entries:
|
||||
print(f"{input_name} {input_score}")
|
||||
|
||||
while True:
|
||||
input_name = input("Enter name or leave blank to exit: ")
|
||||
if input_name:
|
||||
input_score = input("enter score blank for random: ")
|
||||
if not input_score:
|
||||
input_score = randint(1, 999999)
|
||||
lb.add_entry(input_name, int(input_score))
|
||||
else:
|
||||
break
|
||||
|
||||
for input_name, input_score in lb.entries:
|
||||
print(f"{input_name} {input_score}")
|
||||
|
||||
lb.save_entries()
|
13
main.py
Executable file
13
main.py
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from shooter_game import ShooterGame
|
||||
|
||||
|
||||
def main():
|
||||
"""The entry function to the game"""
|
||||
game = ShooterGame()
|
||||
game.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
282
menu.py
Normal file
282
menu.py
Normal file
|
@ -0,0 +1,282 @@
|
|||
import re
|
||||
|
||||
from font import Font
|
||||
from game import Game, GameSprite
|
||||
from sprite import Sprite
|
||||
|
||||
|
||||
class MenuItem(GameSprite):
|
||||
"""A selectable item in a menu"""
|
||||
|
||||
def __init__(self, game: Game, text, callback):
|
||||
"""Initialise the item
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param text: Text to display for this item
|
||||
:param callback: function to call when this item is selected
|
||||
"""
|
||||
image = Font.load_text(game.texture_factory, text)
|
||||
self.text = text
|
||||
self.callback = callback
|
||||
super().__init__(game, image)
|
||||
|
||||
def set_text(self, text):
|
||||
"""Update the text of an entry
|
||||
|
||||
:param text:
|
||||
"""
|
||||
self.text = text
|
||||
image = Font.load_text(self.game.texture_factory, text)
|
||||
self.set_image(image)
|
||||
|
||||
|
||||
class Menu():
|
||||
"""Menu object with selectable entries"""
|
||||
|
||||
def __init__(self, game: Game, title) -> None:
|
||||
"""Initialise the menu object
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param title: The title of this menu
|
||||
"""
|
||||
self.game = game
|
||||
self.title = title
|
||||
self.padding = 5
|
||||
|
||||
self.game.inputs.add_keypress_handler(self.on_key)
|
||||
|
||||
self.menu_items = []
|
||||
|
||||
self.selection = 0
|
||||
|
||||
carret_image = Font.load_text(game.texture_factory, ">")
|
||||
self.carret = GameSprite(self.game, carret_image)
|
||||
|
||||
title_image = Font.load_text(game.texture_factory, title)
|
||||
|
||||
position = ((self.game.w - len(title)*Font.FONT_WIDTH)//2, 5*2)
|
||||
self.title = GameSprite(self.game, title_image)
|
||||
self.title.set_pos(position)
|
||||
|
||||
self.hidden = True
|
||||
|
||||
self.alpha = 0
|
||||
|
||||
def on_key(self, _):
|
||||
"""Handle Key press events
|
||||
|
||||
|
||||
:param event: The key press event to handle
|
||||
"""
|
||||
if not self.hidden:
|
||||
inp = self.game.inputs
|
||||
|
||||
if inp.k_down or inp.k_right:
|
||||
self.selection += 1
|
||||
|
||||
if inp.k_up or inp.k_left:
|
||||
self.selection -= 1
|
||||
|
||||
self.selection %= len(self.menu_items)
|
||||
self.update_carret()
|
||||
|
||||
if inp.k_action:
|
||||
self.menu_items[self.selection].callback()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_carret(self):
|
||||
"""Move the carret to the correct location"""
|
||||
selected = self.menu_items[self.selection]
|
||||
x = selected.x - Font.FONT_SIZE
|
||||
y = selected.y
|
||||
self.carret.set_pos((x, y))
|
||||
|
||||
def arrange_items(self):
|
||||
"""Move the menu items to their correct positions"""
|
||||
cy = self.game.h // 2
|
||||
|
||||
max_height = sum(item.h for item in self.menu_items
|
||||
) + self.padding*(len(self.menu_items)-1)
|
||||
top = cy - max_height//2
|
||||
|
||||
y = top
|
||||
|
||||
for item in self.menu_items:
|
||||
item_x = (self.game.w - item.w) // 2
|
||||
item_y = y
|
||||
y += item.h + self.padding
|
||||
item.set_pos((item_x, item_y))
|
||||
self.update_carret()
|
||||
|
||||
def add_item(self, text: str, callback, index=-1):
|
||||
"""Add a menu item
|
||||
|
||||
:param text: Label of this item
|
||||
:type text: str
|
||||
:param callback: Function to call when it is selected
|
||||
:param index: Where to insert this item
|
||||
"""
|
||||
if index == -1:
|
||||
index = len(self.menu_items)
|
||||
self.menu_items.insert(index, MenuItem(self.game, text, callback))
|
||||
self.arrange_items()
|
||||
|
||||
def show(self):
|
||||
"""Make this object visible"""
|
||||
if self.hidden:
|
||||
for m in self.menu_items:
|
||||
m.show()
|
||||
m.send_to_front()
|
||||
self.carret.show()
|
||||
self.carret.send_to_front()
|
||||
|
||||
self.title.show()
|
||||
self.title.send_to_front()
|
||||
|
||||
self.hidden = False
|
||||
|
||||
def hide(self):
|
||||
"""Make this object invisible"""
|
||||
if not self.hidden:
|
||||
for m in self.menu_items:
|
||||
m.hide()
|
||||
self.carret.hide()
|
||||
|
||||
self.title.hide()
|
||||
|
||||
self.hidden = True
|
||||
|
||||
def tick(self):
|
||||
"""Update this object"""
|
||||
self.alpha += 1
|
||||
if not self.hidden:
|
||||
if (self.alpha//15) % 2 == 0:
|
||||
self.carret.show()
|
||||
else:
|
||||
self.carret.hide()
|
||||
else:
|
||||
self.carret.hide()
|
||||
|
||||
def has_item(self, text):
|
||||
"""Return true if matching item is found
|
||||
|
||||
:param text: Label text to match
|
||||
"""
|
||||
for entry in self.menu_items:
|
||||
if entry.text == text:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_item(self, regex) -> MenuItem:
|
||||
"""Return an item that is matched
|
||||
|
||||
:param regex: regular expression to match item text to
|
||||
:rtype: MenuItem
|
||||
"""
|
||||
for entry in self.menu_items:
|
||||
if re.match(regex, entry.text):
|
||||
return entry
|
||||
|
||||
return self.menu_items[0]
|
||||
|
||||
def edit_item(self, regex, new_text):
|
||||
"""Edit the text of a menu item
|
||||
|
||||
:param regex: Regular expression to use to match the item's text to
|
||||
:param new_text: Text to replace to
|
||||
"""
|
||||
for entry in self.menu_items:
|
||||
if re.match(regex, entry.text):
|
||||
entry.set_text(new_text)
|
||||
|
||||
self.arrange_items()
|
||||
|
||||
def del_item(self, text):
|
||||
"""Remove an item
|
||||
|
||||
:param text: Label text to match
|
||||
"""
|
||||
for entry in self.menu_items:
|
||||
if entry.text == text:
|
||||
entry.destroy()
|
||||
self.menu_items = Sprite.remove_destroyed(self.menu_items)
|
||||
self.arrange_items()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class KeybindsMenu(Menu):
|
||||
"""A menu for selecting keybinds on"""
|
||||
|
||||
def __init__(self, game: Game, title):
|
||||
"""Initialise the menu
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param title: The title of this menu
|
||||
"""
|
||||
super().__init__(game, title)
|
||||
self.key_selecting = ""
|
||||
|
||||
image = Font.load_text(game.texture_factory, "press any key")
|
||||
self.press_key_sprite = GameSprite(self.game, image)
|
||||
self.press_key_sprite.set_pos(
|
||||
((self.game.w - self.press_key_sprite.w) // 2, self.game.h // 2))
|
||||
|
||||
def on_key(self, event):
|
||||
"""Handle Key press events
|
||||
|
||||
|
||||
:param event: The key press event to handle
|
||||
"""
|
||||
if self.key_selecting:
|
||||
key = event.keysym
|
||||
self.set_keybind(self.key_selecting, key)
|
||||
self.press_key_sprite.hide()
|
||||
self.show()
|
||||
return True
|
||||
return super().on_key(event)
|
||||
|
||||
def set_keybind(self, name, key):
|
||||
"""set_keybind.
|
||||
|
||||
:param name:
|
||||
:param key:
|
||||
"""
|
||||
setattr(self.game.inputs.settings, self.key_selecting, key)
|
||||
self.game.inputs.settings.save_inputs()
|
||||
self.key_selecting = ""
|
||||
self.edit_item(f"{name}\\s*<.*>", self.get_label(name, key))
|
||||
|
||||
def select_keybind(self, keyname):
|
||||
"""Allow the user to press a key to decide their keybind
|
||||
|
||||
:param keyname:
|
||||
"""
|
||||
self.hide()
|
||||
self.press_key_sprite.show()
|
||||
self.key_selecting = keyname
|
||||
|
||||
def get_set_keybind(self, keyname):
|
||||
"""Return a function that sets the keybind of a particular keyname
|
||||
|
||||
:param keyname:
|
||||
"""
|
||||
return lambda: self.select_keybind(keyname)
|
||||
|
||||
def get_label(self, name, value):
|
||||
"""Get a label for a keybind item
|
||||
|
||||
:param name:
|
||||
:param value:
|
||||
"""
|
||||
available_width = (self.game.w - Font.FONT_SIZE*2) // Font.FONT_WIDTH
|
||||
num_spaces = available_width - (len(name) + len(value) + 2) - 1
|
||||
spaces = " " * num_spaces
|
||||
return f"{name} {spaces}<{value}>"
|
88
shooter.py
Normal file
88
shooter.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from game import DamageableSprite, Game, GameSprite
|
||||
from sprite import Sprite
|
||||
|
||||
|
||||
class Lazer(GameSprite):
|
||||
"""Lazer object that is shot by a shooter"""
|
||||
|
||||
def __init__(self, game: Game, velocity=-4, color="white"):
|
||||
"""Initialise the lazer
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param velocity: Velocity to move the lazer at
|
||||
:param color: name of the colour of the lazer
|
||||
"""
|
||||
self.velocity = velocity
|
||||
self.game = game
|
||||
super().__init__(game, game.texture_factory.get_image(
|
||||
f"lazer:{color}"))
|
||||
|
||||
def tick(self):
|
||||
"""Update this object"""
|
||||
self.move(0, self.velocity)
|
||||
if self.y + self.h > self.game.h or self.y < 0:
|
||||
self.destroy()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShooterAttributes:
|
||||
"""Attributes for a shooter object"""
|
||||
|
||||
lazer_color: str = "white"
|
||||
cooldown: int = 40
|
||||
velocity: int = 1
|
||||
hp: int = 3
|
||||
|
||||
|
||||
class Shooter(DamageableSprite):
|
||||
"""A game object that is able to shoot lazers"""
|
||||
|
||||
def __init__(self, game: Game,
|
||||
image_name: str, attributes: ShooterAttributes):
|
||||
"""Initialise the shooter
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
:param image_name: The name of the image to use for this sprite
|
||||
:type image_name: str
|
||||
:param attributes: The attributes to use for this object
|
||||
:type attributes: ShooterAttributes
|
||||
"""
|
||||
super().__init__(game, image_name, hp=attributes.hp)
|
||||
self.lazers: List[Lazer] = []
|
||||
self.attributes = attributes
|
||||
self.last_shot = self.game.alpha
|
||||
|
||||
def shoot(self):
|
||||
"""Soot a lazer if possible"""
|
||||
next_shot = self.last_shot + self.attributes.cooldown
|
||||
|
||||
if not self.destroyed \
|
||||
and self.game.alpha > next_shot:
|
||||
self.last_shot = self.game.alpha
|
||||
|
||||
lazer = Lazer(self.game,
|
||||
velocity=self.attributes.velocity,
|
||||
color=self.attributes.lazer_color)
|
||||
lazer.set_pos((self.x + self.w//2 - 1, self.y +
|
||||
self.h//2 - 1))
|
||||
lazer.show()
|
||||
self.lazers.append(lazer)
|
||||
|
||||
def tick(self):
|
||||
"""Update this object"""
|
||||
super().tick()
|
||||
for lazer in self.lazers:
|
||||
lazer.tick()
|
||||
|
||||
self.lazers = Sprite.remove_destroyed(self.lazers)
|
||||
|
||||
def destroy(self):
|
||||
"""Remove all the associated objects"""
|
||||
super().destroy()
|
||||
for lazer in self.lazers:
|
||||
self.game.sprites.append(lazer)
|
363
shooter_game.py
Normal file
363
shooter_game.py
Normal file
|
@ -0,0 +1,363 @@
|
|||
from enum import Enum, auto
|
||||
from os import path, remove
|
||||
from random import random
|
||||
|
||||
from boss_key import BossKey
|
||||
from cheat_engine import Cheat, CheatEngine, DevModeCheat, InvincibilityCheat
|
||||
from config import Config
|
||||
from formation_spawner import FormationSpawner
|
||||
from game import Game
|
||||
from hud import GameHud
|
||||
from leaderboard import Leaderboard
|
||||
from menu import KeybindsMenu, Menu
|
||||
from shooter import Shooter, ShooterAttributes
|
||||
from textures import Textures
|
||||
|
||||
|
||||
class GameState(Enum):
|
||||
"""Enum of possible game states"""
|
||||
|
||||
MAIN_MENU = auto()
|
||||
GAME = auto()
|
||||
PAUSED = auto()
|
||||
END_LEADERBOARD = auto()
|
||||
LEADERBOARD = auto()
|
||||
SETTINGS = auto()
|
||||
|
||||
|
||||
class GameSave:
|
||||
"""Static class for saving and loading game"""
|
||||
|
||||
@staticmethod
|
||||
def save_game(game):
|
||||
"""Save game state to a file
|
||||
|
||||
:param game: Game to save
|
||||
"""
|
||||
|
||||
phase = int.to_bytes(game.formation_spawner.phase, 2, "big")
|
||||
hp = int.to_bytes(game.player.hp, 1, "big")
|
||||
score = int.to_bytes(game.score, 8, "big")
|
||||
|
||||
with open(Config.SAVE_FILE, "wb") as file:
|
||||
file.write(phase)
|
||||
file.write(hp)
|
||||
file.write(score)
|
||||
|
||||
if not game.menu.has_item("Continue"):
|
||||
game.menu.add_item("Continue", game.restore_game, index=0)
|
||||
|
||||
@staticmethod
|
||||
def load_game(game):
|
||||
"""load game state from file
|
||||
|
||||
:param game: Game to load
|
||||
"""
|
||||
with open(Config.SAVE_FILE, "rb") as file:
|
||||
game.formation_spawner.phase = int.from_bytes(file.read(2), "big")
|
||||
game.player.hp = int.from_bytes(file.read(1), "big")
|
||||
game.score = int.from_bytes(file.read(8), "big")
|
||||
|
||||
@staticmethod
|
||||
def remove_save(game):
|
||||
"""Remove the game save file
|
||||
|
||||
:param game:
|
||||
"""
|
||||
if path.exists(Config.SAVE_FILE):
|
||||
remove(Config.SAVE_FILE)
|
||||
if game.menu.has_item("Continue"):
|
||||
game.menu.del_item("Continue")
|
||||
|
||||
|
||||
class Player(Shooter):
|
||||
"""Controllable player object"""
|
||||
|
||||
def __init__(self, game: Game):
|
||||
"""Initialise the player
|
||||
|
||||
:param game: The game which this belongs to
|
||||
:type game: Game
|
||||
"""
|
||||
attributes = ShooterAttributes(
|
||||
cooldown=12,
|
||||
velocity=-2,
|
||||
hp=10
|
||||
)
|
||||
|
||||
super().__init__(game, "ship", attributes)
|
||||
self.set_pos(
|
||||
((self.game.w - self.w) // 2, (self.game.h - self.h)))
|
||||
|
||||
def tick(self):
|
||||
"""Update this object"""
|
||||
super().tick()
|
||||
if self.game.inputs.k_left:
|
||||
self.move(-1, 0)
|
||||
if self.game.inputs.k_right:
|
||||
self.move(1, 0)
|
||||
if self.game.inputs.k_up:
|
||||
self.move(0, -1)
|
||||
if self.game.inputs.k_down:
|
||||
self.move(0, 1)
|
||||
|
||||
# clamp the player to the screen
|
||||
if self.x < 0:
|
||||
self.set_pos((0, self.y))
|
||||
if self.y < 0:
|
||||
self.set_pos((self.x, 0))
|
||||
if self.x + self.w > self.game.w:
|
||||
self.set_pos((self.game.w - self.w, self.y))
|
||||
if self.y + self.h > self.game.h:
|
||||
self.set_pos((self.x, self.game.h - self.h))
|
||||
|
||||
if self.game.inputs.k_action:
|
||||
self.shoot()
|
||||
|
||||
|
||||
class ShooterGame(Game):
|
||||
"""Game with menus and enemies to be shot at """
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise the game"""
|
||||
super().__init__()
|
||||
|
||||
self.state = GameState.MAIN_MENU
|
||||
self.death_time = -1
|
||||
self.paused_frame = 0
|
||||
|
||||
# load textures
|
||||
Textures.load_textures(self.texture_factory)
|
||||
self.effect_player.load_textures()
|
||||
self.effect_player.create_stars()
|
||||
|
||||
self.state = GameState.MAIN_MENU
|
||||
|
||||
self.formation_spawner = FormationSpawner(self)
|
||||
|
||||
self.player = Player(self)
|
||||
|
||||
# make the game hud last to make sure its ontop
|
||||
self.game_hud = GameHud(self)
|
||||
|
||||
# create the leaderboard sprites
|
||||
self.leaderboard = Leaderboard(self)
|
||||
self.leaderboard.callback = self.show_menu
|
||||
|
||||
# make the settings menu
|
||||
self.settings_menu = KeybindsMenu(self, "Keybinds")
|
||||
for name, value in vars(self.inputs.settings).items():
|
||||
label = self.settings_menu.get_label(name, value)
|
||||
self.settings_menu.add_item(
|
||||
label,
|
||||
self.settings_menu.get_set_keybind(name)
|
||||
)
|
||||
self.settings_menu.add_item("Return", self.show_menu)
|
||||
|
||||
# make the main menu
|
||||
self.menu = Menu(self, "Main Menu")
|
||||
if path.exists(Config.SAVE_FILE):
|
||||
self.menu.add_item("Continue", self.restore_game)
|
||||
self.menu.add_item("New Game", self.start_game)
|
||||
self.menu.add_item("Leaderboard", self.show_leaderboard)
|
||||
self.menu.add_item("Settings", self.show_settings)
|
||||
self.menu.show()
|
||||
|
||||
# make the pause menu
|
||||
self.pause_menu = Menu(self, "Game Paused")
|
||||
self.pause_menu.add_item("Resume", self.resume_game)
|
||||
|
||||
self.pause_menu.add_item("Save", lambda: (
|
||||
self.save_game(),
|
||||
self.effect_player.splash_text("Game saved"),
|
||||
self.resume_game())
|
||||
)
|
||||
|
||||
self.pause_menu.add_item("Exit", self.show_menu)
|
||||
|
||||
# initialise cheats
|
||||
self.cheat_engine = CheatEngine(self)
|
||||
self.cheat_engine.add_cheat(
|
||||
Cheat(self, list("test"),
|
||||
(lambda: self.effect_player.splash_text("test ok"))
|
||||
))
|
||||
|
||||
self.cheat_engine.add_cheat(
|
||||
Cheat(self, [
|
||||
"Up",
|
||||
"Up",
|
||||
"Down",
|
||||
"Down",
|
||||
"Left",
|
||||
"Right",
|
||||
"Left",
|
||||
"Right",
|
||||
"b",
|
||||
"a",
|
||||
],
|
||||
(lambda: [self.formation_spawner.spawn_rectangle()
|
||||
for _ in range(20)])
|
||||
))
|
||||
|
||||
self.cheat_engine.add_cheat(DevModeCheat(self, [
|
||||
"Left",
|
||||
"Right",
|
||||
"Left",
|
||||
"Right",
|
||||
"Escape",
|
||||
"d",
|
||||
"Up",
|
||||
"t",
|
||||
"b",
|
||||
"b",
|
||||
"a",
|
||||
"b",
|
||||
"s"
|
||||
]))
|
||||
|
||||
self.cheat_engine.add_cheat(InvincibilityCheat(self, list("xyzzy")))
|
||||
|
||||
self.boss_key = BossKey(self, self.pause_game)
|
||||
|
||||
def tick(self):
|
||||
"""Update the game state"""
|
||||
if self.state != GameState.PAUSED:
|
||||
super().tick()
|
||||
|
||||
if random() > 0.9:
|
||||
self.effect_player.create_star()
|
||||
|
||||
if self.state == GameState.MAIN_MENU:
|
||||
self.menu.tick()
|
||||
elif self.state == GameState.SETTINGS:
|
||||
self.settings_menu.tick()
|
||||
elif self.state == GameState.GAME:
|
||||
self.tick_game()
|
||||
elif self.state == GameState.PAUSED:
|
||||
self.alpha = self.paused_frame
|
||||
self.pause_menu.tick()
|
||||
elif self.state == GameState.END_LEADERBOARD:
|
||||
self.leaderboard.tick()
|
||||
elif self.state == GameState.LEADERBOARD:
|
||||
self.leaderboard.tick()
|
||||
|
||||
def tick_game(self):
|
||||
"""Update the game during game play"""
|
||||
self.game_hud.tick()
|
||||
self.formation_spawner.tick()
|
||||
self.player.tick()
|
||||
|
||||
if self.player.destroyed:
|
||||
if self.death_time == -1:
|
||||
self.death_time = self.alpha
|
||||
self.effect_player.splash_text("GAME OVER", 100)
|
||||
elif self.alpha - self.death_time > 100:
|
||||
self.show_score()
|
||||
|
||||
if self.inputs.k_pause:
|
||||
self.pause_game()
|
||||
|
||||
def pause_game(self):
|
||||
"""Set the game to paused state"""
|
||||
if self.state == GameState.GAME:
|
||||
self.state = GameState.PAUSED
|
||||
|
||||
self.paused_frame = self.alpha
|
||||
self.pause_menu.show()
|
||||
|
||||
def resume_game(self):
|
||||
"""Resume the game from paused state"""
|
||||
self.state = GameState.GAME
|
||||
|
||||
self.pause_menu.hide()
|
||||
|
||||
def start_game(self):
|
||||
"""Start a new game"""
|
||||
self.state = GameState.GAME
|
||||
|
||||
GameSave.remove_save(self)
|
||||
|
||||
self.menu.hide()
|
||||
self.pause_menu.hide()
|
||||
self.formation_spawner.phase = -1
|
||||
self.clear_all()
|
||||
|
||||
self.score = 0
|
||||
self.player = Player(self)
|
||||
|
||||
self.formation_spawner.next_phase()
|
||||
|
||||
self.player.show()
|
||||
self.game_hud.show()
|
||||
|
||||
self.death_time = -1
|
||||
|
||||
def show_leaderboard(self):
|
||||
"""Show the game's leaderboard"""
|
||||
self.state = GameState.LEADERBOARD
|
||||
|
||||
self.menu.hide()
|
||||
self.pause_menu.hide()
|
||||
self.leaderboard.editing = False
|
||||
self.leaderboard.populate_entries()
|
||||
self.leaderboard.start_animation()
|
||||
|
||||
self.leaderboard.show()
|
||||
|
||||
def show_score(self):
|
||||
"""Allow the user to enter their name into the leaderboard"""
|
||||
self.state = GameState.END_LEADERBOARD
|
||||
|
||||
self.clear_all()
|
||||
self.game_hud.hide()
|
||||
self.leaderboard.editing = True
|
||||
self.leaderboard.populate_entries()
|
||||
self.leaderboard.start_animation()
|
||||
self.leaderboard.show()
|
||||
GameSave.remove_save(self)
|
||||
|
||||
def show_menu(self):
|
||||
"""Show the main menu"""
|
||||
self.state = GameState.MAIN_MENU
|
||||
|
||||
self.clear_all()
|
||||
self.leaderboard.hide()
|
||||
self.game_hud.hide()
|
||||
self.player.hide()
|
||||
self.pause_menu.hide()
|
||||
self.settings_menu.hide()
|
||||
|
||||
self.menu.show()
|
||||
|
||||
def clear_all(self):
|
||||
"""Remove all the associated game objects"""
|
||||
self.formation_spawner.clear_all()
|
||||
self.player.destroy()
|
||||
|
||||
def restore_game(self):
|
||||
"""Restore the game's state from file"""
|
||||
self.state = GameState.GAME
|
||||
|
||||
self.menu.hide()
|
||||
self.pause_menu.hide()
|
||||
self.clear_all()
|
||||
|
||||
self.player = Player(self)
|
||||
self.death_time = -1
|
||||
|
||||
GameSave.load_game(self)
|
||||
|
||||
self.formation_spawner.start_phase()
|
||||
self.game_hud.show()
|
||||
self.player.show()
|
||||
|
||||
def save_game(self):
|
||||
"""Save the game's state to a file"""
|
||||
GameSave.save_game(self)
|
||||
|
||||
def show_settings(self):
|
||||
"""Show the keybind setting menu"""
|
||||
self.state = GameState.SETTINGS
|
||||
|
||||
self.menu.hide()
|
||||
self.settings_menu.show()
|
127
sprite.py
Normal file
127
sprite.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
from tkinter import Canvas, NW, PhotoImage
|
||||
|
||||
from config import Config
|
||||
|
||||
|
||||
class Sprite:
|
||||
"""Sprite."""
|
||||
|
||||
@staticmethod
|
||||
def remove_destroyed(sprite_list):
|
||||
"""Remove all destroyed sprites from a list
|
||||
|
||||
:param sprite_list:
|
||||
:type sprite_list: list[Sprite]
|
||||
"""
|
||||
return list(filter(lambda s: not s.destroyed, sprite_list))
|
||||
|
||||
def __init__(self, canvas: Canvas, image: PhotoImage, position=(0, 0)):
|
||||
"""Initialise the sprite class
|
||||
|
||||
:param canvas: The canvas to draw the sprites to
|
||||
:type canvas: Canvas
|
||||
:param image: The image to be used for the sprite
|
||||
:type image: PhotoImage
|
||||
:param position: The default position to place the sprite
|
||||
"""
|
||||
# set positions
|
||||
self.x, self.y = position
|
||||
|
||||
self.canvas = canvas
|
||||
self.canvas_image = canvas.create_image(
|
||||
self.x * Config.SCALE, self.y * Config.SCALE,
|
||||
anchor=NW, image=image, state="hidden")
|
||||
|
||||
# get pixel width and heigh ignoring scale
|
||||
self.w = image.width() // Config.SCALE
|
||||
self.h = image.height() // Config.SCALE
|
||||
|
||||
self.destroyed = False
|
||||
self.hide()
|
||||
|
||||
def update_position(self):
|
||||
"""Move the image to the sprites position"""
|
||||
self.canvas.coords(self.canvas_image, self.x *
|
||||
Config.SCALE, self.y*Config.SCALE)
|
||||
|
||||
def set_pos(self, pos):
|
||||
"""Set the player position
|
||||
|
||||
:param pos: Position to move to
|
||||
"""
|
||||
self.x, self.y = pos
|
||||
self.update_position()
|
||||
|
||||
def get_pos(self):
|
||||
"""Return the current position of the sprite"""
|
||||
return (self.x, self.y)
|
||||
|
||||
def move(self, x, y):
|
||||
"""Move the sprite by x and y
|
||||
|
||||
:param x: the number of pixels right to move
|
||||
:param y: the number of pixels down to move
|
||||
"""
|
||||
self.x += x
|
||||
self.y += y
|
||||
self.update_position()
|
||||
|
||||
def collides(self, other):
|
||||
"""Check if the sprite collides with another sprite
|
||||
|
||||
:param other: The other sprite
|
||||
"""
|
||||
return self.x < other.x + other.w \
|
||||
and self.x + self.w > other.x \
|
||||
and self.y < other.y + other.h \
|
||||
and self.h + self.y > other.y
|
||||
|
||||
def collide_all(self, others):
|
||||
"""Check if the sprite collides with a list of sprites
|
||||
|
||||
:param others: Array of other sprites to check if collides with
|
||||
:returns: index of the sprite that it collided with first
|
||||
or -1 if not colliding
|
||||
"""
|
||||
for i, other in enumerate(others):
|
||||
if self.collides(other):
|
||||
return i
|
||||
return -1
|
||||
|
||||
def tick(self):
|
||||
"""Update the sprite"""
|
||||
|
||||
def destroy(self):
|
||||
"""Remove the image from the canvas"""
|
||||
self.canvas.delete(self.canvas_image)
|
||||
self.destroyed = True
|
||||
|
||||
def send_to_front(self):
|
||||
"""Move the sprite to the foreground"""
|
||||
self.canvas.tag_raise(self.canvas_image)
|
||||
|
||||
def send_to_back(self):
|
||||
"""Move the sprite to the background"""
|
||||
self.canvas.tag_lower(self.canvas_image)
|
||||
|
||||
def set_image(self, image: PhotoImage):
|
||||
"""Change the image used by the sprite
|
||||
|
||||
:param image: the image to set the sprite to
|
||||
:type image: PhotoImage
|
||||
"""
|
||||
self.canvas.itemconfig(self.canvas_image, image=image)
|
||||
|
||||
def show(self):
|
||||
"""Set the sprite to be shown"""
|
||||
self.canvas.itemconfig(self.canvas_image, state="normal")
|
||||
return self
|
||||
|
||||
def hide(self):
|
||||
"""Set the sprite to be hidden"""
|
||||
self.canvas.itemconfig(self.canvas_image, state="hidden")
|
||||
return self
|
||||
|
||||
def is_hidden(self):
|
||||
"""Return True if the sprite is hidden"""
|
||||
return self.canvas.itemcget(self.canvas_image, "state") == "hidden"
|
406
textures.py
Normal file
406
textures.py
Normal file
|
@ -0,0 +1,406 @@
|
|||
from tkinter import PhotoImage
|
||||
|
||||
|
||||
# tell pylint to ignore long lines in this file, since they make more sense
|
||||
# to not be linewrapped
|
||||
#
|
||||
# ignore a particular guideline "when applying the guideline would make the code less readable"
|
||||
# https://peps.python.org/pep-0008
|
||||
#
|
||||
# pylint: disable=line-too-long
|
||||
class Textures:
|
||||
"""Static class containing game textures"""
|
||||
STAR = [
|
||||
["#AAAAAA"]
|
||||
]
|
||||
|
||||
ENEMY = [
|
||||
[
|
||||
[None, "#00E436", "#008751", "#008751", None, None, None, ],
|
||||
["#00E436", "#00E436", "#00E436", None, None, "#008751", None, ],
|
||||
["#008751", "#00E436", "#00E436", None, None, "#00E436", "#008751",],
|
||||
[None, "#008751", "#008751", None, "#00E436", "#00E436", "#008751",],
|
||||
[None, None, None, "#00E436", "#00E436", "#00E436", "#00E436",],
|
||||
[None, None, "#00E436", "#00E436", "#1D2B53", "#00E436", "#008751",],
|
||||
[None, None, "#008751", "#00E436", "#1D2B53", "#1D2B53", "#00E436",],
|
||||
[None, None, "#008751", "#008751", "#00E436", "#FF004D", "#00E436",],
|
||||
[None, "#1D2B53", None, "#008751", "#008751", "#00E436", "#00E436",],
|
||||
[None, "#008751", "#00E436", None, "#008751", "#1D2B53", "#1D2B53",],
|
||||
["#008751", "#00E436", "#00E436", "#00E436", None, "#1D2B53", "#008751",],
|
||||
["#00E436", "#00E436", "#00E436", None, None, None, None, ],
|
||||
["#008751", "#00E436", "#00E436", "#00E436", "#008751", None, None, ],
|
||||
["#1D2B53", "#008751", "#00E436", "#008751", None, None, None, ],
|
||||
],
|
||||
[
|
||||
[None, None, "#C2C3C7", "#1D2B53", None, None, None, ],
|
||||
[None, None, "#83769C", "#C2C3C7", "#1D2B53", None, None, ],
|
||||
[None, None, None, "#83769C", "#C2C3C7", "#83769C", "#C2C3C7",],
|
||||
["#83769C", "#1D2B53", None, None, "#83769C", "#C2C3C7", "#C2C3C7",],
|
||||
["#83769C", "#83769C", "#83769C", "#1D2B53", "#C2C3C7", "#83769C", "#C2C3C7",],
|
||||
["#1D2B53", "#C2C3C7", "#83769C", "#83769C", "#83769C", "#7E2553", "#1D2B53",],
|
||||
["#C2C3C7", "#C2C3C7", "#1D2B53", None, "#C2C3C7", "#7E2553", "#7E2553",],
|
||||
["#C2C3C7", "#C2C3C7", None, None, "#83769C", "#C2C3C7", "#C2C3C7",],
|
||||
["#C2C3C7", "#C2C3C7", "#C2C3C7", "#83769C", "#1D2B53", None, None, ],
|
||||
["#83769C", "#C2C3C7", "#C2C3C7", "#83769C", None, None, None, ],
|
||||
["#1D2B53", "#C2C3C7", "#83769C", "#1D2B53", None, None, None, ],
|
||||
[None, "#C2C3C7", "#83769C", "#83769C", None, None, None, ],
|
||||
[None, "#1D2B53", "#83769C", "#1D2B53", None, None, None, ],
|
||||
],
|
||||
[
|
||||
[None, None, None, None, "#7E2553", "#FFA300", "#FFA300",],
|
||||
[None, None, "#7E2553", "#FFA300", "#FFA300", "#FFA300", "#FFEC27",],
|
||||
[None, "#7E2553", "#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27",],
|
||||
[None, "#FFA300", "#FFA300", "#FFEC27", "#FFA300", "#FFEC27", "#FFEC27",],
|
||||
["#7E2553", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFA300", "#FFA300",],
|
||||
["#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27",],
|
||||
["#AB5236", "#AB5236", "#000000", "#000000", "#FFA300", "#FFEC27", "#FFEC27",],
|
||||
["#AB5236", "#AB5236", "#1D2B53", "#FF004D", "#000000", "#FFA300", "#FFEC27",],
|
||||
["#AB5236", "#FFA300", "#AB5236", "#1D2B53", "#1D2B53", "#AB5236", "#AB5236",],
|
||||
["#7E2553", "#AB5236", "#FFA300", "#AB5236", "#AB5236", "#FFA300", "#FFA300",],
|
||||
["#7E2553", "#7E2553", "#AB5236", "#AB5236", "#AB5236", "#AB5236", "#AB5236",],
|
||||
[None, "#7E2553", "#7E2553", "#7E2553", None, None, None, ],
|
||||
]
|
||||
]
|
||||
|
||||
ROCK1 = [
|
||||
[None, None, "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#1D2B53", None, None, None, None, ],
|
||||
[None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, None, None, ],
|
||||
["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, None, ],
|
||||
["#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", None, ],
|
||||
["#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", None, ],
|
||||
["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
[None, "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
|
||||
[None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
|
||||
[None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
|
||||
[None, None, None, None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
|
||||
]
|
||||
|
||||
ROCK2 = [
|
||||
[None, None, None, None, "#FFA300", "#FFA300", "#5F574F", "#FFA300", "#5F574F", None, None, None, ],
|
||||
[None, None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53", None, ],
|
||||
[None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
[None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#FFA300", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
[None, None, "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
|
||||
[None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, ],
|
||||
]
|
||||
|
||||
ROCK3 = [
|
||||
[None, None, None, None, None, None, None, None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", None, None, ],
|
||||
[None, None, None, None, "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, ],
|
||||
[None, "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53",],
|
||||
["#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
|
||||
["#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
|
||||
["#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, None, None, None, None, None, None, ],
|
||||
[None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, None, None, None, None, None, None, None, None, ],
|
||||
]
|
||||
|
||||
ROCK4 = [
|
||||
[None, None, "#5F574F", "#FFA300", "#FFA300", "#5F574F", None, ],
|
||||
[None, "#5F574F", "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
|
||||
["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", None, ],
|
||||
["#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
|
||||
[None, "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, ],
|
||||
]
|
||||
|
||||
ROCK5 = [
|
||||
[None, "#FFA300", "#5F574F", "#5F574F", "#1D2B53", None, ],
|
||||
["#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
["#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, ],
|
||||
[None, "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
|
||||
]
|
||||
|
||||
SHIP = [
|
||||
[None, None, None, None, None, None, None, None, ],
|
||||
[None, None, None, "#83769C", "#83769C", None, None, None, ],
|
||||
[None, "#83769C", "#1D2B53", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", None, ],
|
||||
["#83769C", "#83769C", "#7E2553", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", "#5F574F",],
|
||||
["#83769C", "#C2C3C7", "#83769C", "#29ADFF", "#29ADFF", "#1D2B53", "#83769C", "#5F574F",],
|
||||
["#83769C", "#C2C3C7", "#83769C", "#5F574F", "#1D2B53", "#1D2B53", "#83769C", "#5F574F",],
|
||||
["#1D2B53", "#83769C", "#5F574F", "#83769C", "#83769C", "#5F574F", "#5F574F", "#1D2B53",],
|
||||
[None, "#1D2B53", "#1D2B53", None, None, "#1D2B53", "#1D2B53", None, ],
|
||||
]
|
||||
|
||||
UFO = [
|
||||
[None, None, None, None, None, "#1D2B53", "#29ADFF", "#FFF1E8",],
|
||||
[None, None, None, None, "#1D2B53", "#29ADFF", "#FFF1E8", "#FFF1E8",],
|
||||
[None, None, None, "#1D2B53", "#29ADFF", "#29ADFF", "#29ADFF", "#29ADFF",],
|
||||
[None, "#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7",],
|
||||
["#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7", "#C2C3C7",],
|
||||
[None, "#7E2553", "#FF004D", "#7E2553", None, None, "#7E2553", "#FF004D",],
|
||||
]
|
||||
|
||||
LAZER = [
|
||||
["#8F8F8F"],
|
||||
["#F8F8F8"],
|
||||
["#F8F8F8"],
|
||||
["#F8F8F8"],
|
||||
["#F8F8F8"],
|
||||
["#8F8F8F"]
|
||||
]
|
||||
|
||||
SMALLENEMY =[
|
||||
[
|
||||
[None, "#00E436", "#008751", None, ],
|
||||
["#00E436", "#008751", None, None, ],
|
||||
["#008751", "#008751", "#00E436", "#00E436",],
|
||||
[None, "#00E436", "#1D2B53", "#00E436",],
|
||||
[None, "#00E436", "#FF004D", "#00E436",],
|
||||
["#008751", "#00E436", "#008751", "#008751",],
|
||||
["#00E436", "#1D2B53", None, None, ],
|
||||
["#008751", "#00E436", "#00E436", None, ],
|
||||
],
|
||||
[
|
||||
[None, "#7E2553", "#FFA300",],
|
||||
["#7E2553", "#FFA300", "#FFEC27",],
|
||||
["#AB5236", "#FFA300", "#FFA300",],
|
||||
["#AB5236", "#1D2B53", "#AB5236",],
|
||||
["#7E2553", "#AB5236", "#FFA300",],
|
||||
[None, None, None, ],
|
||||
["#FFA300", "#AB5236", None, ],
|
||||
["#AB5236", "#7E2553", None, ],
|
||||
],
|
||||
[
|
||||
["#FF004D", None, "#7E2553", "#FF004D",],
|
||||
["#7E2553", "#7E2553", "#FF004D", "#FF77A8",],
|
||||
[None, "#FF004D", "#FF004D", "#FF004D",],
|
||||
[None, "#7E2553", None, "#7E2553",],
|
||||
["#7E2553", "#FF004D", "#1D2B53", "#7E2553",],
|
||||
["#FF004D", "#FF004D", None, "#FF004D",],
|
||||
["#FF004D", "#7E2553", None, None, ],
|
||||
["#7E2553", "#FF004D", "#7E2553", None, ],
|
||||
],
|
||||
[
|
||||
[None, "#1D2B53", "#008751",],
|
||||
[None, "#008751", "#00E436",],
|
||||
["#1D2B53", "#008751", "#00E436",],
|
||||
["#1D2B53", "#7E2553", "#008751",],
|
||||
[None, "#1D2B53", "#008751",],
|
||||
["#008751", "#1D2B53", None, ],
|
||||
["#008751", None, None, ],
|
||||
["#1D2B53", "#008751", None, ],
|
||||
],
|
||||
[
|
||||
[None, None, "#FFA300", "#FFEC27",],
|
||||
[None, "#FFA300", "#FFEC27", "#FFEC27",],
|
||||
["#AB5236", "#FFA300", "#1D2B53", "#FFA300",],
|
||||
["#FFA300", "#FFEC27", "#FFA300", "#FFEC27",],
|
||||
["#FFEC27", "#FFA300", None, "#FFEC27",],
|
||||
["#FFA300", None, None, None, ],
|
||||
["#AB5236", "#FFA300", None, "#AB5236",],
|
||||
[None, "#AB5236", None, None, ],
|
||||
],
|
||||
[
|
||||
[None, None, "#7E2553", "#FF77A8",],
|
||||
[None, "#7E2553", "#FF77A8", "#FFCCAA",],
|
||||
[None, "#FF77A8", "#FFCCAA", "#FFCCAA",],
|
||||
[None, "#FFCCAA", "#1D2B53", "#FF77A8",],
|
||||
[None, "#7E2553", "#FF77A8", "#FFCCAA",],
|
||||
[None, "#FF77A8", "#1D2B53", "#FFCCAA",],
|
||||
[None, "#FF77A8", "#1D2B53", "#FF77A8",],
|
||||
[None, "#7E2553", "#FF77A8", "#7E2553",],
|
||||
],
|
||||
[
|
||||
["#1D2B53", "#83769C", "#1D2B53", None, ],
|
||||
[None, "#1D2B53", "#C2C3C7", None, ],
|
||||
["#83769C", "#1D2B53", "#83769C", "#C2C3C7",],
|
||||
["#C2C3C7", "#83769C", "#C2C3C7", "#C2C3C7",],
|
||||
["#C2C3C7", "#1D2B53", "#7E2553", "#83769C",],
|
||||
["#C2C3C7", "#1D2B53", "#C2C3C7", "#C2C3C7",],
|
||||
["#1D2B53", "#83769C", "#1D2B53", None, ],
|
||||
[None, "#1D2B53", "#83769C", None, ],
|
||||
],
|
||||
[
|
||||
["#1D2B53", None, None, ],
|
||||
["#29ADFF", None, None, ],
|
||||
["#29ADFF", "#1D2B53", None, ],
|
||||
["#1D2B53", "#29ADFF", "#1D2B53",],
|
||||
["#29ADFF", None, "#29ADFF",],
|
||||
["#29ADFF", "#29ADFF", None, ],
|
||||
["#29ADFF", "#29ADFF", "#1D2B53",],
|
||||
["#29ADFF", "#1D2B53", None, ],
|
||||
]
|
||||
]
|
||||
|
||||
EXPLOSION =[
|
||||
[
|
||||
["#FFEC27", "#FFEC27"],
|
||||
["#FFF1E8", "#FFEC27"],
|
||||
["#7E2553", "#7E2553"]
|
||||
],
|
||||
[
|
||||
["#7E2553", "#FFEC27"],
|
||||
["#FFEC27", "#FFF1E8"],
|
||||
],
|
||||
[
|
||||
[ None, "#7E2553", "#FFEC27", "#FFEC27"],
|
||||
[ "#7E2553", "#FFEC27", "#FFEC27", "#FFF1E8"],
|
||||
[ "#FFEC27", "#FFEC27", "#FFF1E8", "#FFF1E8"],
|
||||
[ "#FFEC27", "#FFF1E8", "#FFF1E8", "#FFF1E8"],
|
||||
]
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def hmirror_texture(texture):
|
||||
"""Horizontally mirror a texture
|
||||
|
||||
:param texture: texture to mirror
|
||||
"""
|
||||
return [(row + row[::-1]) for row in texture]
|
||||
|
||||
@staticmethod
|
||||
def vmirror_texture(texture):
|
||||
"""Vertically mirror a texture
|
||||
|
||||
:param texture: texture to mirror
|
||||
"""
|
||||
return texture + texture[::-1]
|
||||
|
||||
@staticmethod
|
||||
def recolor(texture, color):
|
||||
"""recolor a texture
|
||||
|
||||
:param texture: texture to recolor
|
||||
:param color: Color to multiply the texture with
|
||||
"""
|
||||
return [[None if col is None else Textures.multiply_colors(col, color) for col in row] for row in texture]
|
||||
|
||||
@staticmethod
|
||||
def multiply_colors(hex1, hex2):
|
||||
"""Multiply two RGB colours
|
||||
|
||||
:param hex1: first colour
|
||||
:param hex2: second colour
|
||||
"""
|
||||
color1 = Textures.hex_to_rgb(hex1)
|
||||
color2 = Textures.hex_to_rgb(hex2)
|
||||
return Textures.rgb_to_hex([color1[i] * color2[i] for i in range(len(color1))])
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(value):
|
||||
"""Convert a hexadecimal colour value to red green and blue
|
||||
|
||||
:param value: hex value
|
||||
"""
|
||||
value = value.lstrip('#')
|
||||
length = len(value)
|
||||
return tuple(int(value[i:i + length // 3], 16)/256 for i in range(0, length, length // 3))
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_hex(value):
|
||||
"""Convert red green and blue value to a hexadecimal representation
|
||||
|
||||
:param value: RGB value
|
||||
"""
|
||||
return "#" + "".join(f"{int(v*256):02X}" for v in value)
|
||||
|
||||
@staticmethod
|
||||
def white_texture(texture):
|
||||
"""Replace all coloured pixels with white
|
||||
|
||||
:param texture: Texture to replace on
|
||||
"""
|
||||
return [[None if col is None else "#FFFFFF" for col in row] for row in texture]
|
||||
|
||||
@staticmethod
|
||||
def load_textures(texture_factory):
|
||||
"""Load all textures within this class
|
||||
|
||||
:param texture_factory:
|
||||
"""
|
||||
texture_factory.load_texture(
|
||||
"ufo", Textures.hmirror_texture(Textures.UFO))
|
||||
texture_factory.load_texture("star", Textures.STAR)
|
||||
|
||||
texture_factory.load_texture("ship", Textures.SHIP)
|
||||
texture_factory.load_texture(
|
||||
"ship:white", Textures.white_texture(Textures.SHIP))
|
||||
|
||||
texture_factory.load_texture("rock1", Textures.ROCK1)
|
||||
texture_factory.load_texture("rock2", Textures.ROCK2)
|
||||
texture_factory.load_texture("rock3", Textures.ROCK3)
|
||||
texture_factory.load_texture("rock4", Textures.ROCK4)
|
||||
texture_factory.load_texture("rock5", Textures.ROCK5)
|
||||
|
||||
texture_factory.load_texture(
|
||||
"lazer:white", Textures.recolor(Textures.LAZER, "#ffffff"))
|
||||
texture_factory.load_texture(
|
||||
"lazer:red", Textures.recolor(Textures.LAZER, "#f2aaaa"))
|
||||
texture_factory.load_texture(
|
||||
"lazer:yellow", Textures.recolor(Textures.LAZER, "#f2ffaa"))
|
||||
|
||||
for i, enemy in enumerate(Textures.SMALLENEMY):
|
||||
name = f"smallenemy{i}"
|
||||
texture = Textures.hmirror_texture(enemy)
|
||||
texture_factory.load_texture(name, texture)
|
||||
texture_factory.load_texture(
|
||||
f"{name}:white", Textures.white_texture(texture))
|
||||
evil_texture = Textures.recolor(texture, "#FF5555")
|
||||
texture_factory.load_texture(f"{name}_evil", evil_texture)
|
||||
texture_factory.load_texture(
|
||||
f"{name}_evil:white", Textures.white_texture(evil_texture))
|
||||
|
||||
for i, enemy in enumerate(Textures.ENEMY):
|
||||
name = f"enemy{i}"
|
||||
texture_factory.load_texture(name, Textures.hmirror_texture(enemy))
|
||||
texture_factory.load_texture(
|
||||
f"{name}:white", Textures.white_texture(Textures.hmirror_texture(enemy)))
|
||||
|
||||
texture_factory.load_texture("explosion3", Textures.EXPLOSION[0])
|
||||
texture_factory.load_texture("explosion2", Textures.hmirror_texture(
|
||||
Textures.vmirror_texture(Textures.EXPLOSION[1])))
|
||||
texture_factory.load_texture("explosion1", Textures.hmirror_texture(
|
||||
Textures.vmirror_texture(Textures.EXPLOSION[2])))
|
||||
|
||||
|
||||
class TextureFactory:
|
||||
"""Object that deals with loading and scaling textures"""
|
||||
|
||||
def __init__(self, scale) -> None:
|
||||
"""Initialise the texture factory
|
||||
|
||||
:param scale: the amount of pixels to upscale by
|
||||
:rtype: None
|
||||
"""
|
||||
self.textures = {}
|
||||
self.scale = scale
|
||||
|
||||
def load_texture(self, namespace, texture_matrix):
|
||||
"""Load and upscale a texture
|
||||
|
||||
:param namespace: namespace to save this texture to
|
||||
:param texture_matrix: A matrix of hex colours that represents the texture
|
||||
"""
|
||||
if namespace not in self.textures:
|
||||
height = len(texture_matrix) * self.scale
|
||||
width = len(texture_matrix[0]) * self.scale
|
||||
photo_image = PhotoImage(width=width, height=height)
|
||||
|
||||
for matrix_y, row in enumerate(texture_matrix):
|
||||
for matrix_x, color in enumerate(row):
|
||||
if color is not None:
|
||||
pixel_string = (
|
||||
"{" + f"{color} "*self.scale + "} ") * self.scale
|
||||
photo_image.put(
|
||||
pixel_string, (matrix_x*self.scale, matrix_y*self.scale))
|
||||
|
||||
self.textures[namespace] = photo_image
|
||||
return photo_image
|
||||
return self.get_image(namespace)
|
||||
|
||||
def get_image(self, namespace):
|
||||
"""Get a loaded image
|
||||
|
||||
:param namespace: to load the image from
|
||||
"""
|
||||
if namespace not in self.textures:
|
||||
raise Exception(
|
||||
f"Provided namespace \"{namespace}\" has not been loaded!")
|
||||
return self.textures[namespace]
|
Loading…
Reference in a new issue