nodes/mob.pyΒΆ

Part of Dodge the Creeps.

 1"""Mob: fly/walk/swim creep that drifts across the screen at a fixed velocity."""
 2
 3from __future__ import annotations
 4
 5import math
 6import random
 7
 8from simvx.core import (
 9    AnimatedSprite2D,
10    CharacterBody2D,
11    CircleShape2D,
12    Vec2,
13)
14
15from .sprite_sheets import mob_sheet
16
17MOB_TYPES = ("fly", "walk", "swim")
18
19
20class Mob(CharacterBody2D):
21    """Random-type creep with a constant linear velocity.
22
23    A ``CharacterBody2D`` is an ordinary ``KINEMATIC`` physics body, so two of them
24    block each other when their layers and masks mutually opt in. This project
25    never calls ``move_and_slide``, so no character here sweeps and none can block
26    another: the collider exists for the group / overlap queries only, which is why
27    the default layer and mask need no retune.
28
29    The Godot original uses a Path2D + RigidBody2D + VisibleOnScreenNotifier.
30    Here the spawning logic is replicated in Main (random edge + random
31    rotation), and the mob just drifts in a straight line and despawns when
32    it leaves a generous off-screen margin.
33    """
34
35    DESPAWN_MARGIN = 200.0
36
37    def __init__(self, screen_size: Vec2, **kwargs):
38        super().__init__(shape=CircleShape2D(30.0), **kwargs)
39        self.add_to_group("mobs")
40        self.screen_size = Vec2(screen_size)
41
42        kind = random.choice(MOB_TYPES)
43        sheet, fw, fh = mob_sheet(kind)
44        self.sprite = self.add_child(
45            AnimatedSprite2D(
46                texture=sheet,
47                frames_h=2,
48                frames_v=1,
49                width=fw,
50                height=fh,
51                scale=Vec2(0.75, 0.75),
52                name="AnimatedSprite2D",
53            )
54        )
55        # Each kind ships a 2-frame loop. Speeds taken from upstream
56        # mob.tscn: walk/swim 4 fps, fly 3 fps.
57        fps = 3.0 if kind == "fly" else 4.0
58        self.sprite.add_animation(kind, frames=[0, 1], fps=fps, loop=True)
59        self.sprite.play(kind)
60
61    def configure(self, position: Vec2, rotation: float, speed: float):
62        """Set spawn pose + velocity. Called by Main after instancing."""
63        self.position = Vec2(position)
64        self.rotation = rotation
65        self.velocity = Vec2(math.cos(rotation), math.sin(rotation)) * speed
66
67    def on_fixed_update(self, dt: float):
68        self.position += self.velocity * dt
69        # Off-screen despawn (Godot uses VisibleOnScreenNotifier2D).
70        x, y = float(self.position.x), float(self.position.y)
71        m = self.DESPAWN_MARGIN
72        sw, sh = self.tree.screen_size if self.tree else (self.screen_size.x, self.screen_size.y)
73        if x < -m or x > float(sw) + m or y < -m or y > float(sh) + m:
74            self.destroy()