def set_action_target(self, value): if isinstance(value, tuple): self.action = MoveXYAction(self, value) elif self.is_an_enemy(value): self.action = AttackAction(self, value) elif value is not None: self.action = MoveAction(self, value) # "use" ? else: self.action = Action(self, value)
def set_action_target(self, value): if isinstance(value, tuple): self.action = MoveXYAction(self, value) elif type(value).__name__ == "ZoomTarget": self.action = MoveXYAction(self, (value.x, value.y)) elif self.is_an_enemy(value): self.action = AttackAction(self, value) elif value is not None: self.action = MoveAction(self, value) else: self.action = Action(self, value)
class Creature(Entity): type_name = None def get_action_target(self): if self.action: return self.action.target def set_action_target(self, value): if isinstance(value, tuple): self.action = MoveXYAction(self, value) elif self.is_an_enemy(value): self.action = AttackAction(self, value) elif value is not None: self.action = MoveAction(self, value) # "use" ? else: self.action = Action(self, value) action_target = property(get_action_target, set_action_target) hp_max = 0 mana_max = 0 mana_regen = 0 walked = [] cost = (0,) * MAX_NB_OF_RESOURCE_TYPES time_cost = 0 food_cost = 0 food_provided = 0 need = None is_fleeing = False ai_mode = None can_switch_ai_mode = False storable_resource_types = () storage_bonus = () is_buildable_anywhere = True transport_capacity = 0 transport_volume = 1 requirements = () is_a = () can_build = () can_train = () can_use = () can_research = () can_upgrade_to = () armor = 0 damage = 0 basic_abilities = [] is_vulnerable = True is_healable = True is_a_gate = False provides_survival = False damage_radius = 0 target_types = ["ground"] range = None is_ballistic = 0 minimal_range = 0 cooldown = None next_attack_time = 0 splash = False player = None number = None expanded_is_a = () time_limit = None rallying_point = None corpse = 1 decay = 0 presence = 1 is_an_explorer = False def next_free_number(self): numbers = [u.number for u in self.player.units if u.type_name == self.type_name and u is not self] n = 1 while n in numbers: n += 1 return n def set_player(self, player): # stop current action self.action_target = None self.cancel_all_orders(unpay=False) # remove from previous player if self.player is not None: self.player.units.remove(self) self.player.food -= self.food_provided self.player.used_food -= self.food_cost self.update_all_dicts(-1) # add to new player self.player = player if player is not None: self.number = self.next_free_number() player.units.append(self) self.player.food += self.food_provided self.player.used_food += self.food_cost self.update_all_dicts(1) self.upgrade_to_player_level() # player units must stop attacking the "not hostile anymore" unit for u in player.units: if u.action_target is self: u.action_target = None # update perception of object by the players if self.place is not None: self.update_perception() # if transporting units, set player for them too for o in self.objects: o.set_player(player) def __init__(self, prototype, player, place, x, y, o=90): if prototype is not None: prototype.init_dict(self) self.orders = [] # transport data self.objects = [] self.world = place.world # XXXXXXXXXX required by transport # set a player self.set_player(player) # stats "with a max" self.hp = self.hp_max self.mana = self.mana_max # move to initial place Entity.__init__(self, place, x, y, o) if self.decay: self.time_limit = self.world.time + self.decay def upgrade_to_player_level(self): for upg in self.can_use: if upg in self.player.upgrades: self.player.world.unit_class(upg).upgrade_unit_to_player_level(self) @property def upgrades(self): return [u for u in self.can_use if u in self.player.upgrades] def contains_enemy(self, player): # XXXXXXXXXX required by transport return False @property def height(self): if self.airground_type == "air": return 2 else: return self.place.height + self.bonus_height def get_observed_squares(self): if self.is_inside or self.place is None: return [] result = [self.place] for sq in self.place.neighbours: if self.height > sq.height \ or self.height == sq.height and self._can_go(sq.x, sq.y): result.append(sq) return result @property def menace(self): return self.damage @property def activity(self): if not self.orders: return o = self.orders[0] if hasattr(o, "mode") and o.mode == "build": return "building" if hasattr(o, "mode") and o.mode == "gather" and hasattr(o.target, "type_name"): return "exploiting_%s" % o.target.type_name # reach (avoiding collisions) def _already_walked(self, x, y): n = 0 radius_2 = self.radius * self.radius for lw, xw, yw, weight in self.walked: if self.place is lw and square_of_distance(x, y, xw, yw) < radius_2: n += weight return n def _future_coords(self, rotation, target_d): d = self.speed * VIRTUAL_TIME_INTERVAL / 1000 if rotation == 0: d = min(d, target_d) # stop before colliding target a = self.o + rotation x = self.x + d * int_cos_1000(a) / 1000 y = self.y + d * int_sin_1000(a) / 1000 return x, y def _heuristic_value(self, rotation, target_d): x, y = self._future_coords(rotation, target_d) return abs(rotation) + self._already_walked(x, y) * 200 def _can_go(self, x, y): if self.airground_type != "ground": return True new_place = self.world.get_place_from_xy(x, y) if new_place is self.place: return True for e in self.place.exits: if e.other_side.place is new_place: if e.is_blocked(self): for o in e.blockers: self.player.observe(o) else: return True def _try(self, rotation, target_d): x, y = self._future_coords(rotation, target_d) if self.place.contains(x, y) and not self.would_collide_if(x, y): if abs(rotation) >= 90: self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end self.move_to(self.place, x, y, self.o + rotation) self.unblock() return True elif not self.place.contains(x, y) and self._can_go(x, y) and not self.would_collide_if(x, y): if abs(rotation) >= 90: self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end new_place = self.world.get_place_from_xy(x, y) self.move_to(new_place, x, y, self.o + rotation) self.unblock() return True return False _rotations = None _smooth_rotations = None def _reach(self, target_d): if self._smooth_rotations: # "smooth rotation" mode rotation = self._smooth_rotations.pop(0) if self._try(rotation, target_d) or self._try(-rotation, target_d): self._smooth_rotations = [] else: if not self._rotations: # update memory self.walked = [x[0:3] + [x[3] - 1] for x in self.walked if x[3] > 1] # "go straight" mode if not self.walked and self._try(0, target_d): return # enter "rotation mode" self._rotations = [(self._heuristic_value(x, target_d), x) for x in (0, 45, -45, 90, -90, 135, -135, 180)] self._rotations.sort() # "rotation" mode for _ in range(min(4, len(self._rotations))): _, rotation = self._rotations.pop(0) if self._try(rotation, target_d): self._rotations = [] return if not self._rotations: # enter "smooth rotation mode" self._smooth_rotations = range(1, 180, 1) self.walked = [] self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end self.notify("collision") # go center def _go_center(self): self.action_target = (self.place.x, self.place.y) def _near_enough_to_use(self, target): if self.is_an_enemy(target): # Melee units (range <= 2) shouldn't attack units # on the other side of a wall. if not self._can_go(target.x, target.y) \ and self.range <= 2 * PRECISION \ and not target.blocked_exit: return False if self.minimal_range and square_of_distance(self.x, self.y, target.x, target.y) < self.minimal_range * self.minimal_range: return False d = target.use_range(self) return square_of_distance(self.x, self.y, target.x, target.y) < d * d elif target.place is self.place: d = target.use_range(self) return square_of_distance(self.x, self.y, target.x, target.y) < d * d def be_used_by(self, actor): if actor.is_an_enemy(self): actor.aim(self) # reach and use def action_reach_and_use(self): target = self.action_target if not self._near_enough_to_use(target): d = int_distance(self.x, self.y, target.x, target.y) self.o = int_angle(self.x, self.y, target.x, target.y) # turn toward the goal self._reach(d - target.collision_range(self)) else: self.walked = [] target.be_used_by(self) def go_to_xy(self, x, y): d = int_distance(self.x, self.y, x, y) if d > self.radius: # execute action self.o = int_angle(self.x, self.y, x, y) # turn towards the goal self._reach(d) else: return True # update def has_imperative_orders(self): return self.orders and self.orders[0].is_imperative def _execute_orders(self): queue = self.orders if queue[0].is_complete or queue[0].is_impossible: queue.pop(0) else: queue[0].update() def _is_attacking(self): return isinstance(self.action, AttackAction) def update(self): assert isinstance(self.hp, int) assert isinstance(self.mana, int) assert isinstance(self.x, int) assert isinstance(self.y, int) assert isinstance(self.o, int) self.is_moving = False # do nothing if inside if self.is_inside: return # passive level (aura) if self.heal_level: self.heal_nearby_units() if self.harm_level: self.harm_nearby_units() # action level if self.action: self.action.update() # order level (warning: completing UpgradeToOrder deletes the object) if self.has_imperative_orders(): self._execute_orders() else: # TODO: use triggers (to optimize) if not self._is_attacking(): self.choose_enemy() # execute orders if the unit is not fighting (targeting an enemy) if self.orders and not self._is_attacking(): # # experimental: execute orders if no current action # if self.orders and not self.action_type: self._execute_orders() # slow update def regenerate(self): if self.mana_regen and self.mana < self.mana_max: self.mana = min(self.mana_max, self.mana + self.mana_regen) def slow_update(self): self.regenerate() if self.time_limit is not None and self.place.world.time >= self.time_limit: self.die() def receive_hit(self, damage, attacker, notify=True): self.hp -= damage if self.hp < 0: self.die(attacker) else: self.on_wounded(attacker, notify) def delete(self): # delete first, because if self.player is None the player will miss the # deletion and keep a memory of his own deleted unit Entity.delete(self) self.set_player(None) def die(self, attacker): for o in self.objects[:]: o.move_to(self.place, self.x, self.y) if o.place is self: # not enough space o.collision = 0 o.move_to(self.place, self.x, self.y) if self.airground_type == "air": o.die(attacker) self.notify("death") if attacker is not None: self.notify("death_by,%s" % attacker.id) self.player.on_unit_attacked(self, attacker) for u in self.place.objects: u.react_death(self) self.delete() heal_level = 0 def heal_nearby_units(self): # level 1 of healing: 1 hp every 7.5 seconds hp = self.heal_level * PRECISION / 25 for p in self.player.allied: for u in p.units: if u.is_healable and u.place is self.place: if u.hp < u.hp_max: u.hp = min(u.hp_max, u.hp + hp) harm_level = 0 harm_target_type = () def can_harm(self, other): d = self.world.harm_target_types k = (self.type_name, other.type_name) if k not in d: result = True for t in self.harm_target_type: if t == "healable" and not other.is_healable or \ t == "building" and not isinstance(other, _Building) or \ t in ("air", "ground") and other.airground_type != t or \ t == "unit" and not isinstance(other, Unit) or \ t == "undead" and not other.is_undead: result = False break d[k] = result return d[k] def harm_nearby_units(self): # level 1: 1 hp every 7.5 seconds hp = self.harm_level * PRECISION / 25 for u in self.place.objects: if u.is_vulnerable and self.can_harm(u): u.receive_hit(hp, self, notify=False) def is_an_enemy(self, c): if isinstance(c, Creature): if self.has_imperative_orders() and \ self.orders[0].__class__ == GoOrder and \ self.orders[0].target is c: return True else: return self.player.is_an_enemy(c.player) else: return False # choose enemy def can_attack_if_in_range(self, other): if self.is_inside: return False if other not in self.player.perception: return False if other is None \ or getattr(other, "hp", 0) < 0 \ or getattr(other, "airground_type", None) not in self.target_types: return False if not other.is_vulnerable: return False return True def can_attack(self, other): # without moving to another square # assert other in self.player.perception # XXX false # assert not self.is_inside # XXX not sure if not self.can_attack_if_in_range(other): return False if self.range and other.place is self.place: return True if self.place.is_near(other.place): return self._near_enough_to_use(other) ## def _can_be_reached_by(self, player): ## for u in player.units: ## if u.can_attack(self): ## return True ## return False def _choose_enemy(self, place): known = self.player.known_enemies(place) reachable_enemies = [x for x in known if self.can_attack(x)] if reachable_enemies: reachable_enemies.sort(key=lambda x: (- x.value, square_of_distance(self.x, self.y, x.x, x.y), x.id)) self.action_target = reachable_enemies[0] # attack nearest self.notify("attack") # XXX move this into set_action_target()? return True ## else: ## for u in enemy_units: ## if u.can_attack(self) and not u._can_be_reached_by(self.player): ## self.flee() ## return def choose_enemy(self, someone=None): if self.has_imperative_orders() or self.is_fleeing: return if not self.damage: return if getattr(self.action_target, "menace", 0): return if someone is not None and self.can_attack(someone): self.action_target = someone self.notify("attack") # XXX move this into set_action_target()? return if self._choose_enemy(self.place): return for p in self.place.neighbours: if self._choose_enemy(p): break # def on_wounded(self, attacker, notify): if self.player is not None: self.player.observe(attacker) # Why level 0 only for "wounded,type,0": # maybe a single sound would be better: simpler, # allowing more levels of upgrade, and examining # unit upgrades in the stats is better? if notify: self.notify("wounded,%s,%s,%s" % (attacker.type_name, attacker.id, 0)) # react only if this is an external attack if self.player is not attacker.player and \ attacker.is_vulnerable and \ attacker in self.player.perception: self.player.on_unit_attacked(self, attacker) for u in self.player.units: if u.place == self.place: u.on_friend_unit_attacked(attacker) def on_friend_unit_attacked(self, attacker): if self.has_imperative_orders() or self.is_fleeing: return if getattr(self.action_target, "menace", 0) < attacker.menace: if self.can_attack(attacker): self.action_target = attacker elif not self.orders and self.place.is_near(attacker.place): self.take_default_order(attacker.id) self.take_order(("go", "zoom-%s-%s-%s" % (self.place.name, self.x, self.y)), forget_previous=False) def react_death(self, creature): if self.action_target == creature: self.action_target = None self.choose_enemy() self.player.update_attack_squares(self) # XXXXXXX ? elif self.place == creature.place: self._flee_or_fight() def react_departure(self, someone, unused_door): if someone == self.action_target: self.action_target = None self.choose_enemy() # choose another enemy def _flee_or_fight_if_enemy(self): if self.place.contains_enemy(self.player): self._flee_or_fight() def _flee_or_fight(self, someone=None): if self.has_imperative_orders() or self.is_fleeing: return if self.ai_mode == "defensive": if self.place.balance(self.player) >= 0: self.choose_enemy(someone) else: self.flee(someone) elif self.ai_mode == "offensive": self.choose_enemy(someone) def react_arrival(self, someone): if self.place is someone.place: self._flee_or_fight(someone) def flee(self, someone=None): self.notify("flee") self.player.on_unit_flee(self) self.orders = [] if self.place.exits: def menace(e): return (- e.other_side.place.balance(self.player), e.id) self.action_target = sorted(self.place.exits, key=menace)[0] self.is_fleeing = True def react_self_arrival(self): if self.is_fleeing: self.is_fleeing = False self._go_center() # don't block the passage self._flee_or_fight_if_enemy() self.notify("enter_square") self.player.update_attack_squares(self) # attack def hit(self, target): damage = max(0, self.damage - target.armor) target.receive_hit(damage, self) def splash_aim(self, target): damage_radius_2 = self.damage_radius * self.damage_radius for o in target.place.objects[:]: if not self.is_an_enemy(o): pass # no friendly fire elif isinstance(o, Creature) \ and square_of_distance(o.x, o.y, target.x, target.y) <= damage_radius_2 \ and self.can_attack_if_in_range(o): self.hit(o) def aim(self, target): if self.can_attack(target) and self.place.world.time >= self.next_attack_time: self.next_attack_time = self.place.world.time + self.cooldown self.notify("launch_attack") if self.splash: self.splash_aim(target) else: self.hit(target) # orders def take_order(self, o, forget_previous=True, imperative=False, order_id=None): if self.is_inside: self.place.notify("order_impossible") return cls = ORDERS_DICT.get(o[0]) if cls is None: warning("unknown order: %s", o) return if not cls.is_allowed(self, *o[1:]): self.notify("order_impossible") debug("wrong order to %s: %s", self.type_name, o) return if forget_previous and not cls.never_forget_previous: self.cancel_all_orders() order = cls(self, o[1:]) order.id = order_id if imperative: order.is_imperative = imperative order.immediate_action() def get_default_order(self, target_id): if target_id and target_id.startswith("zoom"): return "go" target = self.player.get_object_by_id(target_id) if not target: return elif getattr(target, "is_an_exit", False): return "block" elif getattr(target, "player", None) is self.player and self.have_enough_space(target): return "load" elif getattr(target, "player", None) is self.player and target.have_enough_space(self): return "enter" elif "gather" in self.basic_abilities and isinstance(target, Deposit): return "gather" elif (isinstance(target, BuildingSite) and target.type.__name__ in self.can_build or hasattr(target, "is_repairable") and target.is_repairable and target.hp < target.hp_max and self.can_build) \ and not self.is_an_enemy(target): return "repair" elif RallyingPointOrder.is_allowed(self): return "rallying_point" elif GoOrder.is_allowed(self): return "go" def take_default_order(self, target_id, forget_previous=True, imperative=False, order_id=None): order = self.get_default_order(target_id) if order: self.take_order([order, target_id], forget_previous, imperative, order_id) def check_if_enough_resources(self, cost, food=0): for i, c in enumerate(cost): if self.player.resources[i] < c: return "not_enough_resource_%s" % i if not self.orders and food > 0 and self.player.available_food < self.player.used_food + food: if self.player.available_food < self.player.world.food_limit: return "not_enough_food" else: return "population_limit_reached" # cancel production def cancel_all_orders(self, unpay=True): while self.orders: self.orders.pop().cancel(unpay) def must_build(self, order): for o in self.orders: if o == order: return True def _put_building_site(self, type, target): place, x, y, _id = target.place, target.x, target.y, target.id # remember before deletion if not hasattr(place, "place"): # target is a square place = target if not (getattr(target, "is_an_exit", False) or type.is_buildable_anywhere): target.delete() # remove the meadow replaced by the building remember_land = True else: remember_land = False site = BuildingSite(self.player, place, x, y, type) if remember_land: site.building_land = target if getattr(target, "is_an_exit", False): site.block(target) # update the orders of the workers order = self.orders[0] for unit in self.player.units: if unit is self: continue for n in range(len(unit.orders)): try: if unit.orders[n] == order: # why not before: unit.orders[n].cancel() ? unit.orders[n] = BuildPhaseTwoOrder(unit, [site.id]) # the other peasants help the first one unit.orders[n].on_queued() except: # if order is not a string? exception("couldn't check unit order") self.orders[0] = BuildPhaseTwoOrder(self, [site.id]) self.orders[0].on_queued() # be repaired def _delta(self, total, percentage): # (percentage / 100) * total / (self.time_cost / VIRTUAL_TIME_INTERVAL) # (reordered for a better precision) delta = int(total * percentage * VIRTUAL_TIME_INTERVAL / self.time_cost / 100) if delta == 0 and total != 0: warning("insufficient precision (delta: %s total: %s)", delta, total) return delta @property def hp_delta(self): return self._delta(self.hp_max, 70) @property def repair_cost(self): return (self._delta(c, 30) for c in self.cost) def be_built(self): # TODO: when allied players, the unit's player should pay, not the building's if self.hp < self.hp_max: result = self.check_if_enough_resources(self.repair_cost) if result is not None: self.notify("order_impossible,%s" % result) else: self.player.pay(self.repair_cost) self.hp = min(self.hp + self.hp_delta, self.hp_max) @property def is_fully_repaired(self): return getattr(self, "is_repairable", False) and self.hp == self.hp_max # transport def have_enough_space(self, target): s = self.transport_capacity for u in self.objects: s -= u.transport_volume return s >= target.transport_volume def load(self, target): target.cancel_all_orders() target.notify("enter") target.move_to(self, 0, 0) def load_all(self): for u in sorted(self.player.units, key=lambda x: x.transport_volume, reverse=True): if u.place is self.place and self.have_enough_space(u): self.load(u) def unload_all(self): for o in self.objects[:]: o.move_to(self.place, self.x, self.y) o.notify("exit")
class Creature(Entity): type_name = None def get_action_target(self): if self.action: return self.action.target def set_action_target(self, value): if isinstance(value, tuple): self.action = MoveXYAction(self, value) elif self.is_an_enemy(value): self.action = AttackAction(self, value) elif value is not None: self.action = MoveAction(self, value) # "use" ? else: self.action = Action(self, value) action_target = property(get_action_target, set_action_target) hp_max = 0 hp_regen = 0 mana_max = 0 mana_start = 0 mana_regen = 0 walked = [] cost = (0,) * MAX_NB_OF_RESOURCE_TYPES time_cost = 0 food_cost = 0 food_provided = 0 is_fleeing = False ai_mode = None can_switch_ai_mode = False storable_resource_types = () storage_bonus = () is_buildable_anywhere = True transport_capacity = 0 transport_volume = 1 requirements = () is_a = () can_build = () can_train = () can_use = () can_research = () can_upgrade_to = () armor = 0 damage = 0 damage_level = 0 basic_abilities = [] is_vulnerable = True is_healable = True is_a_gate = False provides_survival = False damage_radius = 0 target_types = ["ground"] range = None is_ballistic = 0 minimal_range = 0 cooldown = 0 next_attack_time = 0 splash = False player = None number = None expanded_is_a = () time_limit = None rallying_point = None corpse = 1 decay = 0 presence = 1 is_an_explorer = False count_limit = 0 def next_free_number(self): numbers = [u.number for u in self.player.units if u.type_name == self.type_name and u is not self] n = 1 while n in numbers: n += 1 return n def set_player(self, player): # stop current action self.action_target = None self.cancel_all_orders(unpay=False) # remove from previous player if self.player is not None: self.player.units.remove(self) self.player.food -= self.food_provided self.player.used_food -= self.food_cost self.update_all_dicts(-1) # add to new player self.player = player if player is not None: self.number = self.next_free_number() player.units.append(self) self.player.food += self.food_provided self.player.used_food += self.food_cost self.update_all_dicts(1) self.upgrade_to_player_level() # player units must stop attacking the "not hostile anymore" unit for u in player.units: if u.action_target is self: u.action_target = None # update perception of object by the players if self.place is not None: self.update_perception() # if transporting units, set player for them too for o in self.objects: o.set_player(player) def __init__(self, prototype, player, place, x, y, o=90): if prototype is not None: prototype.init_dict(self) self.orders = [] # transport data self.objects = [] self.world = place.world # XXXXXXXXXX required by transport # set a player self.set_player(player) # stats "with a max" self.hp = self.hp_max if self.mana_start > 0: self.mana = self.mana_start else: self.mana = self.mana_max # stat defined for the whole game self.minimal_damage = rules.get("parameters", "minimal_damage") if self.minimal_damage is None: self.minimal_damage = DEFAULT_MINIMAL_DAMAGE # move to initial place Entity.__init__(self, place, x, y, o) if self.decay: self.time_limit = self.world.time + self.decay def upgrade_to_player_level(self): for upg in self.can_use: if upg in self.player.upgrades: self.player.world.unit_class(upg).upgrade_unit_to_player_level(self) @property def upgrades(self): return [u for u in self.can_use if u in self.player.upgrades] def contains_enemy(self, player): # XXXXXXXXXX required by transport return False @property def height(self): if self.airground_type == "air": return 2 else: return self.place.height + self.bonus_height def get_observed_squares(self): if self.is_inside or self.place is None: return [] result = [self.place] for sq in self.place.neighbours: # Blockers (walls, etc) are ignored for now because if a blocker disappears # it would be necessary to update the player's perception. if self.height > sq.height \ or self.height == sq.height and self._can_go(sq.x, sq.y, ignore_blockers=True): result.append(sq) return result @property def menace(self): return self.damage @property def activity(self): if not self.orders: return o = self.orders[0] if hasattr(o, "mode") and o.mode == "build": return "building" if hasattr(o, "mode") and o.mode == "gather" and hasattr(o.target, "type_name"): return "exploiting_%s" % o.target.type_name # reach (avoiding collisions) def _already_walked(self, x, y): n = 0 radius_2 = self.radius * self.radius for lw, xw, yw, weight in self.walked: if self.place is lw and square_of_distance(x, y, xw, yw) < radius_2: n += weight return n def _future_coords(self, rotation, target_d): d = self.speed * VIRTUAL_TIME_INTERVAL / 1000 if rotation == 0: d = min(d, target_d) # stop before colliding target a = self.o + rotation x = self.x + d * int_cos_1000(a) / 1000 y = self.y + d * int_sin_1000(a) / 1000 return x, y def _heuristic_value(self, rotation, target_d): x, y = self._future_coords(rotation, target_d) return abs(rotation) + self._already_walked(x, y) * 200 def _can_go(self, x, y, ignore_blockers=False): if self.airground_type != "ground": return True new_place = self.world.get_place_from_xy(x, y) if new_place is self.place: return True for e in self.place.exits: if e.other_side.place is new_place: if ignore_blockers: return True if e.is_blocked(self): for o in e.blockers: self.player.observe(o) else: return True def _try(self, rotation, target_d): x, y = self._future_coords(rotation, target_d) if self.place.contains(x, y) and not self.would_collide_if(x, y): if abs(rotation) >= 90: self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end self.move_to(self.place, x, y, self.o + rotation) self.unblock() return True elif not self.place.contains(x, y) and self._can_go(x, y) and not self.would_collide_if(x, y): if abs(rotation) >= 90: self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end new_place = self.world.get_place_from_xy(x, y) self.move_to(new_place, x, y, self.o + rotation) self.unblock() return True return False _rotations = None _smooth_rotations = None def _reach(self, target_d): if self._smooth_rotations: # "smooth rotation" mode rotation = self._smooth_rotations.pop(0) if self._try(rotation, target_d) or self._try(-rotation, target_d): self._smooth_rotations = [] else: if not self._rotations: # update memory self.walked = [x[0:3] + [x[3] - 1] for x in self.walked if x[3] > 1] # "go straight" mode if not self.walked and self._try(0, target_d): return # enter "rotation mode" self._rotations = [(self._heuristic_value(x, target_d), x) for x in (0, 45, -45, 90, -90, 135, -135, 180)] self._rotations.sort() # "rotation" mode for _ in range(min(4, len(self._rotations))): _, rotation = self._rotations.pop(0) if self._try(rotation, target_d): self._rotations = [] return if not self._rotations: # enter "smooth rotation mode" self._smooth_rotations = range(1, 180, 1) self.walked = [] self.walked.append([self.place, self.x, self.y, 5]) # mark the dead end self.notify("collision") # go center def _go_center(self): self.action_target = (self.place.x, self.place.y) def _near_enough_to_aim(self, target): # Melee units (range <= 2) shouldn't attack units # on the other side of a wall. if not self._can_go(target.x, target.y) \ and self.range <= 2 * PRECISION \ and not target.blocked_exit: return False if self.minimal_range and square_of_distance(self.x, self.y, target.x, target.y) < self.minimal_range * self.minimal_range: return False d = target.aim_range(self) return square_of_distance(self.x, self.y, target.x, target.y) < d * d def _near_enough_to_use(self, target): if self.is_an_enemy(target): return self._near_enough_to_aim(target) elif target.place is self.place: d = target.use_range(self) return square_of_distance(self.x, self.y, target.x, target.y) < d * d def be_used_by(self, actor): if actor.is_an_enemy(self): actor.aim(self) # reach and use def action_reach_and_use(self): target = self.action_target if not self._near_enough_to_use(target): d = int_distance(self.x, self.y, target.x, target.y) self.o = int_angle(self.x, self.y, target.x, target.y) # turn toward the goal self._reach(d - target.collision_range(self)) else: self.walked = [] target.be_used_by(self) def go_to_xy(self, x, y): d = int_distance(self.x, self.y, x, y) if d > self.radius: # execute action self.o = int_angle(self.x, self.y, x, y) # turn towards the goal self._reach(d) else: return True # update def has_imperative_orders(self): return self.orders and self.orders[0].is_imperative def _execute_orders(self): queue = self.orders if queue[0].is_complete or queue[0].is_impossible: queue.pop(0) else: queue[0].update() def _is_attacking(self): return isinstance(self.action, AttackAction) def update(self): assert isinstance(self.hp, (int, long)) assert isinstance(self.mana, (int, long)) assert isinstance(self.x, int) assert isinstance(self.y, int) assert isinstance(self.o, int) self.is_moving = False # do nothing if inside if self.is_inside: return # passive level (aura) if self.heal_level: self.heal_nearby_units() if self.harm_level: self.harm_nearby_units() # action level if self.action: self.action.update() # order level (warning: completing UpgradeToOrder deletes the object) if self.has_imperative_orders(): self._execute_orders() else: # TODO: use triggers (to optimize) if not self._is_attacking(): self.choose_enemy() # execute orders if the unit is not fighting (targeting an enemy) if self.orders and not self._is_attacking(): # # experimental: execute orders if no current action # if self.orders and not self.action_type: self._execute_orders() # slow update def regenerate(self): if self.hp_regen and self.hp < self.hp_max: self.hp = min(self.hp_max, self.hp + self.hp_regen) if self.mana_regen and self.mana < self.mana_max: self.mana = min(self.mana_max, self.mana + self.mana_regen) def slow_update(self): self.regenerate() if self.time_limit is not None and self.place.world.time >= self.time_limit: self.die() def receive_hit(self, damage, attacker, notify=True): self.hp -= damage if self.hp < 0: self.die(attacker) else: self.on_wounded(attacker, notify) def delete(self): # delete first, because if self.player is None the player will miss the # deletion and keep a memory of his own deleted unit Entity.delete(self) self.set_player(None) def die(self, attacker): for o in self.objects[:]: o.move_to(self.place, self.x, self.y) if o.place is self: # not enough space o.collision = 0 o.move_to(self.place, self.x, self.y) if self.airground_type == "air": o.die(attacker) self.notify("death") if attacker is not None: self.notify("death_by,%s" % attacker.id) self.player.on_unit_attacked(self, attacker) for u in self.place.objects: u.react_death(self) self.delete() heal_level = 0 def heal_nearby_units(self): # level 1 of healing: 1 hp every 7.5 seconds hp = self.heal_level * PRECISION / 25 allies = self.player.allied units = self.world.get_objects(self.x, self.y, 6 * PRECISION, filter=lambda x: x.player in allies and x.is_healable and x.hp < x.hp_max) for u in units: u.hp = min(u.hp_max, u.hp + hp) harm_level = 0 harm_target_type = () def can_harm(self, other): try: return self.world.harm_target_types[(self.type_name, other.type_name)] except: result = True for t in self.harm_target_type: if t == "healable" and not other.is_healable or \ t == "building" and not isinstance(other, _Building) or \ t in ("air", "ground") and other.airground_type != t or \ t == "unit" and not isinstance(other, Unit) or \ t == "undead" and not other.is_undead: result = False break self.world.harm_target_types[(self.type_name, other.type_name)] = result return result def harm_nearby_units(self): # level 1: 1 hp every 7.5 seconds hp = self.harm_level * PRECISION / 25 units = self.world.get_objects(self.x, self.y, 6 * PRECISION, filter=lambda x: x.is_vulnerable and self.can_harm(x)) for u in units: u.receive_hit(hp, self, notify=False) def is_an_enemy(self, c): if isinstance(c, Creature): if self.has_imperative_orders() and \ self.orders[0].__class__ == GoOrder and \ self.orders[0].target is c: return True else: return self.player.is_an_enemy(c.player) else: return False # choose enemy def can_attack_if_in_range(self, other): if self.is_inside: return False if other not in self.player.perception: return False if other is None \ or getattr(other, "hp", 0) < 0 \ or getattr(other, "airground_type", None) not in self.target_types: return False if not other.is_vulnerable: return False return True def can_attack(self, other): # without moving to another square # assert other in self.player.perception # XXX false # assert not self.is_inside # XXX not sure if not self.can_attack_if_in_range(other): return False if self.range and other.place is self.place: return True return self._near_enough_to_aim(other) ## def _can_be_reached_by(self, player): ## for u in player.units: ## if u.can_attack(self): ## return True ## return False def _choose_enemy(self, place): known = self.player.known_enemies(place) reachable_enemies = [x for x in known if self.can_attack(x)] if reachable_enemies: reachable_enemies.sort(key=lambda x: (- x.value, square_of_distance(self.x, self.y, x.x, x.y), x.id)) self.action_target = reachable_enemies[0] # attack nearest self.notify("attack") # XXX move this into set_action_target()? return True ## else: ## for u in enemy_units: ## if u.can_attack(self) and not u._can_be_reached_by(self.player): ## self.flee() ## return def choose_enemy(self, someone=None): if self.has_imperative_orders() or self.is_fleeing: return if not self.damage: return if getattr(self.action_target, "menace", 0): return if someone is not None and self.can_attack(someone): self.action_target = someone self.notify("attack") # XXX move this into set_action_target()? return # _choose_enemy requires that self.player is not None if self.player is None: warning("in choose_enemy: %s.player is None", self) return if self._choose_enemy(self.place): return for p in self.place.neighbours: if self._choose_enemy(p): break # def on_wounded(self, attacker, notify): if self.player is not None: self.player.observe(attacker) if notify: self.notify("wounded,%s,%s,%s" % (attacker.type_name, attacker.id, attacker.damage_level)) # react only if this is an external attack if self.player is not attacker.player and \ attacker.is_vulnerable and \ attacker in self.player.perception: self.player.on_unit_attacked(self, attacker) for u in self.player.units: if u.place == self.place: u.on_friend_unit_attacked(attacker) def on_friend_unit_attacked(self, attacker): if self.has_imperative_orders() or self.is_fleeing: return if getattr(self.action_target, "menace", 0) < attacker.menace: if self.can_attack(attacker): self.action_target = attacker elif not self.orders and self.place.is_near(attacker.place): self.take_default_order(attacker.id) self.take_order(("go", "zoom-%s-%s-%s" % (self.place.name, self.x, self.y)), forget_previous=False) def react_death(self, creature): if self.action_target == creature: self.action_target = None self.choose_enemy() self.player.update_attack_squares(self) # XXXXXXX ? elif self.place == creature.place: self._flee_or_fight() def react_departure(self, someone, unused_door): if someone == self.action_target: self.action_target = None self.choose_enemy() # choose another enemy def _flee_or_fight_if_enemy(self): if self.place.contains_enemy(self.player): self._flee_or_fight() def _flee_or_fight(self, someone=None): if self.has_imperative_orders() or self.is_fleeing: return if self.ai_mode == "defensive": if self.place.balance(self.player) >= 0: self.choose_enemy(someone) else: self.flee(someone) elif self.ai_mode == "offensive": self.choose_enemy(someone) def react_arrival(self, someone): if self.place is someone.place: self._flee_or_fight(someone) def flee(self, someone=None): self.notify("flee") self.player.on_unit_flee(self) self.orders = [] if self.place.exits: def menace(e): return (- e.other_side.place.balance(self.player), e.id) self.action_target = sorted(self.place.exits, key=menace)[0] self.is_fleeing = True def react_self_arrival(self): if self.is_fleeing: self.is_fleeing = False self._go_center() # don't block the passage self._flee_or_fight_if_enemy() self.notify("enter_square") self.player.update_attack_squares(self) # attack def hit(self, target): damage = max(self.minimal_damage, self.damage - target.armor) target.receive_hit(damage, self) def splash_aim(self, target): damage_radius_2 = self.damage_radius * self.damage_radius for o in target.place.objects[:]: if not self.is_an_enemy(o): pass # no friendly fire elif isinstance(o, Creature) \ and square_of_distance(o.x, o.y, target.x, target.y) <= damage_radius_2 \ and self.can_attack_if_in_range(o): self.hit(o) def aim(self, target): if self.can_attack(target) and self.place.world.time >= self.next_attack_time: self.next_attack_time = self.place.world.time + self.cooldown self.notify("launch_attack") if self.splash: self.splash_aim(target) else: self.hit(target) # orders def take_order(self, o, forget_previous=True, imperative=False, order_id=None): if self.is_inside: self.place.notify("order_impossible") return cls = ORDERS_DICT.get(o[0]) if cls is None: warning("unknown order: %s", o) return if not cls.is_allowed(self, *o[1:]): self.notify("order_impossible") debug("wrong order to %s: %s", self.type_name, o) return if forget_previous and not cls.never_forget_previous: self.cancel_all_orders() order = cls(self, o[1:]) order.id = order_id if imperative: order.is_imperative = imperative order.immediate_action() def get_default_order(self, target_id): if target_id and target_id.startswith("zoom"): return "go" target = self.player.get_object_by_id(target_id) if not target: return elif getattr(target, "is_an_exit", False): return "block" elif getattr(target, "player", None) is self.player and self.have_enough_space(target): return "load" elif getattr(target, "player", None) is self.player and target.have_enough_space(self): return "enter" elif "gather" in self.basic_abilities and isinstance(target, Deposit): return "gather" elif (isinstance(target, BuildingSite) and target.type.__name__ in self.can_build or hasattr(target, "is_repairable") and target.is_repairable and target.hp < target.hp_max and self.can_build) \ and not self.is_an_enemy(target): return "repair" elif RallyingPointOrder.is_allowed(self): return "rallying_point" elif GoOrder.is_allowed(self): return "go" def take_default_order(self, target_id, forget_previous=True, imperative=False, order_id=None): order = self.get_default_order(target_id) if order: self.take_order([order, target_id], forget_previous, imperative, order_id) def check_if_enough_resources(self, cost, food=0): for i, c in enumerate(cost): if self.player.resources[i] < c: return "not_enough_resource_%s" % i if not self.orders and food > 0 and self.player.available_food < self.player.used_food + food: if self.player.available_food < self.player.world.food_limit: return "not_enough_food" else: return "population_limit_reached" # cancel production def cancel_all_orders(self, unpay=True): while self.orders: self.orders.pop().cancel(unpay) def must_build(self, order): for o in self.orders: if o == order: return True def _put_building_site(self, type, target): place, x, y, _id = target.place, target.x, target.y, target.id # remember before deletion if not hasattr(place, "place"): # target is a square place = target if not (getattr(target, "is_an_exit", False) or type.is_buildable_anywhere): target.delete() # remove the meadow replaced by the building remember_land = True else: remember_land = False site = BuildingSite(self.player, place, x, y, type) if remember_land: site.building_land = target if getattr(target, "is_an_exit", False): site.block(target) # update the orders of the workers order = self.orders[0] for unit in self.player.units: if unit is self: continue for n in range(len(unit.orders)): try: if unit.orders[n] == order: # why not before: unit.orders[n].cancel() ? unit.orders[n] = BuildPhaseTwoOrder(unit, [site.id]) # the other peasants help the first one unit.orders[n].on_queued() except: # if order is not a string? exception("couldn't check unit order") self.orders[0] = BuildPhaseTwoOrder(self, [site.id]) self.orders[0].on_queued() # be repaired def _delta(self, total, percentage): # (percentage / 100) * total / (self.time_cost / VIRTUAL_TIME_INTERVAL) # (reordered for a better precision) delta = int(total * percentage * VIRTUAL_TIME_INTERVAL / self.time_cost / 100) if delta == 0 and total != 0: warning("insufficient precision (delta: %s total: %s)", delta, total) return delta @property def hp_delta(self): return self._delta(self.hp_max, 70) @property def repair_cost(self): return (self._delta(c, 30) for c in self.cost) def be_built(self): # TODO: when allied players, the unit's player should pay, not the building's if self.hp < self.hp_max: result = self.check_if_enough_resources(self.repair_cost) if result is not None: self.notify("order_impossible,%s" % result) else: self.player.pay(self.repair_cost) self.hp = min(self.hp + self.hp_delta, self.hp_max) @property def is_fully_repaired(self): return getattr(self, "is_repairable", False) and self.hp == self.hp_max # transport def have_enough_space(self, target): s = self.transport_capacity for u in self.objects: s -= u.transport_volume return s >= target.transport_volume def load(self, target): target.cancel_all_orders() target.notify("enter") target.move_to(self, 0, 0) def load_all(self): for u in sorted(self.player.units, key=lambda x: x.transport_volume, reverse=True): if u.place is self.place and self.have_enough_space(u): self.load(u) def unload_all(self): for o in self.objects[:]: o.move_to(self.place, self.x, self.y) o.notify("exit")