diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..99abd78 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +on: + push: + tags: + - '*' + +name: Release + +jobs: + create-release: + name: Create release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + tag_name: ${{ steps.tag_name.outputs.tag }} + steps: + - name: Get tag name + id: tag_name + uses: olegtarasov/get-tag@v2.1 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_API_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ steps.tag_name.outputs.tag }} + body: | + # Draft release (${{ steps.tag_name.outputs.tag }}) + [Guide for updating](https://github.com/YariKartoshe4ka/Space-Way/blob/master/docs/UPDATE.md) + ### Changes + draft: true + prerelease: false + + build-windows: + name: Build for Windows + runs-on: windows-latest + needs: create-release + steps: + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Pyinstaller + run: pip install pyinstaller==4.3 + - name: Checkout code + uses: actions/checkout@v2 + - name: Install Space Way + run: pip install . + - name: Build binary + run: pyinstaller --onefile --noconsole --icon=spaceway/icon.ico --collect-all spaceway "Space Way.py" + - name: Upload binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_API_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: "./dist/Space Way.exe" + asset_name: "Space-Way-${{ needs.create-release.outputs.tag_name }}.exe" + asset_content_type: application/exe + + publish-pypi: + name: Publish on PyPI + runs-on: ubuntu-latest + needs: create-release + steps: + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: pip install build + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: pypi + - name: Build project + run: python -m build --sdist --wheel --outdir dist/ . + - name: Publish to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.PYPI_API_TOKEN }} + - name: Get path to wheel + id: wheel_path + run: | + cd ./dist + echo '::set-output name=path::'$(ls -t *.whl | head -1) + - name: Upload wheel to GitHub + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_API_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./dist/${{ steps.wheel_path.outputs.path }} + asset_name: ${{ steps.wheel_path.outputs.path }} + asset_content_type: application/x-wheel+zip + diff --git a/docs/CODESTYLE.md b/docs/CODESTYLE.md index 3a635de..14267f5 100644 --- a/docs/CODESTYLE.md +++ b/docs/CODESTYLE.md @@ -12,6 +12,7 @@ Now project has this structure: |____ debug.py |____ main.py |____ mixins.py +|____ rect.py |____ updater.py |____ assets | |____ @@ -34,6 +35,7 @@ General files: - *debug.py* - file with some objects for easier debugging game - *main.py* - main file, import all modules, contains the entrypoint of game and connects all the scenes together - *mixins.py* - file with mixins which are needed for simple creation of the same type of objects (DRY principle) +- *rect.py* - file with implementation of a `pygame.Rect` for working with float values - *updater.py* - file responsible for updating Space Way Assets: diff --git a/requirements.txt b/requirements.txt index 0c37842..978ae6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pygame==1.9.6 +pygame==2.0.2.dev2 packaging==20.4 requests==2.24.0 appdirs==1.4.4 \ No newline at end of file diff --git a/spaceway/collection.py b/spaceway/collection.py index 48838da..bf3b5db 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -4,8 +4,6 @@ import pygame -from .mixins import BoostMixin, SceneButtonMixin - class BoostsGroup(pygame.sprite.Group): """ Extension of default pygame.sprite.Group for more easier control @@ -22,19 +20,19 @@ class BoostsGroup(pygame.sprite.Group): {: 0} """ # Define additional groups - active: Dict[str, BoostMixin] = {} - passive: Dict[BoostMixin, int] = {} + active: Dict[str, 'BoostMixin'] = {} + passive: Dict['BoostMixin', int] = {} # Define interval for next boost spawn (in score) next_spawn = 3 - def add_internal(self, boost: BoostMixin) -> None: + def add_internal(self, boost: 'BoostMixin') -> None: """ Adds boost to passive group """ self.passive[boost] = 0 pygame.sprite.Group.add_internal(self, boost) - def remove_internal(self, boost: BoostMixin) -> None: + def remove_internal(self, boost: 'BoostMixin') -> None: """ Removes boost. If boost is located in passive group, it simply will remove it from group. If boost is located in active group, it will update number in queue of other @@ -75,7 +73,7 @@ def empty(self) -> None: # Reset `next_spawn` self.next_spawn = 3 - def __contains__(self, item: Union[str, BoostMixin]) -> bool: + def __contains__(self, item: Union[str, 'BoostMixin']) -> bool: """ Will return True, if group contains activated boost with passed name, else - False """ @@ -83,7 +81,7 @@ def __contains__(self, item: Union[str, BoostMixin]) -> bool: return bool(self.get(item)) return self.has(item) - def activate(self, boost: BoostMixin) -> None: + def activate(self, boost: 'BoostMixin') -> None: """ Activates passed boost and move boost from passive group to active. If boost with boost's name have already activated, it will nullify tick (boost timer will start again) """ @@ -102,7 +100,7 @@ def activate(self, boost: BoostMixin) -> None: self.active[boost.name] = boost boost.activate() - def get(self, name: str) -> Union[BoostMixin, None]: + def get(self, name: str) -> Union['BoostMixin', None]: """ Will return boost if active group contains boost with passed name. Else it will return `None` """ @@ -203,9 +201,9 @@ class SceneButtonsGroup(pygame.sprite.Group): } """ # Define an additional dictionary for structuring buttons by scenes - buttons: Dict[str, Dict[str, List[SceneButtonMixin]]] = {} + buttons: Dict[str, Dict[str, List['SceneButtonMixin']]] = {} - def __init__(self, config, *buttons: List[SceneButtonMixin]) -> None: + def __init__(self, config, *buttons: List['SceneButtonMixin']) -> None: """ Initialization of group. Pass `config` argument and list of buttons `buttons` to add them to group """ @@ -215,7 +213,7 @@ def __init__(self, config, *buttons: List[SceneButtonMixin]) -> None: # Set `config` for the further use self.config = config - def add_internal(self, button: SceneButtonMixin) -> None: + def add_internal(self, button: 'SceneButtonMixin') -> None: """ Adding button to group and structuring by scene """ # If there were not buttons with scene of current button yet @@ -231,7 +229,7 @@ def add_internal(self, button: SceneButtonMixin) -> None: pygame.sprite.Group.add_internal(self, button) - def remove_internal(self, button: SceneButtonMixin) -> None: + def remove_internal(self, button: 'SceneButtonMixin') -> None: """ Remove button from group. It is assumed that button has already been added """ # Remove button from group @@ -281,7 +279,7 @@ def draw(self) -> None: button.update() button.blit() - def get_by_scene(self, scene: str = '', sub_scene: str = '') -> List[SceneButtonMixin]: + def get_by_scene(self, scene: str = '', sub_scene: str = '') -> List['SceneButtonMixin']: """ Returns all buttons of selected scene. If no scene was selected, buttons of current scene will be returned """ @@ -289,7 +287,7 @@ def get_by_scene(self, scene: str = '', sub_scene: str = '') -> List[SceneButton .get(scene or self.config['scene'], {}) \ .get(sub_scene or self.config['sub_scene'], []) - def get_by_instance(self, instance: any) -> Union[SceneButtonMixin, None]: + def get_by_instance(self, instance: any) -> Union['SceneButtonMixin', None]: """ Returns only one button or `None` which is an instance of passed class """ for button in self: diff --git a/spaceway/config.py b/spaceway/config.py index 3334f27..7995ae8 100644 --- a/spaceway/config.py +++ b/spaceway/config.py @@ -7,6 +7,14 @@ from appdirs import user_config_dir +class Namespace: + """ Stores variables that do not need to be imported or exported + Since `ConfigManager` is passed everywhere, it allows you to + safely exchange variables from different functions and even + scenes """ + pass + + class ConfigManager(dict): """ Configuration manager. Inherited from `dict` class and can be used as default dictionary. Different configuration files are @@ -91,6 +99,9 @@ def __load(self) -> None: config['score_list'].append((int(score), nick[:-1])) + # Creating namespace for temporary variables + config['ns'] = Namespace() + # Initializing ConfigManager as dictionary dict.__init__(self, config) diff --git a/spaceway/config/config.json b/spaceway/config/config.json index b5cdb23..8978d5b 100644 --- a/spaceway/config/config.json +++ b/spaceway/config/config.json @@ -1,11 +1,9 @@ { "caption": "Space Way", "mode": [700, 450], - "FPS": 30, + "FPS": 60, "scene": "headpiece", "sub_scene": "headpiece", - "speed": 2, - "score": 0, - "version": "2.0.1", + "version": "2.1.0", "debug": false } \ No newline at end of file diff --git a/spaceway/main.py b/spaceway/main.py index 3fb7c1d..2d098ba 100644 --- a/spaceway/main.py +++ b/spaceway/main.py @@ -1,6 +1,7 @@ """ Root file with main entrypoint """ import os +from sys import platform import pygame @@ -8,13 +9,16 @@ from .config import ConfigManager -def main() -> None: - # Set environment variable for centering window - os.environ['SDL_VIDEO_CENTERED'] = '1' +# Set environment variable for centering window +os.environ['SDL_VIDEO_CENTERED'] = '1' + +# Initialization of pygame +pygame.mixer.pre_init(44100, -16, 1, 512) +pygame.init() - # Initialization of pygame - pygame.mixer.pre_init(44100, -16, 1, 512) - pygame.init() + +def main() -> None: + """ Main entrypoint of Space Way. Execute this to run game """ # Get base directory to create absolute paths base_dir = os.path.dirname(os.path.abspath(__file__)) @@ -27,10 +31,8 @@ def main() -> None: updater.check_software_updates(config['version'], base_dir) # Сreate screen with accounting for user settings - if config['user']['full_screen']: - screen = pygame.display.set_mode(config['mode'], pygame.FULLSCREEN) - else: - screen = pygame.display.set_mode(config['mode']) + flags = (pygame.FULLSCREEN | pygame.NOFRAME * int(not platform.startswith('win'))) * int(config['user']['full_screen']) | pygame.SCALED + screen = pygame.display.set_mode(config['mode'], flags=flags) # Configure screen pygame.display.set_caption(config['caption']) @@ -46,8 +48,9 @@ def main() -> None: debugger.enable_module(debug.DebugStat, screen, base_dir, clock) debugger.enable_module(debug.DebugHitbox) - # Set tick for calculating the past time in seconds - tick = 0 + # Define variables in namespace + config['ns'].dt = 0 # Set delta-time for the further use + config['ns'].tick = 0 # Set tick for calculating the past time in seconds # Initialization of headpiece scene text = scenes.headpiece.init(screen, base_dir, config) @@ -84,12 +87,12 @@ def main() -> None: while True: # Update tick - tick += 1 + config['ns'].tick += 1 # Showing a specific scene if config['scene'] == 'headpiece': scenes.headpiece.functions.check_events(config, base_dir) - scenes.headpiece.functions.update(screen, config, text, tick) + scenes.headpiece.functions.update(screen, config, text) elif config['scene'] == 'lobby': scenes.lobby.functions.check_events(config, base_dir, scene_buttons, caption) @@ -105,16 +108,13 @@ def main() -> None: # If fullscreen button was pressed, change screen to fullscreen and back again if full_screen_button.changed: - screen = pygame.display.set_mode(config['mode'], pygame.FULLSCREEN * int(full_screen_button.state)) + flags = (pygame.FULLSCREEN | pygame.NOFRAME * int(not platform.startswith('win'))) * int(config['user']['full_screen']) | pygame.SCALED + screen = pygame.display.set_mode(config['mode'], flags=flags) full_screen_button.changed = False elif config['scene'] == 'game': scenes.game.functions.check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_buttons) - scenes.game.functions.update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause, tick, pause_buttons, end_buttons, scene_buttons) - - # Zeroize tick from overflow - if tick >= config['FPS'] * 10: - tick = 0 + scenes.game.functions.update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause, pause_buttons, end_buttons, scene_buttons) # Update debugger if debug mode enabled if config['debug']: @@ -122,4 +122,4 @@ def main() -> None: # Update screen and adjust speed to FPS pygame.display.update() - clock.tick(config['FPS']) + config['ns'].dt = clock.tick(config['FPS']) * 0.03 diff --git a/spaceway/mixins.py b/spaceway/mixins.py index 92401a1..5d59d3b 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -6,6 +6,9 @@ import pygame +from .collection import SceneButtonsGroup +from .rect import FloatRect + class SceneButtonMixin(pygame.sprite.Sprite): """ Mixin for scene buttons, which can change current scene """ @@ -50,7 +53,7 @@ def update(self) -> None: # Check, if move can be continued if self.keep_move(): # If can be, move button - self.rect.y += self.speed if self.action == 'leave' else -self.speed + self.rect.y += (self.speed if self.action == 'leave' else -self.speed) * self.config['ns'].dt else: # Else, stop button and call action callback if self.action == 'enter': @@ -85,6 +88,15 @@ def change_scene(self) -> None: def press(self) -> None: """ Сallback of button that is performed when it is pressed """ + + # Find `SceneButtonsGroup` button belongs to + for group in self.groups(): + if isinstance(group, SceneButtonsGroup): + # Leave buttons of current scene, and enter of next + group.leave_buttons() + group.enter_buttons(self.change_scene_to, self.change_sub_scene_to) + break + self.leave(self.change_scene) @@ -235,7 +247,7 @@ def __init__(self, screen, base_dir, config, name: str, life: int) -> None: self.tick = 0 # Generating a rectangle of `img_idle` and randomly positioning it - self.rect_idle = self.img_idle.get_rect() + self.rect_idle = FloatRect(self.img_idle.get_rect()) self.rect_idle.y = randint(self.screen_rect.top, self.screen_rect.bottom - self.rect_idle.height - 2) self.rect_idle.left = self.screen_rect.right @@ -276,7 +288,7 @@ def update(self) -> None: self.tick += 1 else: # Continue movement of boost if it has not activated yet - self.rect_idle.x -= self.config['speed'] + self.rect_idle.x -= self.config['ns'].speed * self.config['ns'].dt # Kill boost if it has left the screen if self.rect_idle.right < 0: diff --git a/spaceway/rect.py b/spaceway/rect.py new file mode 100644 index 0000000..55e41df --- /dev/null +++ b/spaceway/rect.py @@ -0,0 +1,426 @@ +""" File with extension of default `pygame.Rect` to use it with float values """ + +import pygame + + +class FloatRect: + def __init__(self, *args): + if len(args) == 2: + if len(args[0]) == 2 and len(args[1]) == 2: + l = [*args[0], *args[1]] + else: + raise TypeError("Argument must be rect style object") + elif len(args) == 4: + l = [*args] + elif len(args) == 1: + if len(args[0]) == 2: + l = [*args[0][0], *args[0][1]] + elif len(args[0]) == 4: + l = list(args[0]) + else: + raise TypeError( + f"sequence argument takes 2 or 4 items ({len(args[0])} given)" + ) + + else: + raise TypeError("Argument must be rect style object") + + self.__dict__["_rect"] = l + + getattr_dict = { + "x": lambda x: x._rect[0], + "y": lambda x: x._rect[1], + "top": lambda x: x._rect[1], + "left": lambda x: x._rect[0], + "bottom": lambda x: x._rect[1] + x._rect[3], + "right": lambda x: x._rect[0] + x._rect[2], + "topleft": lambda x: (x._rect[0], x._rect[1]), + "bottomleft": lambda x: (x._rect[0], x._rect[1] + x._rect[3]), + "topright": lambda x: (x._rect[0] + x._rect[2], x._rect[1]), + "bottomright": lambda x: (x._rect[0] + x._rect[2], x._rect[1] + x._rect[3]), + "midtop": lambda x: (x._rect[0] + x._rect[2] / 2, x._rect[1]), + "midleft": lambda x: (x._rect[0], x._rect[1] + x._rect[3] / 2), + "midbottom": lambda x: (x._rect[0] + x._rect[2] / 2, x._rect[1] + x._rect[3]), + "midright": lambda x: (x._rect[0] + x._rect[2], x._rect[1] + x._rect[3] / 2), + "center": lambda x: (x._rect[0] + x._rect[2] / 2, x._rect[1] + x._rect[3] / 2), + "centerx": lambda x: x._rect[0] + x._rect[2] / 2, + "centery": lambda x: x._rect[1] + x._rect[3] / 2, + "size": lambda x: (x._rect[2], x._rect[3]), + "width": lambda x: x._rect[2], + "height": lambda x: x._rect[3], + "w": lambda x: x._rect[2], + "h": lambda x: x._rect[3], + } + + def __getattr__(self, name): + try: + return self.__class__.getattr_dict[name](self) + except KeyError: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute 'name'" + ) + + def __setattr__(self, name, value): + if name == "x": + self._rect[0] = value + return + + if name == "y": + self._rect[1] = value + return + + if name == "top": + self._rect[1] = value + return + + if name == "left": + self._rect[0] = value + return + + if name == "bottom": + self._rect[1] += value - self.bottom + return + + if name == "right": + self._rect[0] += value - self.right + return + + if name == "topleft": + self._rect[0], self._rect[1] = value + return + + if name == "bottomleft": + self._rect[0], self.bottom = value + return + + if name == "topright": + self.right, self._rect[1] = value + return + + if name == "bottomright": + self.right, self.bottom = value + return + + if name == "midtop": + self.centerx, self._rect[1] = value + return + + if name == "midleft": + self._rect[0], self.centery = value + return + + if name == "midbottom": + self.centerx, self.bottom = value + return + + if name == "midright": + self.right, self.centery = value + return + + if name == "center": + self.centerx, self.centery = value + return + + if name == "centerx": + self._rect[0] += value - self.centerx + return + + if name == "centery": + self._rect[1] += value - self.centery + return + + if name == "size": + self._rect[2], self._rect[3] = value + return + + if name == "width": + self._rect[2] = value + return + + if name == "height": + self._rect[3] = value + return + + if name == "w": + self._rect[2] = value + return + + if name == "h": + self._rect[3] = value + return + + self.__dict__[name] = value + + def __getitem__(self, index): + return self._rect[index] + + def __setitem__(self, index, value): + self._rect[index] = value + + def __len__(self): + return 4 + + def __str__(self): + return f"" + + def __repr__(self): + return self.__str__() + + def __eq__(self, other): + try: + return self._rect == self.__class__(other)._rect + except: + return False + + def __bool__(self): + return self._rect[2] != 0 and self._rect[3] != 0 + + def copy(self): + return self.__class__(self._rect) + + def move(self, x, y): + c = self.copy() + c.move_ip(x, y) + return c + + def move_ip(self, x, y): + self._rect[0] += x + self._rect[1] += y + + def inflate(self, x, y): + c = self.copy() + c.inflate_ip(x, y) + return c + + def inflate_ip(self, x, y): + self._rect[0] -= x / 2 + self._rect[2] += x + + self._rect[1] -= y / 2 + self._rect[3] += y + + def update(self, *args): + self.__init__(*args) + + def clamp(self, argrect): + c = self.copy() + c.clamp_ip(argrect) + return c + + def clamp_ip(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + if self._rect[2] >= argrect.w: + x = argrect.x + argrect.w / 2 - self._rect[2] / 2 + elif self._rect[0] < argrect.x: + x = argrect.x + elif self._rect[0] + self._rect[2] > argrect.x + argrect.w: + x = argrect.x + argrect.w - self._rect[2] + else: + x = self._rect[0] + + if self._rect[3] >= argrect.h: + y = argrect.y + argrect.h / 2 - self._rect[3] / 2 + elif self._rect[1] < argrect.y: + y = argrect.y + elif self._rect[1] + self._rect[3] > argrect.y + argrect.h: + y = argrect.y + argrect.h - self._rect[3] + else: + y = self._rect[1] + + self._rect[0] = x + self._rect[1] = y + + def clip(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + # left + if self.x >= argrect.x and self.x < argrect.x + argrect.w: + x = self.x + elif argrect.x >= self.x and argrect.x < self.x + self.w: + x = argrect.x + else: + return self.__class__(self.x, self.y, 0, 0) + + # right + if self.x + self.w > argrect.x and self.x + self.w <= argrect.x + argrect.w: + w = self.x + self.w - x + elif ( + argrect.x + argrect.w > self.x and argrect.x + argrect.w <= self.x + self.w + ): + w = argrect.x + argrect.w - x + else: + return self.__class__(self.x, self.y, 0, 0) + + # top + if self.y >= argrect.y and self.y < argrect.y + argrect.h: + y = self.y + elif argrect.y >= self.y and argrect.y < self.y + self.h: + y = argrect.y + else: + return self.__class__(self.x, self.y, 0, 0) + + # bottom + if self.y + self.h > argrect.y and self.y + self.h <= argrect.y + argrect.h: + h = self.y + self.h - y + elif ( + argrect.y + argrect.h > self.y and argrect.y + argrect.h <= self.y + self.h + ): + h = argrect.y + argrect.h - y + else: + return self.__class__(self.x, self.y, 0, 0) + + return self.__class__(x, y, w, h) + + def union(self, argrect): + c = self.copy() + c.union_ip(argrect) + return c + + def union_ip(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + x = min(self.x, argrect.x) + y = min(self.y, argrect.y) + w = max(self.x + self.w, argrect.x + argrect.w) - x + h = max(self.y + self.h, argrect.y + argrect.h) - y + + self._rect = [x, y, w, h] + + def unionall(self, argrects): + c = self.copy() + c.unionall_ip(argrects) + return c + + def unionall_ip(self, argrects): + for i, argrect in enumerate(argrects): + try: + argrects[i] = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + x = min([self.x] + [r.x for r in argrects]) + y = min([self.y] + [r.y for r in argrects]) + w = max([self.right] + [r.right for r in argrects]) - x + h = max([self.bottom] + [r.bottom for r in argrects]) - y + + self._rect = [x, y, w, h] + + def fit(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + xratio = self.w / argrect.w + yratio = self.h / argrect.h + maxratio = max(xratio, yratio) + + w = self.w / maxratio + h = self.h / maxratio + + x = argrect.x + (argrect.w - w) / 2 + y = argrect.y + (argrect.h - h) / 2 + + return self.__class__(x, y, w, h) + + def normalize(self): + if self._rect[2] < 0: + self._rect[0] += self._rect[2] + self._rect[2] = -self._rect[2] + + if self._rect[3] < 0: + self._rect[1] += self._rect[3] + self._rect[3] = -self._rect[3] + + def contains(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + if self._rect[0] <= argrect[0] and argrect[0] + argrect[2] <= self.right: + if self._rect[1] <= argrect[1] and argrect[1] + argrect[3] <= self.bottom: + return True + return False + + def collidepoint(self, *args): + if len(args) == 1: + point = args[0] + elif len(args) == 2: + point = tuple(args) + else: + raise TypeError("argument must contain two numbers") + + # conforms with no collision on right / bottom edge behavior of pygame FloatRects + if self._rect[0] <= point[0] < self.right: + if self._rect[1] <= point[1] < self.bottom: + return True + return False + + def colliderect(self, argrect): + try: + argrect = self.__class__(argrect) + except: + raise TypeError("Argument must be rect style object") + + if any(0 == d for d in [self.w, self.h, argrect.w, argrect.h]): + return False + + return ( + min(self.x, self.x + self.w) < max(argrect.x, argrect.x + argrect.w) + and min(self.y, self.y + self.h) < max(argrect.y, argrect.y + argrect.h) + and max(self.x, self.x + self.w) > min(argrect.x, argrect.x + argrect.w) + and max(self.y, self.y + self.h) > min(argrect.y, argrect.y + argrect.h) + ) + + def collidelist(self, argrects): + for i, argrect in enumerate(argrects): + if self.colliderect(argrect): + return i + + return -1 + + def collidelistall(self, argrects): + out = [] + + for i, argrect in enumerate(argrects): + if self.colliderect(argrect): + out.append(i) + + return out + + def collidedict(self, rects_dict, use_values=0): + for key in rects_dict: + if use_values == 0: + argrect = key + else: + argrect = rects_dict[key] + + if self.colliderect(argrect): + return (key, rects_dict[key]) + + return None # explicit rather than implicit + + def collidedictall(self, rects_dict, use_values=0): + out = [] + + for key in rects_dict: + if use_values == 0: + argrect = key + else: + argrect = rects_dict[key] + + if self.colliderect(argrect): + out.append((key, rects_dict[key])) + + return out diff --git a/spaceway/scenes/game/__init__.py b/spaceway/scenes/game/__init__.py index 49c445b..36f8157 100644 --- a/spaceway/scenes/game/__init__.py +++ b/spaceway/scenes/game/__init__.py @@ -3,7 +3,10 @@ def init(screen, base_dir, config, astrs, boosts): - bg = Background(screen, base_dir, 0, 0) + config['ns'].speed = 2 + config['ns'].score = 0 + + bg = Background(screen, base_dir, config) plate = SpacePlate(screen, base_dir, config) score = Score(screen, base_dir, 'Score: 0') end = EndCaption(screen, base_dir, config) diff --git a/spaceway/scenes/game/functions.py b/spaceway/scenes/game/functions.py index 2ec0f3e..250fe64 100644 --- a/spaceway/scenes/game/functions.py +++ b/spaceway/scenes/game/functions.py @@ -41,8 +41,6 @@ def check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_butto exit() elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: - scene_buttons.leave_buttons() - scene_buttons.enter_buttons('lobby', 'lobby') scene_buttons.get_by_instance(EndLobbyButton).press() elif event.type == pygame.MOUSEBUTTONDOWN: @@ -64,7 +62,7 @@ def check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_butto scene_buttons.perform_point_collides((x, y)) -def spawn(screen, base_dir, config, tick, plate, astrs, boosts): +def spawn(screen, base_dir, config, plate, astrs, boosts): # Spawn asteroid if len(astrs) == 0 or astrs.sprites()[-1].rect.x < config['mode'][0] - 200: astr = Asteroid(screen, base_dir, config) @@ -73,7 +71,7 @@ def spawn(screen, base_dir, config, tick, plate, astrs, boosts): astrs.add(astr) # Spawn flying asteroid if difficulty >= middle - if config['score'] >= 10 and config['score'] % 5 == 0 and config['user']['difficulty'] >= 1: + if config['ns'].score >= 10 and config['ns'].score % 5 == 0 and config['user']['difficulty'] >= 1: for sprite in astrs: if sprite.name == 'flying': break @@ -81,7 +79,7 @@ def spawn(screen, base_dir, config, tick, plate, astrs, boosts): astrs.add(FlyingAsteroid(screen, base_dir, config)) # Spawn boost - if config['score'] >= boosts.next_spawn: + if config['ns'].score >= boosts.next_spawn: boosts.next_spawn += randint(4, 8) choices = {'time': TimeBoost, 'double': DoubleBoost, 'shield': ShieldBoost} @@ -107,20 +105,18 @@ def spawn(screen, base_dir, config, tick, plate, astrs, boosts): boosts.add(boost) -def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause, tick, pause_buttons, end_buttons, scene_buttons): +def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause, pause_buttons, end_buttons, scene_buttons): if config['sub_scene'] == 'game': - if tick % 2 == 0: - bg.update() - + bg.update() bg.blit() - if tick % (config['FPS'] * 7) == 0: + if config['ns'].tick % (config['FPS'] * 7) == 0: if 'time' in boosts: boosts.get('time').speed += 1 else: - config['speed'] += 1 + config['ns'].speed += 1 - spawn(screen, base_dir, config, tick, plate, astrs, boosts) + spawn(screen, base_dir, config, plate, astrs, boosts) for astr in astrs: if astr.rect.right < 0 or astr.rect.top > config['mode'][1]: @@ -129,9 +125,9 @@ def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause pygame.mixer.Sound(plate.sounds['score']).play() if 'double' in boosts: - config['score'] += 2 + config['ns'].score += 2 else: - config['score'] += 1 + config['ns'].score += 1 for astr in astrs: astr.update() @@ -143,7 +139,7 @@ def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause boost.update() boost.blit() - score.msg = f"score: {config['score']}" + score.msg = f"score: {config['ns'].score}" score.update() score.blit() @@ -198,16 +194,16 @@ def check_collides(config, base_dir, astrs, boosts, plate, end): def defeat(plate, astrs, boosts, end, config, base_dir): - config['score_list'].append((config['score'], config['user']['nick'])) + config['score_list'].append((config['ns'].score, config['user']['nick'])) config.filter_score() config.save() - end.score = config['score'] + end.score = config['ns'].score plate.reset() astrs.empty() boosts.empty() - config['speed'] = 2 - config['score'] = 0 + config['ns'].speed = 2 + config['ns'].score = 0 config['scene'] = 'game' config['sub_scene'] = 'end' diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index 83d01f7..c4aae48 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -3,20 +3,20 @@ import pygame from ...mixins import BoostMixin, CaptionMixin, SceneButtonMixin +from ...rect import FloatRect class Background: - def __init__(self, screen, base_dir, x, y): + def __init__(self, screen, base_dir, config): self.screen = screen - self.img = pygame.image.load(f'{base_dir}/assets/images/bg/background.bmp') - self.rect = self.img.get_rect() + self.config = config - self.rect.x = x - self.rect.y = y + self.img = pygame.image.load(f'{base_dir}/assets/images/bg/background.bmp') + self.rect = FloatRect(self.img.get_rect()) def update(self): - self.rect.x -= 1 + self.rect.x -= 0.5 * self.config['ns'].dt if self.rect.x <= -840: self.rect.x = 0 @@ -42,10 +42,12 @@ def __init__(self, screen, base_dir, config): self.is_flame = False self.img_flame = pygame.image.load(f'{base_dir}/assets/images/plate/flame.bmp') + self.img_flame_flip = pygame.transform.flip(self.img_flame, False, True) self.rect_flame = self.img_flame.get_rect() self.img = self.imgs[self.config['user']['color']] - self.rect = self.img.get_rect() + self.img_flip = pygame.transform.flip(self.img, False, True) + self.rect = FloatRect(self.img.get_rect()) self.rect.x = 5 self.rect.centery = self.screen_rect.centery @@ -73,31 +75,27 @@ def reset(self): def update(self): if self.img != self.imgs[self.config['user']['color']]: self.img = self.imgs[self.config['user']['color']] + self.img_flip = pygame.transform.flip(self.img, False, True) if not self.is_jump: - self.gravity += self.gravity_scale + self.gravity += self.gravity_scale * self.config['ns'].dt if self.flip: - self.rect.y -= self.gravity + self.rect.y -= self.gravity * self.config['ns'].dt else: - self.rect.y += self.gravity + self.rect.y += self.gravity * self.config['ns'].dt else: self.gravity = self.gravity_default if self.jump >= -5: + inc = self.jump ** 2 // 3 * self.config['ns'].dt if self.jump < 0: - if self.flip: - self.rect.y -= (self.jump ** 2) // 3 - else: - self.rect.y += (self.jump ** 2) // 3 + self.is_flame = False + self.rect.y += -inc if self.flip else inc else: self.is_flame = True - if self.flip: - self.rect.y += (self.jump ** 2) // 3 - else: - self.rect.y -= (self.jump ** 2) // 3 - self.jump -= 1 + self.rect.y += inc if self.flip else -inc + self.jump -= 1 * self.config['ns'].dt else: - self.is_flame = False self.is_jump = False self.jump = 10 @@ -108,9 +106,9 @@ def update(self): def blit(self): if self.flip: - self.screen.blit(pygame.transform.flip(self.img, False, True), self.rect) + self.screen.blit(self.img_flip, self.rect) if self.is_flame: - self.screen.blit(pygame.transform.flip(self.img_flame, False, True), self.rect_flame) + self.screen.blit(self.img_flame_flip, self.rect_flame) else: self.screen.blit(self.img, self.rect) if self.is_flame: @@ -131,7 +129,7 @@ def __init__(self, screen, base_dir, config): self.img_idle = pygame.image.load(f'{base_dir}/assets/images/asteroid/gray_idle.bmp') self.img = self.img_idle - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.y = randint(1, self.screen_rect.height - self.rect.height - 2) self.rect.left = self.screen_rect.right @@ -140,7 +138,7 @@ def blit(self): self.screen.blit(self.img, self.rect) def update(self): - self.rect.x -= self.config['speed'] + self.rect.x -= self.config['ns'].speed * self.config['ns'].dt class FlyingAsteroid(pygame.sprite.Sprite): @@ -159,7 +157,7 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[randint(0, 1)] - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.bottom = self.screen_rect.top self.rect.left = self.screen_rect.right @@ -167,8 +165,8 @@ def blit(self): self.screen.blit(self.img, self.rect) def update(self): - self.rect.x -= self.config['speed'] * 1.5 - self.rect.y += self.config['speed'] + self.rect.x -= self.config['ns'].speed * 1.5 * self.config['ns'].dt + self.rect.y += self.config['ns'].speed * self.config['ns'].dt class TimeBoost(BoostMixin, pygame.sprite.Sprite): @@ -184,11 +182,11 @@ def __init__(self, screen, base_dir, config, life=5): def activate(self): self.is_active = True - self.speed = self.config['speed'] - self.config['speed'] = 2 + self.speed = self.config['ns'].speed + self.config['ns'].speed = 2 def deactivate(self): - self.config['speed'] = self.speed + self.config['ns'].speed = self.speed class DoubleBoost(BoostMixin, pygame.sprite.Sprite): @@ -211,7 +209,7 @@ def __init__(self, screen, base_dir, config, plate, life=5): self.img_small = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_small.bmp') self.img_active = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_activate.bmp') - self.rect_active = self.img_active.get_rect() + self.rect_active = FloatRect(self.img_active.get_rect()) BoostMixin.__init__(self, screen, base_dir, config, 'shield', life) diff --git a/spaceway/scenes/headpiece/functions.py b/spaceway/scenes/headpiece/functions.py index 3f4171f..583c5d4 100644 --- a/spaceway/scenes/headpiece/functions.py +++ b/spaceway/scenes/headpiece/functions.py @@ -9,13 +9,13 @@ def check_events(config, base_dir): exit() -def update(screen, config, text, tick): +def update(screen, config, text): screen.fill((0, 0, 0)) - if tick % (config['FPS'] * 4) == 0: + if config['ns'].tick % (config['FPS'] * 4) == 0: config['scene'] = config['sub_scene'] = 'lobby' - if tick % (config['FPS'] * 2) == 0: + elif config['ns'].tick % (config['FPS'] * 2) == 0: text.msg = 'With love' text.update() diff --git a/spaceway/scenes/lobby/objects.py b/spaceway/scenes/lobby/objects.py index e6f97f7..959236a 100644 --- a/spaceway/scenes/lobby/objects.py +++ b/spaceway/scenes/lobby/objects.py @@ -1,6 +1,7 @@ import pygame from ...mixins import CaptionMixin, SceneButtonMixin +from ...rect import FloatRect class PlayButton(SceneButtonMixin): @@ -11,7 +12,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 90 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/play.bmp') - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.top @@ -35,7 +36,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/table.bmp') - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom @@ -59,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/settings.bmp') - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.right = self.screen_rect.right - 5 self.rect.top = self.screen_rect.bottom diff --git a/spaceway/scenes/settings/objects.py b/spaceway/scenes/settings/objects.py index 723a8c4..25b58e9 100644 --- a/spaceway/scenes/settings/objects.py +++ b/spaceway/scenes/settings/objects.py @@ -1,6 +1,7 @@ import pygame from ...mixins import SettingsButtonMixin, SceneButtonMixin +from ...rect import FloatRect class EffectsButton(SettingsButtonMixin): @@ -62,7 +63,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 @@ -86,7 +87,7 @@ def __init__(self, screen, base_dir, config): self.height = 42 self.fg_color = (0, 0, 0) self.bg_color = (255, 255, 255) - self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 36) + self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 36) self.config = config diff --git a/spaceway/scenes/table/functions.py b/spaceway/scenes/table/functions.py index 249fda9..11e319f 100644 --- a/spaceway/scenes/table/functions.py +++ b/spaceway/scenes/table/functions.py @@ -11,8 +11,6 @@ def check_events(config, scene_buttons): exit() elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: - scene_buttons.leave_buttons('table', 'table') - scene_buttons.enter_buttons('lobby', 'lobby') scene_buttons.get_by_instance(TableBackButton).press() elif event.type == pygame.MOUSEBUTTONDOWN: diff --git a/spaceway/scenes/table/objects.py b/spaceway/scenes/table/objects.py index 45b8e85..9a13521 100644 --- a/spaceway/scenes/table/objects.py +++ b/spaceway/scenes/table/objects.py @@ -1,6 +1,7 @@ import pygame from ...mixins import SceneButtonMixin +from ...rect import FloatRect class TableScore: @@ -59,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = self.img.get_rect() + self.rect = FloatRect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 diff --git a/spaceway/updater.py b/spaceway/updater.py index 26d55aa..613e8ff 100644 --- a/spaceway/updater.py +++ b/spaceway/updater.py @@ -11,10 +11,6 @@ def dialog(base_dir) -> None: """ Creator of information dialog """ - # Preinitialization and initialization - os.environ['SDL_VIDEO_CENTERED'] = '1' - pygame.init() - # Setup screen MODE = (WIDTH, HEIGHT) = (300, 200) screen = pygame.display.set_mode(MODE) @@ -86,7 +82,7 @@ def check_software_updates(version, base_dir) -> None: # Get remote vesrion of `config.json` if network connection available try: - r = get('https://raw.githubusercontent.com/YariKartoshe4ka/Space-Way/develop/spaceway/config/config.json') + r = get('https://raw.githubusercontent.com/YariKartoshe4ka/Space-Way/master/spaceway/config/config.json') except: return