Merge branch '101-opponent-defense-per-attack-type' into 'main'

Display opponent defense in attack rolls and make it adjustable via effects per attack type

Closes #101

See merge request dungeonslayers/ds4!220
This commit is contained in:
Johannes Loher 2022-11-10 00:49:49 +00:00
commit 0ecfdbda02
20 changed files with 2801 additions and 1670 deletions

View file

@ -28,6 +28,8 @@
"DS4.OpponentDefense": "Gegnerabwehr",
"DS4.OpponentDefenseAbbr": "GA",
"DS4.SortByOpponentDefense": "Nach Gegnerabwehr sortieren",
"DS4.OpponentDefenseMelee": "Gegnerabwehr für Schlagen",
"DS4.OpponentDefenseRanged": "Gegnerabwehr für Schießen",
"DS4.AttackTypeMelee": "Schlagen",
"DS4.AttackTypeRanged": "Schießen",
"DS4.AttackTypeMeleeRanged": "Schlagen + Schießen",
@ -74,7 +76,9 @@
"DS4.ItemTypeSpecialCreatureAbility": "Besondere Kreaturenfähigkeit",
"DS4.ItemTypeSpecialCreatureAbilityPlural": "Besondere Kreaturenfähigkeiten",
"DS4.ItemWeaponCheckFlavor": "{actor} greift mit {weapon} an.",
"DS4.ItemWeaponCheckFlavorWithOpponentDefense": "{actor} greift mit {weapon} an.<br>Gegnerabwehr: {opponentDefense}",
"DS4.ItemSpellCheckFlavor": "{actor} wirkt {spell}.",
"DS4.ItemSpellCheckFlavorWithOpponentDefense": "{actor} wirkt {spell}.<br>Gegnerabwehr: {opponentDefense}",
"DS4.ItemPropertiesArmor": "Panzerungseigenschaften",
"DS4.ItemPropertiesEquipable": "Ausrüstungseigenschaften",
"DS4.ItemPropertiesPhysical": "Physische Eigenschaften",
@ -160,6 +164,8 @@
"DS4.CooldownDuration100R": "100 Kampfrunden",
"DS4.CooldownDuration1D": "1 Tag",
"DS4.CooldownDurationD20D": "W20 Tage",
"DS4.SpellAllowsDefense": "Erlaubt Abwehr",
"DS4.SpellAllowsDefenseDescription": "Ist eine Abwehren-Probe gegen diesen Zauber erlaubt?",
"DS4.SpellMinimumLevel": "Zugangsstufe",
"DS4.SpellMinimumLevelDescription": "Die minimale Stufe, ab der ein Zauberwirker den Zauberspruch erlernen kann.",
"DS4.SpellCasterClassHealer": "Heiler",

View file

@ -28,6 +28,8 @@
"DS4.OpponentDefense": "Opponent Defense",
"DS4.OpponentDefenseAbbr": "OD",
"DS4.SortByOpponentDefense": "Sort by Opponent Defense",
"DS4.OpponentDefenseMelee": "Opponent Defense for melee attacks",
"DS4.OpponentDefenseRanged": "Opponent Defense for ranged attacks",
"DS4.AttackTypeMelee": "Melee",
"DS4.AttackTypeRanged": "Ranged",
"DS4.AttackTypeMeleeRanged": "Melee / Ranged",
@ -74,7 +76,9 @@
"DS4.ItemTypeSpecialCreatureAbility": "Special Creature Ability",
"DS4.ItemTypeSpecialCreatureAbilityPlural": "Special Creature Abilities",
"DS4.ItemWeaponCheckFlavor": "{actor} attacks with {weapon}.",
"DS4.ItemWeaponCheckFlavorWithOpponentDefense": "{actor} attacks with {weapon}<br>Opponent defense: {opponentDefense}",
"DS4.ItemSpellCheckFlavor": "{actor} casts {spell}.",
"DS4.ItemSpellCheckFlavorWithOpponentDefense": "{actor} casts {spell}.<br>Opponent Defense: {opponentDefense}",
"DS4.ItemPropertiesArmor": "Armor Properties",
"DS4.ItemPropertiesEquipable": "Equipment Properties",
"DS4.ItemPropertiesPhysical": "Physical Properties",
@ -160,6 +164,8 @@
"DS4.CooldownDuration100R": "100 Rounds",
"DS4.CooldownDuration1D": "1 Day",
"DS4.CooldownDurationD20D": "D20 Days",
"DS4.SpellAllowsDefense": "Allows Defense",
"DS4.SpellAllowsDefenseDescription": "Ist it alowed to perform a defense check against this spell?",
"DS4.SpellMinimumLevel": "Minimum Level",
"DS4.SpellMinimumLevelDescription": "The minimum level at which a spell caster may learn the spell.",
"DS4.SpellCasterClassHealer": "Healer",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -93,7 +93,7 @@
"startTime": null
},
"icon": "icons/svg/aura.svg",
"label": "Proben mit Agilität -1",
"label": "Proben mit Agilität -4",
"transfer": true,
"flags": {},
"tint": null

View file

@ -124,7 +124,7 @@
"type": "specialCreatureAbility",
"img": "systems/ds4/assets/icons/official/special-creature-abilities/petrification.png",
"data": {
"description": "<p>Bei einem erfolgreichen Blickangriff versteinert das Ziel, sofern diesem K&Ouml;R+AU misslingt. Eine Versteinerung kann durch den Zauber Allheilung aufgehoben werden.</p>",
"description": "<p>Bei einem erfolgreichen Blickangriff versteinert das Ziel, sofern diesem K&Ouml;R+AU misslingt. Eine Versteinerung kann durch den Zauber @Compendium[ds4.spells.pmYcjLXv1EB9bM59]{Allheilung} aufgehoben werden.</p>",
"experiencePoints": 50
},
"effects": [],
@ -158,7 +158,7 @@
"type": "specialCreatureAbility",
"img": "systems/ds4/assets/icons/official/special-creature-abilities/attribute-loss.png",
"data": {
"description": "<p>Pro schadensverursachendem Treffer wird AGI um 1 gesenkt (bei AGI Null ist das Opfer bewegungsunf&auml;hig). Pro Tag oder Anwendung des Zaubers <em>Allheilung</em> wird 1 verlorener Attributspunkt regeneriert.</p>",
"description": "<p>Pro schadensverursachendem Treffer wird AGI um 1 gesenkt (bei AGI Null ist das Opfer bewegungsunf&auml;hig). Pro Tag oder Anwendung des Zaubers @Compendium[ds4.spells.pmYcjLXv1EB9bM59]{Allheilung}&nbsp;wird 1 verlorener Attributspunkt regeneriert.</p>",
"experiencePoints": 15
},
"effects": [],
@ -294,7 +294,7 @@
"type": "specialCreatureAbility",
"img": "systems/ds4/assets/icons/official/special-creature-abilities/charm.png",
"data": {
"description": "<p>Kann Gegner mit einem &bdquo;Lockruf&ldquo; bezaubern. Dieser Zauber funktioniert wie der Zauberspruch <em>Gehorche</em>. Abklingzeit des <em>Lockrufs</em>: 10 Kampfrunden</p>",
"description": "<p>Kann Gegner mit einem &bdquo;Lockruf&ldquo; bezaubern. Dieser Zauber funktioniert wie der Zauberspruch @Compendium[ds4.spells.wZYElRaDmhqgzUvQ]{Gehorche}. Abklingzeit des <em>Lockrufs</em>: 10 Kampfrunden</p>",
"experiencePoints": 25
},
"effects": [],
@ -447,7 +447,7 @@
"type": "specialCreatureAbility",
"img": "systems/ds4/assets/icons/official/special-creature-abilities/attribute-loss.png",
"data": {
"description": "<p>Pro schadensverursachendem Treffer wird K&Ouml;R um 1 gesenkt (bei K&Ouml;R Null ist das Opfer tot). Pro Tag oder Anwendung des Zaubers <em>Allheilung</em> wird 1 verlorener Attributspunkt regeneriert.</p>",
"description": "<p>Pro schadensverursachendem Treffer wird K&Ouml;R um 1 gesenkt (bei K&Ouml;R Null ist das Opfer tot). Pro Tag oder Anwendung des Zaubers @Compendium[ds4.spells.pmYcjLXv1EB9bM59]{Allheilung}&nbsp;wird 1 verlorener Attributspunkt regeneriert.</p>",
"experiencePoints": 15
},
"effects": [],
@ -778,7 +778,7 @@
"type": "specialCreatureAbility",
"img": "systems/ds4/assets/icons/official/special-creature-abilities/attribute-loss.png",
"data": {
"description": "<p>Pro schadensverursachendem Treffer wird GEI um 1 gesenkt (bei GEI Null ist das Opfer wahnsinnig). Pro Tag oder Anwendung des Zaubers <em>Allheilung</em> wird 1 verlorener Attributspunkt regeneriert.</p>",
"description": "<p>Pro schadensverursachendem Treffer wird GEI um 1 gesenkt (bei GEI Null ist das Opfer wahnsinnig). Pro Tag oder Anwendung des Zaubers @Compendium[ds4.spells.pmYcjLXv1EB9bM59]{Allheilung}&nbsp;wird 1 verlorener Attributspunkt regeneriert.</p>",
"experiencePoints": 15
},
"effects": [],

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@
"flags": {}
},
{
"_id": "1VAiKGCnqKfNC8AE",
"name": "Waffenloser Meister",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -38,8 +39,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "1VAiKGCnqKfNC8AE"
"flags": {}
},
{
"_id": "2ASdMhcx0hN3ZpPc",
@ -84,6 +84,7 @@
"flags": {}
},
{
"_id": "2zY11r1tKxBzNB0e",
"name": "Mächtige Erweckung",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -101,10 +102,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "2zY11r1tKxBzNB0e"
"flags": {}
},
{
"_id": "3AkPGw4beW52LIAY",
"name": "Rüstträger",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -122,8 +123,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "3AkPGw4beW52LIAY"
"flags": {}
},
{
"_id": "42FNsShgm1B6MClC",
@ -238,6 +238,7 @@
"flags": {}
},
{
"_id": "61Dz3XpStwlMfsbL",
"name": "Jäger",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -288,8 +289,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "61Dz3XpStwlMfsbL"
"flags": {}
},
{
"_id": "6YJLvjCIUmhqlaFb",
@ -313,6 +313,7 @@
"flags": {}
},
{
"_id": "6oXmRM21CLfELqKv",
"name": "Verheerer",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -324,14 +325,41 @@
"mod": 0
}
},
"effects": [],
"effects": [
{
"_id": "aqgIfpGWXYeXn1y6",
"changes": [
{
"key": "data.opponentDefense",
"mode": 2,
"value": "-1"
}
],
"disabled": false,
"duration": {
"startTime": null
},
"icon": "icons/svg/aura.svg",
"label": "Gegnerabwehr bei Zaubern/Zielzaubern -1",
"transfer": true,
"flags": {
"ds4": {
"itemEffectConfig": {
"applyToItems": true,
"itemName": "",
"condition": "'@item.type' === 'spell' && @item.data.allowsDefense"
}
}
},
"tint": null
}
],
"folder": null,
"sort": 0,
"permission": {
"default": 0
},
"flags": {},
"_id": "6oXmRM21CLfELqKv"
"flags": {}
},
{
"_id": "6z0JXGEqdzDTWQ7f",
@ -474,6 +502,7 @@
"flags": {}
},
{
"_id": "8apgKsktW4pyWmMq",
"name": "Hinterhältiger Angriff",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -491,8 +520,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "8apgKsktW4pyWmMq"
"flags": {}
},
{
"_id": "8nkrwGAE0HPoAAEm",
@ -537,6 +565,7 @@
"flags": {}
},
{
"_id": "8wHCsoZEQp3rScWe",
"name": "Parade",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -554,10 +583,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "8wHCsoZEQp3rScWe"
"flags": {}
},
{
"_id": "9hpucJC8WArBiXUR",
"name": "Tod entrinnen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -575,8 +604,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "9hpucJC8WArBiXUR"
"flags": {}
},
{
"_id": "9qdc56F4XTntYoo9",
@ -622,7 +650,7 @@
}
}
},
"tint": ""
"tint": null
}
],
"folder": null,
@ -696,6 +724,7 @@
"flags": {}
},
{
"_id": "Bp7OVgurG40CR1Mw",
"name": "Knechtschaft",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -713,10 +742,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "Bp7OVgurG40CR1Mw"
"flags": {}
},
{
"_id": "DUexlPzqyH2xPxYP",
"name": "Meister seiner Klasse",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -734,8 +763,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "DUexlPzqyH2xPxYP"
"flags": {}
},
{
"_id": "DZcu8KQFWChBVPRR",
@ -759,6 +787,7 @@
"flags": {}
},
{
"_id": "FoY7VbBTatyHOrb8",
"name": "Untote Horden",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -776,10 +805,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "FoY7VbBTatyHOrb8"
"flags": {}
},
{
"_id": "G3fbdAorLMCa2hGu",
"name": "Meucheln",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -797,10 +826,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "G3fbdAorLMCa2hGu"
"flags": {}
},
{
"_id": "GVuVyP3uLw3Fkiwf",
"name": "Prügler",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -818,10 +847,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "GVuVyP3uLw3Fkiwf"
"flags": {}
},
{
"_id": "GWVLcfQ2fm3Hc0zP",
"name": "Schütze",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -872,8 +901,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "GWVLcfQ2fm3Hc0zP"
"flags": {}
},
{
"_id": "HFCY3fxIbeXapRan",
@ -918,6 +946,7 @@
"flags": {}
},
{
"_id": "IIvsBSAqFFUFqALo",
"name": "Sattelschütze",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -935,10 +964,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "IIvsBSAqFFUFqALo"
"flags": {}
},
{
"_id": "IfyKb7y4YoUTssTs",
"name": "Tiermeister",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -984,8 +1013,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "IfyKb7y4YoUTssTs"
"flags": {}
},
{
"_id": "JbGKvhxVEAdczQib",
@ -1009,6 +1037,7 @@
"flags": {}
},
{
"_id": "JldVU3O4mmDWqk8s",
"name": "Spruchmeister",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1026,8 +1055,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "JldVU3O4mmDWqk8s"
"flags": {}
},
{
"_id": "KgOHPx5oQHKBuVPc",
@ -1224,6 +1252,7 @@
"flags": {}
},
{
"_id": "Mz5glQvRowlF5U8X",
"name": "Manipulator",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1269,10 +1298,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "Mz5glQvRowlF5U8X"
"flags": {}
},
{
"_id": "NR3BzKbROxHjpGrs",
"name": "Schlossknacker",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1323,10 +1352,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "NR3BzKbROxHjpGrs"
"flags": {}
},
{
"_id": "NSBiWy4FTIu6Y2Vv",
"name": "Präziser Schuss",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1344,10 +1373,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "NSBiWy4FTIu6Y2Vv"
"flags": {}
},
{
"_id": "Nu7TKGp987s5mHA0",
"name": "Instrument",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1365,8 +1394,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "Nu7TKGp987s5mHA0"
"flags": {}
},
{
"_id": "ODepf0g8Us5jBqLm",
@ -1432,6 +1460,7 @@
"flags": {}
},
{
"_id": "Q98LHOFZmKVoafp8",
"name": "Todeskraft",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1449,10 +1478,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "Q98LHOFZmKVoafp8"
"flags": {}
},
{
"_id": "QAxxIGpwledNp0n1",
"name": "Panzerung zerschmettern",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1470,10 +1499,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "QAxxIGpwledNp0n1"
"flags": {}
},
{
"_id": "RJauLusDDQWo77JU",
"name": "Schwimmen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1519,8 +1548,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "RJauLusDDQWo77JU"
"flags": {}
},
{
"_id": "RTLVQgPmjiPDdTFw",
@ -1544,6 +1572,7 @@
"flags": {}
},
{
"_id": "TQG9TbBb9S0nHogC",
"name": "Schnelle Reflexe",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1589,8 +1618,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "TQG9TbBb9S0nHogC"
"flags": {}
},
{
"_id": "UUYS4u0DmEbGzXxI",
@ -1711,6 +1739,7 @@
"flags": {}
},
{
"_id": "WMXI5ckyEdlC29j4",
"name": "Heldenglück",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1728,8 +1757,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "WMXI5ckyEdlC29j4"
"flags": {}
},
{
"_id": "Wwvj3V65hIe0JWul",
@ -1753,6 +1781,7 @@
"flags": {}
},
{
"_id": "XNjKX9xKkktkwAHk",
"name": "Kann ich mal vorbei?",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1770,8 +1799,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "XNjKX9xKkktkwAHk"
"flags": {}
},
{
"_id": "XUyuomVVOxuSSKXl",
@ -1837,6 +1865,7 @@
"flags": {}
},
{
"_id": "YPFshcSE5pZS0dto",
"name": "Schmerzhafter Wechsel",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -1854,8 +1883,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "YPFshcSE5pZS0dto"
"flags": {}
},
{
"_id": "ZnT8LMCRqZS3zpJO",
@ -1988,6 +2016,7 @@
"flags": {}
},
{
"_id": "aojENPok9Guo3JN1",
"name": "Wahrnehmung",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2053,10 +2082,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "aojENPok9Guo3JN1"
"flags": {}
},
{
"_id": "bA8wUU0bKouuxkQ5",
"name": "Reiten",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2102,10 +2131,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "bA8wUU0bKouuxkQ5"
"flags": {}
},
{
"_id": "bu9alxaRfnzzTyX1",
"name": "Macht des Blutes",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2123,8 +2152,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "bu9alxaRfnzzTyX1"
"flags": {}
},
{
"_id": "cLkCx5hxP7rVYUqD",
@ -2169,6 +2197,7 @@
"flags": {}
},
{
"_id": "dtynnRNkxg59Nqz4",
"name": "Verletzen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2180,16 +2209,44 @@
"mod": 0
}
},
"effects": [],
"effects": [
{
"_id": "x1FVRsSyjHwOfYRs",
"changes": [
{
"key": "data.opponentDefenseForAttackType.melee",
"mode": 2,
"value": "-1"
}
],
"disabled": false,
"duration": {
"startTime": null
},
"icon": "icons/svg/aura.svg",
"label": "Gegnerabwehr bei Nahkampfangriffen -1",
"transfer": true,
"flags": {
"ds4": {
"itemEffectConfig": {
"applyToItems": true,
"itemName": "",
"condition": "'@item.type' === 'weapon' && ('@item.data.attackType' === 'melee' || '@item.data.attackType' === 'meleeRanged')"
}
}
},
"tint": null
}
],
"folder": null,
"sort": 0,
"permission": {
"default": 0
},
"flags": {},
"_id": "dtynnRNkxg59Nqz4"
"flags": {}
},
{
"_id": "fIrcapAlXMqto18X",
"name": "Unersättliches Beschwören",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2207,10 +2264,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "fIrcapAlXMqto18X"
"flags": {}
},
{
"_id": "g4XI9wikUdxoCFNg",
"name": "Ritual der Narben",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2271,8 +2328,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "g4XI9wikUdxoCFNg"
"flags": {}
},
{
"_id": "gwCc6niwZL45wklE",
@ -2429,6 +2485,7 @@
"flags": {}
},
{
"_id": "iP5aZcqriVLjdVcd",
"name": "Tiergestalt",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2446,10 +2503,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "iP5aZcqriVLjdVcd"
"flags": {}
},
{
"_id": "inRlUNgoiaHm4pf6",
"name": "Verdrücken",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2467,10 +2524,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "inRlUNgoiaHm4pf6"
"flags": {}
},
{
"_id": "jmjtmMy7DnG205xR",
"name": "Schlachtruf",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2488,8 +2545,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "jmjtmMy7DnG205xR"
"flags": {}
},
{
"_id": "kZti1KQIbf4UPvI7",
@ -2723,6 +2779,7 @@
"flags": {}
},
{
"_id": "ml6GkLIsqDII9Mcp",
"name": "Untote zerschmettern",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2740,8 +2797,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "ml6GkLIsqDII9Mcp"
"flags": {}
},
{
"_id": "nMxDermxN1pUziUJ",
@ -2765,6 +2821,7 @@
"flags": {}
},
{
"_id": "nbDef1IPNyYcXmua",
"name": "Meister aller Klassen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2782,10 +2839,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "nbDef1IPNyYcXmua"
"flags": {}
},
{
"_id": "oxWYfqhbcsDoaaUJ",
"name": "Nekromantie",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2831,8 +2888,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "oxWYfqhbcsDoaaUJ"
"flags": {}
},
{
"_id": "pAOP7wkvhtsNIPQ8",
@ -2889,6 +2945,7 @@
"flags": {}
},
{
"_id": "pC1K0VHWTpaJqwtt",
"name": "Raserei",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2939,10 +2996,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "pC1K0VHWTpaJqwtt"
"flags": {}
},
{
"_id": "pDPVZpnhvlabmcvT",
"name": "Langfinger",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2960,10 +3017,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "pDPVZpnhvlabmcvT"
"flags": {}
},
{
"_id": "pEH7q5M85j50f45J",
"name": "Mächtige Beschwörung",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -2981,10 +3038,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "pEH7q5M85j50f45J"
"flags": {}
},
{
"_id": "qnYeR3a3LNUJ329t",
"name": "Zehrender Spurt",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3002,10 +3059,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "qnYeR3a3LNUJ329t"
"flags": {}
},
{
"_id": "rCRPchtSJye0K5nt",
"name": "Zauberwaffe",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3023,10 +3080,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "rCRPchtSJye0K5nt"
"flags": {}
},
{
"_id": "rXXw5aS0pCr5amWa",
"name": "Zwei Waffen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3044,10 +3101,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "rXXw5aS0pCr5amWa"
"flags": {}
},
{
"_id": "rbHZFVutiQ25glBq",
"name": "Totenrufer",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3065,10 +3122,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "rbHZFVutiQ25glBq"
"flags": {}
},
{
"_id": "s37iJhz4IQVhCWbe",
"name": "Sehnenschneider",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3086,8 +3143,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "s37iJhz4IQVhCWbe"
"flags": {}
},
{
"_id": "sSKiZ5hdQMBnAYRA",
@ -3132,6 +3188,7 @@
"flags": {}
},
{
"_id": "soobr7uyQgDm3DoN",
"name": "Stabbindung",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3149,10 +3206,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "soobr7uyQgDm3DoN"
"flags": {}
},
{
"_id": "sqGJRKlgFoD2vLCD",
"name": "Heimlichkeit",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3213,10 +3270,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "sqGJRKlgFoD2vLCD"
"flags": {}
},
{
"_id": "sqWBOfkvuv7ZTrVM",
"name": "Kreiszeichner",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3234,8 +3291,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "sqWBOfkvuv7ZTrVM"
"flags": {}
},
{
"_id": "srLA4jC8lsZbp3nT",
@ -3280,6 +3336,7 @@
"flags": {}
},
{
"_id": "tkLyvmSYvVslMXVE",
"name": "Vertrautenband",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3297,10 +3354,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "tkLyvmSYvVslMXVE"
"flags": {}
},
{
"_id": "tmFeIA1PSVHqGGjx",
"name": "Vernichtender Schlag",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3346,8 +3403,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "tmFeIA1PSVHqGGjx"
"flags": {}
},
{
"_id": "v5axYsQQ2w57Iu4p",
@ -3399,6 +3455,7 @@
"flags": {}
},
{
"_id": "v9ocoi91dKJahAe3",
"name": "Waffenkenner",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3416,8 +3473,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "v9ocoi91dKJahAe3"
"flags": {}
},
{
"_id": "vnEDVqVCsZuf8NYN",
@ -3462,6 +3518,7 @@
"flags": {}
},
{
"_id": "w34myctr1EDmXSPI",
"name": "Herr der Elemente",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3507,10 +3564,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "w34myctr1EDmXSPI"
"flags": {}
},
{
"_id": "wpZ1LCG8nLu4PSc9",
"name": "Mächtige Herbeirufung",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3528,10 +3585,10 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "wpZ1LCG8nLu4PSc9"
"flags": {}
},
{
"_id": "yCHMzXoqCRrNU5Br",
"name": "Schnutz vor Elementen",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3549,8 +3606,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "yCHMzXoqCRrNU5Br"
"flags": {}
},
{
"_id": "yIcgnr9Xr7Kwocaj",
@ -3574,6 +3630,7 @@
"flags": {}
},
{
"_id": "yMVciLvr77vbTw6r",
"name": "Magieresistent",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3591,8 +3648,7 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "yMVciLvr77vbTw6r"
"flags": {}
},
{
"_id": "zhzVJz6WhSMMeTuY",
@ -3644,6 +3700,7 @@
"flags": {}
},
{
"_id": "zvZelUv5qQz3adKN",
"name": "Salve",
"type": "talent",
"img": "icons/svg/item-bag.svg",
@ -3661,7 +3718,6 @@
"permission": {
"default": 0
},
"flags": {},
"_id": "zvZelUv5qQz3adKN"
"flags": {}
}
]

View file

@ -16,6 +16,7 @@ const defaultData: DS4SpellDataSourceData = {
numerical: 0,
complex: "",
},
allowsDefense: false,
spellGroups: {
lightning: false,
earth: false,

View file

@ -60,8 +60,8 @@ class CheckFactory {
}
createCheckTargetNumberModifier(): string {
const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.checkModifier, 0);
return `v${totalCheckTargetNumber}`;
const totalCheckTargetNumber = this.checkTargetNumber + this.checkModifier;
return totalCheckTargetNumber >= 0 ? `v(${this.checkTargetNumber} + ${this.checkModifier})` : "v0";
}
createCoupFumbleModifier(): string | null {

View file

@ -12,4 +12,5 @@ export interface DS4SpellDataProperties {
interface DS4SpellDataPropertiesData extends DS4SpellDataSourceData, DS4ItemDataPropertiesDataRollable {
price: number | null;
opponentDefense?: number;
}

View file

@ -16,6 +16,7 @@ export interface DS4SpellDataSourceData extends DS4ItemDataSourceDataBase, DS4It
numerical: number;
complex: string;
};
allowsDefense: boolean;
spellGroups: Record<keyof typeof DS4.i18n.spellGroups, boolean>;
maxDistance: UnitData<DistanceUnit>;
effectRadius: UnitData<DistanceUnit>;

View file

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { createCheckRoll } from "../../../dice/check-factory";
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item";
@ -12,6 +12,9 @@ export class DS4Spell extends DS4Item {
override prepareDerivedData(): void {
this.data.data.rollable = this.data.data.equipped;
this.data.data.price = calculateSpellPrice(this.data.data);
if (this.data.data.allowsDefense) {
this.data.data.opponentDefense = 0;
}
}
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
@ -42,17 +45,29 @@ export class DS4Spell extends DS4Item {
);
}
const spellType = this.data.data.spellType;
const opponentDefense = this.data.data.opponentDefense;
const checkTargetNumber =
ownerDataData.combatValues[spellType].total +
(hasComplexModifier ? 0 : this.data.data.spellModifier.numerical);
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
const flavor =
opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemSpellCheckFlavorWithOpponentDefense"
: "DS4.ItemSpellCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
actor: speaker.alias ?? this.actor.name,
spell: this.name,
};
if (opponentDefense !== undefined && opponentDefense !== 0) {
flavorData.opponentDefense = (opponentDefense < 0 ? "" : "+") + opponentDefense;
}
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: "DS4.ItemSpellCheckFlavor",
flavorData: { actor: speaker.alias ?? this.actor.name, spell: this.name },
flavor: flavor,
flavorData: flavorData,
speaker,
});

View file

@ -5,7 +5,12 @@
import type { DS4ItemDataPropertiesDataRollable } from "../item-data-properties-base";
import type { DS4WeaponDataSourceData } from "./weapon-data-source";
interface DS4WeaponDataPropertiesData extends DS4WeaponDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4WeaponDataPropertiesData extends DS4WeaponDataSourceData, DS4ItemDataPropertiesDataRollable {
opponentDefenseForAttackType: {
melee?: number;
ranged?: number;
};
}
export interface DS4WeaponDataProperties {
type: "weapon";

View file

@ -3,16 +3,22 @@
// SPDX-License-Identifier: MIT
import { DS4 } from "../../../config";
import { createCheckRoll } from "../../../dice/check-factory";
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item";
import type { AttackType } from "./weapon-data-source";
export class DS4Weapon extends DS4Item {
override prepareDerivedData(): void {
this.data.data.rollable = this.data.data.equipped;
const data = this.data.data;
data.rollable = data.equipped;
data.opponentDefenseForAttackType = {};
if (data.attackType === "melee" || data.attackType === "meleeRanged") {
data.opponentDefenseForAttackType.melee = data.opponentDefense;
}
if (data.attackType === "ranged" || data.attackType === "meleeRanged") {
data.opponentDefenseForAttackType.ranged = data.opponentDefense;
}
}
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
@ -33,54 +39,67 @@ export class DS4Weapon extends DS4Item {
const ownerDataData = this.actor.data.data;
const weaponBonus = this.data.data.weaponBonus;
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
const attackType = await this.getPerformedAttackType();
const opponentDefense = this.data.data.opponentDefenseForAttackType[attackType];
const combatValue = `${attackType}Attack` as const;
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
const flavor =
opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemWeaponCheckFlavorWithOpponentDefense"
: "DS4.ItemWeaponCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
actor: speaker.alias ?? this.actor.name,
weapon: this.name,
};
if (opponentDefense !== undefined && opponentDefense !== 0) {
flavorData.opponentDefense = (opponentDefense < 0 ? "" : "+") + opponentDefense;
}
await createCheckRoll(checkTargetNumber, {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: "DS4.ItemWeaponCheckFlavor",
flavorData: { actor: speaker.alias ?? this.actor.name, weapon: this.name },
speaker,
flavor,
flavorData,
});
Hooks.callAll("ds4.rollItem", this);
}
private async getCombatValueKeyForAttackType(attackType: AttackType): Promise<"meleeAttack" | "rangedAttack"> {
if (attackType === "meleeRanged") {
const { melee, ranged } = { ...DS4.i18n.attackTypes };
const identifier = "attack-type-selection";
return Dialog.prompt({
title: getGame().i18n.localize("DS4.DialogAttackTypeSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: getGame().i18n.localize("DS4.AttackType"),
identifier,
options: { melee, ranged },
},
],
}),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttackType = html.find(`#${identifier}`).val();
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
actualType: selectedAttackType,
expectedTypes: "'melee', 'ranged'",
}),
);
}
return `${selectedAttackType}Attack` as const;
},
});
} else {
return `${attackType}Attack` as const;
private async getPerformedAttackType(): Promise<"melee" | "ranged"> {
if (this.data.data.attackType !== "meleeRanged") {
return this.data.data.attackType;
}
const { melee, ranged } = { ...DS4.i18n.attackTypes };
const identifier = `attack-type-selection-${foundry.utils.randomID()}`;
return Dialog.prompt({
title: getGame().i18n.localize("DS4.DialogAttackTypeSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: getGame().i18n.localize("DS4.AttackType"),
identifier,
options: { melee, ranged },
},
],
}),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttackType = html.find(`#${identifier}`).val();
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
actualType: selectedAttackType,
expectedTypes: "'melee', 'ranged'",
}),
);
}
return selectedAttackType;
},
});
}
}

39
src/migration/007.ts Normal file
View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
if (itemData.type !== "spell") return;
return {
data: {
allowsDefense: false,
},
};
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -11,13 +11,14 @@ import { migration as migration003 } from "./003";
import { migration as migration004 } from "./004";
import { migration as migration005 } from "./005";
import { migration as migration006 } from "./006";
import { migration as migration007 } from "./007";
async function migrate(): Promise<void> {
if (!getGame().user?.isGM) {
return;
}
const oldMigrationVersion = getGame().settings.get("ds4", "systemMigrationVersion");
const oldMigrationVersion = getCurrentMigrationVersion();
const targetMigrationVersion = migrations.length;
@ -47,7 +48,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
for (const [i, { migrate }] of migrationsToExecute.entries()) {
const currentMigrationVersion = oldMigrationVersion + i + 1;
logger.info("executing migration script ", currentMigrationVersion);
logger.info("executing migration script", currentMigrationVersion);
try {
await migrate();
getGame().settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
@ -127,6 +128,10 @@ async function migrateCompendiumFromTo(
}
}
function getCurrentMigrationVersion(): number {
return getGame().settings.get("ds4", "systemMigrationVersion");
}
function getTargetMigrationVersion(): number {
return migrations.length;
}
@ -136,7 +141,15 @@ interface Migration {
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
}
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005, migration006];
const migrations: Migration[] = [
migration001,
migration002,
migration003,
migration004,
migration005,
migration006,
migration007,
];
function isFirstWorldStart(migrationVersion: number): boolean {
return migrationVersion < 0;
@ -145,6 +158,7 @@ function isFirstWorldStart(migrationVersion: number): boolean {
export const migration = {
migrate,
migrateFromTo,
getCurrentMigrationVersion,
getTargetMigrationVersion,
migrateCompendiumFromTo,
};

View file

@ -177,6 +177,7 @@
"numerical": 0,
"complex": ""
},
"allowsDefense": false,
"spellGroups": {
"lightning": false,
"earth": false,

View file

@ -36,14 +36,26 @@ SPDX-License-Identifier: MIT
hasQuantity=true}}
{{!-- attack type --}}
<img class="ds4-embedded-document-list__image"
src="{{lookup ../../config.icons.attackTypes itemData.data.attackType}}"
title="{{lookup ../../config.i18n.attackTypes itemData.data.attackType}}" />
src="{{lookup @root/config.icons.attackTypes itemData.data.attackType}}"
title="{{lookup @root/config.i18n.attackTypes itemData.data.attackType}}" />
{{!-- weapon bonus --}}
<div>{{ itemData.data.weaponBonus}}</div>
{{!-- opponent defense --}}
<div>{{ itemData.data.opponentDefense}}</div>
<div>
{{#if itemData.data.opponentDefenseForAttackType.melee includeZero=true}}
{{#if itemData.data.opponentDefenseForAttackType.ranged includeZero=true}}
<span
title="{{localize 'DS4.OpponentDefenseMelee'}}">{{itemData.data.opponentDefenseForAttackType.melee}}</span>/<span
title="{{localize 'DS4.OpponentDefenseRanged'}}">{{itemData.data.opponentDefenseForAttackType.ranged}}</span>
{{else}}
{{itemData.data.opponentDefenseForAttackType.melee}}
{{/if}}
{{else}}
{{itemData.data.opponentDefenseForAttackType.ranged}}
{{/if}}
</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
</ol>
@ -76,13 +88,13 @@ documentType='item' type='weapon'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-entry.hbs itemData=itemData isEquipable=true
hasQuantity=true}}
{{!-- armor material type --}}
<div title="{{lookup ../../config.i18n.armorMaterialTypes itemData.data.armorMaterialType}}">
{{lookup ../../config.i18n.armorMaterialTypesAbbr itemData.data.armorMaterialType}}
<div title="{{lookup @root/config.i18n.armorMaterialTypes itemData.data.armorMaterialType}}">
{{lookup @root/config.i18n.armorMaterialTypesAbbr itemData.data.armorMaterialType}}
</div>
{{!-- armor type --}}
<div title="{{lookup ../../config.i18n.armorTypes itemData.data.armorType}}">
{{lookup ../../config.i18n.armorTypesAbbr itemData.data.armorType}}
<div title="{{lookup @root/config.i18n.armorTypes itemData.data.armorType}}">
{{lookup @root/config.i18n.armorTypesAbbr itemData.data.armorType}}
</div>
{{!-- armor value --}}

View file

@ -105,6 +105,14 @@ SPDX-License-Identifier: MIT
{{/each}}
</div>
</div>
<div class="form-group">
<label for="data.allowsDefense-{{data._id}}" title="{{localize 'DS4.SpellAllowsDefenseDescription'}}">{{localize
"DS4.SpellAllowsDefense"}}</label>
<div class="form-fields">
<input id="data.allowsDefense-{{data._id}}" data-dtype="Boolean" type="checkbox" name="data.allowsDefense"
{{checked data.data.allowsDefense}} />
</div>
</div>
<div class="form-group slim">
<label title="{{localize 'DS4.SpellMinimumLevelDescription'}}">{{localize "DS4.SpellMinimumLevel"}}</label>
<div class="form-fields">