Package horizons :: Package world :: Package units :: Module weaponholder
[hide private]
[frames] | no frames]

Source Code for Module horizons.world.units.weaponholder

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import logging 
 23  import math 
 24   
 25  from horizons.component.stancecomponent import ( 
 26          AggressiveStance, FleeStance, HoldGroundStance, NoneStance) 
 27  from horizons.component.storagecomponent import StorageComponent 
 28  from horizons.constants import GAME_SPEED 
 29  from horizons.i18n import gettext as T 
 30  from horizons.scheduler import Scheduler 
 31  from horizons.util.changelistener import metaChangeListenerDecorator 
 32  from horizons.util.python.callback import Callback 
 33  from horizons.util.shapes import Annulus, Point 
 34  from horizons.util.worldobject import WorldObject 
 35  from horizons.world.storage import PositiveTotalNumSlotsStorage 
 36  from horizons.world.units.ship import Ship 
 37  from horizons.world.units.unitexeptions import MoveNotPossible 
 38  from horizons.world.units.weapon import SetStackableWeaponNumberError, StackableWeapon, Weapon 
39 40 41 @metaChangeListenerDecorator("storage_modified") 42 @metaChangeListenerDecorator("user_attack_issued") 43 -class WeaponHolder:
44 log = logging.getLogger("world.combat") 45
46 - def __init__(self, **kwargs):
47 super().__init__(**kwargs) 48 self.__init()
49
50 - def __init(self):
51 self.create_weapon_storage() 52 self._target = None 53 self.add_storage_modified_listener(self.update_range) 54 self.equipped_weapon_number = 0 55 Scheduler().add_new_object(self._stance_tick, self, run_in=2, loops=-1, 56 loop_interval=GAME_SPEED.TICKS_PER_SECOND)
57
58 - def remove(self):
59 self.remove_storage_modified_listener(self.update_range) 60 self.stop_attack() 61 for weapon in self._weapon_storage: 62 weapon.remove_attack_ready_listener(Callback(self._add_to_fireable, weapon)) 63 weapon.remove_weapon_fired_listener(Callback(self._remove_from_fireable, weapon)) 64 weapon.remove_weapon_fired_listener(self._increase_fired_weapons_number) 65 super().remove()
66
67 - def create_weapon_storage(self):
68 self._weapon_storage = [] 69 self._fireable = [] 70 #TODO make a system for making it load from db 71 self.total_number_of_weapons = 30
72
73 - def update_range(self, caller=None):
74 if self._weapon_storage: 75 self._min_range = min([w.get_minimum_range() for w in self._weapon_storage]) 76 self._max_range = max([w.get_maximum_range() for w in self._weapon_storage]) 77 else: 78 self._min_range = 0 79 self._max_range = 0
80
81 - def _add_to_fireable(self, weapon):
82 """ 83 Callback executed when weapon attack is ready 84 """ 85 self._fireable.append(weapon)
86
87 - def _remove_from_fireable(self, weapon):
88 """ 89 Callback executed when weapon is fired 90 """ 91 # remove in the next tick 92 Scheduler().add_new_object(Callback(self._fireable.remove, weapon), self, run_in=0)
93
94 - def _increase_fired_weapons_number(self, caller=None):
95 """ 96 Callback that helps keeping tack of successful weapon fire number 97 """ 98 self._fired_weapons_number += 1
99
100 - def add_weapon_to_storage(self, weapon_id):
101 """ 102 adds weapon to storage 103 @param weapon_id : id of the weapon to be added 104 """ 105 self.log.debug("%s add weapon %s", self, weapon_id) 106 # If weapon is stackable, try to stack. 107 weapon = None 108 if self.equipped_weapon_number == self.total_number_of_weapons: 109 self.log.debug("%s weapon storage full", self) 110 return False 111 if self.session.db.get_weapon_stackable(weapon_id): 112 stackable = [w for w in self._weapon_storage if weapon_id == w.weapon_id] 113 # Try to increase the number of weapons for one stackable weapon. 114 increased = False 115 for weapon in stackable: 116 try: 117 weapon.increase_number_of_weapons(1) 118 increased = True 119 break 120 except SetStackableWeaponNumberError: 121 continue 122 123 if not increased: 124 weapon = StackableWeapon(self.session, weapon_id) 125 else: 126 weapon = Weapon(self.session, weapon_id) 127 if weapon: 128 self._weapon_storage.append(weapon) 129 weapon.add_weapon_fired_listener(Callback(self._remove_from_fireable, weapon)) 130 weapon.add_attack_ready_listener(Callback(self._add_to_fireable, weapon)) 131 weapon.add_weapon_fired_listener(self._increase_fired_weapons_number) 132 self._fireable.append(weapon) 133 self.equipped_weapon_number += 1 134 self.on_storage_modified() # This will update the range. 135 return True
136
137 - def remove_weapon_from_storage(self, weapon_id):
138 """ 139 removes weapon to storage 140 @param weapon_id : id of the weapon to be removed 141 """ 142 self.log.debug("%s remove weapon %s", self, weapon_id) 143 weapons = [w for w in self._weapon_storage if w.weapon_id == weapon_id] 144 if not weapons: 145 self.log.debug("%s can't remove, no weapons there", self) 146 return False 147 # Remove the weapon last added. 148 weapon = weapons[-1] 149 # 150 remove_from_storage = False 151 # If the weapon to be removed was stackable, try to decrease number. 152 if self.session.db.get_weapon_stackable(weapon_id): 153 try: 154 weapon.decrease_number_of_weapons(1) 155 except SetStackableWeaponNumberError: 156 remove_from_storage = True 157 else: 158 remove_from_storage = True 159 160 if remove_from_storage: 161 self._weapon_storage.remove(weapon) 162 weapon.remove_weapon_fired_listener(Callback(self._remove_from_fireable, weapon)) 163 weapon.remove_attack_ready_listener(Callback(self._add_to_fireable, weapon)) 164 weapon.remove_weapon_fired_listener(self._increase_fired_weapons_number) 165 try: 166 self._fireable.remove(weapon) 167 except ValueError: 168 pass 169 170 self.on_storage_modified() 171 self.equipped_weapon_number -= 1 172 return True
173
174 - def equip_from_inventory(self, weapon_id, number):
175 """Equips weapon if present in inventory 176 @param weapon_id: weapon id to be equipped 177 @param number: number of weapons to be equipped 178 @return: number of weapons that were not equipped 179 """ 180 while number: 181 if self.get_component(StorageComponent).inventory.alter(weapon_id, -1) == 0: 182 # try to decrease number from inventory 183 if not self.add_weapon_to_storage(weapon_id): 184 # if not added, put back in inventory and break 185 self.get_component(StorageComponent).inventory.alter(weapon_id, 1) 186 break 187 else: 188 break 189 number -= 1 190 return number
191
192 - def unequip_to_inventory(self, weapon_id, number):
193 """Unequips weapon and adds it to inventory 194 @param weapon_id: weapon id to be unequipped 195 @param number: number of weapons to be unequipped 196 @return: number of weapons that were not added to storage 197 """ 198 while number: 199 if self.remove_weapon_from_storage(weapon_id): 200 # try to remove from weapon storage 201 if self.get_component(StorageComponent).inventory.alter(weapon_id, 1) == 1: 202 # if not added to holder inventory move back to storage and break 203 self.add_weapon_to_storage(weapon_id) 204 break 205 else: 206 break 207 number -= 1 208 return number
209
210 - def get_weapon_storage(self):
211 """ 212 Returns storage object for self._weapon_storage 213 """ 214 storage = PositiveTotalNumSlotsStorage(self.total_number_of_weapons, 4) 215 for weapon in self._weapon_storage: 216 weapon_id = weapon.weapon_id 217 if self.session.db.get_weapon_stackable(weapon_id): 218 number = weapon.number_of_weapons 219 else: 220 number = 1 221 storage.alter(weapon_id, number) 222 return storage
223
224 - def attack_in_range(self):
225 """ 226 Returns True if the target is in range, False otherwise 227 """ 228 if not self._target: 229 return False 230 distance = self.position.distance(self._target.position.center) 231 return self._min_range <= distance <= self._max_range
232
233 - def can_attack_position(self, position):
234 """ 235 Returns True if the holder can attack position at call time 236 @param position: position of desired attack 237 """ 238 # if no fireable weapon return False 239 if not self._fireable: 240 return False 241 # if position not in range return False 242 return self._min_range <= self.position.distance(position.center) <= self._max_range
243
244 - def try_attack_target(self):
245 """ 246 Attacking loop 247 """ 248 self.log.debug("%s try attack target %s", self, self._target) 249 if self._target is None: 250 return 251 252 if self.attack_in_range(): 253 dest = self._target.position.center 254 if self._target.movable and self._target.is_moving(): 255 dest = self._target._next_target 256 257 self.fire_all_weapons(dest) 258 Scheduler().add_new_object(self.try_attack_target, self, GAME_SPEED.TICKS_PER_SECOND) 259 self.log.debug("%s fired, fire again in %s ticks", self, GAME_SPEED.TICKS_PER_SECOND) 260 else: 261 self.log.debug("%s target not in range", self)
262
263 - def _stance_tick(self):
264 """ 265 Executes every few seconds, doing movement depending on the stance. 266 Static WeaponHolders are aggressive, attacking all enemies that are in range 267 """ 268 enemies = [u for u in self.session.world.get_health_instances(self.position.center, self._max_range) 269 if self.session.world.diplomacy.are_enemies(u.owner, self.owner)] 270 271 self.log.debug("%s stance tick, found enemies: %s", self, [str(i) for i in enemies]) 272 if not enemies: 273 return 274 275 self.attack(enemies[0])
276
277 - def attack(self, target):
278 """ 279 Triggers attack on target 280 @param target: target to be attacked 281 """ 282 self.log.debug("%s attack %s", self, target) 283 if self._target is not None: 284 if self._target is not target: 285 #if target is changed remove the listener 286 if self._target.has_remove_listener(self.remove_target): 287 self._target.remove_remove_listener(self.remove_target) 288 else: 289 #else do not update the target 290 self.log.debug("%s already targeting this one", self) 291 return 292 if not target.has_remove_listener(self.remove_target): 293 target.add_remove_listener(self.remove_target) 294 self._target = target 295 296 self.try_attack_target()
297
298 - def user_attack(self, targetid):
299 """ 300 Called when the user triggeres the attack, executes the user_attack_issued callbacks 301 @param targetid: world id of the unit that is to be attacked 302 """ 303 self.attack(WorldObject.get_object_by_id(targetid)) 304 self.on_user_attack_issued()
305
306 - def is_attacking(self):
307 """ 308 Returns True if the WeaponHolder is trying to attack a target 309 """ 310 return True if self._target else False
311
312 - def remove_target(self):
313 """ 314 Removes reference from target, 315 this happens when the attack is stopped or the target is dead 316 either way the refs are checked using gc module 317 this is used because after unit death it's possbile that it still has refs 318 """ 319 self._target = None
320
321 - def stop_attack(self):
322 # When the ship is told to move, the target is None and the listeners in target removed 323 #TODO make another listener for target_changed 324 self.log.debug("%s stop attack", self) 325 if self._target is not None: 326 self._target.discard_remove_listener(self.remove_target) 327 self.remove_target()
328
329 - def fire_all_weapons(self, dest, rotated=False):
330 """ 331 Fires all weapons in storage at a given position 332 @param dest: Point with the given position 333 @param rotated: If True weapons will be fired at different locations, rotated around dest 334 override to True for units that need to fire at rotated coords 335 """ 336 self.log.debug("%s fire all weapons", self) 337 self._fired_weapons_number = 0 338 if not self.can_attack_position(dest): 339 self.log.debug("%s can't attack this position", self) 340 return 341 342 if not rotated: 343 for weapon in self._fireable: 344 weapon.fire(dest, self.position.center) 345 else: 346 angle = (math.pi / 60) * (-len(self._fireable) / 2) 347 cos = math.cos(angle) 348 sin = math.sin(angle) 349 350 x = self.position.center.x 351 y = self.position.center.y 352 353 dest_x = dest.x 354 dest_y = dest.y 355 356 dest_x = (dest_x - x) * cos - (dest_y - y) * sin + x 357 dest_y = (dest_x - x) * sin + (dest_y - y) * cos + y 358 359 angle = math.pi / 60 360 cos = math.cos(angle) 361 sin = math.sin(angle) 362 363 for weapon in self._fireable: 364 destination = Point(dest_x, dest_y) 365 weapon.fire(destination, self.position.center) 366 dest_x = (dest_x - x) * cos - (dest_y - y) * sin + x 367 dest_y = (dest_x - x) * sin + (dest_y - y) * cos + y 368 369 if self._fired_weapons_number != 0: 370 self.act_attack(dest)
371
372 - def act_attack(self, dest):
373 """ 374 Override in subclasses for action code 375 """ 376 pass
377
378 - def get_attack_target(self):
379 return self._target
380
381 - def save(self, db):
382 super().save(db) 383 # save weapon storage 384 for weapon in self._weapon_storage: 385 number = 1 386 ticks = weapon.get_ticks_until_ready() 387 if self.session.db.get_weapon_stackable(weapon.weapon_id): 388 number = weapon.number_of_weapons 389 390 db("INSERT INTO weapon_storage(owner_id, weapon_id, number, remaining_ticks) VALUES(?, ?, ?, ?)", 391 self.worldid, weapon.weapon_id, number, ticks) 392 # save target 393 if self._target: 394 db("INSERT INTO target(worldid, target_id) VALUES(?, ?)", self.worldid, self._target.worldid)
395
396 - def load_target(self, db):
397 """ 398 Loads target from database 399 """ 400 target_id = db("SELECT target_id from target WHERE worldid = ?", self.worldid) 401 if target_id: 402 target = self.session.world.get_object_by_id(target_id[0][0]) 403 self.attack(target)
404
405 - def load(self, db, worldid):
406 super().load(db, worldid) 407 self.__init() 408 weapons = db("SELECT weapon_id, number, remaining_ticks FROM weapon_storage WHERE owner_id = ?", worldid) 409 for weapon_id, number, ticks in weapons: 410 # create weapon and add to storage manually 411 if self.session.db.get_weapon_stackable(weapon_id): 412 weapon = StackableWeapon(self.session, weapon_id) 413 weapon.set_number_of_weapons(number) 414 else: 415 weapon = Weapon(self.session, weapon_id) 416 self._weapon_storage.append(weapon) 417 # if weapon not ready add scheduled call and remove from fireable 418 if ticks: 419 weapon.attack_ready = False 420 Scheduler().add_new_object(weapon.make_attack_ready, weapon, ticks) 421 else: 422 self._fireable.append(weapon) 423 weapon.add_weapon_fired_listener(Callback(self._remove_from_fireable, weapon)) 424 weapon.add_attack_ready_listener(Callback(self._add_to_fireable, weapon)) 425 weapon.add_weapon_fired_listener(self._increase_fired_weapons_number) 426 self.on_storage_modified() 427 # load target after all objects have been loaded 428 Scheduler().add_new_object(Callback(self.load_target, db), self, run_in=0) 429 self.log.debug("%s weapon storage after load: %s", self, self._weapon_storage)
430
431 - def get_status(self):
432 """Return the current status of the ship.""" 433 if self.is_attacking(): 434 target = self.get_attack_target() 435 if isinstance(target, Ship): 436 string = T("Attacking {target} '{name}' ({owner})") 437 return (string.format(target=target.classname.lower(), name=target.name, 438 owner=target.owner.name), 439 target.position) 440 return (T('Attacking {owner}').format(owner=target.owner.name), 441 target.position) 442 return super().get_status()
443
444 445 @metaChangeListenerDecorator("user_move_issued") 446 -class MovingWeaponHolder(WeaponHolder):
447 - def __init__(self, **kwargs):
448 super().__init__(**kwargs) 449 self.__init()
450
451 - def __init(self):
457
458 - def _stance_tick(self):
459 """ 460 Executes every few seconds, doing movement depending on the stance. 461 """ 462 self.get_component(self.stance).act()
463
464 - def stop_for(self, ticks):
465 """ 466 Delays movement for a number of ticks. 467 Used when shooting in specialized unit code. 468 """ 469 if Scheduler().rem_call(self, self._move_tick): 470 Scheduler().add_new_object(Callback(self._move_tick, resume=False), self, ticks)
471
472 - def _move_and_attack(self, destination, not_possible_action=None, in_range_callback=None):
473 """ 474 Callback for moving to a destination, then attack 475 @param destination : moving destination 476 @param not_possible_action : execute if MoveNotPossible is thrown 477 @param in_range_callback : sets up a conditional callback that is executed if the target is in range 478 """ 479 if not_possible_action: 480 assert callable(not_possible_action) 481 if in_range_callback: 482 assert callable(in_range_callback) 483 484 try: 485 self.move(destination, callback=self.try_attack_target, 486 blocked_callback=self.try_attack_target) 487 if in_range_callback: 488 self.add_conditional_callback(self.attack_in_range, in_range_callback) 489 490 except MoveNotPossible: 491 if not_possible_action: 492 not_possible_action()
493
494 - def try_attack_target(self):
495 """ 496 Attacking loop 497 """ 498 if self._target is None: 499 return 500 501 if not self.attack_in_range(): 502 destination = Annulus(self._target.position.center, self._min_range, self._max_range) 503 not_possible_action = self.stop_attack 504 # if target passes near self, attack! 505 in_range_callback = self.try_attack_target 506 # if executes attack action try to move in 1 second 507 self._move_and_attack(destination, not_possible_action, in_range_callback) 508 else: 509 if self.is_moving() and self._fireable: 510 # stop to shoot 511 self.stop() 512 # finish the move before removing the move tick 513 self._movement_finished() 514 # do not execute the next move tick 515 Scheduler().rem_call(self, self._move_tick) 516 517 distance = self.position.distance(self._target.position.center) 518 dest = self._target.position.center 519 if self._target.movable and self._target.is_moving(): 520 dest = self._target._next_target 521 522 fireable_number = len(self._fireable) 523 self.fire_all_weapons(dest) 524 move_closer = False 525 # if no weapon was fired, because of holder positioned in dead range, move closer 526 if self._fired_weapons_number == 0 and fireable_number != 0: 527 # no weapon was fired but i could have fired weapons 528 # check if i have weapons that could be shot from this position 529 move_closer = True 530 distance = self.position.center.distance(self._target.position.center) 531 for weapon in self._weapon_storage: 532 if weapon.check_target_in_range(distance): 533 move_closer = False 534 break 535 536 if move_closer: 537 destination = Annulus(self._target.position.center, self._min_range, self._min_range) 538 self._move_and_attack(destination) 539 else: 540 Scheduler().add_new_object(self.try_attack_target, self, GAME_SPEED.TICKS_PER_SECOND)
541
542 - def set_stance(self, stance):
543 """ 544 Sets the stance to a specific one and passes the current state 545 """ 546 state = self.get_component(self.stance).get_state() 547 self.stance = stance 548 self.get_component(stance).set_state(state)
549
550 - def go(self, x, y):
551 super().go(x, y) 552 self.on_user_move_issued()
553
554 - def save(self, db):
555 super().save(db) 556 db("INSERT INTO stance(worldid, stance, state) VALUES(?, ?, ?)", 557 self.worldid, self.stance.NAME, self.get_component(self.stance).get_state())
558
559 - def load(self, db, worldid):
560 super().load(db, worldid) 561 self.__init() 562 stance, state = db("SELECT stance, state FROM stance WHERE worldid = ?", worldid)[0] 563 self.stance = self.get_component_by_name(stance) 564 self.stance.set_state(state)
565
566 - def user_attack(self, targetid):
567 super().user_attack(targetid) 568 if self.owner.is_local_player: 569 self.session.ingame_gui.minimap.show_unit_path(self)
570
571 572 -class StationaryWeaponHolder(WeaponHolder):
573 """Towers and stuff""" 574 # TODO: stances (shoot on sight, don't do anything) 575
576 - def __init__(self, *args, **kwargs):
577 super().__init__(*args, **kwargs) 578 self.__init()
579
580 - def __init(self):
581 self.add_component(HoldGroundStance()) 582 self.stance = HoldGroundStance
583
584 - def load(self, db, worldid):
585 super().load(db, worldid) 586 self.__init()
587