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