bascenev1lib.actor.flag

Implements a flag used for marking bases, capture-the-flag games, etc.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Implements a flag used for marking bases, capture-the-flag games, etc."""
  4
  5from __future__ import annotations
  6
  7from dataclasses import dataclass
  8from typing import TYPE_CHECKING, override
  9
 10import bascenev1 as bs
 11
 12from bascenev1lib.gameutils import SharedObjects
 13
 14if TYPE_CHECKING:
 15    from typing import Any, Sequence
 16
 17
 18class FlagFactory:
 19    """Wraps up media and other resources used by `Flag`s.
 20
 21    A single instance of this is shared between all flags
 22    and can be retrieved via FlagFactory.get().
 23    """
 24
 25    flagmaterial: bs.Material
 26    """The bs.Material applied to all `Flag`s."""
 27
 28    impact_sound: bs.Sound
 29    """The bs.Sound used when a `Flag` hits the ground."""
 30
 31    skid_sound: bs.Sound
 32    """The bs.Sound used when a `Flag` skids along the ground."""
 33
 34    no_hit_material: bs.Material
 35    """A bs.Material that prevents contact with most objects;
 36       applied to 'non-touchable' flags."""
 37
 38    flag_texture: bs.Texture
 39    """The bs.Texture for flags."""
 40
 41    _STORENAME = bs.storagename()
 42
 43    def __init__(self) -> None:
 44        """Instantiate a `FlagFactory`.
 45
 46        You shouldn't need to do this; call FlagFactory.get() to
 47        get a shared instance.
 48        """
 49        shared = SharedObjects.get()
 50        self.flagmaterial = bs.Material()
 51        self.flagmaterial.add_actions(
 52            conditions=(
 53                ('we_are_younger_than', 100),
 54                'and',
 55                ('they_have_material', shared.object_material),
 56            ),
 57            actions=('modify_node_collision', 'collide', False),
 58        )
 59
 60        self.flagmaterial.add_actions(
 61            conditions=(
 62                'they_have_material',
 63                shared.footing_material,
 64            ),
 65            actions=(
 66                ('message', 'our_node', 'at_connect', 'footing', 1),
 67                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 68            ),
 69        )
 70
 71        self.impact_sound = bs.getsound('metalHit')
 72        self.skid_sound = bs.getsound('metalSkid')
 73        self.flagmaterial.add_actions(
 74            conditions=(
 75                'they_have_material',
 76                shared.footing_material,
 77            ),
 78            actions=(
 79                ('impact_sound', self.impact_sound, 2, 5),
 80                ('skid_sound', self.skid_sound, 2, 5),
 81            ),
 82        )
 83
 84        self.no_hit_material = bs.Material()
 85        self.no_hit_material.add_actions(
 86            conditions=(
 87                ('they_have_material', shared.pickup_material),
 88                'or',
 89                ('they_have_material', shared.attack_material),
 90            ),
 91            actions=('modify_part_collision', 'collide', False),
 92        )
 93
 94        # We also don't want anything moving it.
 95        self.no_hit_material.add_actions(
 96            conditions=(
 97                ('they_have_material', shared.object_material),
 98                'or',
 99                ('they_dont_have_material', shared.footing_material),
100            ),
101            actions=(
102                ('modify_part_collision', 'collide', False),
103                ('modify_part_collision', 'physical', False),
104            ),
105        )
106
107        self.flag_texture = bs.gettexture('flagColor')
108
109    @classmethod
110    def get(cls) -> FlagFactory:
111        """Get/create a shared `FlagFactory` instance."""
112        activity = bs.getactivity()
113        factory = activity.customdata.get(cls._STORENAME)
114        if factory is None:
115            factory = FlagFactory()
116            activity.customdata[cls._STORENAME] = factory
117        assert isinstance(factory, FlagFactory)
118        return factory
119
120
121@dataclass
122class FlagPickedUpMessage:
123    """A message saying a `Flag` has been picked up."""
124
125    flag: Flag
126    """The `Flag` that has been picked up."""
127
128    node: bs.Node
129    """The bs.Node doing the picking up."""
130
131
132@dataclass
133class FlagDiedMessage:
134    """A message saying a `Flag` has died."""
135
136    flag: Flag
137    """The `Flag` that died."""
138
139    self_kill: bool = False
140    """If the `Flag` killed itself or not."""
141
142
143@dataclass
144class FlagDroppedMessage:
145    """A message saying a `Flag` has been dropped."""
146
147    flag: Flag
148    """The `Flag` that was dropped."""
149
150    node: bs.Node
151    """The bs.Node that was holding it."""
152
153
154class Flag(bs.Actor):
155    """A flag; used in games such as capture-the-flag or king-of-the-hill.
156
157    Can be stationary or carry-able by players.
158    """
159
160    def __init__(
161        self,
162        *,
163        position: Sequence[float] = (0.0, 1.0, 0.0),
164        color: Sequence[float] = (1.0, 1.0, 1.0),
165        materials: Sequence[bs.Material] | None = None,
166        touchable: bool = True,
167        dropped_timeout: int | None = None,
168    ):
169        """Instantiate a flag.
170
171        If 'touchable' is False, the flag will only touch terrain;
172        useful for things like king-of-the-hill where players should
173        not be moving the flag around.
174
175        'materials can be a list of extra `bs.Material`s to apply to the flag.
176
177        If 'dropped_timeout' is provided (in seconds), the flag will die
178        after remaining untouched for that long once it has been moved
179        from its initial position.
180        """
181
182        super().__init__()
183
184        self._initial_position: Sequence[float] | None = None
185        self._has_moved = False
186        shared = SharedObjects.get()
187        factory = FlagFactory.get()
188
189        if materials is None:
190            materials = []
191        elif not isinstance(materials, list):
192            # In case they passed a tuple or whatnot.
193            materials = list(materials)
194        if not touchable:
195            materials = [factory.no_hit_material] + materials
196
197        finalmaterials = [
198            shared.object_material,
199            factory.flagmaterial,
200        ] + materials
201        self.node = bs.newnode(
202            'flag',
203            attrs={
204                'position': (position[0], position[1] + 0.75, position[2]),
205                'color_texture': factory.flag_texture,
206                'color': color,
207                'materials': finalmaterials,
208            },
209            delegate=self,
210        )
211
212        if dropped_timeout is not None:
213            dropped_timeout = int(dropped_timeout)
214        self._dropped_timeout = dropped_timeout
215        self._counter: bs.Node | None
216        if self._dropped_timeout is not None:
217            self._count = self._dropped_timeout
218            self._tick_timer = bs.Timer(
219                1.0, call=bs.WeakCall(self._tick), repeat=True
220            )
221            self._counter = bs.newnode(
222                'text',
223                owner=self.node,
224                attrs={
225                    'in_world': True,
226                    'color': (1, 1, 1, 0.7),
227                    'scale': 0.015,
228                    'shadow': 0.5,
229                    'flatness': 1.0,
230                    'h_align': 'center',
231                },
232            )
233        else:
234            self._counter = None
235
236        self._held_count = 0
237        self._score_text: bs.Node | None = None
238        self._score_text_hide_timer: bs.Timer | None = None
239
240    def _tick(self) -> None:
241        if self.node:
242            # Grab our initial position after one tick (in case we fall).
243            if self._initial_position is None:
244                self._initial_position = self.node.position
245
246                # Keep track of when we first move; we don't count down
247                # until then.
248            if not self._has_moved:
249                nodepos = self.node.position
250                if (
251                    max(
252                        abs(nodepos[i] - self._initial_position[i])
253                        for i in list(range(3))
254                    )
255                    > 1.0
256                ):
257                    self._has_moved = True
258
259            if self._held_count > 0 or not self._has_moved:
260                assert self._dropped_timeout is not None
261                assert self._counter
262                self._count = self._dropped_timeout
263                self._counter.text = ''
264            else:
265                self._count -= 1
266                if self._count <= 10:
267                    nodepos = self.node.position
268                    assert self._counter
269                    self._counter.position = (
270                        nodepos[0],
271                        nodepos[1] + 1.3,
272                        nodepos[2],
273                    )
274                    self._counter.text = str(self._count)
275                    if self._count < 1:
276                        self.handlemessage(
277                            bs.DieMessage(how=bs.DeathType.LEFT_GAME)
278                        )
279                else:
280                    assert self._counter
281                    self._counter.text = ''
282
283    def _hide_score_text(self) -> None:
284        assert self._score_text is not None
285        assert isinstance(self._score_text.scale, float)
286        bs.animate(
287            self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
288        )
289
290    def set_score_text(self, text: str) -> None:
291        """Show a message over the flag; handy for scores."""
292        if not self.node:
293            return
294        if not self._score_text:
295            start_scale = 0.0
296            math = bs.newnode(
297                'math',
298                owner=self.node,
299                attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
300            )
301            self.node.connectattr('position', math, 'input2')
302            self._score_text = bs.newnode(
303                'text',
304                owner=self.node,
305                attrs={
306                    'text': text,
307                    'in_world': True,
308                    'scale': 0.02,
309                    'shadow': 0.5,
310                    'flatness': 1.0,
311                    'h_align': 'center',
312                },
313            )
314            math.connectattr('output', self._score_text, 'position')
315        else:
316            assert isinstance(self._score_text.scale, float)
317            start_scale = self._score_text.scale
318            self._score_text.text = text
319        self._score_text.color = bs.safecolor(self.node.color)
320        bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
321        self._score_text_hide_timer = bs.Timer(
322            1.0, bs.WeakCall(self._hide_score_text)
323        )
324
325    @override
326    def handlemessage(self, msg: Any) -> Any:
327        assert not self.expired
328        if isinstance(msg, bs.DieMessage):
329            if self.node:
330                self.node.delete()
331                if not msg.immediate:
332                    self.activity.handlemessage(
333                        FlagDiedMessage(
334                            self, (msg.how is bs.DeathType.LEFT_GAME)
335                        )
336                    )
337        elif isinstance(msg, bs.HitMessage):
338            assert self.node
339            assert msg.force_direction is not None
340            self.node.handlemessage(
341                'impulse',
342                msg.pos[0],
343                msg.pos[1],
344                msg.pos[2],
345                msg.velocity[0],
346                msg.velocity[1],
347                msg.velocity[2],
348                msg.magnitude,
349                msg.velocity_magnitude,
350                msg.radius,
351                0,
352                msg.force_direction[0],
353                msg.force_direction[1],
354                msg.force_direction[2],
355            )
356        elif isinstance(msg, bs.PickedUpMessage):
357            self._held_count += 1
358            if self._held_count == 1 and self._counter is not None:
359                self._counter.text = ''
360            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
361        elif isinstance(msg, bs.DroppedMessage):
362            self._held_count -= 1
363            if self._held_count < 0:
364                print('Flag held count < 0.')
365                self._held_count = 0
366            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
367        else:
368            super().handlemessage(msg)
369
370    @staticmethod
371    def project_stand(pos: Sequence[float]) -> None:
372        """Project a flag-stand onto the ground at the given position.
373
374        Useful for games such as capture-the-flag to show where a
375        movable flag originated from.
376        """
377        assert len(pos) == 3
378        bs.emitfx(position=pos, emit_type='flag_stand')
class FlagFactory:
 19class FlagFactory:
 20    """Wraps up media and other resources used by `Flag`s.
 21
 22    A single instance of this is shared between all flags
 23    and can be retrieved via FlagFactory.get().
 24    """
 25
 26    flagmaterial: bs.Material
 27    """The bs.Material applied to all `Flag`s."""
 28
 29    impact_sound: bs.Sound
 30    """The bs.Sound used when a `Flag` hits the ground."""
 31
 32    skid_sound: bs.Sound
 33    """The bs.Sound used when a `Flag` skids along the ground."""
 34
 35    no_hit_material: bs.Material
 36    """A bs.Material that prevents contact with most objects;
 37       applied to 'non-touchable' flags."""
 38
 39    flag_texture: bs.Texture
 40    """The bs.Texture for flags."""
 41
 42    _STORENAME = bs.storagename()
 43
 44    def __init__(self) -> None:
 45        """Instantiate a `FlagFactory`.
 46
 47        You shouldn't need to do this; call FlagFactory.get() to
 48        get a shared instance.
 49        """
 50        shared = SharedObjects.get()
 51        self.flagmaterial = bs.Material()
 52        self.flagmaterial.add_actions(
 53            conditions=(
 54                ('we_are_younger_than', 100),
 55                'and',
 56                ('they_have_material', shared.object_material),
 57            ),
 58            actions=('modify_node_collision', 'collide', False),
 59        )
 60
 61        self.flagmaterial.add_actions(
 62            conditions=(
 63                'they_have_material',
 64                shared.footing_material,
 65            ),
 66            actions=(
 67                ('message', 'our_node', 'at_connect', 'footing', 1),
 68                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 69            ),
 70        )
 71
 72        self.impact_sound = bs.getsound('metalHit')
 73        self.skid_sound = bs.getsound('metalSkid')
 74        self.flagmaterial.add_actions(
 75            conditions=(
 76                'they_have_material',
 77                shared.footing_material,
 78            ),
 79            actions=(
 80                ('impact_sound', self.impact_sound, 2, 5),
 81                ('skid_sound', self.skid_sound, 2, 5),
 82            ),
 83        )
 84
 85        self.no_hit_material = bs.Material()
 86        self.no_hit_material.add_actions(
 87            conditions=(
 88                ('they_have_material', shared.pickup_material),
 89                'or',
 90                ('they_have_material', shared.attack_material),
 91            ),
 92            actions=('modify_part_collision', 'collide', False),
 93        )
 94
 95        # We also don't want anything moving it.
 96        self.no_hit_material.add_actions(
 97            conditions=(
 98                ('they_have_material', shared.object_material),
 99                'or',
100                ('they_dont_have_material', shared.footing_material),
101            ),
102            actions=(
103                ('modify_part_collision', 'collide', False),
104                ('modify_part_collision', 'physical', False),
105            ),
106        )
107
108        self.flag_texture = bs.gettexture('flagColor')
109
110    @classmethod
111    def get(cls) -> FlagFactory:
112        """Get/create a shared `FlagFactory` instance."""
113        activity = bs.getactivity()
114        factory = activity.customdata.get(cls._STORENAME)
115        if factory is None:
116            factory = FlagFactory()
117            activity.customdata[cls._STORENAME] = factory
118        assert isinstance(factory, FlagFactory)
119        return factory

Wraps up media and other resources used by Flags.

A single instance of this is shared between all flags and can be retrieved via FlagFactory.get().

FlagFactory()
 44    def __init__(self) -> None:
 45        """Instantiate a `FlagFactory`.
 46
 47        You shouldn't need to do this; call FlagFactory.get() to
 48        get a shared instance.
 49        """
 50        shared = SharedObjects.get()
 51        self.flagmaterial = bs.Material()
 52        self.flagmaterial.add_actions(
 53            conditions=(
 54                ('we_are_younger_than', 100),
 55                'and',
 56                ('they_have_material', shared.object_material),
 57            ),
 58            actions=('modify_node_collision', 'collide', False),
 59        )
 60
 61        self.flagmaterial.add_actions(
 62            conditions=(
 63                'they_have_material',
 64                shared.footing_material,
 65            ),
 66            actions=(
 67                ('message', 'our_node', 'at_connect', 'footing', 1),
 68                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 69            ),
 70        )
 71
 72        self.impact_sound = bs.getsound('metalHit')
 73        self.skid_sound = bs.getsound('metalSkid')
 74        self.flagmaterial.add_actions(
 75            conditions=(
 76                'they_have_material',
 77                shared.footing_material,
 78            ),
 79            actions=(
 80                ('impact_sound', self.impact_sound, 2, 5),
 81                ('skid_sound', self.skid_sound, 2, 5),
 82            ),
 83        )
 84
 85        self.no_hit_material = bs.Material()
 86        self.no_hit_material.add_actions(
 87            conditions=(
 88                ('they_have_material', shared.pickup_material),
 89                'or',
 90                ('they_have_material', shared.attack_material),
 91            ),
 92            actions=('modify_part_collision', 'collide', False),
 93        )
 94
 95        # We also don't want anything moving it.
 96        self.no_hit_material.add_actions(
 97            conditions=(
 98                ('they_have_material', shared.object_material),
 99                'or',
100                ('they_dont_have_material', shared.footing_material),
101            ),
102            actions=(
103                ('modify_part_collision', 'collide', False),
104                ('modify_part_collision', 'physical', False),
105            ),
106        )
107
108        self.flag_texture = bs.gettexture('flagColor')

Instantiate a FlagFactory.

You shouldn't need to do this; call FlagFactory.get() to get a shared instance.

flagmaterial: _bascenev1.Material

The bs.Material applied to all Flags.

impact_sound: _bascenev1.Sound

The bs.Sound used when a Flag hits the ground.

skid_sound: _bascenev1.Sound

The bs.Sound used when a Flag skids along the ground.

no_hit_material: _bascenev1.Material

A bs.Material that prevents contact with most objects; applied to 'non-touchable' flags.

flag_texture: _bascenev1.Texture

The bs.Texture for flags.

@classmethod
def get(cls) -> FlagFactory:
110    @classmethod
111    def get(cls) -> FlagFactory:
112        """Get/create a shared `FlagFactory` instance."""
113        activity = bs.getactivity()
114        factory = activity.customdata.get(cls._STORENAME)
115        if factory is None:
116            factory = FlagFactory()
117            activity.customdata[cls._STORENAME] = factory
118        assert isinstance(factory, FlagFactory)
119        return factory

Get/create a shared FlagFactory instance.

@dataclass
class FlagPickedUpMessage:
122@dataclass
123class FlagPickedUpMessage:
124    """A message saying a `Flag` has been picked up."""
125
126    flag: Flag
127    """The `Flag` that has been picked up."""
128
129    node: bs.Node
130    """The bs.Node doing the picking up."""

A message saying a Flag has been picked up.

FlagPickedUpMessage(flag: Flag, node: _bascenev1.Node)
flag: Flag

The Flag that has been picked up.

node: _bascenev1.Node

The bs.Node doing the picking up.

@dataclass
class FlagDiedMessage:
133@dataclass
134class FlagDiedMessage:
135    """A message saying a `Flag` has died."""
136
137    flag: Flag
138    """The `Flag` that died."""
139
140    self_kill: bool = False
141    """If the `Flag` killed itself or not."""

A message saying a Flag has died.

FlagDiedMessage(flag: Flag, self_kill: bool = False)
flag: Flag

The Flag that died.

self_kill: bool = False

If the Flag killed itself or not.

@dataclass
class FlagDroppedMessage:
144@dataclass
145class FlagDroppedMessage:
146    """A message saying a `Flag` has been dropped."""
147
148    flag: Flag
149    """The `Flag` that was dropped."""
150
151    node: bs.Node
152    """The bs.Node that was holding it."""

A message saying a Flag has been dropped.

FlagDroppedMessage(flag: Flag, node: _bascenev1.Node)
flag: Flag

The Flag that was dropped.

node: _bascenev1.Node

The bs.Node that was holding it.

class Flag(bascenev1._actor.Actor):
155class Flag(bs.Actor):
156    """A flag; used in games such as capture-the-flag or king-of-the-hill.
157
158    Can be stationary or carry-able by players.
159    """
160
161    def __init__(
162        self,
163        *,
164        position: Sequence[float] = (0.0, 1.0, 0.0),
165        color: Sequence[float] = (1.0, 1.0, 1.0),
166        materials: Sequence[bs.Material] | None = None,
167        touchable: bool = True,
168        dropped_timeout: int | None = None,
169    ):
170        """Instantiate a flag.
171
172        If 'touchable' is False, the flag will only touch terrain;
173        useful for things like king-of-the-hill where players should
174        not be moving the flag around.
175
176        'materials can be a list of extra `bs.Material`s to apply to the flag.
177
178        If 'dropped_timeout' is provided (in seconds), the flag will die
179        after remaining untouched for that long once it has been moved
180        from its initial position.
181        """
182
183        super().__init__()
184
185        self._initial_position: Sequence[float] | None = None
186        self._has_moved = False
187        shared = SharedObjects.get()
188        factory = FlagFactory.get()
189
190        if materials is None:
191            materials = []
192        elif not isinstance(materials, list):
193            # In case they passed a tuple or whatnot.
194            materials = list(materials)
195        if not touchable:
196            materials = [factory.no_hit_material] + materials
197
198        finalmaterials = [
199            shared.object_material,
200            factory.flagmaterial,
201        ] + materials
202        self.node = bs.newnode(
203            'flag',
204            attrs={
205                'position': (position[0], position[1] + 0.75, position[2]),
206                'color_texture': factory.flag_texture,
207                'color': color,
208                'materials': finalmaterials,
209            },
210            delegate=self,
211        )
212
213        if dropped_timeout is not None:
214            dropped_timeout = int(dropped_timeout)
215        self._dropped_timeout = dropped_timeout
216        self._counter: bs.Node | None
217        if self._dropped_timeout is not None:
218            self._count = self._dropped_timeout
219            self._tick_timer = bs.Timer(
220                1.0, call=bs.WeakCall(self._tick), repeat=True
221            )
222            self._counter = bs.newnode(
223                'text',
224                owner=self.node,
225                attrs={
226                    'in_world': True,
227                    'color': (1, 1, 1, 0.7),
228                    'scale': 0.015,
229                    'shadow': 0.5,
230                    'flatness': 1.0,
231                    'h_align': 'center',
232                },
233            )
234        else:
235            self._counter = None
236
237        self._held_count = 0
238        self._score_text: bs.Node | None = None
239        self._score_text_hide_timer: bs.Timer | None = None
240
241    def _tick(self) -> None:
242        if self.node:
243            # Grab our initial position after one tick (in case we fall).
244            if self._initial_position is None:
245                self._initial_position = self.node.position
246
247                # Keep track of when we first move; we don't count down
248                # until then.
249            if not self._has_moved:
250                nodepos = self.node.position
251                if (
252                    max(
253                        abs(nodepos[i] - self._initial_position[i])
254                        for i in list(range(3))
255                    )
256                    > 1.0
257                ):
258                    self._has_moved = True
259
260            if self._held_count > 0 or not self._has_moved:
261                assert self._dropped_timeout is not None
262                assert self._counter
263                self._count = self._dropped_timeout
264                self._counter.text = ''
265            else:
266                self._count -= 1
267                if self._count <= 10:
268                    nodepos = self.node.position
269                    assert self._counter
270                    self._counter.position = (
271                        nodepos[0],
272                        nodepos[1] + 1.3,
273                        nodepos[2],
274                    )
275                    self._counter.text = str(self._count)
276                    if self._count < 1:
277                        self.handlemessage(
278                            bs.DieMessage(how=bs.DeathType.LEFT_GAME)
279                        )
280                else:
281                    assert self._counter
282                    self._counter.text = ''
283
284    def _hide_score_text(self) -> None:
285        assert self._score_text is not None
286        assert isinstance(self._score_text.scale, float)
287        bs.animate(
288            self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
289        )
290
291    def set_score_text(self, text: str) -> None:
292        """Show a message over the flag; handy for scores."""
293        if not self.node:
294            return
295        if not self._score_text:
296            start_scale = 0.0
297            math = bs.newnode(
298                'math',
299                owner=self.node,
300                attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
301            )
302            self.node.connectattr('position', math, 'input2')
303            self._score_text = bs.newnode(
304                'text',
305                owner=self.node,
306                attrs={
307                    'text': text,
308                    'in_world': True,
309                    'scale': 0.02,
310                    'shadow': 0.5,
311                    'flatness': 1.0,
312                    'h_align': 'center',
313                },
314            )
315            math.connectattr('output', self._score_text, 'position')
316        else:
317            assert isinstance(self._score_text.scale, float)
318            start_scale = self._score_text.scale
319            self._score_text.text = text
320        self._score_text.color = bs.safecolor(self.node.color)
321        bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
322        self._score_text_hide_timer = bs.Timer(
323            1.0, bs.WeakCall(self._hide_score_text)
324        )
325
326    @override
327    def handlemessage(self, msg: Any) -> Any:
328        assert not self.expired
329        if isinstance(msg, bs.DieMessage):
330            if self.node:
331                self.node.delete()
332                if not msg.immediate:
333                    self.activity.handlemessage(
334                        FlagDiedMessage(
335                            self, (msg.how is bs.DeathType.LEFT_GAME)
336                        )
337                    )
338        elif isinstance(msg, bs.HitMessage):
339            assert self.node
340            assert msg.force_direction is not None
341            self.node.handlemessage(
342                'impulse',
343                msg.pos[0],
344                msg.pos[1],
345                msg.pos[2],
346                msg.velocity[0],
347                msg.velocity[1],
348                msg.velocity[2],
349                msg.magnitude,
350                msg.velocity_magnitude,
351                msg.radius,
352                0,
353                msg.force_direction[0],
354                msg.force_direction[1],
355                msg.force_direction[2],
356            )
357        elif isinstance(msg, bs.PickedUpMessage):
358            self._held_count += 1
359            if self._held_count == 1 and self._counter is not None:
360                self._counter.text = ''
361            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
362        elif isinstance(msg, bs.DroppedMessage):
363            self._held_count -= 1
364            if self._held_count < 0:
365                print('Flag held count < 0.')
366                self._held_count = 0
367            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
368        else:
369            super().handlemessage(msg)
370
371    @staticmethod
372    def project_stand(pos: Sequence[float]) -> None:
373        """Project a flag-stand onto the ground at the given position.
374
375        Useful for games such as capture-the-flag to show where a
376        movable flag originated from.
377        """
378        assert len(pos) == 3
379        bs.emitfx(position=pos, emit_type='flag_stand')

A flag; used in games such as capture-the-flag or king-of-the-hill.

Can be stationary or carry-able by players.

Flag( *, position: Sequence[float] = (0.0, 1.0, 0.0), color: Sequence[float] = (1.0, 1.0, 1.0), materials: Optional[Sequence[_bascenev1.Material]] = None, touchable: bool = True, dropped_timeout: int | None = None)
161    def __init__(
162        self,
163        *,
164        position: Sequence[float] = (0.0, 1.0, 0.0),
165        color: Sequence[float] = (1.0, 1.0, 1.0),
166        materials: Sequence[bs.Material] | None = None,
167        touchable: bool = True,
168        dropped_timeout: int | None = None,
169    ):
170        """Instantiate a flag.
171
172        If 'touchable' is False, the flag will only touch terrain;
173        useful for things like king-of-the-hill where players should
174        not be moving the flag around.
175
176        'materials can be a list of extra `bs.Material`s to apply to the flag.
177
178        If 'dropped_timeout' is provided (in seconds), the flag will die
179        after remaining untouched for that long once it has been moved
180        from its initial position.
181        """
182
183        super().__init__()
184
185        self._initial_position: Sequence[float] | None = None
186        self._has_moved = False
187        shared = SharedObjects.get()
188        factory = FlagFactory.get()
189
190        if materials is None:
191            materials = []
192        elif not isinstance(materials, list):
193            # In case they passed a tuple or whatnot.
194            materials = list(materials)
195        if not touchable:
196            materials = [factory.no_hit_material] + materials
197
198        finalmaterials = [
199            shared.object_material,
200            factory.flagmaterial,
201        ] + materials
202        self.node = bs.newnode(
203            'flag',
204            attrs={
205                'position': (position[0], position[1] + 0.75, position[2]),
206                'color_texture': factory.flag_texture,
207                'color': color,
208                'materials': finalmaterials,
209            },
210            delegate=self,
211        )
212
213        if dropped_timeout is not None:
214            dropped_timeout = int(dropped_timeout)
215        self._dropped_timeout = dropped_timeout
216        self._counter: bs.Node | None
217        if self._dropped_timeout is not None:
218            self._count = self._dropped_timeout
219            self._tick_timer = bs.Timer(
220                1.0, call=bs.WeakCall(self._tick), repeat=True
221            )
222            self._counter = bs.newnode(
223                'text',
224                owner=self.node,
225                attrs={
226                    'in_world': True,
227                    'color': (1, 1, 1, 0.7),
228                    'scale': 0.015,
229                    'shadow': 0.5,
230                    'flatness': 1.0,
231                    'h_align': 'center',
232                },
233            )
234        else:
235            self._counter = None
236
237        self._held_count = 0
238        self._score_text: bs.Node | None = None
239        self._score_text_hide_timer: bs.Timer | None = None

Instantiate a flag.

If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.

'materials can be a list of extra bs.Materials to apply to the flag.

If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.

node
def set_score_text(self, text: str) -> None:
291    def set_score_text(self, text: str) -> None:
292        """Show a message over the flag; handy for scores."""
293        if not self.node:
294            return
295        if not self._score_text:
296            start_scale = 0.0
297            math = bs.newnode(
298                'math',
299                owner=self.node,
300                attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
301            )
302            self.node.connectattr('position', math, 'input2')
303            self._score_text = bs.newnode(
304                'text',
305                owner=self.node,
306                attrs={
307                    'text': text,
308                    'in_world': True,
309                    'scale': 0.02,
310                    'shadow': 0.5,
311                    'flatness': 1.0,
312                    'h_align': 'center',
313                },
314            )
315            math.connectattr('output', self._score_text, 'position')
316        else:
317            assert isinstance(self._score_text.scale, float)
318            start_scale = self._score_text.scale
319            self._score_text.text = text
320        self._score_text.color = bs.safecolor(self.node.color)
321        bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
322        self._score_text_hide_timer = bs.Timer(
323            1.0, bs.WeakCall(self._hide_score_text)
324        )

Show a message over the flag; handy for scores.

@override
def handlemessage(self, msg: Any) -> Any:
326    @override
327    def handlemessage(self, msg: Any) -> Any:
328        assert not self.expired
329        if isinstance(msg, bs.DieMessage):
330            if self.node:
331                self.node.delete()
332                if not msg.immediate:
333                    self.activity.handlemessage(
334                        FlagDiedMessage(
335                            self, (msg.how is bs.DeathType.LEFT_GAME)
336                        )
337                    )
338        elif isinstance(msg, bs.HitMessage):
339            assert self.node
340            assert msg.force_direction is not None
341            self.node.handlemessage(
342                'impulse',
343                msg.pos[0],
344                msg.pos[1],
345                msg.pos[2],
346                msg.velocity[0],
347                msg.velocity[1],
348                msg.velocity[2],
349                msg.magnitude,
350                msg.velocity_magnitude,
351                msg.radius,
352                0,
353                msg.force_direction[0],
354                msg.force_direction[1],
355                msg.force_direction[2],
356            )
357        elif isinstance(msg, bs.PickedUpMessage):
358            self._held_count += 1
359            if self._held_count == 1 and self._counter is not None:
360                self._counter.text = ''
361            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
362        elif isinstance(msg, bs.DroppedMessage):
363            self._held_count -= 1
364            if self._held_count < 0:
365                print('Flag held count < 0.')
366                self._held_count = 0
367            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
368        else:
369            super().handlemessage(msg)

General message handling; can be passed any message object.

@staticmethod
def project_stand(pos: Sequence[float]) -> None:
371    @staticmethod
372    def project_stand(pos: Sequence[float]) -> None:
373        """Project a flag-stand onto the ground at the given position.
374
375        Useful for games such as capture-the-flag to show where a
376        movable flag originated from.
377        """
378        assert len(pos) == 3
379        bs.emitfx(position=pos, emit_type='flag_stand')

Project a flag-stand onto the ground at the given position.

Useful for games such as capture-the-flag to show where a movable flag originated from.