def __init__(self, damage, accuracy=0, speed=1, range=1.5, range_increments=1, magazine_size=0, calibre=None, skill="Brawl", sprite="unarmed", ranged=False, martial_art=None): # valid damage formats: "XdY+Z" (some parts can be omitted) or (X,Y,Z) # it used to accept more complex expressions but seriously let's not do this if isinstance(damage, str): # damage_list = [] # for i in damage.split("+"): # if "d" in i: # if i.split("d")[0]: # damage_list.append(tuple(map(int, i.split("d")))) # else: # damage_list.append((1,int(i.split("d")[1]))) # else: # damage_list.append((int(i),1)) # self.damage = tuple(damage_list) x, y, z = 0, 0, 0 for i in damage.split("+"): if "d" in i: if i.split("d")[0]: x, y = map(int, i.split("d")) else: x, y = 1, int(i.split("d")[1]) else: z = int(i) self.damage = x, y, z else: self.damage = damage self.accuracy = AnnotatedValue(accuracy, "weapon accuracy") self.speed = speed self.range = AnnotatedValue(range, "weapon basic range") self.range_increments = AnnotatedValue(range_increments, "number of range increments") self.magazine_size = magazine_size self.calibre = calibre self.skill = skill self.sprite = sprite self.magazine = [] self.ranged = ranged self.martial_art = martial_art
def skillTotal(self, skill): return AnnotatedValue( self.skillBase(skill) + self.skills[skill] + self.skillModifier(skill), "{} total".format(skill))
def attributeModifier(self, attribute): return AnnotatedValue(self.attributes[attribute] // 2 - 5, "{} modifier".format(attribute))
def wounds_skill_penalty(self): modifier = 0 for wound in self.wounds: modifier += getattr(wound, "skill_penalty", 0) return AnnotatedValue(modifier, "wounds skill penalty")
def wounds_pdm_penalty(self): modifier = 0 for wound in self.wounds: modifier += getattr(wound, "pdm_penalty", 0) return AnnotatedValue(modifier, "wounds PDM penalty")
def passive_defense_modifier(self): return (AnnotatedValue(2, "PDM base") - AnnotatedValue(self.skills["Dodge"], "Dodge ranks") - AnnotatedValue(self.attributeModifier("REF"), "REF modifier") + self.wounds_pdm_penalty)
def initiative(self): return (AnnotatedValue(40, "initiative base") - AnnotatedValue(self.attributes["WIT"], "WIT") - AnnotatedValue(self.attributes["REF"], "REF"))
def attack(self, target, attack_type, attack_count=0): attacker = self.combatants[self.current_combatant] if attacker == target: # trying to attack self, aborting return False if target not in self.combatants: # target not in combat, aborting return False if not self.can_attack and attack_count == 0: # not enough AP, aborting return False if attack_type not in self.available_attacks: # selected attack cannot be performed, aborting return False weapon = attacker.inventory.hands dist = AnnotatedValue( gridhelper.distance(attacker.coords, target.coords), "distance") # calculate effective PDM if dist >= 5: pdm = AnnotatedValue(0, "PDM ignored (too far)") elif target in self.surprised_combatants: pdm = AnnotatedValue(0, "PDM ignored (surprised)") else: # using PDM if closer than 5m pdm = target.rpg_stats.passive_defense_modifier # martial artists can in some circumstances add 2 to target's PDM if (attacker.martial_art_used and not target.martial_art_used and target.rpg_stats.skillTotal("Dodge") < 12 and target.rpg_stats.skillTotal(weapon[0].weapon_data.skill if weapon else "Brawl") < 14): pdm += AnnotatedValue(2, "martial arts PDM modifier") # attack roll # TODO: golden success/tumble effects if weapon: # weapon attack if dist > (weapon[0].weapon_data.total_range): # not in range, aborting return False if (weapon[0].weapon_data.magazine_size > 0) and (len( weapon[0].weapon_data.magazine) == 0): # no ammo, aborting return False if len(weapon[0].weapon_data.magazine) > 0: # remove the bullet weapon[0].weapon_data.magazine.pop(0) # weapon skill check, or Rapid Fire skill check if ranged Full Attack skill_used = ( "Rapid Shooting" if attack_type == "Full Attack" and attacker.rpg_stats.skills["Rapid Shooting"] < attacker.rpg_stats.skills[weapon[0].weapon_data.skill] else weapon[0].weapon_data.skill) # modifier = target's PDM + weapon accuracy - range penalty - multi attack penalty attack_mods = ( pdm + weapon[0].weapon_data.accuracy - dist // weapon[0].weapon_data.range - AnnotatedValue(attack_count, "penalty for multiple attacks")) hit = attacker.rpg_stats.skillCheck(skill_used, attack_mods) # damage roll damage = weapon[0].weapon_data.damage_roll # STR bonus for balanced and heavy weapons; can't be higher than weapon's max damage if weapon[0].weapon_data.skill in ("Melee, Balanced", "Melee, Heavy"): # heavy STR bonus if (weapon[0].weapon_data.skill == "Melee, Heavy" and attacker.rpg_stats.skills["Melee, Heavy"] >= 3 and (attacker.rpg_stats.skills["Melee, Heavy"] >= 6 or not self.moved)): str_mod = max(attacker.rpg_stats.attributeModifier("STR"), attacker.rpg_stats.attributes["STR"] - 10) # regular STR bonus else: str_mod = attacker.rpg_stats.attributeModifier("STR") damage += min(str_mod, weapon[0].weapon_data.max_damage) if attacker.martial_art_used: # martial art skill check; additional damage on success # reuses the attack roll but checks against a different skill martial_art_hit = attacker.rpg_stats.skillCheck( attacker.martial_art_used, attack_mods) martial_art_hit.results = hit.results if martial_art_hit.golden: damage += AnnotatedValue( 3, annotation="martial arts golden success bonus") elif martial_art_hit.success: damage += Roll(3, annotation="martial arts damage bonus") # determine hit location if weapon[0].weapon_data.ranged: location_roll = roll(20) if location_roll >= 19: hit_location = "head" elif location_roll >= 13: hit_location = "torso" #elif location_roll >= 10: # hit_location = "right arm" elif location_roll >= 7: hit_location = "arm" #elif location_roll >= 4: # hit_location = "right leg" else: hit_location = "leg" else: if attack_type == "High attack": location_roll = roll(7) if location_roll >= 7: hit_location = "head" elif location_roll >= 5: hit_location = "torso" #elif location_roll >= 3: # hit_location = "right arm" else: hit_location = "arm" else: location_roll = roll(8) if location_roll >= 7: hit_location = "torso" #elif location_roll >= 4: # hit_location = "right leg" else: hit_location = "leg" else: # unarmed attack if dist > 1.5: # not in range, aborting return False # Brawl skill check; modifier = target's PDM hit = attacker.rpg_stats.skillCheck("Brawl", pdm) # damage roll + unarmed STR bonus damage = (Roll(3, annotation="unarmed damage") + attacker.rpg_stats.attributeModifier("STR")) if attacker.martial_art_used: # martial art skill check; additional damage on success # reuses the attack roll but checks against a different skill martial_art_hit = attacker.rpg_stats.skillCheck( attacker.martial_art_used, pdm) martial_art_hit.results = hit.results if martial_art_hit.golden: damage += AnnotatedValue( 4, annotation="martial arts golden success bonus") elif martial_art_hit.success: damage += Roll(4, annotation="martial arts damage bonus") # determine hit location if attack_type == "High attack": location_roll = roll(7) if location_roll >= 7: hit_location = "head" elif location_roll >= 5: hit_location = "torso" #elif location_roll >= 3: # hit_location = "right arm" else: hit_location = "arm" else: location_roll = roll(8) if location_roll >= 7: hit_location = "torso" #elif location_roll >= 4: # hit_location = "right leg" else: hit_location = "leg" # damage bonus from hit roll margin damage += AnnotatedValue(hit.margin, "hit roll's margin of success") // 2 # save hit results to be used later by onTargetHit self.target = target self.hit = hit self.damage = damage self.hit_location = hit_location # play the attack animation attacker.visual.attack(target.visual.instance.getLocation()) # after attacking immediately end turn #self.endTurn() self.current_AP = 0 self.application.view.clearTiles() # additional attacks if weapon: # ranged Full Attack if (attack_type == "Full attack" and not self.moved and weapon[0].weapon_data.speed > attack_count + 1): self.multi_attack.append( lambda: self.attack(target, attack_type, attack_count + 1)) # Finesse weapon with skill >= 3 elif (weapon[0].weapon_data.skill == "Melee, Finesse" and attack_count == 0 and attacker.rpg_stats.skills["Melee, Finesse"] >= 3 and (attacker.rpg_stats.skills["Melee, Finesse"] >= 6 or not self.moved)): self.multi_attack.append( lambda: self.attack(target, attack_type, attack_count + 1)) return True