from dataclasses import dataclass from typing import Generator from abc import ABC, abstractmethod from math import pi, cos, sin import pygame as pg from transform import Transform @dataclass class Face: begin: pg.Vector2 end: pg.Vector2 @property def normal(self) -> pg.Vector2: return (self.end-self.begin).rotate(-90).normalize() @dataclass class ColliderContact: __slots__ = ["points", "normal", "penetration"] points: list[pg.Vector2] normal: pg.Vector2 penetration: float @dataclass(frozen=True) class PolygonalHull: _vertices: list[pg.Vector2] def get_vertex_at_index(self, key: int) -> pg.Vector2: return self._vertices[key] def get_face_at_index(self, key: int) -> Face: return Face(self._vertices[key], self._vertices[(key + 1) % len(self._vertices)]) def vertices(self) -> Generator[pg.Vector2, None, None]: for v in self._vertices: yield v def faces(self) -> Generator[Face, None, None]: for i in range(len(self._vertices)): yield self.get_face_at_index(i) def project(self, axis: pg.Vector2) -> tuple[float, float]: projections = [v.dot(axis) for v in self._vertices] return (min(projections), max(projections)) def closest_vertex(self, v: pg.Vector2) -> pg.Vector2: return min(self._vertices, key=lambda p: (v-p).length()) class BaseCollider(ABC): @abstractmethod def moment_of_inertia(self, mass: float) -> float: pass class ConvexCollider(BaseCollider): @abstractmethod def hull(self, transform: Transform) -> PolygonalHull: pass @dataclass class CircleCollider(BaseCollider): radius: float def moment_of_inertia(self, mass: float) -> float: return 0.5 * self.radius ** 2 * mass @dataclass class RectCollider(ConvexCollider): dimensions: tuple[float, float] @property def width(self): return self.dimensions[0] @property def height(self): return self.dimensions[1] def hull(self, transform: Transform) -> PolygonalHull: return PolygonalHull([ transform.global_position - pg.Vector2(self.width / 2.0, self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale, transform.global_position - pg.Vector2(-self.width / 2.0, self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale, transform.global_position - pg.Vector2(-self.width / 2.0, -self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale, transform.global_position - pg.Vector2(self.width / 2.0, -self.height / 2.0).rotate(transform.global_degrees) * transform.global_scale ]) def moment_of_inertia(self, mass: float) -> float: return (1.0 / 12.0) * mass * (self.width ** 2 + self.height ** 2) @dataclass class CapsuleCollider(ConvexCollider): width: float height: float resolution: int = 8 @property def radius(self) -> float: return self.width / 2 @property def _half_height(self) -> float: return (self.height / 2) - self.radius @dataclass class CapsuleCollider(ConvexCollider): width: float height: float resolution: int = 10 @property def radius(self) -> float: return self.width / 2 def hull(self, transform: Transform) -> PolygonalHull: verts: list[pg.Vector2] = [] center_offset = (self.height / 2.0 - self.radius) top_center = transform.global_position - pg.Vector2(0, center_offset).rotate(transform.global_degrees) * transform.global_scale bottom_center = transform.global_position + pg.Vector2(0, center_offset).rotate(transform.global_degrees) * transform.global_scale angle = pi for _ in range(0, self.resolution): verts.append( top_center + (self.radius * pg.Vector2(cos(angle), -sin(angle))).rotate(transform.global_degrees) * transform.global_scale ) angle -= pi / self.resolution angle = 0.0 for _ in range(0, self.resolution): verts.append( bottom_center + (self.radius * pg.Vector2(cos(angle), -sin(angle))).rotate(transform.global_degrees) * transform.global_scale ) angle -= pi / self.resolution return PolygonalHull(verts) def moment_of_inertia(self, mass: float) -> float: return (1.0 / 12.0) * mass * (self.width ** 2 + self.height ** 2)