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')
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 Flag
s.
Category: Gameplay Classes
A single instance of this is shared between all flags and can be retrieved via FlagFactory.get().
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.
A bs.Material that prevents contact with most objects; applied to 'non-touchable' flags.
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.
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
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
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
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.
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.Material
s 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.
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.
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.
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.