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
  9
 10from bascenev1lib.gameutils import SharedObjects
 11import bascenev1 as bs
 12
 13if TYPE_CHECKING:
 14    from typing import Any, Sequence
 15
 16
 17class FlagFactory:
 18    """Wraps up media and other resources used by `Flag`s.
 19
 20    Category: **Gameplay Classes**
 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
120
121
122@dataclass
123class FlagPickedUpMessage:
124    """A message saying a `Flag` has been picked up.
125
126    Category: **Message Classes**
127    """
128
129    flag: Flag
130    """The `Flag` that has been picked up."""
131
132    node: bs.Node
133    """The bs.Node doing the picking up."""
134
135
136@dataclass
137class FlagDiedMessage:
138    """A message saying a `Flag` has died.
139
140    Category: **Message Classes**
141    """
142
143    flag: Flag
144    """The `Flag` that died."""
145
146
147@dataclass
148class FlagDroppedMessage:
149    """A message saying a `Flag` has been dropped.
150
151    Category: **Message Classes**
152    """
153
154    flag: Flag
155    """The `Flag` that was dropped."""
156
157    node: bs.Node
158    """The bs.Node that was holding it."""
159
160
161class Flag(bs.Actor):
162    """A flag; used in games such as capture-the-flag or king-of-the-hill.
163
164    Category: **Gameplay Classes**
165
166    Can be stationary or carry-able by players.
167    """
168
169    def __init__(
170        self,
171        position: Sequence[float] = (0.0, 1.0, 0.0),
172        color: Sequence[float] = (1.0, 1.0, 1.0),
173        materials: Sequence[bs.Material] | None = None,
174        touchable: bool = True,
175        dropped_timeout: int | None = None,
176    ):
177        """Instantiate a flag.
178
179        If 'touchable' is False, the flag will only touch terrain;
180        useful for things like king-of-the-hill where players should
181        not be moving the flag around.
182
183        'materials can be a list of extra `bs.Material`s to apply to the flag.
184
185        If 'dropped_timeout' is provided (in seconds), the flag will die
186        after remaining untouched for that long once it has been moved
187        from its initial position.
188        """
189
190        super().__init__()
191
192        self._initial_position: Sequence[float] | None = None
193        self._has_moved = False
194        shared = SharedObjects.get()
195        factory = FlagFactory.get()
196
197        if materials is None:
198            materials = []
199        elif not isinstance(materials, list):
200            # In case they passed a tuple or whatnot.
201            materials = list(materials)
202        if not touchable:
203            materials = [factory.no_hit_material] + materials
204
205        finalmaterials = [
206            shared.object_material,
207            factory.flagmaterial,
208        ] + materials
209        self.node = bs.newnode(
210            'flag',
211            attrs={
212                'position': (position[0], position[1] + 0.75, position[2]),
213                'color_texture': factory.flag_texture,
214                'color': color,
215                'materials': finalmaterials,
216            },
217            delegate=self,
218        )
219
220        if dropped_timeout is not None:
221            dropped_timeout = int(dropped_timeout)
222        self._dropped_timeout = dropped_timeout
223        self._counter: bs.Node | None
224        if self._dropped_timeout is not None:
225            self._count = self._dropped_timeout
226            self._tick_timer = bs.Timer(
227                1.0, call=bs.WeakCall(self._tick), repeat=True
228            )
229            self._counter = bs.newnode(
230                'text',
231                owner=self.node,
232                attrs={
233                    'in_world': True,
234                    'color': (1, 1, 1, 0.7),
235                    'scale': 0.015,
236                    'shadow': 0.5,
237                    'flatness': 1.0,
238                    'h_align': 'center',
239                },
240            )
241        else:
242            self._counter = None
243
244        self._held_count = 0
245        self._score_text: bs.Node | None = None
246        self._score_text_hide_timer: bs.Timer | None = None
247
248    def _tick(self) -> None:
249        if self.node:
250            # Grab our initial position after one tick (in case we fall).
251            if self._initial_position is None:
252                self._initial_position = self.node.position
253
254                # Keep track of when we first move; we don't count down
255                # until then.
256            if not self._has_moved:
257                nodepos = self.node.position
258                if (
259                    max(
260                        abs(nodepos[i] - self._initial_position[i])
261                        for i in list(range(3))
262                    )
263                    > 1.0
264                ):
265                    self._has_moved = True
266
267            if self._held_count > 0 or not self._has_moved:
268                assert self._dropped_timeout is not None
269                assert self._counter
270                self._count = self._dropped_timeout
271                self._counter.text = ''
272            else:
273                self._count -= 1
274                if self._count <= 10:
275                    nodepos = self.node.position
276                    assert self._counter
277                    self._counter.position = (
278                        nodepos[0],
279                        nodepos[1] + 1.3,
280                        nodepos[2],
281                    )
282                    self._counter.text = str(self._count)
283                    if self._count < 1:
284                        self.handlemessage(bs.DieMessage())
285                else:
286                    assert self._counter
287                    self._counter.text = ''
288
289    def _hide_score_text(self) -> None:
290        assert self._score_text is not None
291        assert isinstance(self._score_text.scale, float)
292        bs.animate(
293            self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
294        )
295
296    def set_score_text(self, text: str) -> None:
297        """Show a message over the flag; handy for scores."""
298        if not self.node:
299            return
300        if not self._score_text:
301            start_scale = 0.0
302            math = bs.newnode(
303                'math',
304                owner=self.node,
305                attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
306            )
307            self.node.connectattr('position', math, 'input2')
308            self._score_text = bs.newnode(
309                'text',
310                owner=self.node,
311                attrs={
312                    'text': text,
313                    'in_world': True,
314                    'scale': 0.02,
315                    'shadow': 0.5,
316                    'flatness': 1.0,
317                    'h_align': 'center',
318                },
319            )
320            math.connectattr('output', self._score_text, 'position')
321        else:
322            assert isinstance(self._score_text.scale, float)
323            start_scale = self._score_text.scale
324            self._score_text.text = text
325        self._score_text.color = bs.safecolor(self.node.color)
326        bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
327        self._score_text_hide_timer = bs.Timer(
328            1.0, bs.WeakCall(self._hide_score_text)
329        )
330
331    def handlemessage(self, msg: Any) -> Any:
332        assert not self.expired
333        if isinstance(msg, bs.DieMessage):
334            if self.node:
335                self.node.delete()
336                if not msg.immediate:
337                    self.activity.handlemessage(FlagDiedMessage(self))
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')
class FlagFactory:
 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

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

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:
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

Get/create a shared FlagFactory instance.

@dataclass
class FlagPickedUpMessage:
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."""

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:
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."""

A message saying a Flag has died.

Category: Message Classes

FlagDiedMessage(flag: Flag)
flag: Flag

The Flag that died.

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

Show a message over the flag; handy for scores.

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

General message handling; can be passed any message object.

@staticmethod
def project_stand(pos: Sequence[float]) -> None:
372    @staticmethod
373    def project_stand(pos: Sequence[float]) -> None:
374        """Project a flag-stand onto the ground at the given position.
375
376        Useful for games such as capture-the-flag to show where a
377        movable flag originated from.
378        """
379        assert len(pos) == 3
380        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.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity