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