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, **kwargs):
38        super().__init__(shape=CircleShape2D(30.0), **kwargs)
39        self.add_to_group("mobs")
40
41        kind = random.choice(MOB_TYPES)
42        sheet, fw, fh = mob_sheet(kind)
43        self.sprite = self.add_child(
44            AnimatedSprite2D(
45                texture=sheet,
46                frames_h=2,
47                frames_v=1,
48                width=fw,
49                height=fh,
50                scale=Vec2(0.75, 0.75),
51                name="AnimatedSprite2D",
52            )
53        )
54        # Each kind ships a 2-frame loop. Speeds taken from upstream
55        # mob.tscn: walk/swim 4 fps, fly 3 fps.
56        fps = 3.0 if kind == "fly" else 4.0
57        self.sprite.add_animation(kind, frames=[0, 1], fps=fps, loop=True)
58        self.sprite.play(kind)
59
60    def configure(self, position: Vec2, rotation: float, speed: float):
61        """Set spawn pose + velocity. Called by Main after instancing."""
62        self.position = Vec2(position)
63        self.rotation = rotation
64        self.velocity = Vec2(math.cos(rotation), math.sin(rotation)) * speed
65
66    def on_fixed_update(self, dt: float):
67        self.position += self.velocity * dt
68        # Off-screen despawn (Godot uses VisibleOnScreenNotifier2D).
69        x, y = float(self.position.x), float(self.position.y)
70        m = self.DESPAWN_MARGIN
71        width, height = self.viewport_size
72        if x < -m or x > float(width) + m or y < -m or y > float(height) + m:
73            self.destroy()