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()