Added weapons, weapon proficiencies, and equipment.

This commit is contained in:
Mark Wolfman
2018-03-28 01:29:41 -05:00
parent 2790296a7c
commit e44bb9203b
12 changed files with 655 additions and 21 deletions
+1
View File
@@ -0,0 +1 @@
from . import weapons, character
+128 -14
View File
@@ -2,8 +2,9 @@
import re
from .stats import Ability, Skill
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
from . import weapons
dice_re = re.compile('(\d+)d(\d+)')
@@ -33,6 +34,10 @@ class Character():
wisdom = Ability()
charisma = Ability()
saving_throw_proficiencies = []
skill_proficiencies = tuple()
weapon_proficienies = tuple()
proficiencies_extra = tuple()
languages = ""
# Skills
acrobatics = Skill(ability='dexterity')
animal_handling = Skill(ability='wisdom')
@@ -63,16 +68,76 @@ class Character():
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = [] # Replaced in __init__ constructor
_proficiencies_text = tuple()
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
self.weapons = []
self.set_attrs(**attrs)
def set_attrs(self, **attrs):
"""Bulk setting of attributes. Useful for loading a character from a
dictionary."""
for attr, val in attrs.items():
setattr(self, attr, val)
if attr == 'weapons':
# Treat weapons specially
for weap in val:
self.wield_weapon(weap)
else:
if not hasattr(self, attr):
warnings.warn(f"Setting unknown character attribute {attr}",
RuntimeWarning)
# Lookup general attributes
setattr(self, attr, val)
@property
def proficiencies_text(self):
final_text = ""
all_proficiencies = (self._proficiencies_text + self.proficiencies_extra)
# Create a single string out of all the proficiencies
for txt in all_proficiencies:
if not final_text:
# Capitalize the first entry
txt = txt.capitalize()
else:
# Put a comma first
txt = ", " + txt
# Add this item to the list text
final_text += txt
# Add a period at the end
final_text += '.'
return final_text
def wield_weapon(self, weapon):
"""Accepts a string and adds it to the list of wielded weapons.
Parameters
----------
weapon : str
Case-insensitive string with a name of the weapon.
"""
# Retrieve the weapon class from the weapons module
try:
NewWeapon = findattr(weapons, weapon)
except AttributeError:
raise AttributeError(f'Weapon {weapon} is not defined')
weapon_ = NewWeapon()
# Set weapon attributes based on character
if weapon_.is_finesse:
ability_mod = max(self.strength.modifier, self.dexterity.modifier)
else:
ability_mod = getattr(self, weapon_.ability).modifier
weapon_.attack_bonus += ability_mod
weapon_.bonus_damage += ability_mod
# Check for prifiency
is_proficient = (weapon_.__class__ in self.weapon_proficienies)
if is_proficient:
weapon_.attack_bonus += self.proficiency_bonus
# Save it to the array
self.weapons.append(weapon_)
@property
def hit_dice(self):
@@ -104,70 +169,119 @@ class Character():
class Barbarian(Character):
class_name = 'Barbarian'
hit_dice_faces = 12
saving_throw_proficiencies = ['strength', 'constitution']
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'simple weapons', 'martial weapons')
weapon_proficienies = (weapons.simple_weapons + weapons.martial_weapons)
class Bard(Character):
class_name = 'Bard'
hit_dice_faces = 8
saving_throw_proficiencies = ['dexterity', 'charisma']
saving_throw_proficiencies = ('dexterity', 'charisma')
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficienies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
class Cleric(Character):
class_name = 'Cleric'
hit_dice_faces = 8
saving_throw_proficiencies = ['wisdom', 'charisma']
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficienies = weapons.simple_weapons
class Druid(Character):
class_name = 'Druid'
hit_dice_faces = 8
saving_throw_proficiencies = ['intelligence', 'wisdom']
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = (
'Light armor', 'medium armor',
'shields (druids will not wear armor or use shields made of metal)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficienies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace, weapons.Quarterstaff,
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
class Fighter(Character):
class_name = 'Fighter'
hit_dice_faces = 10
saving_throw_proficiencies = ['strength', 'constitution']
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armar', 'shields', 'simple weapons', 'martial weapons')
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
class Monk(Character):
class_name = 'Monk'
hit_dice_faces = 8
saving_throw_proficiencies = ['strength', 'dexterity']
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficienies = (weapons.Shortsword,) + weapons.simple_weapons
class Paladin(Character):
class_name = 'Paladin'
hit_dice_faces = 10
saving_throw_proficiencies = ['wisdom', 'charisma']
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
class Ranger(Character):
class_name = 'Ranger'
hit_dice_faces = 10
saving_throw_proficiencies = ['strength', 'dexterity']
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
class Rogue(Character):
class_name = 'Rogue'
hit_dice_faces = 8
saving_throw_proficiencies = ['dexterity', 'intelligence']
saving_throw_proficiencies = ('dexterity', 'intelligence')
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficienies = (weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) + weapons.simple_weapons
class Sorceror(Character):
class_name = 'Sorceror'
hit_dice_faces = 6
saving_throw_proficiencies = ['constitution', 'charisma']
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficienies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class Warlock(Character):
class_name = 'Warlock'
hit_dice_faces = 8
saving_throw_proficiencies = ['wisdom', 'charisma']
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
weapon_proficienies = weapons.simple_weapons
class Wizard(Character):
class_name = 'Wizard'
hit_dice_faces = 6
saving_throw_proficiencies = ['intelligence', 'wisdom']
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficienies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
+23 -2
View File
@@ -4,6 +4,7 @@ import argparse
import importlib.util
import os
import subprocess
import warnings
from fdfgen import forge_fdf
@@ -121,6 +122,7 @@ def create_fdf(character, fdfname):
('EP', character.ep),
('GP', character.gp),
('PP', character.pp),
('Equipment', text_box(character.equipment)),
]
# Check boxes for proficiencies
ST_boxes = {
@@ -153,8 +155,22 @@ def create_fdf(character, fdfname):
'stealth': 'Check Box 39',
'survival': 'Check Box 40',
}
# Add skill proficienies
for skill in character.skill_proficiencies:
fields.append((skill_boxes[skill], 'Yes'))
# Add weapons
weapon_fields = [('Wpn Name', 'Wpn1 AtkBonus', 'Wpn1 Damage'),
('Wpn Name 2', 'Wpn2 AtkBonus ', 'Wpn2 Damage '),
('Wpn Name 3', 'Wpn3 AtkBonus ', 'Wpn3 Damage '),]
for _fields, weapon in zip(weapon_fields, character.weapons):
name_field, atk_field, dmg_field = _fields
fields.append((name_field, weapon.name))
fields.append((atk_field, mod_str(weapon.attack_bonus)))
fields.append((dmg_field, f'{weapon.damage} {weapon.damage_type}'))
# Other proficiencies and languages
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages)
fields.append(('ProficienciesLang', prof_text))
# Create the actual FDF file
fdf = forge_fdf("", fields, [], [], [])
fdf_file = open(fdfname, "wb")
@@ -207,8 +223,13 @@ def main():
filenames = [args.filename]
for filename in filenames:
print(f"Processing {os.path.splitext(filename)[0]}...", end='')
make_sheet(character_file=filename, flatten=args.flatten)
print("done")
try:
make_sheet(character_file=filename, flatten=args.flatten)
except Exception as e:
print('failed')
raise
else:
print("done")
if __name__ == '__main__':
+18
View File
@@ -1,6 +1,24 @@
import math
def findattr(obj, name):
"""Similar to builtin getattr(obj, name) but more forgiving to
whitespace and capitalization.
"""
# Come up with several options
py_name = name.replace('-', '_').replace(' ', '_')
camel_case = "".join([s.capitalize() for s in py_name.split('_')])
if hasattr(obj, py_name):
# Direct lookup
attr = getattr(obj, py_name)
elif hasattr(obj, camel_case):
# CamelCase lookup
attr = getattr(obj, camel_case)
else:
raise AttributeError(f'{obj} has no attribute {name}')
return attr
def mod_str(modifier):
"""Converts a modifier to a string, eg 2 -> '+2'."""
if modifier > 0:
+410
View File
@@ -0,0 +1,410 @@
from .stats import mod_str
class Weapon():
name = ""
cost = "0 gp"
base_damage = "1d4"
bonus_damage = 0
damage_type = "piercing"
attack_bonus = 0
weight = 1 # In lbs
properties = "Light"
ability = 'strength'
is_finesse = False
@property
def damage(self):
dam_str = str(self.base_damage)
if self.bonus_damage != 0:
dam_str += ' ' + mod_str(self.bonus_damage)
return dam_str
class Club(Weapon):
name = "Club"
cost = "1 sp"
base_damage = "1d4"
damage_type = "bludgeoning"
weight = 2
properties = "Light"
ability = 'strength'
class Dagger(Weapon):
name = "Dagger"
cost = "2 gp"
base_damage = "1d4"
damage_type = "piercing"
weight = 1
properties = "Finesse, light, thrown (range 20/60)"
is_finesse = True
ability = 'strength'
class Greatclub(Weapon):
name = "Greatclub"
cost = "2 sp"
base_damage = "1d8"
damage_type = "bludgeoning"
weight = 10
properties = "Two-handed"
ability = 'strength'
class Handaxe(Weapon):
name = "Handaxe"
cost = "5 gp"
base_damage = "1d6"
damage_type = "slashing"
weight = 2
properties = "Light, thrown (range 20/60)"
ability = 'strength'
class Javelin(Weapon):
name = "Javelin"
cost = "5 sp"
base_damage = "1d6"
damage_type = "piercing"
weight = 2
properties = "Thrown (range 30/120)"
ability = 'strength'
class LightHammer(Weapon):
name = "Light hammer"
cost = "2 gp"
base_damage = "1d4"
damage_type = "bludgeoning"
weight = 2
properties = "Light, thrown (range 20/60)"
ability = 'strength'
class Mace(Weapon):
name = "Mace"
cost = "5 gp"
base_damage = "1d6"
damage_type = "bludgeoning"
weight = 4
properties = ""
ability = 'strength'
class Quarterstaff(Weapon):
name = "Quarterstaff"
cost = "2 sp"
base_damage = "1d6"
damage_type = "bludgeoning"
weight = 4
properties = "Versatile (1d8)"
ability = 'strength'
class Sickle(Weapon):
name = "Sickle"
cost = "1 gp"
base_damage = "1d4"
damage_type = "slashing"
weight = 2
properties = "Light"
ability = 'strength'
class Spear(Weapon):
name = "Spear"
cost = "1 gp"
base_damage = "1d6"
damage_type = "piercing"
weight = 3
properties = "Thrown (range 20/60), versatile (1d8)"
ability = 'strength'
class LightCrossbow(Weapon):
name = "Light crossbow"
cost = "25 gp"
base_damage = "1d8"
damage_type = "piercing"
weight = 5
properties = "Ammunition (range 80/320, loading, two-handed"
ability = 'dexterity'
class Dart(Weapon):
name = "Dart"
cost = "5 cp"
base_damage = "1d4"
damage_type = "piercing"
weight = 0.25
properties = "Finesse, thrown (range 20/60)"
is_finesse = True
ability = 'dexterity'
class Shortbow(Weapon):
name = "Shortbow"
cost = "25 gp"
base_damage = "1d6"
damage_type = "piercing"
weight = 2
properties = "Ammunition (range 80/320), two-handed"
ability = 'dexterity'
class Sling(Weapon):
name = "Sling"
cost = "1 sp"
base_damage = "1d4"
damage_type = "bludgeoning"
weight = 0
properties = "Ammunition (range 30/120)"
ability = 'dexterity'
class Battleaxe(Weapon):
name = "Battleaxe"
cost = "10 gp"
base_damage = "1d8"
damage_type = "slashing"
weight = 4
properties = "Versatile (1d10)"
ability = 'strength'
class Flail(Weapon):
name = "Flail"
cost = "10gp"
base_damage = "1d8"
damage_type = "bludgeoning"
weight = 2
properties = ""
ability = 'strength'
class Glaive(Weapon):
name = "Glaive"
cost = "20 gp"
base_damage = "1d10"
damage_type = "slashing"
weight = 6
properties = "Heavy, reach, two-handed"
ability = 'strength'
class Greataxe(Weapon):
name = "Greataxe"
cost = "30 gp"
base_damage = "1d12"
damage_type = "slashing"
weight = 7
properties = "Heavy, two-handed"
ability = 'strength'
class Greatsword(Weapon):
name = "Greatsword"
cost = "50 gp"
base_damage = "2d6"
damage_type = "slashing"
weight = 6
properties = "Heavy, two-handed"
ability = 'strength'
class Halberd(Weapon):
name = "Halberd"
cost = "20 gp"
base_damage = "1d10"
damage_type = "slashing"
weight = 6
properties = "Heavy, reach, two-handed"
ability = 'strength'
class Lance(Weapon):
name = "Lance"
cost = "10gp"
base_damage = "1d12"
damage_type = "piercing"
weight = 6
properties = "Reach, special"
ability = 'strength'
class Longsword(Weapon):
name = "Longsword"
cost = "15 gp"
base_damage = "1d8"
damage_type = "slashing"
weight = 3
properties = "Versatile (1d10)"
ability = 'strength'
class Maul(Weapon):
name = "Maul"
cost = "10 gp"
base_damage = "2d6"
damage_type = "bludgeoning"
weight = 10
properties = "Heavy, two-handed"
ability = 'strength'
class Morningstar(Weapon):
name = "Morningstar"
cost = "15 gp"
base_damage = "1d8"
damage_type = "piercing"
weight = 4
properties = ""
ability = 'strength'
class Pike(Weapon):
name = "Pike"
cost = "5 gp"
base_damage = "1d10"
damage_type = "piercing"
weight = 18
properties = "Heavy, reach, two-handed"
ability = 'strength'
class Rapier(Weapon):
name = "Rapier"
cost = "25 gp"
base_damage = "1d8"
damage_type = "piercing"
weight = 2
properties = "Finesse"
is_finesse = True
ability = 'strength'
class Scimitar(Weapon):
name = "Scimitar"
cost = "25 gp"
base_damage = "1d6"
damage_type = "slashing"
weight = 3
properties = "Finesse, light"
is_finesse = True
ability = 'strength'
class Shortsword(Weapon):
name = "Shortsword"
cost = "10 gp"
base_damage = "1d6"
damage_type = "piercing"
weight = 0
properties = "Finesse, light"
is_finesse = True
ability = 'strength'
class Trident(Weapon):
name = "Trident"
cost = "5 gp"
base_damage = "1d6"
damage_type = "piercing"
weight = 4
properties = "Thrown (range 20/60), versatile (1d8)"
ability = 'strength'
class WarPick(Weapon):
name = "War pick"
cost = "5 gp"
base_damage = "1d8"
damage_type = "piercing"
weight = 2
properties = ""
ability = 'strength'
class Warhammer(Weapon):
name = "Warhammer"
cost = "15 gp"
base_damage = "1d8"
damage_type = "bludgeoning"
weight = 2
properties = "Versatile (1d10)"
ability = 'strength'
class Whip(Weapon):
name = "Whip"
cost = "2 gp"
base_damage = "1d4"
damage_type = "slashing"
weight = 3
properties = "Finesse, reach"
is_finesse = True
ability = 'strength'
class Blowgun(Weapon):
name = "Blowgun"
cost = "10 gp"
base_damage = "1"
damage_type = "piercing"
weight = 1
properties = "Ammunition (range 25/100), loading"
ability = 'dexterity'
class HandCrossbow(Weapon):
name = "Crossbow, hand"
cost = "75 gp"
base_damage = "1d6"
damage_type = "piercing"
weight = 3
properties = "Ammunition (range 30/120), light, loading"
ability = 'dexterity'
class HeavyCrossbow(Weapon):
name = "Crossbow, heavy"
cost = "50 gp"
base_damage = "1d10"
damage_type = "piercing"
weight = 18
properties = "Ammunition (range 100/400), heaving, loading, two-handed"
ability = 'strength'
class Longbow(Weapon):
name = "Longbow"
cost = "50 gp"
base_damage = "1d8"
damage_type = "piercing"
weight = 2
properties = "Ammunition (range 150/600), heavy, two-handed"
ability = 'strength'
class Net(Weapon):
name = "Net"
cost = "1 gp"
base_damage = "-"
damage_type = ""
weight = 3
properties = "Special, thrown (range 5/15)"
ability = 'strength'
# Some lists of weapons for easy proficiency resolution
simple_melee_weapons = (Club, Dagger, Greatclub, Handaxe, Javelin,
LightHammer, Mace, Quarterstaff, Sickle, Spear)
simple_ranged_weapons = (LightCrossbow, Dart, Shortbow, Sling)
simple_weapons = simple_melee_weapons + simple_ranged_weapons
martial_melee_weapons = (Battleaxe, Flail, Glaive, Greataxe,
Greatsword, Halberd, Lance, Longsword, Maul, Morningstar, Pike,
Rapier, Scimitar, Shortsword, Trident, WarPick, Warhammer, Whip)
martial_ranged_weapons = (Blowgun, HandCrossbow, HeavyCrossbow,
Longbow, Net)
martial_weapons = martial_melee_weapons + martial_ranged_weapons
Binary file not shown.
+11 -2
View File
@@ -16,14 +16,16 @@ constitution = 12
intelligence = 13
wisdom = 10
charisma = 16
skill_proficiencies = [
skill_proficiencies = (
'acrobatics',
'deception',
'investigation',
'performance',
'sleight_of_hand',
'stealth'
]
)
proficiencies_extra = ('playing cards', "carpenter's tools")
languages = 'Common, halfling'
# Inventory
cp = 950
@@ -31,6 +33,13 @@ sp = 75
ep = 50
gp = 120
pp = 0
weapons = ('shortsword', 'shortbow')
equipment = (
"""Shortsword, shortbow, 20 arrows, leather armor, thieves tools,
backpack, bell, 5 candles, crowbar, hammer, 10 pitons, 50 feet of
hempen rope, hooded lantern, 2 flasks of oil, 5 days rations,
tinderbox, waterskin, crowbar, set of dark common clothes
including a hood, pouch.""")
# Backstory
personality_traits = """I never have a plan, but Im great at making things up as I go
+1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python
from distutils.core import setup
# from setuptools import setup
setup(name='dungeonsheets',
version='0.1dev',
+32
View File
@@ -3,6 +3,7 @@
from unittest import TestCase
from dungeonsheets.character import Character
from dungeonsheets.weapons import Weapon, Shortsword
class TestCharacter(TestCase):
@@ -22,6 +23,37 @@ class TestCharacter(TestCase):
char = Character()
char.set_attrs(name='Inara')
self.assertEqual(char.name, 'Inara')
# Check that the weapons get loaded as objects not string
char.set_attrs(weapons=['shortsword'])
self.assertEqual(len(char.weapons), 1)
self.assertTrue(isinstance(char.weapons[0], Shortsword))
def test_wield_weapon(self):
char = Character()
char.strength = 14
char.weapon_proficienies = [Shortsword]
# Add a weapon
char.wield_weapon('shortsword')
self.assertEqual(len(char.weapons), 1)
sword = char.weapons[0]
self.assertTrue(isinstance(sword, Weapon))
self.assertTrue(isinstance(sword, Shortsword))
self.assertEqual(sword.attack_bonus, 4) # str + prof
self.assertEqual(sword.bonus_damage, 2) # str
# Check if dexterity is used if it's higher (Finesse weapon)
char.weapons = []
char.dexterity = 16
char.wield_weapon('shortsword')
sword = char.weapons[0]
self.assertEqual(sword.attack_bonus, 5) # dex + prof
def test_proficiencies_text(self):
char = Character()
char._proficiencies_text = ('hello', 'world')
self.assertEqual(char.proficiencies_text, 'Hello, world.')
# Check for extra proficiencies
char.proficiencies_extra = ("it's", "me")
self.assertEqual(char.proficiencies_text, "Hello, world, it's, me.")
def test_proficiency_bonus(self):
char = Character()
+1 -2
View File
@@ -10,7 +10,7 @@ class CharacterFileTestCase(unittest.TestCase):
def test_load_character_file(self):
charfile = CHARFILE
result = make_sheets.load_character_file(charfile)
self.assertEqual(result['strength'], 10)
self.assertEqual(result['strength'], 8)
class FDFTestCase(unittest.TestCase):
def tearDown(self):
@@ -22,4 +22,3 @@ class FDFTestCase(unittest.TestCase):
char = character.Character()
make_sheets.create_fdf(char, fdfname=fdfname)
self.assertTrue(os.path.exists(fdfname))
+17
View File
@@ -70,3 +70,20 @@ class TestStats(TestCase):
# Check for a proficiency
my_class.skill_proficiencies = ['acrobatics']
self.assertEqual(my_class.acrobatics, 4)
def test_findattr(self):
"""Check if the function can find attributes."""
class TestClass():
my_attr = 47
YourAttr = 53
test_class = TestClass()
# Direct access
self.assertEqual(stats.findattr(test_class, 'my_attr'),
test_class.my_attr)
self.assertEqual(stats.findattr(test_class, 'YourAttr'),
test_class.YourAttr)
# Swapping spaces for capitalization
self.assertEqual(stats.findattr(test_class, 'my attr'),
test_class.my_attr)
self.assertEqual(stats.findattr(test_class, 'your attr'),
test_class.YourAttr)
+12
View File
@@ -0,0 +1,12 @@
import unittest
from dungeonsheets.weapons import Weapon
class WeaponTestCase(unittest.TestCase):
def test_weapon_damage(self):
weapon = Weapon()
weapon.base_damage = '1d6'
self.assertEqual(weapon.damage, '1d6')
# Now add some bonus damage
weapon.bonus_damage = 2
self.assertEqual(weapon.damage, '1d6 +2')