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

Wraps up media and other resources used by Flags.

Category: Gameplay Classes

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

FlagFactory()
 46    def __init__(self) -> None:
 47        """Instantiate a `FlagFactory`.
 48
 49        You shouldn't need to do this; call FlagFactory.get() to
 50        get a shared instance.
 51        """
 52        shared = SharedObjects.get()
 53        self.flagmaterial = bs.Material()
 54        self.flagmaterial.add_actions(
 55            conditions=(
 56                ('we_are_younger_than', 100),
 57                'and',
 58                ('they_have_material', shared.object_material),
 59            ),
 60            actions=('modify_node_collision', 'collide', False),
 61        )
 62
 63        self.flagmaterial.add_actions(
 64            conditions=(
 65                'they_have_material',
 66                shared.footing_material,
 67            ),
 68            actions=(
 69                ('message', 'our_node', 'at_connect', 'footing', 1),
 70                ('message', 'our_node', 'at_disconnect', 'footing', -1),
 71            ),
 72        )
 73
 74        self.impact_sound = bs.getsound('metalHit')
 75        self.skid_sound = bs.getsound('metalSkid')
 76        self.flagmaterial.add_actions(
 77            conditions=(
 78                'they_have_material',
 79                shared.footing_material,
 80            ),
 81            actions=(
 82                ('impact_sound', self.impact_sound, 2, 5),
 83                ('skid_sound', self.skid_sound, 2, 5),
 84            ),
 85        )
 86
 87        self.no_hit_material = bs.Material()
 88        self.no_hit_material.add_actions(
 89            conditions=(
 90                ('they_have_material', shared.pickup_material),
 91                'or',
 92                ('they_have_material', shared.attack_material),
 93            ),
 94            actions=('modify_part_collision', 'collide', False),
 95        )
 96
 97        # We also don't want anything moving it.
 98        self.no_hit_material.add_actions(
 99            conditions=(
100                ('they_have_material', shared.object_material),
101                'or',
102                ('they_dont_have_material', shared.footing_material),
103            ),
104            actions=(
105                ('modify_part_collision', 'collide', False),
106                ('modify_part_collision', 'physical', False),
107            ),
108        )
109
110        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:
112    @classmethod
113    def get(cls) -> FlagFactory:
114        """Get/create a shared `FlagFactory` instance."""
115        activity = bs.getactivity()
116        factory = activity.customdata.get(cls._STORENAME)
117        if factory is None:
118            factory = FlagFactory()
119            activity.customdata[cls._STORENAME] = factory
120        assert isinstance(factory, FlagFactory)
121        return factory

Get/create a shared FlagFactory instance.

@dataclass
class FlagPickedUpMessage:
124@dataclass
125class FlagPickedUpMessage:
126    """A message saying a `Flag` has been picked up.
127
128    Category: **Message Classes**
129    """
130
131    flag: Flag
132    """The `Flag` that has been picked up."""
133
134    node: bs.Node
135    """The bs.Node doing the picking up."""

A message saying a Flag has been picked up.

Category: Message Classes

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:
138@dataclass
139class FlagDiedMessage:
140    """A message saying a `Flag` has died.
141
142    Category: **Message Classes**
143    """
144
145    flag: Flag
146    """The `Flag` that died."""
147
148    self_kill: bool = False
149    """If the `Flag` killed itself or not."""

A message saying a Flag has died.

Category: Message Classes

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:
152@dataclass
153class FlagDroppedMessage:
154    """A message saying a `Flag` has been dropped.
155
156    Category: **Message Classes**
157    """
158
159    flag: Flag
160    """The `Flag` that was dropped."""
161
162    node: bs.Node
163    """The bs.Node that was holding it."""

A message saying a Flag has been dropped.

Category: Message Classes

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

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

Category: Gameplay Classes

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)
174    def __init__(
175        self,
176        *,
177        position: Sequence[float] = (0.0, 1.0, 0.0),
178        color: Sequence[float] = (1.0, 1.0, 1.0),
179        materials: Sequence[bs.Material] | None = None,
180        touchable: bool = True,
181        dropped_timeout: int | None = None,
182    ):
183        """Instantiate a flag.
184
185        If 'touchable' is False, the flag will only touch terrain;
186        useful for things like king-of-the-hill where players should
187        not be moving the flag around.
188
189        'materials can be a list of extra `bs.Material`s to apply to the flag.
190
191        If 'dropped_timeout' is provided (in seconds), the flag will die
192        after remaining untouched for that long once it has been moved
193        from its initial position.
194        """
195
196        super().__init__()
197
198        self._initial_position: Sequence[float] | None = None
199        self._has_moved = False
200        shared = SharedObjects.get()
201        factory = FlagFactory.get()
202
203        if materials is None:
204            materials = []
205        elif not isinstance(materials, list):
206            # In case they passed a tuple or whatnot.
207            materials = list(materials)
208        if not touchable:
209            materials = [factory.no_hit_material] + materials
210
211        finalmaterials = [
212            shared.object_material,
213            factory.flagmaterial,
214        ] + materials
215        self.node = bs.newnode(
216            'flag',
217            attrs={
218                'position': (position[0], position[1] + 0.75, position[2]),
219                'color_texture': factory.flag_texture,
220                'color': color,
221                'materials': finalmaterials,
222            },
223            delegate=self,
224        )
225
226        if dropped_timeout is not None:
227            dropped_timeout = int(dropped_timeout)
228        self._dropped_timeout = dropped_timeout
229        self._counter: bs.Node | None
230        if self._dropped_timeout is not None:
231            self._count = self._dropped_timeout
232            self._tick_timer = bs.Timer(
233                1.0, call=bs.WeakCall(self._tick), repeat=True
234            )
235            self._counter = bs.newnode(
236                'text',
237                owner=self.node,
238                attrs={
239                    'in_world': True,
240                    'color': (1, 1, 1, 0.7),
241                    'scale': 0.015,
242                    'shadow': 0.5,
243                    'flatness': 1.0,
244                    'h_align': 'center',
245                },
246            )
247        else:
248            self._counter = None
249
250        self._held_count = 0
251        self._score_text: bs.Node | None = None
252        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:
304    def set_score_text(self, text: str) -> None:
305        """Show a message over the flag; handy for scores."""
306        if not self.node:
307            return
308        if not self._score_text:
309            start_scale = 0.0
310            math = bs.newnode(
311                'math',
312                owner=self.node,
313                attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
314            )
315            self.node.connectattr('position', math, 'input2')
316            self._score_text = bs.newnode(
317                'text',
318                owner=self.node,
319                attrs={
320                    'text': text,
321                    'in_world': True,
322                    'scale': 0.02,
323                    'shadow': 0.5,
324                    'flatness': 1.0,
325                    'h_align': 'center',
326                },
327            )
328            math.connectattr('output', self._score_text, 'position')
329        else:
330            assert isinstance(self._score_text.scale, float)
331            start_scale = self._score_text.scale
332            self._score_text.text = text
333        self._score_text.color = bs.safecolor(self.node.color)
334        bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
335        self._score_text_hide_timer = bs.Timer(
336            1.0, bs.WeakCall(self._hide_score_text)
337        )

Show a message over the flag; handy for scores.

@override
def handlemessage(self, msg: Any) -> Any:
339    @override
340    def handlemessage(self, msg: Any) -> Any:
341        assert not self.expired
342        if isinstance(msg, bs.DieMessage):
343            if self.node:
344                self.node.delete()
345                if not msg.immediate:
346                    self.activity.handlemessage(
347                        FlagDiedMessage(
348                            self, (msg.how is bs.DeathType.LEFT_GAME)
349                        )
350                    )
351        elif isinstance(msg, bs.HitMessage):
352            assert self.node
353            assert msg.force_direction is not None
354            self.node.handlemessage(
355                'impulse',
356                msg.pos[0],
357                msg.pos[1],
358                msg.pos[2],
359                msg.velocity[0],
360                msg.velocity[1],
361                msg.velocity[2],
362                msg.magnitude,
363                msg.velocity_magnitude,
364                msg.radius,
365                0,
366                msg.force_direction[0],
367                msg.force_direction[1],
368                msg.force_direction[2],
369            )
370        elif isinstance(msg, bs.PickedUpMessage):
371            self._held_count += 1
372            if self._held_count == 1 and self._counter is not None:
373                self._counter.text = ''
374            self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
375        elif isinstance(msg, bs.DroppedMessage):
376            self._held_count -= 1
377            if self._held_count < 0:
378                print('Flag held count < 0.')
379                self._held_count = 0
380            self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
381        else:
382            super().handlemessage(msg)

General message handling; can be passed any message object.

@staticmethod
def project_stand(pos: Sequence[float]) -> None:
384    @staticmethod
385    def project_stand(pos: Sequence[float]) -> None:
386        """Project a flag-stand onto the ground at the given position.
387
388        Useful for games such as capture-the-flag to show where a
389        movable flag originated from.
390        """
391        assert len(pos) == 3
392        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.