From 99e2bcc3fe41adcbd720bb70cb68903974c0b753 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:47:09 -0600 Subject: [PATCH 01/42] Version 2.3 start with armor given priority over weapons during import. --- .../2.3/HeroSystem6eHeroic.hde | 3533 ++++++++++++ .../2.3/HeroSystem6eHeroic_HDImporter.js | 4881 +++++++++++++++++ .../2.3/Sample_Character.TXT | 1 + .../2.3/Sample_Character.hdc | Bin 0 -> 127314 bytes .../2.3/Sample_Character_MA.TXT | 1 + .../2.3/Sample_Character_MA.hdc | Bin 0 -> 108564 bytes 6 files changed, 8416 insertions(+) create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT create mode 100644 HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.hdc diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde new file mode 100644 index 0000000000..60e3a1e554 --- /dev/null +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde @@ -0,0 +1,3533 @@ +HeroSystem6eHeroic

HeroSystem6eHeroic

Version 2.2

Export format for the Roll20 API script HeroSystem6eHeroic_HDImporter, which imports Hero Designer characters into the HeroSystem6eHeroic character sheet.

For documentation see https://github.com/Roll20/roll20-api-scripts/tree/master/HeroSystem6eHeroic_HDImporter

By Villain In Glasses (Roll20 ID 633423)

+txt +!hero --import { + "character":{ + "character_name":"", + "character_title":"", + "height":"", + "weight":"", + "eyes":"", + "hair":"", + "backgroundText":"", + "historyText":"", + "appearance":"", + "tactics":"", + "campaignUse":"", + "quote":"", + "experience":"", + "experienceBenefit":"", + "strength":"", + "dexterity":"", + "constitution":"", + "intelligence":"", + "ego":"", + "presence":"", + "ocv":"", + "dcv":"", + "omcv":"", + "dmcv":"", + "speed":"", + "pd":"", + "ed":"", + "body":"", + "stun":"", + "endurance":"", + "recovery":"", + "running":"", + "leaping":"", + "swimming":"", + "equipment":{ + "equipment01":{1 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 1}, + "equipment02":{2 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 2}, + "equipment03":{3 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 3}, + "equipment04":{4 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 4}, + "equipment05":{5 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 5}, + "equipment06":{6 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 6}, + "equipment07":{7 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 7}, + "equipment08":{8 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 8}, + "equipment09":{9 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 9}, + "equipment10":{10 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 10}, + "equipment11":{11 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 11}, + "equipment12":{12 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 12}, + "equipment13":{13 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 13}, + "equipment14":{14 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 14}, + "equipment15":{15 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 15}, + "equipment16":{16 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 16} + }, + "maneuvers":{ + "maneuver01":{ + + 1 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 1 + + }, + "maneuver02":{ + + 2 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 2 + + }, + "maneuver03":{ + + 3 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 3 + + }, + "maneuver04":{ + + 4 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 4 + + }, + "maneuver05":{ + + 5 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 5 + + }, + "maneuver06":{ + + 6 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 6 + + }, + "maneuver07":{ + + 7 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 7 + + }, + "maneuver08":{ + + 8 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 8 + + }, + "maneuver09":{ + + 9 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 9 + + }, + "maneuver10":{ + + 10 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 10 + + }, + "maneuver11":{ + + 11 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 11 + + }, + "maneuver12":{ + + 12 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 12 + + }, + "maneuver13":{ + + + + 1 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 1 + + + + }, + "maneuver14":{ + + + + 2 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 2 + + + + }, + "maneuver15":{ + + + + 3 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 3 + + + + }, + "maneuver16":{ + + + + 4 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 4 + + + + }, + "maneuver17":{ + + + + 5 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 5 + + + + }, + "maneuver18":{ + + + + 6 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 6 + + + + }, + "maneuver19":{ + + + + 7 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 7 + + + + }, + "maneuver20":{ + + + + 8 + "name":"", + "points":"", + "phase":"", + "ocv":"", + "dcv":"", + "effect":"", + "notes":"" + 8 + + + + } + }, + "perks":{ + "perk01":{ + + 1 + "type":"", + "points":"", + "text":" ", + "notes":"" + 1 + }, + "perk02":{ + + 2 + "type":"", + "points":"", + "text":" ", + "notes":"" + 2 + }, + "perk03":{ + + 3 + "type":"", + "points":"", + "text":" ", + "notes":"" + 3 + }, + "perk04":{ + + 4 + "type":"", + "points":"", + "text":" ", + "notes":"" + 4 + }, + "perk05":{ + + 5 + "type":"", + "points":"", + "text":" ", + "notes":"" + 5 + }, + "perk06":{ + + 6 + "type":"", + "points":"", + "text":" ", + "notes":"" + 6 + }, + "perk07":{ + + 7 + "type":"", + "points":"", + "text":" ", + "notes":"" + 7 + }, + "perk08":{ + + 8 + "type":"", + "points":"", + "text":" ", + "notes":"" + 8 + }, + "perk09":{ + + 9 + "type":"", + "points":"", + "text":" ", + "notes":"" + 9 + }, + "perk10":{ + + 10 + "type":"", + "points":"", + "text":" ", + "notes":"" + 10 + } + }, + "talents":{ + "talent01":{ + + 1 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 1 + }, + "talent02":{ + + 2 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 2 + }, + "talent03":{ + + 3 + "type":"", + "points":"", + "text":" ", + "notes":"" + 3 + }, + "talent04":{ + + 4 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 4 + }, + "talent05":{ + + 5 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 5 + }, + "talent06":{ + + 6 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 6 + }, + "talent07":{ + + 7 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 7 + }, + "talent08":{ + + 8 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 8 + }, + "talent09":{ + + 9 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 9 + }, + "talent10":{ + + 10 + "type":"", + "points":"", + "text":" + ", + "notes":"" + 10 + } + }, + "complications":{ + "complication01":{ + + 1 + "type":"", + "points":"", + "text":"", + "notes":"" + 1 + }, + "complication02":{ + + 2 + "type":"", + "points":"", + "text":"", + "notes":"" + 2 + }, + "complication03":{ + + 3 + "type":"", + "points":"", + "text":"", + "notes":"" + 3 + }, + "complication04":{ + + 4 + "type":"", + "points":"", + "text":"", + "notes":"" + 4 + }, + "complication05":{ + 5 + "type":"", + "points":"", + "text":"", + "notes":"" + 5 + }, + "complication06":{ + 6 + "type":"", + "points":"", + "text":"", + "notes":"" + 6 + }, + "complication07":{ + 7 + "type":"", + "points":"", + "text":"", + "notes":"" + 7 + }, + "complication08":{ + 8 + "type":"", + "points":"", + "text":"", + "notes":"" + 8 + }, + "complication09":{ + 9 + "type":"", + "points":"", + "text":"", + "notes":"" + 9 + }, + "complication10":{ + 10 + "type":"", + "points":"", + "text":"", + "notes":"" + 10 + }, + "complication11":{ + 11 + "type":"", + "points":"", + "text":"", + "notes":"" + 11 + }, + "complication12":{ + 12 + "type":"", + "points":"", + "text":"", + "notes":"" + 12 + }, + "complication13":{ + 13 + "type":"", + "points":"", + "text":"", + "notes":"" + 13 + }, + "complication14":{ + 14 + "type":"", + "points":"", + "text":"", + "notes":"" + 14 + }, + "complication15":{ + 15 + "type":"", + "points":"", + "text":"", + "notes":"" + 15 + }, + "complication16":{ + 16 + "type":"", + "points":"", + "text":"", + "notes":"" + 16 + }, + "complication17":{ + 17 + "type":"", + "points":"", + "text":"", + "notes":"" + 17 + }, + "complication18":{ + 18 + "type":"", + "points":"", + "text":"", + "notes":"" + 18 + }, + "complication19":{ + 19 + "type":"", + "points":"", + "text":"", + "notes":"" + 19 + }, + "complication20":{ + 20 + "type":"", + "points":"", + "text":"", + "notes":"" + 20 + } + }, + "powers":{ + "power01":{ + 1 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 1 + }, + "power02":{ + 2 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 2 + }, + "power03":{ + 3 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 3 + }, + "power04":{ + 4 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 4 + }, + "power05":{ + 5 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 5 + }, + "power06":{ + 6 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 6 + }, + "power07":{ + 7 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 7 + }, + "power08":{ + 8 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 8 + }, + "power09":{ + 9 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 9 + }, + "power10":{ + 10 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 10 + }, + "power11":{ + 11 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 11 + }, + "power12":{ + 12 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 12 + }, + "power13":{ + 13 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 13 + }, + "power14":{ + 14 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 14 + }, + "power15":{ + 15 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 15 + }, + "power16":{ + 16 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 16 + }, + "power17":{ + 17 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 17 + }, + "power18":{ + 18 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 18 + }, + "power19":{ + 19 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 19 + }, + "power20":{ + 20 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 20 + }, + "power21":{ + 21 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 21 + }, + "power22":{ + 22 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 22 + }, + "power23":{ + 23 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 23 + }, + "power24":{ + 24 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 24 + }, + "power25":{ + 25 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 25 + }, + "power26":{ + 26 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 26 + }, + "power27":{ + 27 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 27 + }, + "power28":{ + 28 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 28 + }, + "power29":{ + 29 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 29 + }, + "power30":{ + 30 + + + "name":"(Multipower) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"(MPSlot ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + + "name":"", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + + "name":"(VPP) ", + "base":"", + "text":"", + "notes":"", + "cost":"", + "endurance":"", + "damage":"", + + + "compound":"true" + + + "compound":"false" + + 30 + } + }, + "skills": { + "skill01": { + 1 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 1 + }, + "skill02": { + 2 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 2 + }, + "skill03": { + 3 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 3 + }, + "skill04": { + 4 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 4 + }, + "skill05": { + 5 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 5 + }, + "skill06": { + 6 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 6 + }, + "skill07": { + 7 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 7 + }, + "skill08": { + 8 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 8 + }, + "skill09": { + 9 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 9 + }, + "skill10": { + 10 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 10 + }, + "skill11": { + 11 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 11 + }, + "skill12": { + 12 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 12 + }, + "skill13": { + 13 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 13 + }, + "skill14": { + 14 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 14 + }, + "skill15": { + 15 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 15 + }, + "skill16": { + 16 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 16 + }, + "skill17": { + 17 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 17 + }, + "skill18": { + 18 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 18 + }, + "skill19": { + 19 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 19 + }, + "skill20": { + 20 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 20 + }, + "skill21": { + 21 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 21 + }, + "skill22": { + 22 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 22 + }, + "skill23": { + 23 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 23 + }, + "skill24": { + 24 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 24 + }, + "skill25": { + 25 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 25 + }, + "skill26": { + 26 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 26 + }, + "skill27": { + 27 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 27 + }, + "skill28": { + 28 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 28 + }, + "skill29": { + 29 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 29 + }, + "skill30": { + 30 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 30 + }, + "skill31": { + 31 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 31 + }, + "skill32": { + 32 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 32 + }, + "skill33": { + 33 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 33 + }, + "skill34": { + 34 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 34 + }, + "skill35": { + 35 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 35 + }, + "skill36": { + 36 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 36 + }, + "skill37": { + 37 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 37 + }, + "skill38": { + 38 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 38 + }, + "skill39": { + 39 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 39 + }, + "skill40": { + 40 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 40 + }, + "skill41": { + 41 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 41 + }, + "skill42": { + 42 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 42 + }, + "skill43": { + 43 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 43 + }, + "skill44": { + 44 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 44 + }, + "skill45": { + 45 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 45 + }, + "skill46": { + 46 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 46 + }, + "skill47": { + 47 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 47 + }, + "skill48": { + 48 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 48 + }, + "skill49": { + 49 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 49 + }, + "skill50": { + 50 + "name":"", + "enhancer":"true", + "text":"", + "display":"", + "attribute":"getCharacteristicString", + "base":"", + "levels":"", + "cost":"" + 50 + } + }, + "playerName":"", + "gmName":"", + "characterFile":"", + "versionHD":"", + "timeStamp":"", + "genre":"", + "campaign":"", + "version":"2.2", + "HeroSystem6eHeroic":"true" + } +} + +\\/ +Flight (\d*)"Flight $1m +within (\d*)"within $1m +Range \((\d*)"\)Range \($1m\) +(?<=[^\^\t:])(")(?=[^\>\}\]\:\n][^\n])'' +(\n)+ +(\t)+ +(\s)+ \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js new file mode 100644 index 0000000000..388e206f0b --- /dev/null +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -0,0 +1,4881 @@ +/* HeroSystem6eHeroic_HDImporter.js +* Hero Designer Importer for the Roll20 Hero System 6e Heroic character sheet +* Version: 2.2 +* By Villain in Glasses +* villaininglasses@icloud.com +* Discord: Villain#0604 +* Roll20: https://app.roll20.net/users/633423/villain-in-glasses +* Hero Games Forum Thread: +* https://www.herogames.com/forums/topic/101627-new-roll20-character-sheet-hero-system-6e-heroic/ +* +* Purpose: Imports characters created in Hero Designer into a Roll20 HeroSystem6eHeroic campaign. +* +* Installation: Paste this script into the API setup area of your Roll20 HeroSystem6eHeroic campaign. +* +* Copy "HeroSystem6eHeroic.hde" into your Hero Designer export format folder. +* +* Use: from Hero Designer export a character using HeroSystem6eHeroic.hde found in this repository as the selected format. +* This will produce a text file with the name of the character (e.g., myCharacter.txt). +* +* Open the exported file in your favorite text editor. Select all of the contents and copy it. +* Paste the copied text in the chat window of your Roll20 HeroSystem6eHeroic campaign. Hit enter. +* +* Commands: +* Import character: "!hero --import {character text}" +* Help: "!hero --help" +* Config: "!hero --config" +* +* Based on BeyondImporter Version O.4.0 by +* Robin Kuiper +* Discord: Atheos#1095 +* Roll20: https://app.roll20.net/users/1226016/robin +* +* Matt DeKok +* Discord: Sillvva#2532 +* Roll20: https://app.roll20.net/users/494585/sillvva +* +* Ammo Goettsch +* Discord: ammo#7063 +* Roll20: https://app.roll20.net/users/2990964/ammo +*/ + +(function() { + // Constants + const versionMod = "2.3"; + const versionSheet = "3.41"; // Note that a newer sheet will make upgrades as well as it can. + const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2"]); // HeroSystem6eHeroic.hde versions allowed. + + const defaultAttributes = { + + // Bio + character_title: "hero", + backgroundText: "", + historyText: "", + experience: 0, + money: 0, + + // Tally Bar + characteristicsCost: 0, + + // Primary Attributes. + // We need to define strengthNet for weapons. + strength: 10, + strengthNet: 10, + dexterity: 10, + constitution: 10, + intelligence: 10, + ego: 10, + presence:10, + + // Combat Attributes + ocv: 3, + dcv: 3, + omcv: 3, + dmcv: 3, + speed: 2, + pd: 2, + ed: 2, + body: 10, + stun: 20, + endurance: 20, + recovery: 4, + + // Movement Attributes + running: 12, + leaping: 4, + swimming: 4, + + // Health Status Attributes + CurrentBODY: 10, + CurrentSTUN: 20, + CurrentEND: 20, + gearCurrentBODY: 10, + gearCurrentSTUN: 20, + gearCurrentEND: 20, + + // Make characteristic maximums default to no. + useCharacteristicMaximums: 0, + optionTakesNoSTUN: 0, + + // Skill levels + skillLevels38: 0, + skillLevels39: 0, + skillLevels40: 0, + interactionLevelsCP: 0, + intellectLevelsCP: 0, + agilityLevelsCP: 0, + noncombatLevelsCP: 0, + overallLevelsCP: 0 + } + + let hero_caller = {}; + let object; // This is the character object. + + + // Styling for the chat responses. + const style = "margin-left: 0px; overflow: hidden; background-color: royalblue; border: 2px solid #fff990; padding: 5px; border-radius: 5px; color: white; div#home a:link { color: #70DB93; }"; + const buttonStyle = "background-color: dodgerblue; border: 1px solid #292929; width: 25%; border-radius: 3px; padding: 5px; color: #fff; text-align: center; float: right;"; + const altButtonStyle = "background-color: orange; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center; float: right;"; + const linkStyle = "color: green;" + + const script_name = 'HDImporter'; + const state_name = 'HDIMPORTER'; + var verbose = false; + + + // Start messages + on('ready', function() { + checkInstall(); + log(script_name + ' Ready! Command: !hero'); + //sendChat(script_name, script_name + ' Ready!\n For help enter "!hero --help"', null, {noarchive:true}); + sendChat(script_name, '

' + script_name + ' Ready!

For help enter "!hero --help"

', null, {noarchive:true}); + }); + + + on('chat:message', (msg) => { + if (msg.type != 'api') return; + + // Split the message into command and argument(s) + let args = msg.content.split(/ --(help|reset|config|imports|import) ?/g); + let command = args.shift().substring(1).trim(); + + if (command === "") { + return; + } + + hero_caller = getObj('player', msg.playerid); + + if (command !== 'hero') { + return; + } + + let importData = ""; + if(args.length < 1) { sendHelpMenu(hero_caller); return; } + + let config = state[state_name][hero_caller.id].config; + + for(let i = 0; i < args.length; i+=2) { + let k = args[i].trim(); + let v = args[i+1] != null ? args[i+1].trim() : null; + let check; + + v = cleanQuotes(v, script_name); + + check = Array.from(v.replace(/\s/g, '')); + + switch(k) { + case 'help': + sendHelpMenu(hero_caller); + return; + + case 'reset': + state[state_name][hero_caller] = {}; + setDefaults(true); + sendConfigMenu(hero_caller); + return; + + case 'config': + if(args.length > 0){ + let setting = v.split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : (setting[0] === '[NONE]') ? '' : setting[0]; + + if(key === 'prefix' && value.charAt(0) !== '_' && value.length > 0) { value = value + ' ';} + if(key === 'suffix' && value.charAt(0) !== '_' && value.length > 0) { value = ' ' + value} + + state[state_name][hero_caller.id].config[key] = value; + } + + sendConfigMenu(hero_caller); + return; + + case 'imports': + if(args.length > 0){ + let setting = v.split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : (setting[0] === '[NONE]') ? '' : setting[0]; + + state[state_name][hero_caller.id].config.imports[key] = value; + } + + sendConfigMenu(hero_caller); + return; + + case 'import': + if (check.length < 2100) { + // Intended character data length is safely less than the minimum character file size if exported with HDE format version 1.0. + // This is likely an error. + sendChat(script_name, '
Hero Importer finished early because the import command does not appear to contain valid character data.
' ); + return; + } else if ( (check[0] !== "{") && (check[check.length - 1] !== "}")) { + // Improper JSON format. + sendChat(script_name, '
Hero Importer finished early because the import command does not appear to contain valid character data.
' ); + return; + } + + importData = v.replace(/[\n\r]/g, ''); + break; + + default: + sendHelpMenu(hero_caller); + return; + } + } + + if ((importData === '') || (typeof importData === "undefined")) { + return; + } + + var json = importData; + var character = null; + + // Try to catch some bad input. Doesn't currently catch no input. + try { + character = JSON.parse(json).character; + } + + catch(error) { + let message = ""; + needsExportedVersion.forEach(function(value) { + message += value + ", "; + }); + + // Drop the last comma. + message = message.slice(0, -2); + + sendChat(script_name, '
Hero Importer ended early due to a source content error.
' ); + sendChat(script_name, "Please verify that the character file was exported using HeroSystem6eHeroic.hde (acceptable versions: "+message+"). For help use the command !hero --help."); + return; + } + + // Verify that the character was exported with the latest version of HeroSystem6eHeroic.hde. If not, report error and abort. + if (needsExportedVersion.has(character.version) === false) { + var last; + needsExportedVersion.forEach(k => { last = k }); + + sendChat(script_name, '
Import of ' + character.character_name + ' ended early due to version mismatch error.
' ); + sendChat(script_name, "Please download and install the latest version of HeroSystem6eHeroic.hde (version: "+last+" recommended) into your Hero Designer export formats folder. Export your character and try HD Importer again. For help use the command !hero --help." ); + + return; + } + + sendChat(script_name, '
Import of ' + character.character_name + ' started.
', null, {noarchive:true}); + + if (character.version === "1.0") { + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v1.0. \n Version 2.2 supports additional content."); + } else if (character.version === "2.0") { + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.0. \n Version 2.2 supports additional content."); + } else if (character.version === "2.1") { + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.1. \n Version 2.2 is available."); + } + + object = null; + + // Assign a random name if the character doesn't have one. + if ((character.character_name).length === 0) { + character.character_name = createRandomString(7); + } + + // Remove characters with the same name if overwrite is enabled. + if(state[state_name][hero_caller.id].config.overwrite) { + let objects = findObjs({ + _type: "character", + name: state[state_name][hero_caller.id].config.prefix + character.character_name + state[state_name][hero_caller.id].config.suffix + }, {caseInsensitive: true}); + + if(objects.length > 0) { + object = objects[0]; + for(let i = 1; i < objects.length; i++){ + objects[i].remove(); + } + } + } + + if(!object) { + // Create character object + object = createObj("character", { + name: state[state_name][hero_caller.id].config.prefix + character.character_name + state[state_name][hero_caller.id].config.suffix, + inplayerjournals: playerIsGM(msg.playerid) ? state[state_name][hero_caller.id].config.inplayerjournals : msg.playerid, + controlledby: playerIsGM(msg.playerid) ? state[state_name][hero_caller.id].config.controlledby : msg.playerid + }); + } + + // Set base character sheet values. + setAttrs(object.id, defaultAttributes); + + // Import Page 1: Characteristics and Bio + importCharacteristics(object, character, script_name); + + // Import Page 2: Martial Arts Maneuvers + // Maneuvers over the sheet maximum will be prepended to excess perks and talents in the treasures field. + character.overflow = importManeuvers(object, character, script_name); + + // Import Page 2: Equipment + importEquipment(object, character, script_name); + + // Import Page 3: Skills + importAllSkills(object, character, script_name); + + // Import Page 4: Powers + // Powers over the sheet maximum will be prepended to excess perks and talents in a text field. + character.overflow = importPowers(object, character, script_name); + + // Import Page 5: Perks and Talents + importPerksAndTalents(object, character, script_name); + + // Import Page 5: Complications + importComplications(object, character, script_name); + + // Version + applyVersion(object, character, script_name, versionSheet); + + // Finished notification + sendChat(script_name, '
Import of ' + character.character_name + ' finished.
', null, {noarchive:true}); + }); + + // END MAIN + + +/* **************************************** */ +/* *** Begin Import Functions *** */ +/* **************************************** */ + + var importCharacteristics = function(object, character, script_name) { + + /* ************************************************* */ + /* *** Import Function: Characteristics *** */ + /* ************************************************* */ + + // Set sticky note to importer details. + let importInfoString = "HDImporter for Roll20\n"; + importInfoString = importInfoString + "Version: " + versionMod + "\n"; + if (typeof character.playerName !== "undefined") { + importInfoString = importInfoString + "Player: " + character.playerName + "\n"; + } + if (typeof character.gmName !== "undefined") { + importInfoString = importInfoString + "GM: " + character.gmName + "\n"; + } + if (typeof character.genre !== "undefined") { + importInfoString = importInfoString + "Genre: " + character.genre + "\n"; + } + if (typeof character.campaign !== "undefined") { + importInfoString = importInfoString + "Campaign: " + character.campaign + "\n"; + } + if (typeof character.versionHD !== "undefined") { + importInfoString = importInfoString + "Hero Designer version: " + character.versionHD + "\n"; + } + importInfoString = importInfoString + "HeroSystem6eHeroic.hde version: " + character.version + "\n"; + if (typeof character.characterFile !== "undefined") { + importInfoString = importInfoString + "Original file: " + character.characterFile + "\n"; + } + if (typeof character.timeStamp !== "undefined") { + importInfoString = importInfoString + "Export date: \n " + character.timeStamp + "\n"; + } + + setAttrs(object.id, {portraitStickyNote: importInfoString}); + + + // Set sticky window as visible portrait. + setAttrs(object.id, {portraitSelection: 2}); + + // Set bio-type attributes and experience points. + + let description = ""; + let quote = ""; + + if (character.version >= 1.2) { + quote = character.quote; + quote = quote.trim(); + + description += ((character.appearance).length > 0) ? character.appearance : ""; + description += ((character.backgroundText).length > 0) ? '\n' + '\n' + character.backgroundText : ""; + description += ((character.historyText).length > 0) ? '\n' + '\n' + character.historyText : ""; + description += ((character.tactics).length > 0) ? '\n' + '\n' + character.tactics : ""; + description += ((character.campaignUse).length > 0) ? '\n' + '\n' + character.campaignUse : ""; + description = description.trim(); + description += '\n' + '\n' + character.height + " and " + Math.round(Number((character.weight).replace(/[^0-9.]/g,''))) + " kg."; + description = description.trim(); + } else { + quote = ""; + + description += ((character.historyText).length > 0) ? character.historyText : ""; + description = description.trim(); + } + + let bio_attributes = { + character_title: character.character_title, + backgroundText: quote, + historyText: description, + experience: parseInt(character.experience)||0, + experienceBenefit: parseInt(character.experienceBenefit)||0 + } + + setAttrs(object.id, bio_attributes); + + if(verbose) { + sendChat(script_name, "Imported bio and experience."); + } + + // Set primary attributes. + let primary_attributes = { + strength: parseInt(character.strength)||10, + strengthNet: parseInt(character.strength)||10, + dexterity: parseInt(character.dexterity)||10, + constitution: parseInt(character.constitution)||10, + intelligence: parseInt(character.intelligence)||10, + ego: parseInt(character.ego)||10, + presence: parseInt(character.presence)||10 + } + + setAttrs(object.id, primary_attributes); + + if(verbose) { + sendChat(script_name, "Imported core attributes."); + } + + // Set combat attributes. + let combat_attributes = { + ocv: parseInt(character.ocv)||3, + dcv: parseInt(character.dcv)||3, + omcv: parseInt(character.omcv)||3, + dmcv: parseInt(character.dmcv)||3, + speed: parseInt(character.speed)||2, + pd: parseInt(character.pd)||0, + ed: parseInt(character.ed)||0, + body: parseInt(character.body)||10, + stun: parseInt(character.stun)||0, + hiddenSTUN: parseInt(character.stun)||0, + endurance: parseInt(character.endurance)||0, + recovery: parseInt(character.recovery)||0 + } + + if (character.stun !== "") { + combat_attributes.stun = parseInt(character.stun); + } else { + combat_attributes.stun = 0; + } + + setAttrs(object.id, combat_attributes); + + if(verbose) { + sendChat(script_name, "Imported combat attributes."); + } + + // Set movement attributes. + + let movement_attributes = { + running: parseInt(character.running)||0, + leaping: parseInt(character.leaping)||0, + swimming: parseInt(character.swimming)||0 + }; + + setAttrs(object.id, movement_attributes); + + if(verbose) { + sendChat(script_name, "Imported movement."); + } + + // Set status attributes to starting values + let health_attributes = { + CurrentBODY: character.body, + CurrentSTUN: character.stun, + CurrentEND: character.endurance, + gearCurrentBODY: character.body, + gearCurrentSTUN: character.stun, + gearCurrentEND: character.endurance + } + + setAttrs(object.id, health_attributes); + + if(verbose) { + sendChat(script_name, "Configured health status."); + } + + return; + } + + + function createRandomString(length) { + // random character name from https://sentry.io/answers/generate-random-string-characters-in-javascript/ + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + } + + + var applyVersion = function(object, character, script_name, version) { + // Set version data to avoid improper sheet auto updates. + let version_attributes = { + version: version, + validateMay23: 1, + + // Show the Treasures slide where equipment will appear. + gearSlideSelection : "gearTreasures" + } + + setAttrs(object.id, version_attributes); + } + + + var importManeuvers = function(object, character, script_name) { + + /* ************************************************* */ + /* *** Import Function: Import Maneuvers *** */ + /* ************************************************* */ + + + // Overall list of maneuvers + let maneuverArray = new Array(); + let maneuverArrayIndex = 0; + let temp = 0; + let tempString = ""; + let diceString = ""; + let tempPosition = 0; + const maxManeuvers = 20; + const maneuverSlots = 10; + let importCount = 0; + let ID = "01"; + + // Imports twenty martial arts maneuvers, skipping empty slots. + // Only the first 10 are imported into sheet slots. + + for (importCount = 1; importCount < maxManeuvers; importCount++) { + + ID = String(importCount).padStart(2,'0'); + + if ((typeof character.maneuvers["maneuver"+ID] !== "undefined") && (typeof character.maneuvers["maneuver"+ID].name !== "undefined")) { + maneuverArray[maneuverArrayIndex] = character.maneuvers["maneuver"+ID]; + + maneuverArrayIndex++; + } + + } + + let importedManeuvers = {}; + importCount = 0; + const nameMax = 16; + + while ( (importCount < maneuverSlots) && (importCount < maneuverArrayIndex) ) { + if (importCount < maneuverArrayIndex) { + ID = String(importCount+1).padStart(2,'0'); + + if ( maneuverArray[importCount].name.length > nameMax) { + importedManeuvers["martialManeuverName"+ID] = maneuverArray[importCount].name.slice(0, nameMax); + importedManeuvers["martialManeuverEffect"+ID] = maneuverArray[importCount].name + '\n' + maneuverArray[importCount].effect; + } else { + importedManeuvers["martialManeuverName"+ID] = maneuverArray[importCount].name; + importedManeuvers["martialManeuverEffect"+ID] = maneuverArray[importCount].effect; + } + + importedManeuvers["martialManeuverCP"+ID] = maneuverArray[importCount].points; + importedManeuvers["martialManeuverPhase"+ID] = maneuverArray[importCount].phase; + temp = Number(maneuverArray[importCount].ocv); + importedManeuvers["martialManeuverOCV"+ID] = isNaN(temp) ? 0 : temp; + temp = Number(maneuverArray[importCount].dcv); + importedManeuvers["martialManeuverDCV"+ID] = isNaN(temp) ? 0 : temp; + + tempString = maneuverArray[importCount].effect.toLowerCase(); + if ( tempString.includes("weapon") ) { + if ( tempString.includes("strike") ) { + importedManeuvers["martialManeuverType"+ID] = "weaponAttack"; + + tempString = maneuverArray[importCount].effect; + + if ( tempString.includes("DC") ) { + tempPosition = tempString.indexOf("DC"); + diceString = tempString.slice(0, tempPosition); + diceString = diceString.slice(-3).replace(/[^0-9.-]/g, ''); + importedManeuvers["martialManeuverDC"+ID] = Math.max(-2, Math.min(2, parseInt(diceString)||0)); + } + } else if ( tempString.includes("block") ) { + importedManeuvers["martialManeuverType"+ID] = "weaponBlock"; + } else { + importedManeuvers["martialManeuverType"+ID] = "weaponContest"; + } + } else { + if ( tempString.includes("strike") ) { + importedManeuvers["martialManeuverType"+ID] = "attack"; + } else if ( tempString.includes("hka") ) { + importedManeuvers["martialManeuverType"+ID] = "attack"; + importedManeuvers["martialManeuverNormal"+ID] = "0"; + } else if ( tempString.includes("block") ) { + importedManeuvers["martialManeuverType"+ID] = "block"; + } else if ( tempString.includes("dodge") ) { + importedManeuvers["martialManeuverType"+ID] = "dodge"; + } else { + importedManeuvers["martialManeuverType"+ID] = "contest"; + } + + // Damage or STR adds. + if ( tempString.includes("d6") ) { + tempPosition = tempString.indexOf("d6"); + diceString = tempString.slice(0, tempPosition); + diceString = diceString.slice(-2).replace(/\D/g,"") + "d6"; + + if (tempString.includes("d3")) { + diceString += "+1d3"; + } else if (tempString.includes("+1")) { + diceString += "+1"; + } else if (tempString.includes("-1")) { + diceString += "-1"; + } + + importedManeuvers["martialManeuverDamage"+ID] = diceString; + } else if ( tempString.includes("str") ) { + tempPosition = tempString.indexOf("str"); + diceString = tempString.slice(0, tempPosition); + diceString = diceString.slice(-3).replace(/\D/g,""); + importedManeuvers["martialManeuverStrMod"+ID] = parseInt(diceString)||0; + } + } + + importCount++; + } + } + + // Import maneuvers. + setAttrs(object.id, importedManeuvers); + + if(verbose) { + if (importCount === 1) { + sendChat(script_name, "Imported 1 maneuver."); + } else { + sendChat(script_name, "Imported " + importCount + " maneuvers."); + } + } + + // Display additional maneuvers in the treasures text box. + if (maneuverArrayIndex > maneuverSlots) { + let extras = 0; + + for (let i = maneuverSlots; i < maneuverArrayIndex; i++) { + tempString = tempString + maneuverArray[i].name + "\n"; + tempString = tempString + "CP: " + maneuverArray[i].points + "\n"; + if (maneuverArray[i].ocv !== "") { + tempString = tempString + "OCV: " + maneuverArray[i].ocv + "\n"; + } + if (maneuverArray[i].dcv !== "") { + tempString = tempString + "DCV: " + maneuverArray[i].dcv + "\n"; + } + if (maneuverArray[i].phase !== "") { + tempString = tempString + "Phase: " + maneuverArray[i].phase + "\n"; + } + tempString = tempString + maneuverArray[i].effect + "\n" + "\n"; + extras++; + } + + if(verbose) { + if (extras === 1) { + sendChat(script_name, extras + " maneuver placed in treasures."); + } else { + sendChat(script_name, extras + " maneuvers placed in treasures."); + } + } + + if ( (typeof character.treasures != "undefined") && (character.treasures !== "")) { + tempString = character.treasures + '\n' + '\n' + tempString.trim(); + } else { + tempString = tempString.trim(); + } + + // Place additional maneuvers in the treasures text box. + setAttrs(object.id, {treasures: tempString}); + } + + // Make the Maneuver window visible. + if (importCount>0) { + setAttrs(object.id, {gearSlideSelection: 2}); + } + + return tempString; + } + + + var importEquipment = function(object, character, script_name) { + + /* ************************************************* */ + /* *** Import Function: Import Equipment *** */ + /* ************************************************* */ + + // Imports equipment and sets carried weight. + // Similar to the way perks and talents are handled, we will parse the imported equipment into temporary arrays. + + const strength = parseInt(character.strength)||10; + let gearTextBox = ""; + + let tempString; + let tempPosition; + let secondPosition; + let subStringA; + let subStringB; + let sampleSize; + + // Needed for adjusted damage. + let advantage = 0; + + // Overall array of equipment. + let equipmentArray = new Array(); + let equipmentArrayIndex = 0; + + // Array of items of equipment that are not weapons, armor, or shields. + let equipmentListArray = new Array(); + let equipmentListArrayIndex = 0; + + // Array of items of equipment that are weapons. + let weaponsArray = new Array(); + let weaponsArrayIndex = 0; + + // Array of items of equipment that are weapons. + let armorArray = new Array(); + let armorArrayIndex = 0; + + // Array for multipowers, which need to be independent from others. + let multipowerArray = new Array(); + let multipowerArrayIndex = 0; + + // Read equipment + const maxEquipment = 16; + let importCount = 0; + let imported = 0; + let ID = "01"; + + // Imports sixteen martial arts maneuvers, skipping empty slots. + + for (importCount = 1; importCount <= maxEquipment; importCount++) { + + ID = String(importCount).padStart(2,'0'); + + if ((typeof character.equipment["equipment"+ID] !== "undefined") && (typeof character.equipment["equipment"+ID].name !== "undefined")) { + + equipmentArray[equipmentArrayIndex]=character.equipment["equipment"+ID]; + + tempString = equipmentArray[equipmentArrayIndex].name; + + if ((tempString !== "") && tempString.length) { + if ((equipmentArray[equipmentArrayIndex].name.includes("Multipower")) || (equipmentArray[equipmentArrayIndex].name.includes("MPSlot"))) { + // Then place in multipower array. + multipowerArray[multipowerArrayIndex]=equipmentArray[equipmentArrayIndex]; + multipowerArrayIndex++; + + } else if ((equipmentArray[equipmentArrayIndex].defense !== "") && (equipmentArray[equipmentArrayIndex].defense === "true")) { + // If the item is a defense add it to the armor list. + // This will need to be updated for shields. + armorArray[armorArrayIndex]=equipmentArray[equipmentArrayIndex]; + armorArrayIndex++; + + } else if ((equipmentArray[equipmentArrayIndex].attack !== "") && (equipmentArray[equipmentArrayIndex].damage !== "") && (equipmentArray[equipmentArrayIndex].attack === "true")) { + // If the item is a damage attack add it to the weapon list. + weaponsArray[weaponsArrayIndex]=equipmentArray[equipmentArrayIndex]; + weaponsArrayIndex++; + + } else { + // If the item is not an attack or defense add it to the equipment list. + equipmentListArray[equipmentListArrayIndex]=equipmentArray[equipmentArrayIndex]; + equipmentListArrayIndex++; + } + } + + equipmentArrayIndex++; + } + } + + // Write raw details of imported equipment to the treasures slide. + if (equipmentArrayIndex > 0) { + + // Get current contents of the treasures text box. + tempString = character.overflow + '\n' + '\n'; + + // Add equipment to treasures. + for (let i = 0; i < equipmentArrayIndex; i++) { + tempString += equipmentArray[i].name + '\n'; + if (equipmentArray[i].damage !== "") { + tempString += "Damage: " + equipmentArray[i].damage + ", "; + } + if (equipmentArray[i].end !== "") { + tempString += "END: " + equipmentArray[i].end + ", "; + } + if (equipmentArray[i].range !== "") { + tempString += "Range: " + equipmentArray[i].range + ", "; + } + if (equipmentArray[i].text !== "") { + tempString += equipmentArray[i].text; + if (equipmentArray[i].notes !== "") { + tempString += ", " + equipmentArray[i].notes; + } + } else if (equipmentArray[i].notes !== "") { + tempString += ", " + equipmentArray[i].notes; + } + if (equipmentArray[i].mass !== "") { + tempString += ", Mass: " + equipmentArray[i].mass; + } + if (i < (equipmentArrayIndex + 2)) { + tempString += '\n' + '\n'; + } + } + + if ( (typeof character.treasures != "undefined") && (character.treasures !== "")) { + tempString = character.treasures + '\n' + '\n' + tempString.trim(); + } else { + tempString = tempString.trim(); + } + + setAttrs(object.id, {treasures: tempString}); + + // Show the Treasures Gear Tab slide where the multipower equipment will appear. + setAttrs(object.id, {gearSlideSelection: 3}); + } + + // Prepare object of items that are not weapons or armor. + // Assign to character sheet Equipment List. + + let importedEquipment = new Array(); + importCount = 0; + imported = 0; + + // Prepare Items + for (importCount = 0; importCount < maxEquipment; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + if (importCount < equipmentListArrayIndex) { + imported += 1; + + // Check for charges. + if (equipmentListArray[importCount].end != "") { + tempString = equipmentListArray[importCount].end; + if (tempString.includes("[")) { + tempString = " (" + parseInt(tempString.replace(/[^\d.-]/g, "")) +")"; + } else { + tempString = ""; + }; + } + + importedEquipment["equipText"+ID] = equipmentListArray[importCount].name; + + // Get item mass. + if (equipmentListArray[importCount].mass !== "") { + tempString = equipmentListArray[importCount].mass; + importedEquipment["equipMass"+ID] = getItemMass(tempString, script_name); + } else { + importedEquipment["equipMass"+ID] = 0; + } + } + } + + // Import equipment. + setAttrs(object.id, importedEquipment); + + if(verbose) { + if (imported === 1) { + sendChat(script_name, "Imported 1 piece of equipment."); + } else { + sendChat(script_name, "Imported "+ imported +" pieces of equipment."); + } + } + + // Prepare objects of weapons. Assign to character sheet Weapon List. + + let importedWeapons = new Array(); + const maxAdvantage = 1; + const maxWeapons = 5; + let tempValue = 0; + + importCount = 0; + imported = 0; + + for (importCount = 0; importCount < maxWeapons; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + if (importCount < weaponsArrayIndex) { + imported += 1; + + importedWeapons["weaponName"+ID] = weaponsArray[importCount].name; + + // Assign weapon base damage. + importedWeapons["weaponDamage"+ID] = getWeaponDamage(weaponsArray[importCount].damage, script_name); + + tempString = weaponsArray[importCount].text; + if ((typeof tempString !== "undefined") && (tempString !== "")) { + // Look for weapon advantages. + tempValue = findDamageAdvantages(tempString, script_name); + if (tempValue > maxAdvantage) { + importedWeapons["weaponAdvantage"+ID] = maxAdvantage; + } else { + importedWeapons["weaponAdvantage"+ID] = tempValue; + } + + // Check for Killing Attack. + if (tempString.includes("Killing Attack") || tempString.includes("RKA") || tempString.includes("HKA")) { + // importedWeapons.weaponNormalDamage01= "off"; + } else { + importedWeapons["weaponNormalDamage"+ID]= "on"; + } + + // Get OCV bonus or penalty. + importedWeapons["weaponOCV"+ID] = getOCVmodifier(tempString, script_name); + + // Check for range mod adjustment. + if (tempString.includes("vs. Range Modifier")) { + tempPosition=tempString.indexOf("vs. Range Modifier"); + importedWeapons["weaponRangeMod"+ID]= parseInt(tempString.substr(tempPosition-3, 2)); + } else { + importedWeapons["weaponRangeMod"+ID]= 0; + } + + // Check for the thrown weapon advantage. + importedWeapons["rangeBasedOnStr"+ID] = (tempString.includes("Range Based On STR")) ? "on" : 0; + + // Check for modified STUN multiplier. + importedWeapons["weaponStunMod"+ID] = getStunModifier(tempString, script_name); + + // Get STR minimum and apply strength. + importedWeapons["weaponStrengthMin"+ID] = getWeaponStrMin(tempString, script_name); + importedWeapons["weaponEnhancedBySTR"+ID] = ( checkDamageBySTR(tempString, script_name) ? "on" : 0); + importedWeapons["weaponStrength"+ID] = ( importedWeapons["weaponEnhancedBySTR"+ID] === "on" ) ? getWeaponStrength(importedWeapons["weaponStrengthMin"+ID], strength, script_name) : Math.min(getWeaponStrMin(tempString, script_name), character.strength); + + // Check for AoE. + importedWeapons["weaponAreaEffect"+ID] = (tempString.includes("Area Of Effect")) ? "on" : 0; + } + + // Check for charges. + tempString = weaponsArray[importCount].end; + if ((typeof tempString !== "undefined") && (tempString !== "")) { + if (tempString.includes("[")) { + importedWeapons["weaponShots"+ID] = parseInt(tempString.replace(/[^\d.-]/g, "")); + } else { + importedWeapons["weaponShots"+ID] = 0; + } + } + + // Get weapon mass. + if (weaponsArray[importCount].mass !== "") { + tempString = weaponsArray[importCount].mass; + importedWeapons["weaponMass"+ID] = getItemMass(tempString, script_name); + } else { + importedWeapons["weaponMass"+ID] = 0; + } + + // Calculate thrown weapon range or assign range without units. + importedWeapons["weaponRange"+ID] = getWeaponRange(weaponsArray[importCount].range, character.strength, importedWeapons["weaponMass"+ID], script_name); + } + + } + + // Import weapons. + + setAttrs(object.id, importedWeapons); + + if(verbose) { + if (imported === 1) { + sendChat(script_name, "Imported 1 weapon."); + } else { + sendChat(script_name, "Imported "+ imported +" weapons."); + } + } + + // Prepare object of armor defenses. Assign to character sheet Armor List. + + let importedArmor = new Array(); + const maxArmor = 4; // The 4th may be overwritten if the character has resistant protection. + + importCount = 0; + imported = 0; + + for (importCount = 0; importCount < maxArmor; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + tempString = "none"; + + if (importCount < armorArrayIndex) { + imported += 1; + + importedArmor["armorName"+ID] = armorArray[importCount].name; + + // Find resistant protection values. + // This needs to be adjusted so that it doesn't pick out other PD/ED stats from elsewhere in the text. + if (typeof armorArray[importCount].text !== "undefined") { + tempString = armorArray[importCount].text; + } + + if (tempString.includes("Resistant Protection")) { + tempPosition = tempString.indexOf("Resistant Protection"); + sampleSize = 14; + subStringA = tempString.substr(tempPosition+20, sampleSize); + + if (subStringA.includes("PD")) { + tempPosition = subStringA.indexOf("PD"); + subStringB = subStringA.slice(Math.max(0, tempPosition-3), tempPosition); + importedArmor["armorPD"+ID] = parseInt(subStringB.replace(/[^\d.-]/g, "")); + importedArmor["totalPD"+ID] = importedArmor["armorPD"+ID] + parseInt(character.pd); + } else { + importedArmor["armorPD"+ID] = 0; + importedArmor["totalPD"+ID] = parseInt(character.pd); + }; + + if (subStringA.includes("ED")) { + tempPosition = subStringA.indexOf("ED"); + subStringB = subStringA.slice(Math.max(0, tempPosition-3), tempPosition); + importedArmor["armorED"+ID] = parseInt(subStringB.replace(/[^\d.-]/g, "")); + importedArmor["totalED"+ID] = importedArmor["armorED"+ID] + parseInt(character.ed); + } else { + importedArmor["armorED"+ID] = 0; + importedArmor["totalED"+ID] = parseInt(character.ed); + }; + }; + + // Activation roll + tempString = armorArray[importCount].text; + + if (tempString.includes("Requires A Roll")) { + tempPosition = tempString.indexOf("Requires A Roll"); + + sampleSize = 4; + subStringA = tempString.substr(tempPosition+15, sampleSize); + subStringB = subStringA.replace(/[^\d]/g, ""); + importedArmor["armorActivation"+ID] = parseInt(subStringB); + } + + // Armor locations. Sometimes locations are stored in the notes field. + if (armorArray[importCount].notes !== "") { + tempString += ", " + armorArray[importCount].notes; + } + importedArmor["armorLocations"+ID] = getArmorLocations(tempString, script_name); + importedArmor["armorEND"+ID] = getArmorEND(tempString, script_name); + + // Get armor mass. + if (armorArray[importCount].mass !== "") { + tempString = armorArray[importCount].mass; + importedArmor["armorMass"+ID] = getItemMass(tempString, script_name); + } else { + importedArmor["armorMass"+ID] = 0; + } + } + } + + // Import armor. + + setAttrs(object.id, importedArmor); + + if(verbose) { + if (imported === 1) { + sendChat(script_name, "Imported 1 piece of armor."); + } else { + sendChat(script_name, "Imported " + imported + " pieces of armor."); + } + } + + // Identify independent multipowers. + let equipmentMultipowers = []; + let shieldSearchIndex; + + for (let i=0; i< multipowerArray.length; i++) { + //sendChat(script_name, "Multipower search "+ i +"."); + if (multipowerArray[i].name.includes("Multipower")) { + equipmentMultipowers.push(i); + } + } + + // Find first shield if any. + let shieldFound = false; + let importedShield = new Array(); + let shieldID = "06"; + + for (let i=0; i < equipmentMultipowers.length; i++) { + // Get next multipower index. + shieldSearchIndex=equipmentMultipowers[i]; + tempString = multipowerArray[shieldSearchIndex].name; + tempString = tempString.toLowerCase(); + + if ( (tempString.includes("shield") || tempString.includes("buckler")) && !shieldFound) { + // Shield found + shieldFound = true; + + if(verbose) { + if (equipmentMultipowers !== "undefined") { + sendChat(script_name, "Found shield multipower."); + } + } + + // Get shield name. + importedShield["weaponName"+shieldID] = multipowerArray[shieldSearchIndex].name.replace("(Multipower)",""); + + // Get STR minimum. + tempString = multipowerArray[shieldSearchIndex].text; + importedShield["weaponStrengthMin"+shieldID] = getWeaponStrMin(tempString, script_name); + + // Get weapon mass. + if (multipowerArray[shieldSearchIndex].mass !== "") { + tempString = multipowerArray[shieldSearchIndex].mass; + importedShield.shieldMass = getItemMass(tempString, script_name); + } else { + importedShield.shieldMass = 0; + } + + // Search for multipower slot that grants DCV. + let foundShieldDCV = false; + + if (i+2 > equipmentMultipowers.length) { + // Shield is the last multipower in the list. + for (let j = shieldSearchIndex; j equipmentMultipowers.length) { + // Shield is the last multipower in the list. + for (let j = shieldSearchIndex; j equipmentMultipowers.length) { + // Item is the last multipower in the list. + for (let j = shieldSearchIndex; j 0) { + + for (importCount = 0; importCount < maxImport; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + importedTalents["talentName"+ID] = perksAndTalentsArray[importCount].type; + importedTalents["talentText"+ID] = perksAndTalentsArray[importCount].text; + importedTalents["talentCP"+ID] = perksAndTalentsArray[importCount].points; + + if (typeof importedTalents["talentText"+ID] !== "undefined") { + + tempString = importedTalents["talentText"+ID]; + + + // Many of the following roll chances won't be used, but are here for completeness. + + if (tempString.includes("10-")) { + importedTalents["talentRollChance"+ID] = 10; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("11-")) { + importedTalents["talentRollChance"+ID] = 11; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("12-")) { + importedTalents["talentRollChance"+ID] = 12; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("13-")) { + importedTalents["talentRollChance"+ID] = 13; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("14-")) { + importedTalents["talentRollChance"+ID] = 14; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("15-")) { + importedTalents["talentRollChance"+ID] = 15; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("16-")) { + importedTalents["talentRollChance"+ID] = 16; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("17-")) { + importedTalents["talentRollChance"+ID] = 17; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("18-")) { + importedTalents["talentRollChance"+ID] = 18; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("19-")) { + importedTalents["talentRollChance"+ID] = 19; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("20-")) { + importedTalents["talentRollChance"+ID] = 20; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("21-")) { + importedTalents["talentRollChance"+ID] = 21; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("3-")) { + importedTalents["talentRollChance"+ID] = 3; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("4-")) { + importedTalents["talentRollChance"+ID] = 4; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("5-")) { + importedTalents["talentRollChance"+ID] = 5; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("6-")) { + importedTalents["talentRollChance"+ID] = 6; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("7-")) { + importedTalents["talentRollChance"+ID] = 7; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("8-")) { + importedTalents["talentRollChance"+ID] = 8; + importedTalents["talentActivate"+ID] = "on"; + } else if (tempString.includes("9-")) { + importedTalents["talentRollChance"+ID] = 9; + importedTalents["talentActivate"+ID] = "on"; + } + + if ( tempString.includes("d6") ) { + tempPosition = tempString.indexOf("d6") + diceString = tempString.slice(0, tempPosition); + diceString = diceString.slice(-2).replace(/\D/g,"") + "d6"; + importedTalents["talentDice"+ID] = diceString; + } else { + importedTalents["talentDice"+ID] = "0"; + } + } + } + + // Display additional perks and talents in the complications text box. + if (perksAndTalentsIndex > maxCombinedSheet) { + let i = maxCombinedSheet; + let extras = 0; + + for (let i = maxCombinedSheet; i= 1.2) ? 10 : 0; + + let tempString; + let damageString; + let tempPosition; + let tempValue = 0; + let endPosition; + let subStringA; + let subStringB; + let subStringC; + let theEffect = ""; + let sampleSize; + let control = 0; + let base = 0; + let active = 0; + let cost = 0; + let advantages = 0; + let limitations = 0; + let count = 0; + let ID = ""; + + let testObject = { + testString : "", + testEndurance : 0, + powerReducedEND : "standard" + } + + var tempObject = new Object(); + + // Overall list of powers + var importedPowers = new Object(); + let powerArray = new Array(); + let powerArrayIndex = 0; + + let importCount = 0; + + /* ------------------------- */ + /* Read Powers */ + /* ------------------------- */ + + for (importCount; importCount < maxPowers; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + if ((typeof character.powers["power"+ID] !== "undefined") && (typeof character.powers["power"+ID].name !== "undefined")) { + + tempString = character.powers["power"+ID].name; + + if (tempString.includes("(VPP)")) { + // Varriable Power Pool found. + // The pool needs to be split into control and base parts. + tempString = character.powers["power"+ID].text; + subStringA = tempString.toLowerCase(); + + if (subStringA.includes("base")) { + subStringA = subStringA.slice(tempString.indexOf("base")-4, tempString.indexOf("base")); + subStringA = subStringA.replace(/\D/g, ''); + base = Number(subStringA); + } else { + // Error + base = 0; + } + + control = Math.round(base/2); + + character.powers["power"+ID].base = heroRoundDown(control, 2); + + // Create entry for Control Cost + powerArray[powerArrayIndex]={ + name: character.powers["power"+ID].name + "(control)", + base: control.toString(), + text: character.powers["power"+ID].text, + cost: control.toString(), + endurance: character.powers["power"+ID].endurance, + damage: character.powers["power"+ID].damage, + compound: false + } + powerArrayIndex++; + + // Create entry for Pool Cost + powerArray[powerArrayIndex]={ + name: character.powers["power"+ID].name, + base: base.toString(), + text: JSON.stringify(base) + "-point Power Pool.", + cost: base.toString(), + endurance: character.powers["power"+ID].endurance, + damage: character.powers["power"+ID].damage, + compound: false + } + powerArrayIndex++; + + } else if (tempString.includes("(Multipower)") || tempString.includes("(MPSlot")) { + // Import multipower or multipower slot. + powerArray[powerArrayIndex]=character.powers["power"+ID]; + + powerArrayIndex++; + } else if (character.powers["power"+ID].compound === "true") { + // Check for compound power and import sub power part separately if found. + + tempString = character.powers["power"+ID].text; + count = (tempString.match(/plus/g) || []).length+1; + + // Remove total costs. + if (tempString.includes("(Total:")) { + tempString = tempString.substring(tempString.indexOf(" Real Cost)") + 12); + } + + if(verbose) { + sendChat(script_name, "Compound power with " + JSON.stringify(count) + " parts."); + } + + damageString = character.powers["power"+ID].damage + for (let i=0; i 0) { + + for (importCount = 0; importCount < maxImport; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + // First fix some known typos. + powerArray[importCount].text = fixKnownSpellingErrors(powerArray[importCount].text, script_name); + + // Assign power effect type. + theEffect = findEffectType(powerArray[importCount].text, script_name); + importedPowers["powerEffect"+ID] = theEffect; + + // If the power does not have a name assign it the effect type. + if (powerArray[importCount].name === "") { + importedPowers["powerName"+ID] = importedPowers["powerEffect"+ID]; + } else { + importedPowers["powerName"+ID] = powerArray[importCount].name; + } + + // Special cases or base cost. + tempCostArray = getPowerBaseCost(character, powerArray[importCount].base, theEffect, powerArray[importCount].text, bonusCP, importedPowers.optionTakesNoSTUN, script_name); + importedPowers["powerBaseCost"+ID] = tempCostArray[0]; + bonusCP = tempCostArray[1]; + + // Determine endurance type, advantages, and limitations. + testObject.testString = powerArray[importCount].text; + testObject.testEndurance = powerArray[importCount].endurance; + + // Get powerReducedEND level and separate endurance limitation or advantage cost. + testObject = findEndurance(testObject); + importedPowers["powerReducedEND"+ID] = testObject.powerReducedEND; + + // Find advantages and limitations values. + importedPowers["powerAdvantages"+ID] = findAdvantages(testObject.testString); + importedPowers["powerLimitations"+ID] = findLimitations(testObject.testString); + importedPowers["powerDamageAdvantage"+ID] = findDamageAdvantages(testObject.testString, script_name); + importedPowers["powerAoE"+ID] = isAoE(testObject.testString) ? "on" : 0; + + // Power descriptions + tempString = powerArray[importCount].text; + if ( (character.version >= 2.1) && (typeof powerArray[importCount].notes !== "undefined") && (powerArray[importCount].notes !== "") ) { + tempString += '\n'+'\n'+powerArray[importCount].notes; + } + importedPowers["powerText"+ID] = (typeof tempString !== "undefined") ? tempString.trim() : ""; + + // Search for skill roll. + tempObject = requiresRoll(testObject.testString); + importedPowers["powerActivate"+ID] = tempObject.hasRoll ? "on" : 0; + importedPowers["powerSkillRoll"+ID] = tempObject.hasRoll ? tempObject.skillRoll : 18; + + // Search for reduced DCV due to the Concentration limitation. + importedPowers["powerDCV"+ID] = reducedDCV(testObject.testString); + + // Search for zero, half, or full range modifiers. + importedPowers["powerRMod"+ID] = reducedRMod(testObject.testString); + + // Search for a STUNx mod. + importedPowers["powerStunMod"+ID] = modifiedSTUNx(testObject.testString); + + // Assign effect dice. + importedPowers["powerDice"+ID] = getPowerDamage(powerArray[importCount].damage, theEffect, character.strength, script_name); + + // Find and assign power type. Remove export notes from names. + tempString = powerArray[importCount].name; + if (tempString.includes("(Multipower)")) { + // Remove note from name. + importedPowers["powerName"+ID] = tempString.replace("(Multipower) ", ""); + importedPowers["powerType"+ID] = "multipower"; + importedPowers["powerEffect"+ID] = "Multipower"; + } else if (tempString.includes("(MPSlot")) { + subStringA = powerArray[importCount].cost; + if (subStringA.includes("v")) { + // Remove note from name. + importedPowers["powerName"+ID] = tempString.replace("(MPSlot", ""); + importedPowers["powerType"+ID] = "variableSlot"; + } else { + // Remove note from name. + importedPowers["powerName"+ID] = tempString.replace("(MPSlot", ""); + importedPowers["powerType"+ID] = "fixedSlot"; + } + } else if (tempString.includes("(VPP)")) { + if (tempString.includes("control")) { + // Remove notes from name. + tempString = tempString.replace("(VPP) ", ""); + importedPowers["powerName"+ID] = tempString.replace("(control)", ""); + importedPowers["powerType"+ID] = "powerPool"; + importedPowers["powerEffect"+ID] = "VPP Control"; + importedPowers["powerAction"+ID] = "false"; + importedPowers["powerBaseCost"+ID] = powerArray[importCount].base; + } else { + // Remove note from name. + importedPowers["powerName"+ID] = tempString.replace("(VPP) ", ""); + importedPowers["powerType"+ID] = "powerPool"; + importedPowers["powerEffect"+ID] = "VPP Pool"; + importedPowers["powerAction"+ID] = "false"; + } + } else if (powerArray[importCount].compound === true) { + importedPowers["powerType"+ID] = "compound"; + } else if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") && ((powerArray[importCount].text).includes("Unified Power")) ) { + importedPowers["powerType"+ID] = "unified"; + } else { + importedPowers["powerType"+ID] = "single"; + } + + // Set attack checkbox for attacks. + importedPowers["powerAttack"+ID] = isAttack(theEffect) ? "on" : 0; + + // Set power type. + importedPowers["powerDamageType"+ID] = getPowerDamageType(theEffect); + + // If Power's effect is Resistant Protection create armor in Armor Slot 4 with a combination of ED and PD. + tempString = (powerArray[importCount].text).toLowerCase(); + + if (theEffect === "Resistant Protection") { + if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if(verbose) { + sendChat(script_name, "Created Resistant Protection armor."); + } + + tempValue = getResistantPD(powerArray[importCount].text, script_name); + if (tempValue > 0) { + charMod.armorPD04 += tempValue; + if ( (specialArray.some(v => tempString.includes(v))) != true) { + // We don't want to add overall modifications for special cases. + charMod.pdMod += tempValue; + } + if (!pdAddedToTotal) { + charMod.totalPD04 = tempValue + parseInt(character.pd); + pdAddedToTotal = true; + } else { + charMod.totalPD04 += tempValue; + } + charMod.armorName04 = importedPowers["powerName"+ID]; + charMod.armorLocations04 = "3-18"; + tempObject = (requiresRoll(powerArray[importCount].text)); + if (tempObject.hasRoll) { + charMod.armorActivation04 = tempObject.skillRoll; + } else { + charMod.armorActivation04 = 18; + } + } + + tempValue = getResistantED(powerArray[importCount].text, script_name); + if (tempValue > 0) { + charMod.armorED04 += tempValue; + if ( (specialArray.some(v => tempString.includes(v))) != true) { + // We don't want to add overall modifications for special cases. + charMod.edMod += tempValue; + } + if (!edAddedToTotal) { + charMod.totalED04 = tempValue + parseInt(character.ed); + edAddedToTotal = true; + } else { + charMod.totalED04 += tempValue; + } + charMod.armorName04 = importedPowers["powerName"+ID]; + charMod.armorLocations04 = "3-18"; + tempObject = (requiresRoll(powerArray[importCount].text)); + if (tempObject.hasRoll) { + charMod.armorActivation04 = tempObject.skillRoll; + } else { + charMod.armorActivation04 = 18; + } + } + } + } else if (theEffect === "Base PD Mod") { + if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if(verbose) { + sendChat(script_name, "Added Resistant PD to armor."); + } + + if ( (powerArray[importCount].text).includes("Resistant")) { + charMod.armorPD04 += parseInt(character.pd); + if (!pdAddedToTotal) { + charMod.totalPD04 += parseInt(character.pd); + pdAddedToTotal = true; + } + charMod.armorName04 = importedPowers["powerName"+ID]; + charMod.armorLocations04 = "3-18"; + tempObject = (requiresRoll(powerArray[importCount].text)); + if (tempObject.hasRoll) { + charMod.armorActivation04 = tempObject.skillRoll; + } else { + charMod.armorActivation04 = 18; + } + } + } + } else if (theEffect === "Base ED Mod") { + if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if(verbose) { + sendChat(script_name, "Added Resistant ED to armor."); + } + + if ( (powerArray[importCount].text).includes("Resistant") ) { + charMod.armorED04 += parseInt(character.ed); + if (!edAddedToTotal) { + charMod.totalED04 += parseInt(character.ed); + edAddedToTotal = true; + } + charMod.armorName04 = importedPowers["powerName"+ID]; + charMod.armorLocations04 = "3-18"; + tempObject = (requiresRoll(powerArray[importCount].text)); + if (tempObject.hasRoll) { + charMod.armorActivation04 = tempObject.skillRoll; + } else { + charMod.armorActivation04 = 18; + } + } + } + } + + // Apply characteristic mods granted by enhancement powers or movement. + tempString = powerArray[importCount].text; + + if ( (typeof tempString != "undefined") && (tempString != "") ) { + switch (theEffect) { + case "Base STR Mod": if (tempString.includes("0 END")) { + importedPowers["optionUntiring"] = "on"; + } + break; + case "Running": charMod.runningMod += getCharacteristicMod(tempString, "Running", script_name); + break; + case "Leaping": charMod.leapingMod += getCharacteristicMod(tempString, "Leaping", script_name); + break; + case "Swimming": charMod.swimmingMod += getCharacteristicMod(tempString, "Swimming", script_name); + break; + case "Flight": charMod.flightMod += getCharacteristicMod(tempString, "Flight", script_name); + break; + case "Enhanced STR": charMod.strengthMod += getCharacteristicMod(tempString, "STR", script_name); + break; + case "Enhanced DEX": charMod.dexterityMod += getCharacteristicMod(tempString, "DEX", script_name); + break; + case "Enhanced CON": charMod.constitutionMod += getCharacteristicMod(tempString, "CON", script_name); + break; + case "Enhanced INT": charMod.intelligenceMod += getCharacteristicMod(tempString, "INT", script_name); + break; + case "Enhanced EGO": charMod.egoMod += getCharacteristicMod(tempString, "EGO", script_name); + break; + case "Enhanced PRE": charMod.presenceMod += getCharacteristicMod(tempString, "PRE", script_name); + break; + case "Enhanced OCV": charMod.ocvMod += getCharacteristicMod(tempString, "OCV", script_name); + break; + case "Enhanced DCV": charMod.dcvMod += getCharacteristicMod(tempString, "DCV", script_name); + break; + case "Enhanced OMCV": charMod.omcvMod += getCharacteristicMod(tempString, "OMCV", script_name); + break; + case "Enhanced DMCV": charMod.dmcvMod += getCharacteristicMod(tempString, "DMCV", script_name); + break; + case "Enhanced BODY": charMod.bodyMod += getCharacteristicMod(tempString, "BODY", script_name); + break; + case "Enhanced PD": charMod.pdMod += getCharacteristicMod(tempString, "PD", script_name); + break; + case "Enhanced ED": charMod.edMod += getCharacteristicMod(tempString, "ED", script_name); + break; + case "Enhanced STUN": charMod.stunMod += getCharacteristicMod(tempString, "STUN", script_name); + break; + case "Enhanced END": charMod.endMod += getCharacteristicMod(tempString, "END", script_name); + break; + case "Enhanced REC": charMod.recMod += getCharacteristicMod(tempString, "REC", script_name); + break; + case "Enhanced PER": if ( tempString.includes("all Sense") ) { + charMod.enhancedPerceptionModifier += getCharacteristicMod(tempString, "PER", script_name); + if ( (tempString.includes("except Sight")) || (tempString.includes("but Sight")) ) { + charMod.perceptionModifierVision += -getCharacteristicMod(tempString, "PER", script_name); + } + if ( (tempString.includes("except Hearing")) || (tempString.includes("but Hearing")) ) { + charMod.perceptionModifierHearing += -getCharacteristicMod(tempString, "PER", script_name); + } + if ( (tempString.includes("except Smell")) || (tempString.includes("but Smell")) ) { + charMod.perceptionModifierSmell += -getCharacteristicMod(tempString, "PER", script_name); + } + } else { + charMod.perceptionModifierVision += (tempString.includes("Sight")) ? getCharacteristicMod(tempString, "PER", script_name) : 0; + charMod.perceptionModifierHearing += (tempString.includes("Hearing")) ? getCharacteristicMod(tempString, "PER", script_name) : 0; + charMod.perceptionModifierSmell += (tempString.includes("Smell")) ? getCharacteristicMod(tempString, "PER", script_name) : 0; + if ( !(tempString.includes("Sight")) && !(tempString.includes("Hearing")) && !(tempString.includes("Smell")) ) { + charMod.enhancedPerceptionModifier += getCharacteristicMod(tempString, "PER", script_name); + } + } + break; + default: break; + } + } + } + } + + // Display additional powers in the talents text box. + tempString = ""; + if (powerArrayIndex > maxPowers) { + let extras = 0; + + for (let i = maxPowers; i < powerArrayIndex; i++) { + tempString += powerArray[i].name + "\n"; + if (powerArray[i].damage !== "") { + tempString += " Damage: " + powerArray[i].damage + "\n"; + } + tempString += " END: " + powerArray[i].endurance + "\n"; + tempString += " Base CP: " + powerArray[i].base + ", " + " Real CP: " + powerArray[i].cost + "\n"; + tempString += powerArray[i].text + "\n" + "\n"; + extras++; + } + + if(verbose) { + sendChat(script_name, extras + " powers placed in notes."); + } + + importedPowers.complicationsTextLeft = tempString; + } + + // Import powers and bonus points to sheet. + importedPowers.bonusBenefit = bonusCP; + + const importedPowersAndMods = Object.assign({}, importedPowers, charMod); + setAttrs(object.id, importedPowersAndMods); + + if(verbose) { + if (powerArrayIndex === 1) { + sendChat(script_name, "Imported 1 power."); + } else { + sendChat(script_name, "Imported " + powerArrayIndex + " powers."); + } + } + + return tempString.trim(); + }; + + + var importComplications = function(object, character, script_name) { + + /* ************************************************* */ + /* *** Import Function: Import Complications *** */ + /* ************************************************* */ + + // Imports the first six complications. + let importCount = 0; + let imported = 0; + let ID = ""; + let tempString = ""; + let diceString = ""; + let tempPosition = 0; + const maxComplications = 10; + const maxOverflow = 10; + let overflowString = ""; + var importedComplications = new Object(); + + /* ------------------------- */ + /* Read Complications */ + /* ------------------------- */ + + for (importCount = 0; importCount < maxComplications + maxOverflow; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + if (importCount < maxComplications) { + if ((typeof character.complications["complication"+ID] !== "undefined") && (typeof character.complications["complication"+ID].type !== "undefined")) { + importedComplications["complicationName"+ID] = character.complications["complication"+ID].type; + importedComplications["complicationCP"+ID] = character.complications["complication"+ID].points; + + tempString = character.complications["complication"+ID].text; + if (typeof character.complications["complication"+ID].notes !== "undefined") { + tempString += '\n'+'\n'+character.complications["complication"+ID].notes; + } + importedComplications["complicationText"+ID] = (typeof tempString !== "undefined") ? tempString.trim() : ""; + + // Type + tempString = character.complications["complication"+ID].type; + tempString = tempString.toLowerCase(); + + if (tempString.includes("accidental change")) { + importedComplications["complicationType"+ID] = "accidental"; + } else if (tempString.includes("dependence")) { + importedComplications["complicationType"+ID] = "dependence"; + } else if (tempString.includes("dependent")) { + importedComplications["complicationType"+ID] = "dependent"; + } else if (tempString.includes("distinctive")) { + importedComplications["complicationType"+ID] = "distinctive"; + } else if ((tempString.includes("enraged")) || (tempString.includes("berserk"))) { + importedComplications["complicationType"+ID] = "enraged"; + } else if (tempString.includes("hunted")) { + importedComplications["complicationType"+ID] = "hunted"; + } else if (tempString.includes("reputation")) { + importedComplications["complicationType"+ID] = "reputation"; + } else if (tempString.includes("physical")) { + importedComplications["complicationType"+ID] = "physical"; + } else if (tempString.includes("psychological")) { + importedComplications["complicationType"+ID] = "psychological"; + } else if (tempString.includes("rival")) { + importedComplications["complicationType"+ID] = "rival"; + } else if (tempString.includes("social")) { + importedComplications["complicationType"+ID] = "social"; + } else if (tempString.includes("susceptibility")) { + importedComplications["complicationType"+ID] = "susceptibility"; + } else if (tempString.includes("unluck")) { + importedComplications["complicationType"+ID] = "unluck"; + } else if (tempString.includes("vulnerability")) { + importedComplications["complicationType"+ID] = "vulnerability"; + } else { + importedComplications["complicationType"+ID] = "custom"; + } + + // Activation Roll + tempString = character.complications["complication"+ID].text + " " + character.complications["complication"+ID].notes; + tempString = tempString.toLowerCase(); + + // Most of these roll options will never be used, but are here for completeness. + + if (tempString.includes("10-")) { + importedComplications["complicationRollChance"+ID] = 10; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("11-")) { + importedComplications["complicationRollChance"+ID] = 11; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("12-")) { + importedComplications["complicationRollChance"+ID] = 12; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("13-")) { + importedComplications["complicationRollChance"+ID] = 13; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("14-")) { + importedComplications["complicationRollChance"+ID] = 14; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("15-")) { + importedComplications["complicationRollChance"+ID] = 15; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("16-")) { + importedComplications["complicationRollChance"+ID] = 16; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("17-")) { + importedComplications["complicationRollChance"+ID] = 17; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("18-")) { + importedComplications["complicationRollChance"+ID] = 18; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("3-")) { + importedComplications["complicationRollChance"+ID] = 3; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("4-")) { + importedComplications["complicationRollChance"+ID] = 4; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("5-")) { + importedComplications["complicationRollChance"+ID] = 5; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("6-")) { + importedComplications["complicationRollChance"+ID] = 6; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("7-")) { + importedComplications["complicationRollChance"+ID] = 7; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("8-")) { + importedComplications["complicationRollChance"+ID] = 8; + importedComplications["complicationActivate"+ID] = "on"; + } else if (tempString.includes("9-")) { + importedComplications["complicationRollChance"+ID] = 9; + importedComplications["complicationActivate"+ID] = "on"; + } + + // Dice + if ( tempString.includes("d6") ) { + tempPosition = tempString.indexOf("d6") + diceString = tempString.slice(0, tempPosition); + diceString = diceString.slice(-2).replace(/\D/g,"") + "d6"; + importedComplications["complicationDice"+ID] = diceString; + } else { + importedComplications["complicationDice"+ID] = "0"; + } + + imported += 1; + } + } else if (importCount < maxComplications + maxOverflow) { + if ((typeof character.complications["complication"+ID] !== "undefined") && (typeof character.complications["complication"+ID].type !== "undefined")) { + overflowString += character.complications["complication"+ID].type + '\n'; + overflowString += character.complications["complication"+ID].text + '\n' + character.complications["complication"+ID].notes + '\n'; + overflowString += "("+character.complications["complication"+ID].points + " points)\n\n"; + + imported += 1; + } + } + } + + importedComplications["complicationsTextRight"] = overflowString; + + if(verbose) { + if (imported === 1) { + sendChat(script_name, "Imported 1 complication."); + } else { + sendChat(script_name, "Imported " + imported + " complications."); + } + } + + // Import complications to sheet. + setAttrs(object.id, importedComplications); + + return; + }; + + + var importAllSkills = function(object, character, script_name) { + + /* ************************************************* */ + /* *** Import Function: Import Skills *** */ + /* ************************************************* */ + + // Struct for counting processed skills. + + let sheetSkillIndexes={ + skillIndex: 0, + generalSkillIndex: 0, + combatSkillIndex: 0, + languageSkillIndex: 0 + } + + const maxSkills = 50; + + for (let importCount = 0; importCount < maxSkills; importCount++) { + + ID = String(importCount+1).padStart(2,'0'); + + if (typeof character.skills["skill"+ID] !== "undefined") {sheetSkillIndexes = importSkill(object, character, script_name, sheetSkillIndexes, character.skills["skill"+ID]);} + } + + return; + }; + + + /* ------------------------- */ + /* Import Helper Functions */ + /* ------------------------- */ + + var cleanQuotes = function(inputString, script_name) { + // Look for double quotes in text that shouldn't be there. Remove them. + + let detailString; + let cleanString; + let frontMatter; + let backMatter; + let startPosition; + let endPosition; + let count = 0; + let matches = 0; + let engagePosition = inputString.indexOf('\"backgroundText\":\"'); + let exitPosition = inputString.indexOf('\"experience\":') - 10; + + for (let i = engagePosition; i < exitPosition; i+=1) { + startPosition = inputString.indexOf('\":\"', i)+1; + endPosition = inputString.indexOf('\", \"', i); + detailString = inputString.slice(startPosition+2, endPosition); + matches = detailString.match(/["]/g); + count = matches ? matches.length : 0; + + if (matches) { + frontMatter = inputString.slice(0, startPosition+2); + backMatter = inputString.slice(endPosition+1); + cleanString = detailString.replace(/["]+/g, ""); + inputString = frontMatter + cleanString + '\"' + backMatter; + + exitPosition -= count; + } + + i += detailString.length - count; + } + + return inputString; + } + + + var importSkill = function(object, character, script_name, sheetSkillIndexes, theSkill) { + // Assign skill to general, combat, language, enhancer, etc. + + if (Object.keys(theSkill).length === 0) { + // Empty Skill. + return sheetSkillIndexes; + + } else if (theSkill.enhancer === "true") { + // Invoke Skill Enhancer Function + importSkillEnhancer(object, character, script_name, theSkill.text); + + } else if (theSkill.display === "Language") { + // Call import language function. + importLanguage(object, character, script_name, theSkill, sheetSkillIndexes.languageSkillIndex); + sheetSkillIndexes.languageSkillIndex++; + + } else if (theSkill.display === "Combat Skill Levels"){ + // Import weapon levels. The combat skill index may or may not be increased. + // This needs to be decided by the function as more general skill levels + // use prepared slots on the character sheet. + sheetSkillIndexes.combatSkillIndex = importWeaponSkill(object, character, script_name, theSkill, sheetSkillIndexes.combatSkillIndex); + + } else if (theSkill.display === "Mental Combat Skill Levels"){ + // Import mental CSLs. Variation of weapon CSLs. + sheetSkillIndexes.combatSkillIndex = importWeaponSkill(object, character, script_name, theSkill, sheetSkillIndexes.combatSkillIndex); + + } else if (theSkill.display === "Penalty Skill Levels") { + // Import penalty skill levels. + sheetSkillIndexes.combatSkillIndex = importWeaponSkill(object, character, script_name, theSkill, sheetSkillIndexes.combatSkillIndex); + + } else if (theSkill.display === ("Weapon Familiarity")) { + // Weapon familiarity skill line. + // There should be only one line since Hero Designer lumps them together. + // We need to break them up. + let tempString = theSkill.text; + tempString = tempString.replace(/\s\s+/g, " "); + tempString = tempString.replace("WF: ", ""); + let weaponFamArrayLength = (tempString.split(",").length - 1); + let weaponFamArray = new Array(weaponFamArrayLength); + + for (let i = 0; i <= weaponFamArrayLength; i++) { + // Split up string into weapon groups. + if (i < weaponFamArrayLength) { + // Get first weapon group before a comma. + weaponFamArray[i] = tempString.substr(0, tempString.indexOf(",")); + + // Remove that weapon group from the string. + tempString = tempString.replace(weaponFamArray[i] + ", ", ""); + } else { + // There is only one group left to get. + weaponFamArray[i] = tempString; + } + } + + // Process the skills in weaponFamArray as individual skills. + // The combatSkillIndex will advance each time. + for (let i = 0; i <= weaponFamArrayLength; i++) { + let tempSkill = { + name: "", + enhancer: "", + text: weaponFamArray[i], + display:"Weapon Familiarity", + cost: 0 + } + if (tempSkill.text.includes("Common") || tempSkill.text.includes("Small Arms") || tempSkill.text.includes("Emplaced Weapons") || tempSkill.text.includes("Beam Weapons") || tempSkill.text.includes("Energy Weapons") || tempSkill.text.includes("Early Firearms") || tempSkill.text.includes("Siege Engines")) { + tempSkill.cost = 2; + } else { + tempSkill.cost = 1; + } + + sheetSkillIndexes.combatSkillIndex = importWeaponSkill(object, character, script_name, tempSkill, sheetSkillIndexes.combatSkillIndex); + } + } else if (theSkill.display === ("Transport Familiarity")) { + // Transport familiarity skill line. + // There should be only one line since Hero Designer lumps them together. + // We need to break them up. + let tempString = theSkill.text; + tempString = tempString.replace(/\s\s+/g, " "); + tempString = tempString.replace("TF: ", ""); + let transportFamArrayLength = (tempString.split(",").length - 1); + let transportFamArray = new Array(transportFamArrayLength); + + for (let i = 0; i <= transportFamArrayLength; i++) { + // Split up string into transport groups. + if (i < transportFamArrayLength) { + // Get first weapon group before a comma. + transportFamArray[i] = tempString.substr(0, tempString.indexOf(",")); + + // Remove that weapon group from the string. + tempString = tempString.replace(transportFamArray[i]+", ", ""); + } else { + // There is only one group left to get. + transportFamArray[i] = tempString; + } + } + + // Process the skills in transportFamArray as individual skills. + // The generalSkillIndex will advance each time. + for (let i = 0; i <= transportFamArrayLength; i++) { + let tempSkill = { + name: transportFamArray[i], + enhancer: "", + text: "TF: " + transportFamArray[i], + display:"Transport Familiarity", + cost: 1 + } + + // Find 2-point TF groups. + if (tempSkill.text.includes("Common") || tempSkill.text.includes("Riding") || tempSkill.text.includes("Space Vehicles") || tempSkill.text.includes("Mecha")) { + tempSkill.cost = 2; + } + + sheetSkillIndexes.generalSkillIndex = importGeneralSkill(object, character, script_name, tempSkill, sheetSkillIndexes.generalSkillIndex); + } + } else if (theSkill.display === "Skill Levels") { + // Import non-combat skill levels. + // Groups of three skills will be a challenge. + + if (theSkill.text.includes("three pre-defined Skills")) { + // This type of skill level is recorded along with general skills. + sheetSkillIndexes.generalSkillIndex = importGeneralSkill(object, character, script_name, theSkill, sheetSkillIndexes.generalSkillIndex); + } else if (parseInt(theSkill.cost/theSkill.levels) === 3) { + // This type of skill level is recorded along with general skills. + + sheetSkillIndexes.generalSkillIndex = importGeneralSkill(object, character, script_name, theSkill, sheetSkillIndexes.generalSkillIndex); + } else { + importSkillLevels(object, character, script_name, theSkill); + } + } else { + // Import general skill + + sheetSkillIndexes.generalSkillIndex = importGeneralSkill(object, character, script_name, theSkill, sheetSkillIndexes.generalSkillIndex); + } + + sheetSkillIndexes.skillIndex++; + + return sheetSkillIndexes; + } + + + var importSkillEnhancer = function(object, character, script_name, enhancerString) { + // This function is called when a skill is identified as an enhancer. + // The skills' text will determine which enhancer it is. + let enhancer; + + switch(enhancerString) { + case "Jack of All Trades": + enhancer = { + enhancerJack: "on", + enhancerJackCP: 3 + } + break; + case "Linguist": + enhancer = { + enhancerLing: "on", + enhancerLingCP: 3 + } + break; + case "Scholar": + enhancer = { + enhancerSch: "on", + enhancerSchCP: 3 + } + break; + case "Scientist": + enhancer = { + enhancerSci: "on", + enhancerSciCP: 3 + } + break; + case "Traveler": + enhancer = { + enhancerTrav: "on", + enhancerTravCP: 3 + } + break; + default: + // Well-Connected + enhancer = { + enhancerWell: "on", + enhancerWellCP: 3 + } + } + + setAttrs(object.id, enhancer); + + return; + } + + + var importLanguage = function(object, character, script_name, languageObject, languageIndex) { + // This function is called when a skill is identified as an enhancer. + // The skills' text will determine which enhancer it is. + // let languages; + + let language; + let name = languageObject.name; + let tempString = languageObject.text; + if (name === "") { + if (tempString.includes("Language:")) { + name = tempString.replace("Language:", ""); + } + if (name.includes("(") && name.includes(")")) { + let endPosition = name.indexOf("("); + name = name.slice(0, endPosition-1); + } + } + + let fluency; + let literacy; + let cost = languageObject.cost; + + // Determine fluency. + if (tempString.includes("native")) { + fluency = "native"; + } else if (cost == 0) { + fluency = "native"; + } else if ((cost == 1) && (tempString.includes("literate"))) { + fluency = "native"; + } else if (tempString.includes("basic")) { + fluency = "basic"; + } else if (tempString.includes("completely")) { + fluency = "accent"; + } else if (tempString.includes("fluent")) { + fluency = "fluent"; + } else if (tempString.includes("idiomatic")) { + fluency = "idiomatic"; + } else if (tempString.includes("imitate")) { + fluency = "imitate"; + } else { + fluency = "none"; + } + + // Determine literacy. + if (tempString.includes("literate")) { + literacy = "on"; + } + else { + literacy = 0; + } + + // Assign this language to the character sheet. + + switch(languageIndex) { + case 0: + language = { + skillName41: name, + skillFluency41: fluency, + skillLiteracy41: literacy + } + break; + case 1: + language = { + skillName42: name, + skillFluency42: fluency, + skillLiteracy42: literacy + } + break; + case 2: + language = { + skillName43: name, + skillFluency43: fluency, + skillLiteracy43: literacy + } + break; + case 3: + language = { + skillName44: name, + skillFluency44: fluency, + skillLiteracy44: literacy + } + break; + case 4: + language = { + skillName45: name, + skillFluency45: fluency, + skillLiteracy45: literacy + } + break; + case 5: + language = { + skillName46: name, + skillFluency46: fluency, + skillLiteracy46: literacy + } + break; + case 6: + language = { + skillName47: name, + skillFluency47: fluency, + skillLiteracy47: literacy + } + break; + case 7: + language = { + skillName48: name, + skillFluency48: fluency, + skillLiteracy48: literacy + } + break; + default: + // Last language slot available. + language = { + skillName49: name, + skillFluency49: fluency, + skillLiteracy49: literacy + } + } + + setAttrs(object.id, language); + + return; + } + + + var importWeaponSkill = function(object, character, script_name, skillObject, weaponSkillIndex) { + // Identify and assign combat levels + + let weaponSkill; + let name = skillObject.name; + let levels = parseInt(skillObject.levels); + let levelCost; + let type = 'none'; + let cost = parseInt(skillObject.cost); + + if (skillObject.text.includes("HTH Combat")) { + // Find the number of levels from the CP spent. + weaponSkill = { + skillLevels38: skillObject.levels + }; + + } else if (skillObject.text.includes("Ranged Combat")) { + // Find the number of levels from the CP spent. + weaponSkill = { + skillLevels39: skillObject.levels + }; + + } else if (skillObject.text.includes("All Attacks")) { + // Find the number of levels from the CP spent. + weaponSkill = { + skillLevels40: skillObject.levels + }; + + } else if (skillObject.text.includes("group") || skillObject.text.includes("single") || (skillObject.display === "Weapon Familiarity") || (skillObject.display === "Penalty Skill Levels") || (skillObject.display === "Combat Skill Levels") || (skillObject.display === "Mental Combat Skill Levels")) { + // Call import weapon skills function. + + // Determine type + if (skillObject.display === "Weapon Familiarity") { + // Weapon familiarity at the moment can be common or single. + if (cost === 1) { + name = skillObject.text; + type = "Fam1"; + levels = 0; + } else { + name = skillObject.text.replace("Weapons", ""); + type = "Fam2"; + levels = 0; + } + } else if (skillObject.display === "Penalty Skill Levels") { + // Determine penalty skill levels + + // Try to shorten the name text. + name = skillObject.text.replace("versus", "vs"); + name = name.replace("Versus", "vs"); + name = name.replace("Location", "Loc"); + name = name.replace("Range", "Rng"); + name = name.replace("Modifiers ", ""); + name = name.replace("Modifier ", ""); + name = name.replace("with", "w/"); + name = name.replace("the ", ""); + + levelCost = parseInt(cost/levels); + switch (levelCost) { + case 1: type = 'PSL1'; + break; + case 2: type = 'PSL2'; + break; + case 3: type = 'PSL3'; + break; + default: 'none'; + } + } else if (skillObject.display === "Mental Combat Skill Levels") { + // Determine mental combat skill levels + name = skillObject.text; + + name = name.replace("with all", "w/"); + name = name.replace("with a", "w/"); + name = name.replace(" single", ""); + name = name.replace("group of Mental Powers", "Mental Group"); + + levelCost = parseInt(cost/levels); + switch (levelCost) { + case 1: type = 'MCSL1'; + break; + case 3: type = 'MCSL3'; + break; + case 6: type = 'MCSL6'; + break; + default: 'none'; + } + } else { + // Determine combat skill levels + name = skillObject.text; + + name = name.replace("with all", "w/"); + name = name.replace("with any", "w/"); + name = name.replace("with a", "w/"); + name = name.replace("small group of attacks", "small group"); + name = name.replace("large group of attacks", "large group"); + + levelCost = parseInt(cost/levels); + switch (levelCost) { + case 2: type = 'CSL2'; + break; + case 3: type = 'CSL3'; + break; + case 5: type = 'CSL5'; + break; + case 8: type = 'CSL8'; + break; + default: 'none'; + } + } + + // Assign skill parameters to an open weapon skill slot. + switch (weaponSkillIndex) { + case 0: + // Weapon skill slot 1. + weaponSkill = { + skillName31: name, + skillType31: type, + skillLevels31: levels, + skillCP31: cost + } + break; + case 1: + // Weapon skill slot 2. + weaponSkill = { + skillName32: name, + skillType32: type, + skillLevels32: levels, + skillCP32: cost + } + break; + case 2: + // Weapon skill slot 3. + weaponSkill = { + skillName33: name, + skillType33: type, + skillLevels33: levels, + skillCP33: cost + } + break; + case 3: + // Weapon skill slot 4. + weaponSkill = { + skillName34: name, + skillType34: type, + skillLevels34: levels, + skillCP34: cost + } + break; + case 4: + // Weapon skill slot 5. + weaponSkill = { + skillName35: name, + skillType35: type, + skillLevels35: levels, + skillCP35: cost + } + break; + case 5: + // Weapon skill slot 6. + weaponSkill = { + skillName36: name, + skillType36: type, + skillLevels36: levels, + skillCP36: cost + } + break; + case 6: + // Last weapon skill slot available. + weaponSkill = { + skillName37: name, + skillType37: type, + skillLevels37: levels, + skillCP37: cost + } + } + weaponSkillIndex++; + } + + setAttrs(object.id, weaponSkill); + + return weaponSkillIndex; + } + + + var importSkillLevels = function(object, character, script_name, skillObject) { + + if (skillObject.text.includes("all Intellect Skills")) { + // The broad group skill level is ambiguous. + // By default we will guess intellect as the most common. + + if (skillObject.name.includes("nteract")) { + // Look at name to see if player added interaction label. + let skillLevel = { + interactionLevels: skillObject.levels, + interactionLevelsCP: skillObject.levels*4 + } + + if(verbose) { + sendChat(script_name, "Found interaction group levels."); + } + setAttrs(object.id, skillLevel); + } else if (skillObject.name.includes("ntellect")) { + // Look at name to see if player added intellect label. + let skillLevel = { + intellectLevels: skillObject.levels, + intellectLevelsCP: skillObject.levels*4 + } + + if(verbose) { + sendChat(script_name, "Found intellect group levels."); + } + setAttrs(object.id, skillLevel); + } else { + // Assume intellect. + let skillLevel = { + intellectLevels: skillObject.levels, + intellectLevelsCP: skillObject.levels*4 + } + + if(verbose) { + sendChat(script_name, "Found broad group levels. Assuming intellect."); + } + setAttrs(object.id, skillLevel); + } + } else if (skillObject.text.includes("all Agility Skills")) { + let skillLevel = { + agilityLevels: skillObject.levels, + agilityLevelsCP: skillObject.levels*6 + } + setAttrs(object.id, skillLevel); + } else if (skillObject.text.includes("all Non-Combat Skills")) { + let skillLevel = { + noncombatLevels: skillObject.levels, + noncombatLevelsCP: skillObject.levels*10 + } + setAttrs(object.id, skillLevel); + } else if (skillObject.text.includes("Overall")) { + let skillLevel = { + overallLevels: skillObject.levels, + overallLevelsCP: skillObject.levels*12 + } + setAttrs(object.id, skillLevel); + } + + return; + } + + + var importGeneralSkill = function(object, character, script_name, skillObject, generalSkillIndex) { + // Identify and import a general skill. + + var theSkill = new Object(); + let attribute = skillObject.attribute; + let text = skillObject.text; + let type = "none"; + let base = skillObject.base; + let levels = skillObject.levels; + let cost = skillObject.cost; + + if (skillObject.display === ("Skill Levels")) { + // Three-group skill. + type = "group"; + } else if (skillObject.text.includes("three pre-defined Skills")) { + // Three-group skill. + type = "group"; + } else if ((base === "0") && (cost === "0") && skillObject.text.includes("11-")) { + // Everyman professional skill. + type = "everymanPS"; + } else if ((base === "0") && (cost === "0")) { + // Everyman skill. + type = "everyman"; + } else if (text.startsWith("KS") && ((base-levels) === 2)) { + // Knowledge Skill + type = "ks"; + } else if (text.startsWith("KS") && ((base-levels) === 3)) { + // Knowledge Skill based on INT. + type = "intKS"; + } else if (text.startsWith("CK") && ((base-levels) === 2)) { + // City Knowledge Skill + type = "ck"; + } else if (text.startsWith("CK") && ((base-levels) === 3)) { + // City Knowledge Skill based on INT. + type = "intCK"; + } else if (text.startsWith("CuK") && ((base-levels) === 2)) { + // Culture Knowledge Skill + type = "cuk"; + } else if (text.startsWith("CuK") && ((base-levels) === 3)) { + // Culture Knowledge Skill based on INT. + type = "intCuK"; + } else if (text.startsWith("Science Skill") && ((base-levels) === 2)) { + // Science Skill + type = "ss"; + } else if (text.startsWith("Science Skill") && ((base-levels) === 3)) { + // Science Skill based on INT. + type = "intSS"; + } else if (text.startsWith("AK") && ((base-levels) === 2)) { + // Area Knowledge. + type = "ak"; + } else if (text.startsWith("AK") && ((base-levels) === 3)) { + // Area Knowledge Skill based on INT. + type = "intAK"; + } else if (text.startsWith("TF")) { + // Transport familiarity. + type = "tf"; + } else if (text.startsWith("PS")) { + // Professional skill. + type = "ps"; + } else if (attribute === "INT") { + // Intellect skill. + type = "int"; + } else if (attribute === "DEX") { + // Agility skill. + type = "dex"; + } else if (attribute === "PRE") { + // Interact skill. + type = "pre"; + } else if (attribute === "EGO") { + // Ego skill. Probably faith. + type = "ego"; + } else if (attribute === "STR") { + // Strength skill (unusual). + type = "str"; + } else if (attribute === "CON") { + // Constitution skill (unusual). + type = "con"; + } else if ((skillObject.display === "Cramming") || (text.toLowerCase().includes("skill"))) { + // A special skill or group of undetermined skills. + type = "other"; + } else if (cost === "") { + // Empty slot. + type = "none"; + } else { + // Best last guess is combat. + type = "combat"; + } + + // Try to find the best name of the skill. + // It may be in .name, .text, or .display. + + let name = skillObject.name; + if (name === "") { + if ((text !== "") && text.includes("AK: ")) { + name = text.replace("AK: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("KS: ")) { + name = text.replace("KS: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("CK: ")) { + name = text.replace("CK: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("CuK: ")) { + name = text.replace("CuK: ", ""); + name = name.slice(0, -4); + } else if ((text !== "") && text.includes("SS: ")) { + name = text.replace("SS: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("Science Skill: ")) { + name = text.replace("Science Skill: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("PS: ")) { + name = text.replace("PS: ", ""); + name = name.slice(0, -3); + } else if ((text !== "") && text.includes("Defense Maneuver")) { + name = text.replace("Defense Maneuver", "DEF Maneuver"); + } else if ((text !== "") && text.includes("Computer Programming")) { + name = text.replace("Programming", "Prog."); + name = name.slice(0, -3); + } else if (skillObject.display !== "") { + name = skillObject.display; + } + } + + // Import the skill + ID = String(generalSkillIndex+1).padStart(2,'0'); + theSkill["skillName"+ID] = name.trim(); + theSkill["skillType"+ID] = type; + theSkill["skillCP"+ID] = cost; + if (type === "everyman") { + theSkill["skillRollChance"+ID] = "8"; + } + + setAttrs(object.id, theSkill); + + generalSkillIndex++; + + return generalSkillIndex; + } + + + var findEndurance = function(testObject) { + // Determine endurance type, advantages, and limitations. + // Remove advantage or limitation from tempString so that they aren't counted twice. + + // testObject should have three items: + // testString, testEndurance, powerReducedEND + + let tempString = testObject.testString; + let endString = testObject.testEndurance; + + if ( ((tempString.includes("Costs Endurance (-1/4)")) || (tempString.includes("Costs Half Endurance"))) && (endString.includes("["))) { + testObject.powerReducedEND = "costsENDhalf"; + tempString = tempString.replace("Costs Endurance (-1/4)", ""); + } else if ((tempString.includes("Costs Endurance (-1/2)")) && (endString.includes("["))) { + testObject.powerReducedEND = "costsENDfull"; + tempString = tempString.replace("Costs Endurance (-1/2)", ""); + } else if (endString.includes("[")) { + testObject.powerReducedEND = "noEND"; + } else if (tempString.includes("Costs Endurance (-1/4)")) { + testObject.powerReducedEND = "costsENDhalf"; + tempString = tempString.replace("Costs Endurance (-1/4)", ""); + } else if (tempString.includes("Costs Endurance (-1/2)")) { + testObject.powerReducedEND = "costsENDfull"; + tempString = tempString.replace("Costs Endurance (-1/2)", ""); + } else if ((tempString.includes("Reduced Endurance (1/2 END; +1/2)")) && (tempString.includes("Autofire"))) { + testObject.powerReducedEND = "reducedENDAF"; + tempString = tempString.replace("Reduced Endurance (1/2 END; +1/2)", ""); + } else if (tempString.includes("Reduced Endurance (0 END; +1/2)")) { + testObject.powerReducedEND = "zeroEND"; + tempString = tempString.replace("Reduced Endurance (0 END; +1/2)", ""); + } else if (tempString.includes("Reduced Endurance (0 END; +1)")) { + testObject.powerReducedEND = "zeroENDAF"; + tempString = tempString.replace("Reduced Endurance (0 END; +1)", ""); + } else if (tempString.includes("Reduced Endurance (1/2 END; +1/4)")) { + testObject.powerReducedEND = "reducedEND"; + tempString = tempString.replace("Reduced Endurance (1/2 END; +1/4)", ""); + } else if (tempString.includes("Increased Endurance Cost (x2 END; -1/2)")) { + testObject.powerReducedEND = "increasedENDx2"; + tempString = tempString.replace("Increased Endurance Cost (x2 END; -1/2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x3 END; -1)")) { + testObject.powerReducedEND = "increasedENDx3"; + tempString = tempString.replace("Increased Endurance Cost (x3 END; -1)", ""); + } else if (tempString.includes("Increased Endurance Cost (x4 END; -1 1/2)")) { + testObject.powerReducedEND = "increasedENDx4"; + tempString = tempString.replace("Increased Endurance Cost (x4 END; -1 1/2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x5 END; -2)")) { + testObject.powerReducedEND = "increasedENDx5"; + tempString = tempString.replace("Increased Endurance Cost (x5 END; -2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x6 END; -2 1/2)")) { + testObject.powerReducedEND = "increasedENDx6"; + tempString = tempString.replace("Increased Endurance Cost (x6 END; -2 1/2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x7 END; -3)")) { + testObject.powerReducedEND = "increasedENDx7"; + tempString = tempString.replace("Increased Endurance Cost (x7 END; -3)", ""); + } else if (tempString.includes("Increased Endurance Cost (x8 END; -3 1/2)")) { + testObject.powerReducedEND = "increasedENDx8"; + tempString = tempString.replace("Increased Endurance Cost (x8 END; -3 1/2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x9 END; -3 1/2)")) { + testObject.powerReducedEND3 = "increasedENDx9"; + tempString = tempString.replace("Increased Endurance Cost (x9 END; -3 1/2)", ""); + } else if (tempString.includes("Increased Endurance Cost (x10 END; -4)")) { + testObject.powerReducedEND = "increasedENDx10"; + tempString = tempString.replace("Increased Endurance Cost (x10 END; -4)", ""); + } else if (endString == "") { + testObject.powerReducedEND = "noEND"; + } else if (endString == 0) { + testObject.powerReducedEND = "noEND"; + } else { + testObject.powerReducedEND = "standardEND"; + } + + testObject.testString = tempString; + + return testObject; + } + + + var findEffectType = function(tempString, script_name) { + // Search for and return effect keywords. + + const talentArray = ["absolute range sense", "absolute time sense", "ambidexterity", "animal friendship", "bump of direction", "combat luck", "combat sense", "danger sense", "deadly blow", "double jointed", "eidetic memory", "environmental movement", "lightning calculator", "lightning reflexes", "lightsleap", "off-hand defense", "perfect pitch", "resistance", "simulate death", "speed reading", "striking appearance", "universal translator", "weaponmaster"]; + const skillArray = ["overall"]; + const senseModifierArray = ["analyze", "concealed", "adjacent", "dimensional", "discriminatory", "increased arc", "microscopic", "penetrative", "range", "rapid", "telescopic", "tracking", "transmit"]; + + if ( (typeof tempString != "undefined") && (tempString != "") ) { + let lowerCaseString = tempString.toLowerCase(); + + if (lowerCaseString.includes("applied to str")) { + return "Base STR Mod"; + } else if (lowerCaseString.includes("range based on str") && lowerCaseString.includes("of hka")) { + return "HKA Mod"; + } else if (lowerCaseString.includes("applied to running")) { + return "Base Running Mod"; + } else if (lowerCaseString.includes("applied to leaping")) { + return "Base Leaping Mod"; + } else if (lowerCaseString.includes("applied to swimming")) { + return "Base Swimming Mod"; + } else if (lowerCaseString.includes("applied to pd")) { + return "Base PD Mod"; + } else if (lowerCaseString.includes("applied to ed")) { + return "Base ED Mod"; + } else if (tempString.includes("Absorption")) { + return "Absorption"; + } else if (tempString.includes("Aid")) { + return "Aid"; + } else if (tempString.includes("Automaton")) { + return "Automaton"; + } else if (tempString.includes("Barrier")) { + return "Barrier"; + } else if (tempString.includes("Mental Blast")) { + return "Mental Blast"; + } else if (tempString.includes("Blast")) { + return "Blast"; + } else if (tempString.includes("Change Environment")) { + return "Change Environment"; + } else if (tempString.includes("Clairsentience")) { + return "Clairsentience"; + } else if (tempString.includes("Clinging")) { + return "Clinging"; + } else if (tempString.includes("Damage Negation")) { + return "Damage Negation"; + } else if (tempString.includes("Damage Reduction")) { + return "Damage Reduction"; + } else if (tempString.includes("Darkness")) { + return "Darkness"; + } else if (tempString.includes("Deflection")) { + return "Deflection"; + } else if (tempString.includes("Density Increase")) { + return "Density Increase"; + } else if (tempString.includes("Desolidification")) { + return "Desolidification"; + } else if ( (tempString.includes("Dispel")) && !(lowerCaseString.includes("difficult to dispel")) ) { + return "Dispel"; + } else if (tempString.includes("Does Not Bleed")) { + return "Does Not Bleed"; + } else if (tempString.includes("Drain")) { + return "Drain"; + } else if (tempString.includes("Duplication")) { + return "Duplication"; + } else if (tempString.includes("Enhanced Senses")) { + return "Enhanced Senses"; + } else if (tempString.includes("Endurance Reserve")) { + return "Endurance Reserve"; + } else if (tempString.includes("Extra Limb")) { + return "Extra Limb"; + } else if (tempString.includes("Extra-Dimensional Movement")) { + return "Extra-Dimensional Movement"; + } else if (tempString.includes("Faster-Than-Light-Travel")) { + return "Faster-Than-Light-Travel"; + } else if (tempString.includes("Flash")) { + return "Flash"; + } else if (tempString.includes("Flash Defense")) { + return "Flash Defense"; + } else if (tempString.includes("Resistant")) { + return "Resistant Protection"; + } else if (tempString.includes("Flight")) { + return "Flight"; + } else if (tempString.includes("Growth")) { + return "Growth"; + } else if (tempString.includes("Hand-To-Hand Attack")) { + return "HTH Attack"; + } else if (tempString.includes("Healing")) { + return "Healing"; + } else if (tempString.includes("Invisibility")) { + return "Invisibility"; + } else if (tempString.includes("Killing Attack - Hand-To-Hand")) { + return "HTH Killing Attack"; + } else if (tempString.includes("HKA")) { + return "HTH Killing Attack"; + } else if ( (tempString.includes("Images")) && !(lowerCaseString.includes("only to perceive images")) ) { + return "Images"; + } else if (tempString.includes("Killing Attack - Ranged")) { + return "Ranged Killing Attack"; + } else if (tempString.includes("RKA")) { + return "Ranged Killing Attack"; + } else if (tempString.includes("Knockback Resistance")) { + return "Knockback Resistance"; + } else if (tempString.includes("Leaping")) { + return "Leaping"; + } else if (tempString.includes("Life Support")) { + return "Life Support"; + } else if (tempString.includes("Luck")) { + return "Luck"; + } else if (tempString.includes("Transform")) { + return "Transform"; + } else if (tempString.includes("Mental Defense")) { + return "Mental Defense"; + } else if (tempString.includes("Mental Illusions")) { + return "Mental Illusions"; + } else if (tempString.includes("Mind Control")) { + return "Mind Control"; + } else if (tempString.includes("Mind Link")) { + return "Mind Link"; + } else if (tempString.includes("Mind Scan")) { + return "Mind Scan"; + } else if (tempString.includes("Multiform")) { + return "Multiform"; + } else if (tempString.includes("No Hit Locations")) { + return "No Hit Locations"; + } else if (tempString.includes("Possession")) { + return "Possession"; + } else if (tempString.includes("Power Defense")) { + return "Power Defense"; + } else if (tempString.includes("Reach")) { + return "Reach"; + } else if (tempString.includes("Reflection")) { + return "Reflection"; + } else if (tempString.includes("Regeneration")) { + return "Regeneration"; + } else if (tempString.includes("Running")) { + return "Running"; + } else if (tempString.includes("Shape Shift")) { + return "Shape Shift"; + } else if (tempString.includes("Shrinking")) { + return "Shrinking"; + } else if (tempString.includes("Stretching")) { + return "Stretching"; + } else if (tempString.includes("Summon")) { + return "Summon"; + } else if (tempString.includes("Swimming")) { + return "Swimming"; + } else if (tempString.includes("Swinging")) { + return "Swinging"; + } else if (tempString.includes("Takes No STUN")) { + return "Takes No STUN"; + } else if (tempString.includes("Telekinesis")) { + return "Telekinesis"; + } else if (tempString.includes("Telepathy")) { + return "Telepathy"; + } else if (tempString.includes("Teleportation")) { + return "Teleportation"; + } else if (tempString.includes("Tunneling")) { + return "Tunneling"; + } else if (tempString.includes("Active Sonar")) { + return "Active Sonar"; + } else if (tempString.includes("Detect")) { + return "Detect"; + } else if (tempString.includes("Enhanced Perception")) { + return "Enhanced PER"; + } else if ( (tempString.includes("High Range Radio")) || (tempString.includes("HRRP")) ) { + return "HR Radio PER"; + } else if (tempString.includes("Infrared Perception")) { + return "IR Perception"; + } else if (tempString.includes("IR Perception")) { + return "IR Perception"; + } else if (tempString.includes("Mental Awareness")) { + return "Mental Awareness"; + } else if (tempString.includes("Nightvision")) { + return "Nightvision"; + } else if (tempString.includes("Radar")) { + return "Radar"; + } else if (tempString.includes("Radio Perception/Transmission")) { + return "Radio PER/Trans"; + } else if (tempString.includes("Radio Perception")) { + return "Radio PER"; + } else if (tempString.includes("Spatial Awareness")) { + return "Spatial Awareness"; + } else if (tempString.includes("Tracking")) { + return "Enhanced Sense"; + } else if (tempString.includes("Ultrasonic Perception")) { + return "Ultrasonic PER"; + } else if (tempString.includes("Ultraviolet Perception")) { + return "UV Perception"; + } else if (skillArray.some(v => lowerCaseString.includes(v))) { + return "Skill"; + } else if (talentArray.some(v => lowerCaseString.includes(v))) { + return "Talent"; + } else if (senseModifierArray.some(v => lowerCaseString.includes(v))) { + return "Sense Modifier"; + } else if (tempString.includes("SPD")) { + return "Enhanced SPD"; + } else if (tempString.includes("PER")) { + return "Enhanced PER"; + } else if (tempString.includes("STR")) { + return "Enhanced STR"; + } else if (tempString.includes("CON")) { + return "Enhanced CON"; + } else if (tempString.includes("INT")) { + return "Enhanced INT"; + } else if (tempString.includes("EGO")) { + return "Enhanced EGO"; + } else if (tempString.includes("PRE")) { + return "Enhanced PRE"; + } else if (tempString.includes("OCV")) { + return "Enhanced OCV"; + } else if (tempString.includes("OMCV")) { + return "Enhanced OMCV"; + } else if (tempString.includes("DMCV")) { + return "Enhanced DMCV"; + } else if (tempString.includes("PD")) { + return "Enhanced PD"; + } else if (tempString.includes("ED")) { + return "Enhanced ED"; + } else if (tempString.includes("BODY")) { + return "Enhanced BODY"; + } else if (tempString.includes("STUN")) { + return "Enhanced STUN"; + } else if (tempString.includes("REC")) { + return "Enhanced REC"; + } else if (tempString.includes("DEX")) { + return "Enhanced DEX"; + } else if (lowerCaseString.includes("sight") || lowerCaseString.includes("hearing") || lowerCaseString.includes("smell") || lowerCaseString.includes("taste") || lowerCaseString.includes("touch") || lowerCaseString.includes("sense")) { + return "Enhanced PER"; + } else if (lowerCaseString.includes("eating") || lowerCaseString.includes("immunity") || lowerCaseString.includes("longevity") || lowerCaseString.includes("safe in") || lowerCaseString.includes("breathing") || lowerCaseString.includes("sleeping")) { + return "Life Support"; + } else if (tempString.includes("Entangle")) { + return "Entangle"; + } else if ( (lowerCaseString.includes("advantage")) || (lowerCaseString.includes("area of effect")) ) { + return "Naked Advantage"; + } else if (lowerCaseString.includes("worth of") || lowerCaseString.includes("powers") || lowerCaseString.includes("spells") || lowerCaseString.includes("abilities")) { + return "To Be Determined"; + } else if (tempString.includes("DCV")) { + return "Enhanced DCV"; + } else if (tempString.includes("END")) { + return "Enhanced END"; + } else { + return "Unknown Effect"; + } + } else { + return "Unknown Effect"; + } + } + + + var fixKnownSpellingErrors = function(theString, script_name) { + // Here we try to catch and correct important typos found in tested commercial sources. + // Add to typoList as needed. + + const typoList = [ + ["Restistant", "Resistant"] + ]; + + const iMax = 1; + let found = false; + let i = 0; + + if ( (typeof theString != "undefined") && (theString != "") ) { + while ( (i < iMax) && !found ) { + if (theString.includes(typoList[i][0])) { + theString = theString.replace(typoList[i][0], typoList[i][1]); + found = true; + } + + i++; + } + } + + return theString; + } + + + var isAttack = function (effect) { + // For setting the attack state. + const attackSet = new Set(["Blast", "Dispel", "Drain", "Entangle", "Flash", "Healing", "HTH Attack", "HTH Killing Attack", "Mental Blast", "Mental Illusions", "Mind Control", "Mind Link", "Mind Scan", "Ranged Killing Attack", "Telekinesis", "Telepathy", "Transform"]); + + return attackSet.has(effect) ? true : false; + } + + + var getResistantPD = function (inputString, script_name) { + // For Armor slot 4. + let protection = 0; + let startPosition = 0; + let endPosition = 0; + let tempString = inputString; + + if (inputString.includes("PD/")) { + endPosition = inputString.indexOf("PD/"); + tempString = inputString.slice(endPosition-Math.min(4,endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } else if (inputString.includes("PD")) { + endPosition = inputString.indexOf("PD"); + tempString = inputString.slice(endPosition-Math.min(4,endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } else { + protection = 0; + } + + return protection; + } + + + var getResistantED = function (inputString, script_name) { + // For Armor slot 4. + let protection = 0; + let startPosition = 0; + let endPosition = 0; + let tempString = inputString; + + if (inputString.includes("PD/ED")) { + endPosition = inputString.indexOf("PD/ED"); + tempString = inputString.slice(endPosition-Math.min(3, endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } else if (inputString.includes("PD/")) { + if (inputString.includes("ED")) { + endPosition = inputString.indexOf("ED"); + tempString = inputString.slice(endPosition-Math.min(4,endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } else if (inputString.includes("ED/")) { + endPosition = inputString.indexOf("ED/"); + tempString = inputString.slice(endPosition-Math.min(4,endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } + } else if (inputString.includes("ED")) { + endPosition = inputString.indexOf("ED"); + tempString = inputString.slice(endPosition-Math.min(4,endPosition), endPosition); + tempString = tempString.replace(/[^0-9]/g, ""); + protection = (tempString !== "") ? Number(tempString) : 0; + protection = isNaN(protection) ? 0 : protection; + } else { + protection = 0; + } + + return protection; + } + + + var getPowerDamageType = function (effect) { + // For setting the attack state. + const killingSet = new Set(["HTH Killing Attack", "Ranged Killing Attack"]); + const normalSet = new Set(["Blast", "HTH Attack"]); + const mentalSet = new Set(["Mental Blast", "Mental Illusions", "Mind Control", "Mind Link", "Mind Scan", "Telepathy"]); + let damageType = null; + + if (killingSet.has(effect)) { + damageType = "killing"; + } else if (mentalSet.has(effect)) { + damageType = "mental"; + } else if (normalSet.has(effect)) { + damageType = "normal"; + } else { + damageType = "power"; + } + + return damageType; + } + + + var getPowerBaseCost = function(character, base, effect, text, bonus, option, script_name) { + // For ordinary powers, this function simply returns the imported base cost. + // For stat modification powers, this function assigns a base cost determined from the characteristic + // and also awards those points as bonus points so that the character is not charged twice. + // For 'to be determined' powers the function attempts to parse the base cost from the power's text. + + let powerBaseCost = parseInt(base); + let bonusCP = parseInt(bonus); + let slicePosition = 0; + let tempValue = 0; + let tempString = ""; + + if (effect === "Base STR Mod") { + powerBaseCost = parseInt(character.strength); + bonusCP = bonusCP + powerBaseCost; + } else if (effect === "Base Running Mod") { + powerBaseCost = parseInt(character.running); + bonusCP = bonusCP + powerBaseCost; + } else if (effect === "Base Leaping Mod") { + powerBaseCost = parseInt(Math.round(character.leaping/2)); + bonusCP = bonusCP + powerBaseCost; + } else if (effect === "Base Swimming Mod") { + powerBaseCost = parseInt(Math.round(character.swimming/2)); + bonusCP = bonusCP + powerBaseCost; + } else if (effect === "Base Defense Mod") { + if (option === "on") { + // Determine pd cost. If character has takes No STUN triple cost over the base 2. + if ((option === "on") && (character.pd > 1)) { + powerBaseCost = parseInt(1 + (character.pd - 1)*3); + bonusCP = bonusCP + powerBaseCost; + } else { + powerBaseCost = parseInt(character.pd*1); + bonusCP = bonusCP + powerBaseCost; + } + + // Add ed cost. If character has takes No STUN triple cost over the base 2. + if ((option === "on") && (character.ed > 1)) { + powerBaseCost = powerBaseCost + parseInt(1 + (character.ed - 1)*3); + bonusCP = bonusCP + powerBaseCost; + } else { + powerBaseCost = powerBaseCost + parseInt(character.ed*1); + bonusCP = bonusCP + powerBaseCost; + } + } else { + powerBaseCost = parseInt(character.pd*1) + parseInt(character.ed*1); + bonusCP = bonusCP + powerBaseCost; + } + } else if (effect === "Base PD Mod") { + // If character has takes No STUN triple cost over the base 2. + if ((option === "on") && (character.pd > 1)) { + powerBaseCost = parseInt(1 + (character.pd - 1)*3); + bonusCP = bonusCP + powerBaseCost; + } else { + powerBaseCost = parseInt(character.pd*1); + bonusCP = bonusCP + powerBaseCost; + } + } else if (effect === "Base ED Mod") { + // If character has takes No STUN triple cost over the base 2. + if ((option === "on") && (character.ed > 1)) { + powerBaseCost = parseInt(1 + (character.ed - 1)*3); + bonusCP = bonusCP + powerBaseCost; + } else { + powerBaseCost = parseInt(character.ed*1); + bonusCP = bonusCP + powerBaseCost; + } + } else if (effect === "Endurance Reserve") { + // Special cost due to separate END and REC purchases. + slicePosition = text.indexOf("END"); + tempString = text.slice(Math.max(0, slicePosition-7), slicePosition); + + tempValue = tempString.replace(/[^0-9\-]/g, ''); + if (tempValue === "") { + tempValue = 0; + } + powerBaseCost = Math.round(tempValue/4); + + slicePosition = text.indexOf("REC"); + tempString = text.slice(Math.max(0, slicePosition-6), slicePosition); + + tempValue = tempString.replace(/[^0-9\-]/g, ''); + if (tempValue === "") { + tempValue = 0; + } + powerBaseCost += Math.round(2 * tempValue/3); + } else if (effect === "To Be Determined") { + // Workaround for when sometimes points reported as base are incorrect. + if ((text.match(/^\d+|\d+\b|\d+(?=\w)/g) !== null) && (text.match(/^\d+|\d+\b|\d+(?=\w)/g) !== "")) { + powerBaseCost = text.match(/^\d+|\d+\b|\d+(?=\w)/g)[0]; + } else { + // If the array came up empty, default to base cost. + powerBaseCost = parseInt(base); + } + } + + if (bonus != bonusCP) { + if(verbose) { + sendChat(script_name, JSON.stringify(bonusCP - bonus) + " CP added to Bonus."); + } + } + + return [powerBaseCost, bonusCP]; + } + + + var findAdvantages = function(tempString) { + // Determine total limitations. This will take some doing. + + let advantages = 0; + + // Find half-integers. Replace larger ones first. + advantages = advantages + ((tempString.match(/\+5 1\/2\)/g) || []).length)*5.5; + tempString = tempString.replace("+5 1/2)",""); + advantages = advantages + ((tempString.match(/\+4 1\/2\)/g) || []).length)*4.5; + tempString = tempString.replace("+4 1/2)",""); + advantages = advantages + ((tempString.match(/\+3 1\/2\)/g) || []).length)*3.5; + tempString = tempString.replace("+3 1/2)",""); + advantages = advantages + ((tempString.match(/\+2 1\/2\)/g) || []).length)*2.5; + tempString = tempString.replace("+2 1/2)",""); + advantages = advantages + ((tempString.match(/\+1 1\/2\)/g) || []).length)*1.5; + tempString = tempString.replace("+1 1/2)",""); + advantages = advantages + ((tempString.match(/\+1\/2\)/g) || []).length)*0.5; + tempString = tempString.replace("+1/2)",""); + + // Find three-quarter integers. Replace larger ones first. + advantages = advantages + ((tempString.match(/\+5 3\/4\)/g) || []).length)*5.75; + tempString = tempString.replace("+5 3/4)",""); + advantages = advantages + ((tempString.match(/\+4 3\/4\)/g) || []).length)*4.75; + tempString = tempString.replace("+4 3/4)",""); + advantages = advantages + ((tempString.match(/\+3 3\/4\)/g) || []).length)*3.75; + tempString = tempString.replace("+3 3/4)",""); + advantages = advantages + ((tempString.match(/\+2 3\/4\)/g) || []).length)*2.75; + tempString = tempString.replace("+2 3/4)",""); + advantages = advantages + ((tempString.match(/\+1 3\/4\)/g) || []).length)*1.75; + tempString = tempString.replace("+1 3/4)",""); + advantages = advantages + ((tempString.match(/\+3\/4\)/g) || []).length)*0.75; + tempString = tempString.replace("+3/4)",""); + + // Find quarter integers. Replace larger ones first. + advantages = advantages + ((tempString.match(/\+5 1\/4\)/g) || []).length)*5.25; + tempString = tempString.replace("+5 1/4)",""); + advantages = advantages + ((tempString.match(/\+4 1\/4\)/g) || []).length)*4.25; + tempString = tempString.replace("+4 1/4)",""); + advantages = advantages + ((tempString.match(/\+3 1\/4\)/g) || []).length)*3.25; + tempString = tempString.replace("+3 1/4)",""); + advantages = advantages + ((tempString.match(/\+2 1\/4\)/g) || []).length)*2.25; + tempString = tempString.replace("+2 1/4)",""); + advantages = advantages + ((tempString.match(/\+1 1\/4\)/g) || []).length)*1.25; + tempString = tempString.replace("+1 1/4)",""); + advantages = advantages + ((tempString.match(/\+1\/4\)/g) || []).length)*0.25; + tempString = tempString.replace("+1/4)",""); + + // Find whole integers. Replace larger ones first. + advantages = advantages + ((tempString.match(/\+6\)/g) || []).length)*6; + tempString = tempString.replace("+6)",""); + advantages = advantages + ((tempString.match(/\+5\)/g) || []).length)*5; + tempString = tempString.replace("+5)",""); + advantages = advantages + ((tempString.match(/\+4\)/g) || []).length)*4; + tempString = tempString.replace("+4)",""); + advantages = advantages + ((tempString.match(/\+3\)/g) || []).length)*3; + tempString = tempString.replace("+3)",""); + advantages = advantages + ((tempString.match(/\+2\)/g) || []).length)*2; + tempString = tempString.replace("+2)",""); + advantages = advantages + ((tempString.match(/\+1\)/g) || []).length)*1; + tempString = tempString.replace("+1)",""); + + return advantages; + } + + var findLimitations = function(tempString) { + // Determine total limitations. This will take some doing. + + let limitations = 0; + + // Find half-integers. Replace larger ones first. + limitations = limitations + ((tempString.match(/-5 1\/2\)/g) || []).length)*5.5; + tempString = tempString.replace("-5 1/2)",""); + limitations = limitations + ((tempString.match(/-4 1\/2\)/g) || []).length)*4.5; + tempString = tempString.replace("-4 1/2)",""); + limitations = limitations + ((tempString.match(/-3 1\/2\)/g) || []).length)*3.5; + tempString = tempString.replace("-3 1/2)",""); + limitations = limitations + ((tempString.match(/-2 1\/2\)/g) || []).length)*2.5; + tempString = tempString.replace("-2 1/2)",""); + limitations = limitations + ((tempString.match(/-1 1\/2\)/g) || []).length)*1.5; + tempString = tempString.replace("-1 1/2)",""); + limitations = limitations + ((tempString.match(/-1\/2\)/g) || []).length)*0.5; + tempString = tempString.replace("-1/2)",""); + + // Find three-quarter integers. Replace larger ones first. + limitations = limitations + ((tempString.match(/-5 3\/4\)/g) || []).length)*5.75; + tempString = tempString.replace("-5 3/4)",""); + limitations = limitations + ((tempString.match(/-4 3\/4\)/g) || []).length)*4.75; + tempString = tempString.replace("-4 3/4)",""); + limitations = limitations + ((tempString.match(/-3 3\/4\)/g) || []).length)*3.75; + tempString = tempString.replace("-3 3/4)",""); + limitations = limitations + ((tempString.match(/-2 3\/4\)/g) || []).length)*2.75; + tempString = tempString.replace("-2 3/4)",""); + limitations = limitations + ((tempString.match(/-1 3\/4\)/g) || []).length)*1.75; + tempString = tempString.replace("-1 3/4)",""); + limitations = limitations + ((tempString.match(/-3\/4\)/g) || []).length)*0.75; + tempString = tempString.replace("-3/4)",""); + + // Find quarter integers. Replace larger ones first. + limitations = limitations + ((tempString.match(/-5 1\/4\)/g) || []).length)*5.25; + tempString = tempString.replace("-5 1/4)",""); + limitations = limitations + ((tempString.match(/-4 1\/4\)/g) || []).length)*4.25; + tempString = tempString.replace("-4 1/4)",""); + limitations = limitations + ((tempString.match(/-3 1\/4\)/g) || []).length)*3.25; + tempString = tempString.replace("-3 1/4)",""); + limitations = limitations + ((tempString.match(/-2 1\/4\)/g) || []).length)*2.25; + tempString = tempString.replace("-2 1/4)",""); + limitations = limitations + ((tempString.match(/-1 1\/4\)/g) || []).length)*1.25; + tempString = tempString.replace("-1 1/4)",""); + limitations = limitations + ((tempString.match(/-1\/4\)/g) || []).length)*0.25; + tempString = tempString.replace("-1/4)",""); + + // Find whole integers. Replace larger ones first. + limitations = limitations + ((tempString.match(/-6\)/g) || []).length)*6; + tempString = tempString.replace("-6)",""); + limitations = limitations + ((tempString.match(/-5\)/g) || []).length)*5; + tempString = tempString.replace("-5)",""); + limitations = limitations + ((tempString.match(/-4\)/g) || []).length)*4; + tempString = tempString.replace("-4)",""); + limitations = limitations + ((tempString.match(/-3\)/g) || []).length)*3; + tempString = tempString.replace("-3)",""); + limitations = limitations + ((tempString.match(/-2\)/g) || []).length)*2; + tempString = tempString.replace("-2)",""); + limitations = limitations + ((tempString.match(/-1\)/g) || []).length)*1; + tempString = tempString.replace("-1)",""); + + return limitations; + } + + + var isAoE = function(inputString) { + // Search advantages for any that indicate an Area of Effect power. + // Written so as to be able to look for more than just "area" but this may be enough. + inputString = inputString.replace(/\W/g, " "); + inputString = inputString.toLowerCase(); + + const searchSet = new Set(["area"]); + let setOfWords = new Set(inputString.split(" ")); + let intersection = new Set([...setOfWords].filter(x => searchSet.has(x))); + let answer = false; + + if (intersection.size != 0) { + answer = true; + } + + return answer; + } + + + var requiresRoll = function(inputString) { + // Determine if the power as an activation roll and find it if it is simple. + let lowerCaseString = inputString.toLowerCase(); + let detailString; + let startPosition; + let endPosition; + let answer = false; + let value = 18; + let searchSet = new Set(["skill", "characteristic", "ps", "ks", "ss", "attack", "per"]); + let setOfWords; + let intersection; + + if (lowerCaseString.includes("requires a roll")) { + + answer = true; + + // Attempt to obtain the skill roll needed if it is a simple activation roll. The others + // would require guesses, which means we need to leave the decision to the players. + + startPosition = lowerCaseString.indexOf("requires a roll"); + startPosition = lowerCaseString.indexOf("(", startPosition); + endPosition = lowerCaseString.indexOf(")", startPosition); + detailString = lowerCaseString.slice(startPosition, endPosition); + setOfWords = new Set((detailString.replace(/\W/g, " ").split(" "))); + intersection = new Set([...setOfWords].filter(x => searchSet.has(x))); + + if (intersection.size === 0) { + endPosition = detailString.indexOf("-", 0); + value = detailString.slice(0, endPosition); + value = value.replace(/\D/g, ''); + if (value.length !== 0) { + value = Number(value); + } + } + } + + return { + "hasRoll": answer, + "skillRoll": value + } + } + + + var reducedDCV = function(inputString) { + // Search for the Concentration limitation. + inputString = inputString.toLowerCase(); + let answer; + + if (inputString.includes("0 dcv")) { + answer = "zero"; + } else if (inputString.includes("1/2 dcv")) { + answer = "half"; + } else { + answer = "full"; + } + + return answer; + } + + + var reducedRMod = function(inputString) { + // Search for half or zero range modifier advantages. + inputString = inputString.toLowerCase(); + let answer; + + if (inputString.includes("no range modifier")) { + answer = "zero"; + } else if (inputString.includes("half range modifier")) { + answer = "half"; + } else { + answer = "STD"; + } + + return answer; + } + + + var modifiedSTUNx = function(inputString) { + // Search for a STUNx multiplier. + inputString = inputString.toLowerCase(); + let answer; + + if (inputString.includes("-2 decreased stun multiplier")) { + answer = "-2"; + } else if (inputString.includes("-1 decreased stun multiplier")) { + answer = "-1"; + } else if (inputString.includes("+1 increased stun multiplier")) { + answer = "1"; + } else if (inputString.includes("+2 increased stun multiplier")) { + answer = "2"; + } else { + answer = "0"; + } + + return answer; + } + + + var getWeaponStrMin = function (weaponString, script_name) { + // Parse weapon text and look for one of three strings used + // by Hero Designer to record a weapon strength minimum. + let strengthMin = 0; + let strengthString; + let startParenthesis; + let endParenthesis; + let valueString; + let defaultStrength = 0; + + if (weaponString !== "") { + if (weaponString.includes("STR Minimum")) { + tempPosition = weaponString.indexOf("STR Minimum"); + startParenthesis = weaponString.indexOf("(", tempPosition + 11); + endParenthesis = weaponString.indexOf(")", tempPosition + 11); + strengthString = weaponString.slice(tempPosition + 11, startParenthesis); + + // Get the limitation value in case no strength is available. + valueString = weaponString.slice(startParenthesis+1, endParenthesis); + valueString = valueString.replace(/\s/g, ""); + + switch(valueString) { + case "-1/4": + defaultStrength = 4; + break; + case "-1/2": + defaultStrength = 9; + break; + case "-3/4": + defaultStrength = 14; + break; + case "-1": + defaultStrength = 19; + break; + default: + defaultStrength = 1; + } + + // Check to see if a strength range is used: + if (strengthString.includes("-")) { + tempPosition = strengthString.indexOf("-"); + strengthString = strengthString.substring(0, tempPosition); + } + + strengthMin = parseInt(strengthString.replace(/\D/g, ""))||defaultStrength; + + } else if (weaponString.includes("STR Min")) { + tempPosition = weaponString.indexOf("STR Min"); + startParenthesis = weaponString.indexOf("(", tempPosition + 8); + endParenthesis = weaponString.indexOf(")", tempPosition + 8); + strengthString = weaponString.slice(tempPosition + 8, startParenthesis); + + // Get the limitation value in case no strength is available. + valueString = weaponString.slice(startParenthesis+1, endParenthesis); + valueString = valueString.replace(/\s/g, ""); + + switch(valueString) { + case "-1/4": + defaultStrength = 4; + break; + case "-1/2": + defaultStrength = 9; + break; + case "-3/4": + defaultStrength = 14; + break; + case "-1": + defaultStrength = 19; + break; + default: + defaultStrength = 1; + } + + // Check to see if a strength range is used: + if (strengthString.includes("-")) { + tempPosition = strengthString.indexOf("-"); + strengthString = strengthString.substring(0, tempPosition); + } + + strengthMin = parseInt(strengthString.replace(/\D/g, ""))||defaultStrength; + + } else { + strengthMin = 0; + } + } else { + strengthMin = 0; + } + + return strengthMin; + } + + + var getWeaponRange = function (rangeString, strength, mass, script_name) { + // Parses range string for numeric characters. + // If "var" is found, calls the range based strength function "calculateRange". + + let range = 0; + + if (rangeString !== "") { + if (rangeString.includes("var")) { + range = calculateRange(strength, mass); + } else { + range = parseInt(rangeString.replace(/[^\d.-]/g, "")); + } + } else { + range = 0; + } + + return range; + } + + + var getWeaponStrength = function (strengthMin, strengthMax, script_name) { + // Returns STR in increments of 5 above strengthMin up to strengthMax. + + let differenceDC = 0; + let strength = strengthMax; + + if (strengthMax >= strengthMin) { + differenceDC = Math.floor( (strengthMax - strengthMin)/5 ); + strength = strengthMin + 5 * differenceDC; + } + + return strength; + } + + + var getPowerDamage = function (damageString, effect, strength, script_name) { + // Parses damageString for damage dice. + + let damage = "0"; + let DC = 0; + let strDC = Math.floor(strength/5); + let halfDie = (((strength % 5) === 3) || ((strength % 5) === 4)) ? true : false; + let lastIndex = 0; + let detailString; + let startPosition; + let endPosition; + var diceSet = new Set(["Aid", "Blast", "Dispel", "Drain", "Entangle", "Flash", "HTH Attack", "HTH Killing Attack", "Ranged Killing Attack", "Healing", "Luck", "Mental Blast", "Mental Illusions", "Mind Control", "Mind Scan", "Transform", "Telepathy"]); + + if (diceSet.has(effect)) { + if (damageString.includes("standard effect")) { + startPosition = damageString.indexOf("standard effect"); + endPosition = damageString.indexOf(")", startPosition); + detailString = damageString.slice(startPosition+16, endPosition); + damage = detailString; + } else { + if ((damageString.match(/d6/g) || []).length > 1) { + damageString = damageString.replace("d6", "d6+"); + lastIndex = damageString.lastIndexOf("d6+"); + damageString = damageString.substring(0, lastIndex) + "d6" + damageString.substring(lastIndex + 2); + } + + // Sometimes the damage string contains extra bits after a comma. Drop them. + if (damageString.includes(",")) { + damageString = damageString.split(",")[0]; + } + + // Look for (xd6 w/STR) and use that. + if (damageString.includes(" w/STR")) { + damageString = damageString.match(/\(([^)]*)\)/)[1]; + damageString = damageString.replace(" w/STR", ""); + damageString = damageString.trim(); + } else if (effect === "HTH Attack") { + endPosition = damageString.indexOf("d"); + detailString = damageString.substring(0,endPosition); + DC = parseInt(detailString.replace(/[^0-9]/g, ""))||0; + DC += strDC; + damageString = DC.toString() + "d6"; + damageString += halfDie ? "+d3" : ""; + } + + // Make sure the 1/2d6 is a 1d3. + if (damageString.includes(" 1/2d6")) { + damage = damageString.replace(" 1/2d6", "d6+d3"); + } else if (damageString.includes("1/2d6")) { + damage = damageString.replace("1/2d6", "d3"); + } else { + damage = damageString; + } + } + } else { + damage = "0"; + } + + return damage; + } + + + var getCharacteristicMod = function (inputString, searchString, script_name) { + let charMod = 0; + let lastIndex = 0; + let detailString = ""; + let startPosition = 0; + let endPosition = 0; + let lowerCaseString = inputString.toLowerCase(); + + const specialArray = ["real weapon", "only works", "only for", "only to", "only applies", "only when", "attacks", "requires a roll", "for up to"]; + var leadingSet = new Set(["STR", "DEX", "CON", "INT", "EGO", "PRE", "OCV", "DCV", "OMCV", "DMCV", "PD", "ED", "BODY", "STUN", "END", "REC", "PER"]); + var trailingSet = new Set(["Running", "Leaping", "Swimming", "Flight"]); + + if (specialArray.some(v => lowerCaseString.includes(v))) { + // We don't want to add overall modifications for special cases. + charMod = 0; + } else if (leadingSet.has(searchString)) { + endPosition = inputString.indexOf(searchString); + detailString = inputString.slice(0, endPosition); + startPosition = detailString.includes("+") ? detailString.indexOf("+") : 0; + detailString = detailString.slice(startPosition, endPosition); + charMod = detailString.replace(/[^0-9\-]/g, ''); + if (charMod === "") { + charMod = 0; + } + } else if (trailingSet.has(searchString)) { + startPosition = inputString.indexOf(searchString); + detailString = inputString.slice(startPosition + searchString.length); + endPosition = detailString.includes("m") ? detailString.indexOf("m") : detailString.length; + detailString = detailString.slice(startPosition, endPosition); + charMod = detailString.replace(/[^0-9\-]/g, ''); + if (charMod === "") { + charMod = 0; + } + } else { + charMod = 0; + } + + if (verbose) { + sendChat(script_name, "Applied characteristic mod " + searchString + " + " + charMod.toString()); + } + + // Make sure we don't return something nasty. + return isNaN(charMod) ? 0 : Math.max(-99, Math.min( (parseInt(charMod)||0), 99)); + } + + + var getGrowthMod = function (inputString, searchString, script_name) { + let charMod = 0; + let lastIndex = 0; + let detailString = ""; + let startPosition; + let lowerCaseString = inputString.toLowerCase(); + + const specialArray = ["real weapon", "only works", "only for", "only to", "only applies", "only when", "requires a roll", "for up to"]; + var leadingSet = new Set(["STR", "DEX", "CON", "INT", "EGO", "PRE", "OCV", "DCV", "OMCV", "DMCV", "PD", "ED", "BODY", "STUN", "END", "REC", "PER","Running", "Leaping", "Swimming", "Flight"]); + + if (specialArray.some(v => lowerCaseString.includes(v))) { + // We don't want to add overall modifications for special cases. + charMod = 0; + } else if (leadingSet.has(searchString)) { + endPosition = inputString.indexOf(searchString); + detailString = inputString.slice(0, endPosition); + startPosition = detailString.includes("+") ? detailString.indexOf("+") : 0; + detailString = detailString.slice(startPosition, endPosition); + charMod = detailString.replace(/[^0-9\-]/g, ''); + if (charMod === "") { + charMod = 0; + } + } else { + charMod = 0; + } + + return Math.max(-99, Math.min( (parseInt(charMod)||0), 99)); + } + + + var getWeaponDamage = function (damageString, script_name) { + // Parses damageString for damage dice. + + let damage = "0"; + let lastIndex = 0; + let detailString; + let startPosition; + let endPosition; + + if (damageString.includes("standard effect")) { + startPosition = damageString.indexOf("standard effect"); + endPosition = damageString.indexOf(")", startPosition); + detailString = damageString.slice(startPosition+16, endPosition); + damage = detailString; + } else { + // Remove dice in w/STR since we'll calculated it. + if (damageString.includes(" w/STR")) { + damageString = damageString.replace(/\([^()]*\)/g, ""); + } + + // Separate joined dice if present. + if ((damageString.match(/d6/g) || []).length > 1) { + damageString = damageString.replace("d6", "d6+"); + lastIndex = damageString.lastIndexOf("d6+"); + damageString = damageString.substring(0, lastIndex) + "d6" + damageString.substring(lastIndex + 2); + } + + // Make sure the 1/2d6 is a 1d3. + if (damageString.includes(" 1/2d6")) { + damage = damageString.replace(" 1/2d6", "d6+d3"); + } else if (damageString.includes("1/2d6")) { + damage = damageString.replace("1/2d6", "d3"); + } else { + damage = damageString; + } + } + + return damage; + } + + + var checkDamageBySTR = function (damageString, script_name) { + damageBySTR = false; + + if (damageString.includes(" w/STR")) { + damageBySTR = true; + } + + return damageBySTR; + } + + + var getArmorLocations = function (inputString, script_name) { + let locations = ""; + let startPosition = 0; + let endPosition = 0; + + inputString = inputString.toLowerCase(); + + if (inputString.includes("location")) { + startPosition = inputString.indexOf("location"); + locations = inputString.slice(startPosition); + if (locations.includes(';')) { + endPosition = locations.indexOf(';'); + locations = locations.slice(0,endPosition); + } else if (locations.includes(')')) { + endPosition = locations.indexOf(')'); + locations = locations.slice(0,endPosition); + } else { + endPosition = Math.min(28, locations.length); + locations = locations.slice(0,endPosition); + } + locations = locations.replace(/[^\d,-]/g, ""); + if (locations.includes(',')) { + locations = locations.replace(',', ", "); + } + } else if (inputString.includes("loc")) { + startPosition = inputString.indexOf("loc"); + locations = inputString.slice(startPosition); + if (locations.includes(';')) { + endPosition = locations.indexOf(';'); + locations = locations.slice(0,endPosition); + } else if (locations.includes(')')) { + endPosition = locations.indexOf(')'); + locations = locations.slice(0,endPosition); + } else { + endPosition = Math.min(11, locations.length); + locations = locations.slice(0,endPosition); + } + locations = locations.replace(/[^\d,-]/g, ""); + if (locations.includes(',')) { + locations = locations.replace(',', ", "); + } + } + + return locations.trim(); + } + + + var getArmorEND = function (inputString, script_name) { + let tempString = ""; + let endurance = 0; + let startPosition = 0; + let endPosition = 0; + + inputString = inputString.toLowerCase(); + + if (inputString.includes("end/turn:")) { + startPosition = inputString.indexOf("end/turn:") + 9; + endPosition = Math.min(inputString.length, startPosition + 2); + tempString = inputString.slice(startPosition, endPosition); + endurance = parseInt(tempString.replace(/[^\d]/g, ""))||0; + } else if (inputString.includes("end/turn")) { + endPosition = inputString.indexOf("end/turn"); + startPosition = Math.max(0, endPosition - 2); + tempString = inputString.slice(startPosition, endPosition); + endurance = parseInt(tempString.replace(/[^\d]/g, ""))||0; + } else { + endurance = 0; + } + + return endurance; + } + + + var findDamageAdvantages = function (weaponString, script_name) { + // See 6E2 98 for a list of advantages that affect weapon damage. + let advantage = 0; + let temp = 0; + let searchString = ""; + + weaponString = weaponString.toLowerCase(); + + searchString = "area of effect"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "armor piercing"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "autofire"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "attack versus alternate defense"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "boostable"; + advantage += getSingleAdvantage (weaponString, searchString); + + if (weaponString.includes("constant")) { + advantage += 0.5; + } + + searchString = "cumulative"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "damage over time"; + advantage += getSingleAdvantage (weaponString, searchString); + + if (weaponString.includes("does body")) { + advantage += 1; + } + + if (weaponString.includes("does knockback")) { + advantage += 0.25; + } + + if (weaponString.includes("double knockback")) { + advantage += 0.5; + } + + if (weaponString.includes("+1 increased stun multiplier")) { + advantage += 0.25; + } else if (weaponString.includes("+2 increased stun multiplier")) { + advantage += 0.50; + } + + searchString = "penetrating"; + advantage += getSingleAdvantage (weaponString, searchString); + + // Check for the ranged advantage. + if (weaponString.includes("range based on str (+1/4)")) { + advantage += 0.25; + } else if (weaponString.includes("ranged (+1/2)")) { + advantage += 0.50; + } + + if (weaponString.includes("sticky")) { + advantage += 0.5; + } + + searchString = "time limit"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "transdimensional"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "trigger"; + advantage += getSingleAdvantage (weaponString, searchString); + + if (weaponString.includes("uncontrolled")) { + advantage += 0.5; + } + + searchString = "variable advantage"; + advantage += getSingleAdvantage (weaponString, searchString); + + searchString = "variable special effects"; + advantage += getSingleAdvantage (weaponString, searchString); + + return advantage; + } + + var getSingleAdvantage = function(weaponString, searchString) { + let advantage = 0; + + if (weaponString.includes(searchString)) { + searchString = weaponString.slice(weaponString.indexOf(searchString) + searchString.length); + searchString = searchString.match(/\(([^)]+)\)/)[0]; + + advantage = findAdvantages(searchString); + + if (advantage < 0) { + advantage = 0; + } + } + + return advantage; + } + + + var calculateRange = function(strength, mass) { + // Determines range based on strength. + let liftCapability; + let freeCapability; + let effectiveStrength; + let range; + + // First calculate carrying capacity. + switch (strength) { + case 0: liftCapability = 0; + break; + case 1: liftCapability = 8; + break; + case 2: liftCapability = 16; + break; + case 3: liftCapability = 25; + break; + case 4: liftCapability = 38; + break; + default: liftCapability=Math.round(25*Math.pow(2,(strength/5))); + } + + // Subtract the thrown weight from capacity. + freeCapability = liftCapability-mass; + + // Determine unused strength and calculate range. + if (freeCapability <= 0) { + range = 0; + } else { + if (freeCapability <= 8) { + effectiveStrength = 1; + range = 2; + } else if (freeCapability <= 16) { + effectiveStrength = 2; + range = 3; + } else if (freeCapability <= 25) { + effectiveStrength = 3; + range = 4; + } else if (freeCapability <= 38) { + effectiveStrength = 4; + range = 6; + } else { + effectiveStrength = 5 * Math.log2(freeCapability/25); + range = Math.round(8 * effectiveStrength/5); + } + } + + return parseInt(Math.round(range)); + } + + + var getItemMass = function(massString, script_name) { + // Remove units from mass and round to one decimal. + let mass = 0; + + if (massString !== "") { + massString = parseFloat(massString.replace(/[^\d.-]/g, "")); + mass = Math.round(10*massString)/10; + } + + return mass; + } + + + var getStunModifier = function(itemString, script_name) { + // Parse string for STUN multiple. + let stunModifier = 0; + let tempPosition; + + if ((typeof itemString !== "undefined") && (itemString.length !== 0)) { + if (itemString.includes("Increased STUN Multiplier")) { + tempPosition = itemString.indexOf("Increased STUN Multiplier"); + stunModifier = parseInt(itemString.substr(tempPosition-3, 2)); + } else if (itemString.includes("Decreased STUN Multiplier")) { + tempPosition = itemString.indexOf("Decreased STUN Multiplier"); + stunModifier = parseInt(itemString.substr(tempPosition-3, 2)); + } + } + + return stunModifier; + } + + + var getOCVmodifier = function(weaponString, script_name) { + // Parse weapon string for OCV modifier or penalty. + let ocvModifier = 0; + let tempPosition; + let subString; + + // First, remove Range Modifier OCV if present. + weaponString = weaponString.replace("OCV modifier",""); + + // Then search for OCV bonus. + if ((weaponString !== "") && (weaponString.includes("OCV"))) { + tempPosition = weaponString.indexOf("OCV"); + subString = weaponString.slice(0, tempPosition); + + // If there is a modifier before the OCV entry, drop characters up to that point. + if (subString.includes(")")) { + tempPosition = subString.lastIndexOf(")"); + subString = subString.substr(tempPosition); + } + + subString = subString.replace(/[^\d-]/g, ""); + ocvModifier = parseInt(subString); + } + + return ocvModifier; + } + + + var heroRoundUp = function(numerator, denominator) { + + if (denominator > 0) { + const intermediate = numerator/denominator; + const remainder = Math.floor((numerator % denominator)*10)/10; + + if (remainder < 0.5) { + return Math.floor(intermediate); + } else { + return Math.ceil(intermediate); + } + } + + // Error. Return unmodified value. + return numerator; + } + + + var heroRoundDown = function(numerator, denominator) { + + if (denominator > 0) { + const intermediate = numerator/denominator; + const remainder = Math.floor((numerator % denominator)*10)/10; + + if (remainder < 0.6) { + return Math.floor(intermediate); + } else { + return Math.ceil(intermediate); + } + } + + // Error. Return unmodified value. + return numerator; + } + + +/* **************************************** */ +/* *** END Importing Functions *** */ +/* **************************************** */ + + // TEST + const createSingleWriteQueue = (attributes) => { + // this is the list of trigger attributes that will trigger class recalculation, as of 5e OGL 2.5 October 2018 + // (see on... handler that calls update_class in sheet html) + // these are written first and individually, since they trigger a lot of changes + let class_update_triggers = [ + 'strength']; + + // set class first, everything else is alphabetical + let classAttribute = class_update_triggers.shift(); + class_update_triggers.sort(); + class_update_triggers.unshift(classAttribute); + + // write in deterministic order (class first, then alphabetical) + + let items = []; + + for (trigger of class_update_triggers) { + let value = attributes[trigger]; + if ((value === undefined) || (value === null)) { + continue; + } + items.push([trigger, value]); + log('hero: trigger attribute ' + trigger); + delete attributes[trigger]; + } + + return items; + } + + + const reportReady = (character) => { + // From Beyond. Left as-is. + // + // TODO this is nonsense. we aren't actually done importing, because notifications in the character sheet are firing for quite a while + // after we finish changing things (especially on first import) and we have no way (?) to wait for it to be done. These are not sheet workers + // on which we can wait. + // sendChat(script_name, '
Import of ' + character.character_name + ' is ready at https://journal.roll20.net/character/' + object.id +'
', null, {noarchive:true}); + return; + } + + + const blankIfNull = (input) => { + return (input === null)?"":input; + } + + + const ucFirst = (string) => { + if(string == null) return string; + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + + const sendConfigMenu = (player, first) => { + let playerid = player.id; + let prefix = (state[state_name][playerid].config.prefix !== '') ? state[state_name][playerid].config.prefix : '[NONE]'; + let prefixButton = makeButton(prefix, '!hero --config prefix|?{Prefix}', buttonStyle); + let suffix = (state[state_name][playerid].config.suffix !== '') ? state[state_name][playerid].config.suffix : '[NONE]'; + let suffixButton = makeButton(suffix, '!hero --config suffix|?{Suffix}', buttonStyle); + let overwriteButton = makeButton(state[state_name][playerid].config.overwrite, '!hero --config overwrite|'+!state[state_name][playerid].config.overwrite, buttonStyle); + let debugButton = makeButton(state[state_name][playerid].config.debug, '!hero --config debug|'+!state[state_name][playerid].config.debug, buttonStyle); + let optionMaximumsButton = makeButton(state[state_name][playerid].config.maximums, '!hero --config maximums|'+!state[state_name][playerid].config.maximums, buttonStyle); + let optionLiteracyButton = makeButton(state[state_name][playerid].config.literacy, '!hero --config literacy|'+!state[state_name][playerid].config.literacy, buttonStyle); + let optionSuperENDButton = makeButton(state[state_name][playerid].config.superEND, '!hero --config superEND|'+!state[state_name][playerid].config.superEND, buttonStyle); + let optionLocationsButton = makeButton(state[state_name][playerid].config.locations, '!hero --config locations|'+!state[state_name][playerid].config.locations, buttonStyle); + + let listItems = [ + 'Overwrite: '+overwriteButton+'
CAUTION: overwrites an existing character sheet that has a matching character name.', + 'Prefix: '+prefixButton, + 'Suffix: '+suffixButton, + 'Verbose Report: '+debugButton, + ] + + let list = 'Importer'+makeList(listItems, 'overflow: hidden; list-style: none; padding: 0; margin: 0;', 'overflow: hidden; margin-top: 5px;'); + + let inPlayerJournalsButton = makeButton(player.get('displayname'), "", buttonStyle); + let controlledByButton = makeButton(player.get('displayname'), "", buttonStyle); + if(playerIsGM(playerid)) { + let players = ""; + let playerObjects = findObjs({ + _type: "player", + }); + for(let i = 0; i < playerObjects.length; i++) { + players += '|'+playerObjects[i]['attributes']['_displayname']+','+playerObjects[i].id; + } + + let ipj = state[state_name][playerid].config.inplayerjournals == "" ? '[NONE]' : state[state_name][playerid].config.inplayerjournals; + if(ipj != '[NONE]' && ipj != 'all') ipj = getObj('player', ipj).get('displayname'); + inPlayerJournalsButton = makeButton(ipj, '!hero --config inplayerjournals|?{Player|None,[NONE]|All Players,all'+players+'}', buttonStyle); + let cb = state[state_name][playerid].config.controlledby == "" ? '[NONE]' : state[state_name][playerid].config.controlledby; + if(cb != '[NONE]' && cb != 'all') cb = getObj('player', cb).get('displayname'); + controlledByButton = makeButton(cb, '!hero --config controlledby|?{Player|None,[NONE]|All Players,all'+players+'}', buttonStyle); + } + + let sheetListItems = [ + 'In Player Journal: ' + inPlayerJournalsButton, + 'Player Control: ' + controlledByButton, + 'Use Char Maximums: ' + optionMaximumsButton, + 'Literacy Costs CP: ' + optionLiteracyButton, + 'Super-Heroic END: ' + optionSuperENDButton, + 'Use Hit Locations: ' + optionLocationsButton + ] + + let sheetList = '
Character Sheet'+makeList(sheetListItems, 'overflow: hidden; list-style: none; padding: 0; margin: 0;', 'overflow: hidden; margin-top: 5px;'); + + // Set verbose (debug) option + let debug = ""; + if(state[state_name][playerid].config.debug){ + // The original version here would generate debug option buttons. For now, we will only change the verbose reporting state. + verbose = true; + } else { + verbose = false; + } + + // Set characteristic maximums option + if(state[state_name][playerid].config.maximums){ + defaultAttributes.useCharacteristicMaximums = "on"; + } else { + defaultAttributes.useCharacteristicMaximums = 0; + } + + // Set literacy cost option + if(state[state_name][playerid].config.literacy){ + defaultAttributes.optionLiteracyCostsPoints = "on"; + } else { + defaultAttributes.optionLiteracyCostsPoints = 0; + } + + // Set super-heroic END option + if(state[state_name][playerid].config.superEND){ + defaultAttributes.optionSuperHeroicEndurance = "on"; + } else { + defaultAttributes.optionSuperHeroicEndurance = 0; + } + + // Set hit location system option + if(state[state_name][playerid].config.locations){ + defaultAttributes.optionHitLocationSystem = "on"; + } else { + defaultAttributes.optionHitLocationSystem = 0; + } + + let resetButton = makeButton('Reset', '!hero --reset', altButtonStyle + ' margin: auto; width: 90%; display: block; float: none;'); + + //let title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config'; + let title_text = (first) ? 'HD Importer First Time Setup' : 'HD Importer Configuration'; + let text = '
'+makeTitle(title_text)+list+sheetList+debug+'
'+resetButton+'
'; + + sendChat(script_name, '/w "' + player.get('displayname') + '" ' + text, null, {noarchive:true}); + }; + + + const sendHelpMenu = (player, first) => { + let configButton = makeButton('Config', '!hero --config', altButtonStyle+' margin: auto; width: 90%; display: block; float: none;'); + + let listItems = [ + '!hero --help
Shows this menu.', + '!hero --config
Shows the configuration menu. (GM only)', + '!hero --import [CHARACTER JSON]
Imports a character from Hero Designer.', + ]; + + let command_list = makeList(listItems, 'list-style: none; padding: 0; margin: 0;'); + + let text = '
'; + //text += makeTitle(script_name + ' Help'); + text += makeTitle('HD Importer Help'); + text += '

Export a character in Hero Designer using the HeroSystem6eHeroic.hde format.

'; + text += '

Locate and open the exported .txt file in a text editor. Copy its entire contents and paste them into the Roll20 chat window. Hit enter.

'; + text += '

For more information see the documentation page in the HDImporter Github repository.

'; + text += '
'; + text += 'Commands:'+command_list; + text += '
'; + text += configButton; + text += '
'; + + sendChat(script_name, '/w "'+ player.get('displayname') + '" ' + text, null, {noarchive:true}); + }; + + + const makeTitle = (title) => { + return '

'+title+'

'; + }; + + + const makeButton = (title, href, style) => { + return ''+title+''; + }; + + + const makeList = (items, listStyle, itemStyle) => { + let list = '
    '; + items.forEach((item) => { + list += '
  • '+item+'
  • '; + }); + list += '
'; + return list; + }; + + + const replaceChars = (text) => { + text = text.replace('\&rsquo\;', '\'').replace('\&mdash\;','—').replace('\ \;',' ').replace('\&hellip\;','…'); + text = text.replace('\ \;', ' '); + text = text.replace('\û\;','û').replace('’', '\'').replace(' ', ' '); + text = text.replace(/]+>/gi,'• ').replace(/<\/li>/gi,''); + text = text.replace(/\r\n(\r\n)+/gm,'\r\n'); + return text; + }; + + + const getRepeatingRowIds = (section, attribute, matchValue, index) => { + let ids = []; + if(state[state_name][hero_caller.id].config.overwrite) { + let matches = findObjs({ type: 'attribute', characterid: object.id }) + .filter((attr) => { + return attr.get('name').indexOf('repeating_'+section) !== -1 && attr.get('name').indexOf(attribute) !== -1 && attr.get('current') == matchValue; + }); + for(let i in matches) { + let row = matches[i].get('name').replace('repeating_'+section+'_','').replace('_'+attribute,''); + ids.push(row); + } + if(ids.length == 0) ids.push(generateRowID()); + } + else ids.push(generateRowID()); + + if(index == null) return ids; + else return ids[index] == null && index >= 0 ? generateRowID() : ids[index]; + } + + + // Return an array of objects according to key, value, or key and value matching, optionally ignoring objects in array of names + const getObjects = (obj, key, val, except) => { + except = except || []; + let objects = []; + for (let i in obj) { + if (!obj.hasOwnProperty(i)) continue; + if (typeof obj[i] == 'object') { + if (except.indexOf(i) != -1) { + continue; + } + objects = objects.concat(getObjects(obj[i], key, val)); + } else + //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) + if (i == key && obj[i] == val || i == key && val == "") { // + objects.push(obj); + } else if (obj[i] == val && key == ""){ + //only add if the object is not already in the array + if (objects.lastIndexOf(obj) == -1){ + objects.push(obj); + } + } + } + return objects; + }; + + // This section from Beyond is not used in HS6eH_HDImporter, but may be useful in future. + // + // Find an existing repeatable item with the same name, or generate new row ID + // const getOrMakeRowID = (character,repeatPrefix,name) => { + // // Get list of all of the character's attributes + // let attrObjs = findObjs({ _type: "attribute", _characterid: character.get("_id") }); + // + // let i = 0; + // while (i < attrObjs.length) + // { + // // If this is a feat taken multiple times, strip the number of times it was taken from the name + // let attrName = attrObjs[i].get("current").toString(); + // if (regexIndexOf(attrName, / x[0-9]+$/) !== -1) + // attrName = attrName.replace(/ x[0-9]+/,""); + // + // if (attrObjs[i].get("name").indexOf(repeatPrefix) !== -1 && attrObjs[i].get("name").indexOf("_name") !== -1 && attrName === name) + // return attrObjs[i].get("name").substring(repeatPrefix.length,(attrObjs[i].get("name").indexOf("_name"))); + // i++; + // i++; + // } + // return generateRowID(); + // }; + + + const generateUUID = (function() { + let a = 0, b = []; + return function() { + let c = (new Date()).getTime() + 0, d = c === a; + a = c; + for (var e = new Array(8), f = 7; 0 <= f; f--) { + e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64); + c = Math.floor(c / 64); + } + c = e.join(""); + if (d) { + for (f = 11; 0 <= f && 63 === b[f]; f--) { + b[f] = 0; + } + b[f]++; + } else { + for (f = 0; 12 > f; f++) { + b[f] = Math.floor(64 * Math.random()); + } + } + for (f = 0; 12 > f; f++){ + c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]); + } + return c; + }; + }()); + + + const generateRowID = function() { + "use strict"; + return generateUUID().replace(/_/g, "Z"); + }; + + + const regexIndexOf = (str, regex, startpos) => { + let indexOf = str.substring(startpos || 0).search(regex); + return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf; + }; + + + const pre_log = (message) => { + log('---------------------------------------------------------------------------------------------'); + log(message); + log('---------------------------------------------------------------------------------------------'); + }; + + + const checkInstall = function() { + if(!_.has(state, state_name)){ + state[state_name] = state[state_name] || {}; + } + setDefaults(); + }; + + + const setDefaults = (reset) => { + const defaults = { + overwrite: false, + debug: false, + prefix: '', + suffix: '', + inplayerjournals: '', + controlledby: '', + maximums: false, + literacy: false, + superEND: false, + locations: false + }; + + let playerObjects = findObjs({ + _type: "player", + }); + playerObjects.forEach((player) => { + if(!state[state_name][player.id]) { + state[state_name][player.id] = {}; + } + + if(!state[state_name][player.id].config) { + state[state_name][player.id].config = defaults; + } + + for(let item in defaults) { + if(!state[state_name][player.id].config.hasOwnProperty(item)) { + state[state_name][player.id].config[item] = defaults[item]; + } + } + + if(!state[state_name][player.id].config.hasOwnProperty('firsttime')){ + if(!reset){ + sendConfigMenu(player, true); + } + state[state_name][player.id].config.firsttime = false; + } + }); + }; + + +})(); \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT new file mode 100644 index 0000000000..e427167b59 --- /dev/null +++ b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT @@ -0,0 +1 @@ + !hero --import { "character":{ "character_name":"Darci", "character_title":"Fae-Cursed", "height":"1.66 m", "weight":"60.00 kg", "eyes":"Brown", "hair":"Brown", "backgroundText":"Darci grew up in a small highland village, the daughter of a village healer, with no ambition save to learn her mother's trade. Her life was turned upside down when she encountered a trol while out collection herbs in the woods. The troll promised to tell her secrets of Fae magic in return for her friendship. Darci has regretted her kindness ever since. Exiled and feard by common folk and given little help by the Fae, Darci has found safety in the service of a mercenary company.", "historyText":"", "appearance":"", "tactics":"", "campaignUse":"", "quote":"Village Herbalist", "experience":"0", "experienceBenefit":"0", "strength":"17", "dexterity":"13", "constitution":"18", "intelligence":"18", "ego":"13", "presence":"10", "ocv":"4", "dcv":"4", "omcv":"3", "dmcv":"3", "speed":"4", "pd":"4", "ed":"3", "body":"14", "stun":"28", "endurance":"40", "recovery":"9", "running":"12", "leaping":"4", "swimming":"6", "equipment":{ "equipment01":{ "name":"Bronze Maille", "text":"Resistant Protection (4 PD/4 ED) (12 Active Points); Normal Mass (-1), OIF (-1/2), Requires A Roll (11- roll; Locations 7-14; -1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"11.40kg", "attack":"", "defense":"true", "notes":"(2 END/turn)" }, "equipment02":{ "name":"Bronze Cap", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.83kg", "attack":"", "defense":"true", "notes":"(Locations 5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"1.20kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"Bronze Battle Axe", "text":"(Total: 46 Active Cost, 16 Real Cost) Killing Attack - Hand-To-Hand 2d6 (3d6 w/STR), Reduced Endurance (0 END; +1/2) (45 Active Points); OAF (-1), STR Min: 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 15) plus (1 Active Points) (Real Cost: 1)", "damage":"2d6 (3d6 w/STR)", "end":"0", "range":"", "mass":"1.60kg", "attack":"true", "defense":"", "notes":"" }, "equipment05":{ "name":"Bronze Dagger", "text":"Killing Attack - Hand-To-Hand 1d6-1 (1d6 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 6 (-1/4)", "damage":"1d6-1 (1d6 w/STR)", "end":"0", "range":"var.", "mass":"0.80kg", "attack":"true", "defense":"", "notes":"" }, "equipment06":{ "name":"Winter Coat", "text":"Life Support (Safe in Intense Cold) (2 Active Points); OIF (-1/2)", "damage":"", "end":"0", "range":"", "mass":"3.30kg", "attack":"", "defense":"", "notes":"" }, "equipment07":{ "name":"(Multipower) Small Shield", "text":"Multipower, 5-point reserve, (5 Active Points); all slots OAF (-1), STR Min 6 (-1/4)", "damage":"", "end":"", "range":"", "mass":"3.00kg", "attack":"", "defense":"", "notes":"" }, "equipment08":{ "name":"(MPSlot1) ", "text":"+1 DCV (5 Active Points); OAF (-1), Real Armor (-1/4), STR Min 6 (-1/4)", "damage":"", "end":"", "range":"", "mass":"", "attack":"", "defense":"", "notes":"" }, "equipment09":{ "name":"(MPSlot2) Bash", "text":"Hand-To-Hand Attack +1d6 (5 Active Points); OAF (-1), Hand-To-Hand Attack (-1/2), Side Effects -1 OCV, Side Effect occurs automatically whenever Power is used (-1/2), Real Weapon (-1/4), STR Min 6 (-1/4)", "damage":"1d6", "end":"1", "range":"", "mass":"", "attack":"true", "defense":"", "notes":"" }, "equipment10":{ "name":"Healing Potion", "text":"Healing BODY 4d6 (40 Active Points); 3 Charges which Never Recover (-3 1/4), OAF Fragile (-1 1/4), Extra Time (Full Phase, -1/2), Gestures (-1/4)", "damage":"4d6", "end":"[3 nr]", "range":"", "mass":"1.30kg", "attack":"", "defense":"", "notes":"" }, "equipment11":{}, "equipment12":{}, "equipment13":{}, "equipment14":{}, "equipment15":{}, "equipment16":{} }, "maneuvers":{ "maneuver01":{ }, "maneuver02":{ }, "maneuver03":{ }, "maneuver04":{ }, "maneuver05":{ }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"1", "text":"Member of a Mercenary Company Fringe Benefit (0 Active Points)", "notes":"Some Perks Notes." }, "perk02":{ "type":"Fringe Benefit", "points":"1", "text":"Low-ranking member of Fae Society Fringe Benefit (0 Active Points)", "notes":"" }, "perk03":{ }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Social Complication", "points":"10", "text":"Social Complication: Regarded as fae-touched and cursed. Frequently, Minor", "notes":"The mortal world tends to distrust anyone or anything touched by Fae." }, "complication02":{ "type":"Hunted", "points":"15", "text":"Hunted: Hunted by agents of Summer. Frequently (Mo Pow; Mildly Punish)", "notes":"While Darci hasn't reached the notoriety that would attract more dangerous agents, Summer won't hesitate to torment her and her companions." }, "complication03":{ "type":"Distinctive Features", "points":"5", "text":"Distinctive Features: Peculiar smell and hard-to-pin-down appearance. Not quite human. Trollish, to those who know of fae. (Easily Concealed; Noticed and Recognizable; Detectable By Commonly-Used Senses)", "notes":"" }, "complication04":{ "type":"Psychological Complication", "points":"20", "text":"Psychological Complication: Finds the touch of iron uncomfortable and won't wear iron armor or jewelry or use iron tools. (Very Common; Strong)", "notes":"" }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Bile and Acid", "base":"15", "text":"Killing Attack - Ranged 1d6, Area Of Effect (4 2m Areas; +1/2), Damage Over Time, Target's defenses only apply once (3 damage increments, damage occurs every four Segments, can be negated by Water; +2 1/2) (60 Active Points); 3 Recoverable Charges (-3/4), Extra Time (Full Phase, -1/2), No Range (-1/2), Gestures (Requires both hands; -1/2), Side Effects (1d6+1d3 drain STUN; -1/4), Concentration (1/2 DCV; -1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work in water; -1/4), Requires A Roll (Skill roll, -1 per 20 Active Points modifier; Magic Roll; -1/4)", "notes":"The effects of this spell aren't pretty, but they get the job done.", "cost":"14", "endurance":"[3 rc]", "damage":"1d6", "compound":"false" }, "power02":{ "name":"Pneuma", "base":"30", "text":"Killing Attack - Ranged 2d6, Invisible Power Effects (Inobvious to [one Sense Group]; +1/4) (37 Active Points); Requires A Roll (Skill roll; -1/2), Gestures (-1/4), Incantations (-1/4), Beam (-1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work under water; -1/4)", "notes":"A pneuma is an invisible dart, which Darci draws from her breath with an exaggerated motion and throws at her target.", "cost":"15", "endurance":"4", "damage":"2d6", "compound":"false" }, "power03":{ "name":"Self Renewal", "base":"55", "text":"Healing BODY 5d6, Can Heal Limbs (55 Active Points); Increased Endurance Cost (x6 END; -2 1/2), Extra Time (1 Turn (Post-Segment 12), Character May Take No Other Actions, -1 1/2), Concentration, Must Concentrate throughout use of Constant Power (0 DCV; Character is totally unaware of nearby events; -1 1/2), OAF (Eat a sprig of evergreen; -1), Gestures (Requires both hands; -1/2), Life Energy Modifier Power loses about a third of its effectiveness (-1/2), Self Only Power loses about a third of its effectiveness (-1/2), Incantations (-1/4), Requires A Roll (Characteristic roll, -1 per 20 Active Points modifier; -1/4)", "notes":"Darci can draw from the regenerative powers of trolls after an intense and painful bout of concentration.", "cost":"6", "endurance":"30", "damage":"5d6", "compound":"false" }, "power04":{ "name":"Underdark Eyes", "base":"5", "text":"Nightvision (5 Active Points); Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"Trolls may be unpleasant creatures, but they can see in the dark.", "cost":"2", "endurance":"0", "damage":"", "compound":"false" }, "power05":{ "name":"Winter's Shawl", "base":"12", "text":"Life Support (Immunity All terrestrial diseases; Immunity: All terrestrial poisons; Safe in Intense Cold) (12 Active Points); Costs Endurance (-1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"The trolls of the Winter Court can survive most any natural storm or plague.", "cost":"5", "endurance":"1", "damage":"", "compound":"false" }, "power06":{ "name":"Fae Sense", "base":"10", "text":"Detect Magic A Class Of Things 13- (no Sense Group), Range (10 Active Points); Increased Endurance Cost (x4 END; -3/4), Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4), Costs Endurance (Only Costs END to Activate; -1/4)", "notes":"The Fae have a knack for spotting ley lines and other magics in their enviornment.", "cost":"3", "endurance":"4", "damage":"13-", "compound":"false" }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"", "enhancer":"", "text":"PS: Soldier 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"", "enhancer":"", "text":"PS: Herbalist 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"", "enhancer":"", "text":"Language: Clan's Tongue (basic conversation; literate) (2 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"0" }, "skill04": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill05": { "name":"", "enhancer":"", "text":"Language: Fae (completely fluent; literate)", "display":"Language", "attribute":"GENERAL", "base":"4", "levels":"0", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"+3 Battleaxe", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill07": { "name":"Fae Society", "enhancer":"", "text":"KS 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"Clan Lands", "enhancer":"", "text":"AK 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill09": { "name":"Common Melee", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"Power Skill Fae Magic", "enhancer":"", "text":"Power 15-", "display":"Power", "attribute":"INT", "base":"7", "levels":"2", "cost":"7" }, "skill11": { "name":"", "enhancer":"", "text":"Stealth 12-", "display":"Stealth", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Concealment 13-", "display":"Concealment", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"", "text":"Science Skill: Herbal Medicine 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill15": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill16": { "name":"Survival", "enhancer":"", "text":"Survival 13-", "display":"Survival", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill17": { }, "skill18": { }, "skill19": { }, "skill20": { }, "skill21": { }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"Test PC", "gmName":"Villain In Glasses", "characterFile":"Sample_Character.hdc", "versionHD":"20220801", "timeStamp":"Sat, 14 Sep 2024 09:56:43", "genre":"Fantasy Hero", "campaign":"Coryn's Company", "version":"2.2", "HeroSystem6eHeroic":"true" } } \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc new file mode 100644 index 0000000000000000000000000000000000000000..165ad6c71153946202343844ff628a18c0be79b7 GIT binary patch literal 127314 zcmeI5X>%M$cCP#LM9hCc=*!6AE0F|u2k$sQEF?{Uz(qr%XvYl(cX7l;<}w`l$J;(n zzI~Kg*;!ko1a>zn7$6!|*|lW8XJ7Ka|7UV*@~@MBOx{l3Og1KepS+lSn0%bPp1hyD zoBZF&H*TMKKTrN-SKOZ5 zoor2x?PuFY?AoW&wXgktZN?l-jwbu|4L)<{r<2cir#F*NlY4f*@9l~_+j~3q%!qmR8J$YlW=^yABD~+R-Fx5eq4(wI(cfC-V+4(hn9Oi8=JYnhV1Dm^p z*?jDT_j+dUz?HY`zQP4#_{6n#>^JLhFZ}p(?b$b97{<2k$-pLa!!?AKNA{g#`*wOS z=7#rY%yt;dOoD>Dc4gN1nyv8nlgsx175o3Pt>tz5xnakz8J4e1Hp90bX5YGGBRB1P zn>PBoeZFGfxN6_noE;HrK>oe(OyF#EB$Wr7kk?cd(-y;5l`&MZ{Z$y><$m??m{H<_|9hKiH-iv#yeVgnmcwM z=H-?01g|DAxck74dV+Nelxk><81_iY?#{lV_JV`JiqZy0XD^cTCrh~U`MKu^r^+kgsx?l+TPl5g;q+<~)Tf@jhy@l0<5UFb@_3YsGjTY5WY z1|;wV;FTx(6rQBImXa<|5^v=54`$xl-yg!!ci~;&3VMrQ!+ZW_?|;jr05Wx+zLE6M zzj%OT;MC|JPP}iNw8%ZgK3E@mk(Pnu}z4>6#B_&p$O;(R_Zh<^1KA^H*BVU(L=lTO~=1 zeuJEjoV%o>q3!rw#*tbz&K%p_elkhH>h1)2p`+i}aX*44c-iLVx=F_?K`)eCy%8jm zw6k4%#=~$gWX?rHnY1Nzkss}6-_CL7diajegjUC~ct`X$G(FepID094;~?lY97jh$ zFOvk0x-624)xK+Ix5IUB2c8V#c$=!^-ZUxCOjmz1ZfdjoQ~3V8_B6GtA5I>c)Rq=} zFT9&1K5xPEJ~GrJQJFvRwr@|47KEf1jZEz`=g?EpG0~5ZvHAB?{fFyq*^}+rmC(Pr zhQ57Z$B*r<)qCe-xdW87Ul~_LgisNmvP1{yv!JH>&0FCOf3jz+Vu|=s!qtHPUUqbg^gfHnkRV>9jmaruB zjaXV1ENQiJEQ!13SklopEZqneA*1^7r4p9n9ZOic5;)n2rDegARx8KSu35u)ZmKd) zN87N3?u0+BFP2I&F5a<(C3!4HEG-L`v|2fq(A}_Ds#wy|b}U_92usq)t1>R$v4kZd zcEr-MU`eZ$W9iT=f*^7YCwwNryBKh7I8`v(@|Q6)^J_bg%R(#!`s@~LG(m9JQi zs;TjWojDCkN81o}$uuaTqo0>RlCr8n#XFYqH2T8vaNGx6x}dxBwMQ=ib`6z->;0K(F49*C}Op86yass zo|9}k+K!|v3n57!$f{(EcPwLR^nfoHmb6|un#ety*OPR#4NW%=crbci0a$(9>EJqa? zDrBtY(G@@5hAQH|LP$SUmEykXJ1oDqpc2RbNsSKzO&p)Dmb`v8E2!xTTYkO`aYT$4u?ovxDzfd# zi*Cc*=v7}X%;l)bQAf5eIkaUSS3TQ?xEl+4vr0T}-*Rf7ST=n#-rQ0c{q4)*AFU!L zqOF|MUd~m&Wf{l!4UzlyZ#w7Si05)`kGR!g$d{hl_f#W@#{W3EUOk3yS}gmr{ZJFe zsFEf~-I&mtXStU!d}R0KN+s#6@gr`PGk^4ZF8tK$Vcu9Z4Ans&!tBjgLPXTSYgHdd zbpUJ4eDB#Oq$pJb#4uIG!q^9Pbw)T+s2lK#Pvz75h;)6ahS{Ix+v|M9H5v3>v*;bu z)o-V0KRu$aTRz5Sh@z7feaU{V8m1SqnEN7nj8wVgN0vrJ-wjAktD!*Pkf@IJC#S_V zb#m11P+da&|G@yYBmGfN{Wuoa5B3}C78gxp{8tUxn}Jv34WiPUiW26v#I<9^P)gOT zRte+#p=$1x*;uMq<*FqPzPb^$LwldJ-b!-@FXUa*vp(nbBdCiKpH*cSIH&URnO%u0 z#CNlLOlljwW|g}@BSTh7RcUV9WC%5$s;NE+&%uhSVtxqupo({|2BX$Wh*g!gQ)HFX zx628QpjQEPGV_&s%(N=*sYZJg*2~w3I;%(*)U{I$PbEC_eR{f3wJTCty$ZN$i7x0P z!MRblp|VibgH;-FbVv^YL$P0m>mNj8sF-er-1sfr1_xZVaCfAbs$qQmq z)U?*qML#XStGtj^v5HiYN(2zLXQ=qWa6lC+l%Ae{_hwwF}#{HSM z;cH2av$ZYyQ4Q8H@}=H~eYuxy+?8r{zjdG*BsP78ny98}Oinf4eoT%hm@9} z&8n<8eMLIgi?)q=TX3I0UZ+}&I)>D!Zp$_31 zLsmB*DAlLXxA0|SU9t{!`c~7JTyoqFGpfEtPpmJJtm2_1#gHB@w~E^QTUDE(-6C1k z;<#_?L01@7WGDEjy2Kc$8n_trsLMtxtfbp*&>PkntKHA~xNqO%N+k*yvzKJ8(0LI4 z@!mXA#1-hT#hiT&gl<&0>3n2i!%*vw$}D zVx+yR)C-OLQ|#b6bl3MmE5#Zf+B4Lld(7Svy3>A)2Zk5yU1=;|1L1u&j5hmIn)X3J zBwcpVD5T}pVUP?3>P(esb$Sg#S~h)9>1i2fc*FD?<=}8eJf);%jQ)PfYUp^hPS3(R z1%+_(W1DY}F4uAGD@G^y`fr+DTCa20`z}k_kVi%+^8xxok-{J_*{# zTjR9!4*#VAqhr1S+_no3T}ONF2Or5`(y z?0TNMx|w5V?IT`KnIChG=@DM9!^UhbG91ZtY|3)%=Qp0te4)Z^s-M;4RzAH|jKvR) zTdo(Q;Aw`hENJrA@ipVS?ZTJWJBM96F~FJ0Nn?)(<|O6LDblgmh_nx-M;w`zLCzw4 zS?f~o%kUb_5##689%F~G9}S!2WU87Z#cA9VgPIDs@@9iY*3TC_wH&w)7S(2d8AW^Q zY_ipjY!AtYOY_xcqphMoQS~jP^>DXL2H^!G23+-6#4~xW?4FHh7LYiPE&hzQylYVi z^2&3r>}VHvrq=R`{V=NJb?%4uJ;SYm>`7o&=nZh1S7sQ6tt zr5{Z>;w|HCNOO4x@;r%K<}*M;r6P(^`aC}>jXK18>RIEt9N%!~twzpz(~yiFA?vCi zXB7f@YMxycSY)x2B`5AmT{wHxJu!QPeeAxopX0Dn?A7LbaFTsg-Ggg!Ui;MUO{R@J zc6evv&X{QS7wrjr4=&v$?!UDatR_8`$I{4NqXNZit$M!#x)%Wj$Ry>c5${xn?R^@_Zz208z6^U0va=*SU;8@}8(;@N_9pz*#why|dkP=dUu>-QkR+#ytSWXnWXE5Q zYyU%X(m`50vN8FaUHz#YXO~PY}^@yfYs*KyR^LgByT4?sMBPR)Ifxdpt z;wazPz5CIbqApcB^XwJsU)ZyBH<>~1P|{#lIaZ!$rGno_VJ_1%ZY`D1=9#Elw*DZ* z=aP9Fl9IWeXIMD*cV9WVq~+Q>_E|SNmE7f z{I!rq)iJq5fwv&B(BKbe)!Oh8{41J2&(5C5KRYAWYfSyB7ud~I`@X_I4rg_~fnN@7wGcKdOi@_zanR*Z4I&o;taEUw5pUFA1MhuO2*XgP&7`3>1o?NPj zj^C?3Cx5wo2Q_ir*Qkl^CLLTtL^_+74Q-U@QUI^cNyv~w=C#WNGFlnsbCzn@cy+qd z;`543zzTR4yqai`zBBi4;a=+|8B!_(b63owVjs9FPfSO-rqn(%r%mk{j`wG3v-&DX z+sM_bv~c>0&yi+xUjTII|L{4q(`=hgM+O?vG3-TX7VLDP+xiTKe!AR=(d~zsou=yd z=Y}lyq9^kg&mR`yJG1}L*6GXl>+G{j@NV<(`!??$0 zN*$f%HAHGjrR|lZ5?b2#!HWd$x@d{p=E-o3pAYSij8e2FagvhcsUML`oIgezJR5pw zu8@m-JaX~e!eJJ9R&_?(GCc{zwex4JZAO(4+)uAMHXi)UI50KO_}THXju!Tzc~Mm} z8#R3Ta-nm%muFb*#eJ_;$&lL{K5DhC4K$mar_b9Y?f$A=+wqvbQ}np%=B2uBp4$5C zpYKdA^sz~Ht-}3NpqlmeO{E+j^zRvU&_Ndtl%8|Dqwajp`}fR(li&X+$WZh_u34pl z)7u%#A`OPZJrOlD`ZNYKoy}amhxWQf$m+FvVV~$3E6_#nnVL5iUA`KhI3F62I_cxf zucLwb5jhnc*gU>72`sC-G^b}p6PIi~uTB2hG~nl^BNIhKBjD-q2WTG@o~7QGw%qXz zEElAOa+EZy&Gb?q(d#QKklbl}T=O^|9baWH@yNHE<7=`{p(FJEE*zT-4vDSOaJ;asQ;r1oR;FxaZTne^i zH4BIwG-X>2!?9!iA5j>zv9#0qx_QM@TM|1HUbb)6CbOQN^A)H&pKI8c%-^tS*VwT& z{c*O2!}f?@7AY|l1(5ZL1w)ngPKds!D@Hs1oA%J`ud(c#JYi=ra#x<)3dX(DRkcj3IVe-(d$D$x{`u~0bIw`7sJS& zd5vz%u6=ClfY0tc&|h4GOU7%+4fb8(N;KJRbWW#f@7WKtxh2;bKL8ofAe*dqa@)xj zNB%>J;)VETM5}b!q6U>ZC#Qz9pwQT9C&$LJ<-j5vkr)p!F{ZGL7xM%Pqlam`kS zJaGCz)l(nVlw!`gXH}fMnzc4eorRv}yw|iH1(`)2C*2?wTZlClXg3^P)fx*p@~_Do zA{CL`*xQnjRQ>gTB-4SZJ*`${SvP9(vDKOhZQeg z^wf^fy-ykgySM6-?)k!EV)WjkGDG%?G&lH@+b*&Cg>Q0w-HU6+`%qcrnL|U=alj3! zzb1N87iWi(=`)Vs4O`AVASH>~*F!?CaK0L;mX=m|tf!|Wm73Uhhqex^2wo9&8Lu8u z?i%!&e$#cJ>R48{dZhgAT>c&!Ww0vf#86IL5u`9{MnzFN;*%ZOnEZ$Rk2b7KN#(M! z+YxB~Y&7}*f9Ezs?nPFzGkHdI`qEQE`LB$|yz=>MWpbO(vKqgnzU?^Fo}A3)E{)Bk zlu5@rde(uAI?60d+ftT)mF~PgE#g4=iBz3Me#D*E-TG9{=vIjJYeh;_*gPtyw*N5M zLiXnqqY?T8UI_HX&P?(VBQ^dQ${99WCuRjzb!`4}M8e_Xxb}=4B81>-s2$TpeOsltXFd%TD<&aL zMyNwe!LH)i`H;KKH0vwYJZhwDkD#tHWe?>qZjWr|l;50#-B-6BBisG=yDI6D@ZweV zp-K`H{YH}{BNPi@+Cd6|%{nR6KW2eG{>)?wp1ByWTTjVS)qciI&0I60UXrYF(B*Qq ztO>EJ@^BcW>lHQhTyfp(lo4~yS!Ug1x4BGapS|HQIL2;yzv^kC-bW-lsm4|5rcM9b*T#Kb)#^s#&*wdEO8I$i3Dq?;<%F$kL+{Me8(}VWf^jS*d8~r^?qnC4ay_{{a z@b>y0X+|lp9nxboZ!L`VJCB2o`grCM$FDrr@2o=oj-*t#NI;ce51T zEjrbuS4qy{n>v~H?^AVNyOpTkTD5y6A5h)-+yg{D{)t&)AA()-tw{x{YCc(}KDKh~ zmAGLZjLpeE%{&|HS!dR!&5(5z^MJg|etkSP=fd?_LH)I6@6Q&qeAr3tN$597cfwz6 z^|TWSK0;ae)LeMA1vd5=y&W~5RnUC;#Qsv6qZdG%rP@96fTxBIpL7P2hwN3Lwgcms zd5zM#MW<5Hxv4&;sx>NW$eANjLN{o#1fEzlg}Mu*iFg|lg(FYH81;Tv z6|8v8*i$R5I~co`ZcN%^1EQMd_<+XbeM|FW^IkkDMpno zkA5;^&v-lg?5wB%E2ogd2|Hghcj+0Q?ef{&%7~+?xe|X&ZN#ogVejO`KAP-&%?`QJ zlzm^MS3!R!zle8RRcm~)9!a?^(^NO!-?uy;H9v1Xl6>!#E*+A(jLqq3-g}R&NGFkJ zhGoyITcwUkt4xF9X;2~^%CxUkx!Y_?clZyQ`vPwE+IK;5b$kL(f^CA|6HoK=5S_y= zc@``dY!$Gg8DB5IKq{fo?ylKh7`$hC+$C4(Tux{Iuq!Z&aW&U7a@nqtEt=QHJK67=Md;Q2XSbhv-uZ)$NWxRhk^N>;vdyEN&#vn(6UjBF>lvQZr-2{h zPi1#K@lE&NX#ZODNbd_@-fP|S;_(LI52A{~_hR?HV!s$4hknv0`tzh3IUO?3_J&0* zH-pDoXfDxEf30rJvmNtnL6)$!X}zZ4p~GNn-eMT@Y!}NAJZ}{a{d~I3l@7yrbNlDn zI!#?R{bx9u8uM(&JX_bXPv<3ad|o$uwHcq?qYd*M|GfE#F3o1MWSf@G<+dqfzB38z znk_4H7T#6vvwYstMeS2!Z|ng+wK&3&{bMgQ^>uXGsoJNi=A;(8o)+rsuS=O9F)wgx zwgx+Wv+G^Ge@699=iNem7%VOIC)5t=)6gyRi&=6Y^z1CucH2|c(d<+c>zx&g@XG1* zZIp?C)kzE^+Ls)`gQ1FQI!7yS0gDxDmaHIItK_6^hthip)>g*wD z^h!gF{B$(>FM;;gJ*!=m$6G?D?&q$j(Wa4oDHN>%8jW>1BYhr)UFp650-Tdf+-;Ls z#OR0*J+evBQIQ-eC3J4gss^3RDH{TE7P8 zMbaBQ5ae>$bN{McrOuD;TpBd3k_%cjMX zuG&8+y*{qgkIs+nO_>|%&LzsGTM`&rMcN)(?{clL+8a6ghwCGrRSicx+WB|so)Xy_@@f**f>9N zDWX*D7#-0)&-=T1Ry8X6R&&o#duPxbTkK!Un+$`kc`IVfSR6AJyF?ZG;J+=paMl@% zIZX{)H%RULwl{4}( zVNw5Rl%f5A<)NwC=(lVH`D={Kt!}c~w(YDeeX@Yb|Bd?!e`DW>8Ugf0&qV6Fu@va1 z+q4%_zr6XWamZ)p!BvJTnFMb`9WSvAxaDg*QeWq-EUD4+sjP~)Zy8$dE1N$mzhBr7 zUUcNrOZ)x8>P?VKpKXTm*y}7ECdgAKKb=gC=f)>X8FgvDK`OH4^Cyp;On0r~m+%)k zXUaw9+T2~zP+7?2C40|4<-W7d>cxi#U&rw2du$+oX7$KawBdAl@K6ICsE!M^h_HV% zRALMJ_5LS%f%M7T+qQT65a#@?&5ybc(%pl(I<(_j4XAP5{QjwZflS%6x!(;@FlF)V z&#Dp9an6ytjLl`vp3U=%$$uK+h`N2UIFtI~4efUwHvUlOAI>8XaonoLAnHBDyM_%a zyNF3D;_Em_`}s!tZ0g0+k2-gaTas5xgdK`htq+{lt137n`J_1Ux>G570uf*F$r5eW zkH{(VAnbz^bTzT&GneAg-9NFsh6}7{3?&zOG-68oDl8uqqQOQb`9(lZ7w84i#TO)^q zZuDrMX!aX+6z%ifcwn}BOr#&)|=qMU%ef+&&_Q{sL&9C;htR-)S zcFO(NUDBz>h>YO};eAW8u4zmz?MD5R%qh50)Kz2F8OJ?~1`qi%)pxO9^pXACHrbDR$HBja=nS%#^Y~1eTmvvZm+U z`mQ^lQyq35TAOygISN`k70y2gdLAk8Yf~VxhG^Y&xBK)R?FAF_#qrcYsrXXorDTjG zkRg3JWOmS31YTUC)qWP?jv)=t4E|zyW|V!t7xdTI0hGIy^jCE@8Dj}~EaAxJ4^K3D z2X+8?``AM2HG)+j8%WUx#TLFZZH0CC&d#xe4&7AIU~*gHdpnNihlPiC7R?5`>u38+ zj{iHeJE_O;o;%_kSaEKd{cPVRUV$aYXH^<-Pxsg<=Fx7yZWtw0@h_iA^qAyvG_pFh zhaQo%R7ZR?<8Gc+1M+W%e`GY#i;60N`mCmYGK#2~Y%}67sg@`&cM0w5B&(DofV|SY zm*iE`m|SLkYrG4O1(FLdM0I|Njln}k+4Z$$R~vmQdqmR7`?$FNqJD7mGX+)er z{;Bh$GpZWwp3w`PS)h>;B{q-BX;IRFY=CN{n)$`w_R2Wl>&brwu7~Gwq)hT^mz^?| z2T2!r{6bo@CRs@iFr!YdC3@YZvI@B7x=TmTDR&N7qRFN)IYp+G$LJdJ_uu|qQW7yh zA3=70XGl~wNm}tLztt!|zc!UFYWp2o4CRq=`Mbtxs1&}s?9SRwDrSn6(Erp)@9n|^e8&$yZQU{rtO^Fx<(`ojis z&Ay~>HjT;YaB4FtS@LSM?!wG|ZQ85T8>`3b>l1F7e}IlR_ssV4zI|2C>#bmwsV*Y- zy2efgdv*n~1>|dRUhjFKmra)5w4X~M&ai2cx4(`let>36wS;;LDg#up2J{zv*GB=N z-jAu-wt$W%69TvheJ>x9BW;FCE!?<~@Bi7tBEU%t7IdFofsgVa?epJxN-o z-I~6&Rq!gT3x;9pKHg3KX@DK>4g#f2XS~|II*QY$GzkjoX_ELvA5HkcbYOaTLF4Q0 z(NPXI^Q`Lml7wvT&&kSVoFv)2h9zFK z75Ea|iPc{>zCI$c9f`6NlugvFd(36iJ5$Zyd*KejchUWor}Du*QC%>0k6DfGG5esvOE2f%L~!8D z^Zc9a8Lw&XYay?0-Bwo%8QX>(n5l|mzgVIIcq;gJ)#jAHnAfjkA*9alRpf#+_RR!%9|-d(I?Vf#iS zCm*e2yn;jo;oh-6^!!@K*aq_L&!coW&Hq}keTbGVl6@s>VOYI?bt`bX>iz8Oc+D(M z^n3MFBMZ*=jW}DK|E^hRuPjpz4U-%xy4+A%bH^fM@59a=il%&R8$hTi*#M50v$FxH z8o+08Bg_<^uMc7sbcrK4y}E5!whKm0DN2x3<3YBRbs_g`m(Pxx(!31nrYW_@fgRE0 zV%6*Zqoq4qy6h^2RX_CpB<)_1}Q8BJQEM!=7hf-`;QehP< zFuK|UmxEu6_&N3AL@nVC!~-sdW14}l69u>(stV|pO4i)g$UWR=Lm6h)lR{H_i((W3J|o>9NwkrU4nv_@qt(n3D5~Zo8AusoXz#XLhvJU$Ne(Wm(2Z5I~kIpreL!)^1k=Y7*9=~O|ys;e< zq7Cy$b;??24xi`Ld}H|awJLM>%f{mwRZl(LGiOwnON|^B>%Z7lH_RU)FT}{*tN95J z4E0p6a+LXBO;k4J8i@0=?&V5c<#wIe`GcT~XjDC7$!!HqbpEVEY`xyCtl+ADg{zcs zxw;v|L+Ts{!76`kcKKVIr*CcU-&(YK!)#Rbrc)2zv-9V%E5xBu%+z=lneUG(y{G*g zJ{w|(>}r?#YW)0tlU3~4bw6-Cey5}E8@E&+BJViwo#o{P!mh?cr0+=jfg;)4>6L!8 zE79NOcBsou&uA|Oe?E1rm&Jn*hO_j=<=T9U6?K2qNx(h6spIf|%6alD@D1-_spo;G z?AtXD>i2Q_I_x(&7B~;*-z&Q$^wJKGmjtq^5KAyrblcO>2bJA(b5H`Yh;v z5s2Y^v68#1=!@+dpMV?7o}`YSOfhm9sac?ZGjR%8mR~1A!5%ZO?4QSwptM!?2&a21 z)+L3Xdqrh*X{y0xz?BrRW|JQd%!+$mz^V~2&Y(p}*B%S@-DpJmn z@)-_3>`rAa>#9%5Uv3>B4Pu6ry0{;ijM=iIjFSbxC+*kbS6rsuT5=mHkMp&A$x3v9 zr;HWxnL_q58_d|~CEJ}m4i=8jNW@STYmW^%?Ad{q_c$Qxv2h`KywHzJI!qOVU&oH> z!fU(4C2pB+R;}VJp|LqOS4LxV=0$g#evHk5x4PJy%f`C%x#w#gVsmGQ{knKu3Gw8> z)brnZzks-Q)h{r{o@rH*j&_FOT~LcJ~8UkBGScr@d+c|Y-l@)YY%IakbLk&nGZA?33< zCCIaLesSIu8ELA*q*o~WpiK?)?A}Y=g8aVDp~&#X+QjqXT|lS?tk2Tu=jq(H=p8%P zYp-K=-BRE6I^@fiSVeD4S3l-;^J9`3L+^ERV#szU=Ok_fht}M^_ZL$n~QX^{l~FoB5`_AE=gS?ZGqP~5nI1&F6Z5Vk=tz40v+2z~HH(aF;x}O~ezrAYPajF?I&G+m z#jZIFZGQST?p(!YQ>-YWq>J7%`YK0X<)0_9&R2LICiawL9?EXxx|4(y+_ zBkd?CE`^+7&qQ)3qkggMozJL{p|U3Jurlo=37R}8pc7viGm}e$a_r}|`HYD2?3xda zD9|(W_p!$Z*dtE_`=d@8tTH}x@3%58@H_Ih=~>sr>qq2Xa8-e7XKi+xo}XdvaatLY zYxc`RA=2gB_%v3;!wK2*)Lm@bKPtewPyqean7@)P)X#%hUK{pAe`f#WmEH)RWp+cy z--<7s-Ei=`vU~a$>jV74;-xP_-(RBAu{+y_jfIl5xB3`AU7j5FZq{ZWf8sD+$M*UQ zk9ej&^=roQ>*(P$7VOxiWL@ho7R}JQYg&Tx#eI)*myIzJNwwn*`?+fWUo;(}4w>~< ztNbz5y!5-qVd1*!2go%`)~RwJ*V#HHvxn#-eJ+)UMNf5lFW$7cq+2e3q>nPyI;yK8 z*@uMmzD8&`v98o>*IB*uWs|l=NL3vSus}9h~%7@mJZdHxIJ86$fd5mj@$RwL)d#Bp8t#eJT;yI@Bd^vcOCCvCtGyS z)_^`9%>Hh8g^C+P=}N{OFGILGRB&!Mvygqd`a783qLWGQ3LexT+r!>%{Mk%J&o3n$mvs3 ze`uOqE}u}Of!u3Pwj()u$6-EsYW>wNb*O-d0_Ib|hOQZ5JIaTdihAefL>Z?WUq>Y}a@2R)h`{vK@ z+TI7Bg5I%V&p{obG>?+)zRHx7YG*{C_Ck*Lewgj_oa@hZ=X05`7jl}(@I4B$m6}?u z;3{@D?-Jb|2KVI8lR-&DflpmfUmsKGZ@b*Fbz%>9A_(iM3k@wTmPJ!(=e>2_Q;vHt Y=cC^v)#BTnbM0I;3FtaHXBPVZ0`Y^P761SM literal 0 HcmV?d00001 diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT new file mode 100644 index 0000000000..ea2ecdf407 --- /dev/null +++ b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT @@ -0,0 +1 @@ + !hero --import { "character":{ "character_name":"Henkle", "character_title":"Clan Doctor", "height":"1.76 m", "weight":"87.00 kg", "eyes":"Green", "hair":"Sandy", "backgroundText":"Henkle is a learned man, trained as a physician by the best court instructors and an expert swordsman. He once had a promising future serving clan royalty. It all fell to pieces when he misinterpreted a joke and subsequently dug himself into a ever deepening hole. Lucky to be alive, he found himself banished. The Company scooped him up after a particularly self destructive drinking binge.", "historyText":"", "appearance":"Like many a clansman, Henkle is not small and his lack of social awareness makes for an intimidating block of a man.", "tactics":"", "campaignUse":"As part of his education, Henkle dabbled in Wizardy and can cast a couple of spells, including a minor healing spell and a light spell.", "quote":"Banished aristocrat", "experience":"0", "experienceBenefit":"0", "strength":"18", "dexterity":"15", "constitution":"13", "intelligence":"15", "ego":"14", "presence":"15", "ocv":"5", "dcv":"4", "omcv":"3", "dmcv":"4", "speed":"3", "pd":"4", "ed":"4", "body":"15", "stun":"36", "endurance":"40", "recovery":"7", "running":"12", "leaping":"2", "swimming":"0", "equipment":{ "equipment01":{ "name":"Light Maille", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4), Requires A Roll (12- roll; Locations 9-14; -1/4)", "damage":"", "end":"0", "range":"", "mass":"10.20kg", "attack":"", "defense":"true", "notes":"(1 END/turn)" }, "equipment02":{ "name":"Open-face Helm", "text":"Resistant Protection (6 PD/6 ED) (18 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.83kg", "attack":"", "defense":"true", "notes":"(Locations 4-5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"1.20kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"Arming Sword", "text":"(Total: 31 Active Cost, 12 Real Cost) Killing Attack - Hand-To-Hand 1d6+1 (1 1/2d6 w/STR), Reduced Endurance (0 END; +1/2) (30 Active Points); OAF (-1), STR Minimum 12 (-1/2), Real Weapon (-1/4) (Real Cost: 11) plus (1 Active Points) (Real Cost: 1)", "damage":"1d6+1 (1 1/2d6 w/STR)", "end":"0", "range":"", "mass":"1.20kg", "attack":"true", "defense":"", "notes":"" }, "equipment05":{ "name":"Long Sword", "text":"(Total: 38 Active Cost, 13 Real Cost) Killing Attack - Hand-To-Hand 1 1/2d6 (2d6 w/STR), Reduced Endurance (0 END; +1/2) (37 Active Points); OAF (-1), STR Minimum 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 12) plus (1 Active Points) (Real Cost: 1)", "damage":"1 1/2d6 (2d6 w/STR)", "end":"0", "range":"", "mass":"1.70kg", "attack":"true", "defense":"", "notes":"" }, "equipment06":{ "name":"Knife", "text":"(Total: 18 Active Cost, 8 Real Cost) Killing Attack - Hand-To-Hand 1/2d6 (1d6+1 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 4 (-1/4) (Real Cost: 7) plus (1 Active Points) (Real Cost: 1)", "damage":"1/2d6 (1d6+1 w/STR)", "end":"0", "range":"", "mass":"0.40kg", "attack":"true", "defense":"", "notes":"" }, "equipment07":{}, "equipment08":{}, "equipment09":{}, "equipment10":{}, "equipment11":{}, "equipment12":{}, "equipment13":{}, "equipment14":{}, "equipment15":{}, "equipment16":{} }, "maneuvers":{ "maneuver01":{ "name":"Slash", "points":"4", "phase":"1/2", "ocv":"+0", "dcv":"+2", "effect":"Weapon +2 DC Strike", "notes":"" }, "maneuver02":{ "name":"Parry", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Block, Abort", "notes":"" }, "maneuver03":{ "name":"Counterstrike", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Weapon +2 DC Strike, Must Follow Block", "notes":"" }, "maneuver04":{ "name":"Half-Sword Disarm", "points":"4", "phase":"1/2", "ocv":"-1", "dcv":"+1", "effect":"Disarm, 28 STR to Disarm roll, Requires Both Hands", "notes":"" }, "maneuver05":{ "name":"Half-Sword Trip", "points":"3", "phase":"1/2", "ocv":"+2", "dcv":"+0", "effect":"Weapon Strike, Target Falls, Requires Both Hands", "notes":"" }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ "name":"Weapon Element: Blades", "points":"0", "phase":"", "ocv":"", "dcv":"", "effect":"", "notes":"" }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"2", "text":"Fringe Benefit: Sergeant", "notes":"" }, "perk02":{ "type":"Positive Reputation", "points":"3", "text":"Positive Reputation: Brillaint Doctor (A medium-sized group) 11-, +3/+3d6", "notes":"" }, "perk03":{ "type":"Fringe Benefit", "points":"1", "text":"Company Soldier Fringe Benefit: Membership", "notes":"" }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Hunted", "points":"15", "text":"Hunted: King's Church Frequently (Mo Pow; NCI; Watching)", "notes":"Henkle's overt interest in science and medicine as well as magic as it relates to the healing arts makes the Church unhappy." }, "complication02":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Airhead (Common; Moderate)", "notes":"Everything seems to go over Henkle's head. He has trouble understanding jokes, ruins the punchlines of his own jokes, and is generally the last to catch on. He is far from stupid, but sometimes you wonder." }, "complication03":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Aristocratic Attitude (Common; Moderate)", "notes":"Henkle is your classic snob. He was educated by elite teachers to respect every rule of court society. He knows how to behave and how to address each person according to their rank. Obviously, this doesn't work well with commoners and he frequently turns people off." }, "complication04":{ "type":"Physical Complication", "points":"15", "text":"Physical Complication: Horrible Hangovers (Infrequently; Greatly Impairing)", "notes":"After a night of drinking, Henkle is a mess. He suffers a -4 to all rolls on the following morning." }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Reknit Flesh", "base":"20", "text":"Healing BODY 2d6 (20 Active Points); Increased Endurance Cost (x5 END; -2), Extra Time (1 Turn (Post-Segment 12), -1 1/4), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), Incantations (-1/4), IIF Expendable (Herbal Ointment; Easy to obtain new Focus; -1/4)", "notes":"Magical energies, if one understands them well enough, can be set to stitching a wound or coaxing the body to more rapidly repair a hematoma or other moderate trauma.", "cost":"3", "endurance":"10", "damage":"2d6", "compound":"false" }, "power02":{ "name":"Light", "base":"22", "text":"Sight Group Images, +/-4 to PER Rolls, Area Of Effect (4m Radius; +1/4) (27 Active Points); Only To Create Light (-1), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), IIF Expendable (Difficult to obtain new Focus; Charcoal coated in saltpeter; -1/2), Incantations (-1/4), Extra Time (Full Phase, Only to Activate, -1/4), 2 Continuing Charges lasting 1 Hour each (-0)", "notes":"If magic ever had a use it would be to enable one continue study late into the night.", "cost":"7", "endurance":"[2 cc]", "damage":"", "compound":"false" }, "power03":{ }, "power04":{ }, "power05":{ }, "power06":{ }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"", "enhancer":"", "text":"PS: Soldier 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"", "enhancer":"", "text":"PS: Doctor 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"Power Skill Wizardry", "enhancer":"", "text":": Wizardry 12-", "display":"Power", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill04": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"5", "levels":"1", "cost":"5" }, "skill05": { "name":"", "enhancer":"", "text":"Science Skill: Medicine 13-", "display":"Science Skill", "attribute":"GENERAL", "base":"4", "levels":"2", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"Science Skill: Anatomy 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill07": { "name":"", "enhancer":"", "text":"KS: Herbalism 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"", "enhancer":"", "text":"High Society 12-", "display":"High Society", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill09": { "name":"", "enhancer":"", "text":"KS: Popular Literature 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"", "enhancer":"", "text":"CuK: Popular Entertainment 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill11": { "name":"", "enhancer":"", "text":"Conversation 12-", "display":"Conversation", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Tactics 12-", "display":"Tactics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"true", "text":"Linguist", "display":"Linguist", "attribute":"", "base":"3", "levels":"0", "cost":"3" }, "skill15": { "name":"", "enhancer":"", "text":"Language: Ancient Elven (basic conversation; literate) (2 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"1" }, "skill16": { "name":"", "enhancer":"", "text":"Language: Clans' Tongue (idiomatic; literate) (5 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"0" }, "skill17": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill18": { "name":"", "enhancer":"", "text":"Language: Southern Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill19": { "name":"", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill20": { "name":"", "enhancer":"", "text":"+3 Long Sword", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill21": { "name":"", "enhancer":"", "text":"Defense Maneuver I-II ", "display":"Defense Maneuver", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"5" }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"Test PC #2", "gmName":"Villain in Glasses", "characterFile":"Sample_Character_MA.hdc", "versionHD":"20220801", "timeStamp":"Sat, 14 Sep 2024 09:56:01", "genre":"Fantasy HERO", "campaign":"Coryn's Company", "version":"2.2", "HeroSystem6eHeroic":"true" } } \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.hdc b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.hdc new file mode 100644 index 0000000000000000000000000000000000000000..77acf99690b651559ecdba8cc09f19a374aea3f2 GIT binary patch literal 108564 zcmeI5dv6p;miFuKk@h>3djHs7&2qmr5<4qn8?Z2#wN2B#s}-WTbhBK?z%a~*f8Tw6 z@l+6*Q4y7u<-(Nhlx4drGcqGH;+zxDxyJwd|DzkD{}}y!^k(#WG#>qJ^n7$M`Y?Jq zdN+DI`uEWfqYI<6qx1Sd)|t1Xr#k*jXWx!~4`&~a_D8oyXVmJd+Wg1pFQb1N{YiIR zAKe+Pjdu0Bt`?j6RJ!-oulJ_Sz0rfwwyxkaJ^wcPq@G@nK9083-&x&}XM3k(Pu1I| zj{Ko#`KXreb*w&ajBe@M$yP+e@%I_KpM&X?9^;~oIUgPsbW58M3dxZ8zU-X@$Z!}WR zzaD4gJZjIw`|YalZS~>zrjHl8+ov$*Kd9Gpt&yFc)5l@lZiOc-oxQKIyEhpRp6%`E zmG14n*VMBqgI0XvZX5c~Ol*bU_+;zhE#HQ5c&B&xC@MGzZ(xe~Rafom`euKO4`VRa z?5ykWKaVbkD;QJg;f{J>wy%z^YfLWc?}gDN{rsf4|4HAkj;`tFf_@o^N7+>uLi@97 zd0uTV=$b2|%i;f}aOBeDI=EmfJR7vQt>4|y>z>B>t>*YkxclaaH!!`XHn#&EFls!( zy6yyraKGDn)Arl0hY|Urw*R8HruDl(9rWTpZmWm;>fO|NEA;Sy}? zM&I6!{!O1g>Zp768J^VFycdr^-%#_*(0(jg!CQRPndkcdTX;6+9(naicj7Z|0Yx0> zNTea(lbPV&yybJr!uR^?Yd(xVhmn1zEB$O|)rPkj>p1d|Pw=eoFxIums5jvqEhop~ zs!!^d=b*K@{cquUe%JS>U#Xibzvy1byf324vF4g5=LpjIg|7D}IDo!h>Il!kdwdRW z&pUV&(la0l4#JZ?*Ztynjz|BiyS&nOzdLX6K~MQxc*4Kx6SVhHJu^qr^uLw&vp zEne$+JgR;g=JWYv1)=?O{VW>-c&9-&Fr|RAF#@jG!e%F`bE>MBrkB8<*K_{``+nIHA^Pt^iG zIGBtDGw8SHEt@puC+F=C!dt)k`tH9?KJoT9Bw1Ofrul@`6FxF8d0p8PA0gScMGgCs z6m7Zemj5YE+tJxg@!HL7L>N<^hUY|j!dD-|Gc>Pi_q-pT4mrH8)uGhmv^;vKmh``- zp3>;SC(sG=#&PC|ndcddGMHyLDy|NU-V;&7Z-G{rC(EBl`u~scF38P9v)l`+=f5RB zlxWUpeT1s~3?IVG!$BW{jN!g;-zW7Ld6jG7oVU6LN`R{1zj!{~BRzp78LRUjdMoG* zx~Ct{b$zVbqrN8j&YT-L8B6>{^Zs(erItW`_arx>WSSmFk2r7Qr{a3c4@N9<0J8#h z!;NXApcJGla_5O;3pB(WLGN6_y^tMzhCj_U%mX9E+%k&HB%i^HaQJJrZ1&^P=4m`q zF)()|gg?`gd)QD!=SvE%%uj`Np0&R@yS!`CGo1iAzzHF4^6JFsTtxYW}8&6!>G z_KVhL79gj!AD@G$Alj{N3ToyJliZM3c)HCjczF6hYYvhepL4rT)cU$K; z^HaF9c2afGRl-Is+9O^CV3IY`{$4(b@HI-<#q=^bXKG z=%tC3E(UZp*1LeR;(yxhX;gm+*H2^DJ>7jrxb3leLI>Fj?`Dj}TkyQU2Kwbr%+R`? z_F+JvU@J@0JzdW^v;+`7*c4QgU*ANY+;2@!c1v$g&)maT-_!A3_1f+`Z%Yrz-|ber zHbs~!(o>fB0Br*LXkU3Fyx}i;#x^a1ynhL!Kbw|7$h=`cTCzUXrlr(liI$Ak2U=Pc zTJmh=v}6rCrzIbSN@mg0RcTw77oep};hHurr5;PP6+7$k9N`0 zPg;8xp{0^sNAu-oPoE%|5{EnN)=ygx0K6%uzMZ75&Cn&nD&!s-g^fXw(SBsuJUpYP9ls{!7M4z>N zD)D$1O%a*0i1o5$32*jUrm4XizFIUDXDp{GB2UOa_=_HZ7Z_&iK5_V%ekm83|9A5v68lFCed3tvb`L! zenWA`cSV!i`q`VZa-?%P=EE+$6QZW~be;1wp{mD?`_-tJ{L@R)TCZxYu1n5~)mZn^ zTKz!%(qBndy8S@6j+}q+H6ngb`5muAHVqkPjNYs~2c)g@63G!@uDRltKCzOLJAjYC zSx>aRue;MCQVQ7vG3(Y*^|3`Wmx1i5?Mc+V&nLPjj(*-)`bg95tDDrmY>I}aL7#Ps zu4ixbrRc$571w@N(?ro5A!8|J950ieQ~t?eDS9jNJa=X$H4Uf@L2hhL-!c1+|Ij*dQH#*G<#8KTKM3i^Xmg##< zR86G-DyJBMoM!62EBp?UMvL7D8giEer(%^W4-v^q-aityp9C*a_}vM@DcL}bY6gUn z+d!yga-dolt{Osh4(4JN>3J;g>#x!aTodZHp4-*Mc=Eim)3>vdPz8c|T+ld~E?9JU zE=H}XIf7IooufBWcdH)r)K7D~BQ#C*7BnMQKA}>QB~gj?2S0F><6U%(Wn(kU<;l?l z`^o2I@gPV2e6Up4>#@`kc3=5C_>d!wwk>+tLEg&`?}}bjB6}%bM`nXST%~Q0*t6`~ zi_t%o7LnT2R;))C_M`uG&DY%^T}o2=dN?y6lpLYt9I&Z%-yA?iNHk0=OdLBNd;k?t zGZEfBTI}LVCbqm&DK=KaDq$Duih@}9q26en2l`3;lIpCFR3)!zrXk}mAN#)hqmg@@>f9D{#yS2uXXuGD0DG>mVqz&#) zR`*;Fbd6>x6z}-OrODELt$X-aT=n-=61D0_#-t_7T)$Nxt%a(p)X0x@Gs~;};0bK%f%{*rqOPHerFAh|S3djm z#|5#=f$3^3Say6tH z@{L%WaWKE(v4=;SXXB{XTTnsPv1qP=i)Gi<=UrvGhP{y;hp8z`t$%E1-Vedl+N|$8 zEmiU=v~9ZwJ{D>^TeBl#=ylKYT*>DA3)27O=0oE_0iw+L3V7YEJvW#F$Qa zfr|C)r-5G#zK-YGyvDb?2(4JvUDx?`D`JjaFuZ@ogH}IOxJSl%R~tVA zdv_qg+w$LWrR+#Et4gz}`<&i3(s%nV#9gP@jr?3XGmT8bcNl5W4{@>ZBjlJi7S&C*AQ6(+Bpe^@%sQO~f#H22F`jTMGf z28(xHhw10{nSX^jzELXq3Uf=yhP#S2@&h!jRMhvM}~D~SZ6*2>NriBj5YmP85iGbojJY{Va;^} z$uVmUIbG`vv4HH{$R2R)QTaWzn(w#PU=MJ#4tt!=$_g3QnqjRu32P0xtJKrLZvD8D zw`j}Dv^Ac{9x_a9O{<}@e)-yu^bFjo?423nJ6Ww5pRO3>Z;=VLY%3GkoeDzG9{HtM znH@JB*W9-6hxi;Sw&2;QQCmN(< zsH3n!nP<>`_a(2JzHd09#5YK3bOgK|_4v!Wls2!meOKliyu$3Vn{r%^IC4 zmAD4W80U_oZ!p&!sM>?amzm(5n&;ze%xaTcsJ16Q1V%OP!|s_qoM%0^7$1lAV^L2a zUUPP$#n(wDxT}L~%97})WsheY&w%`iJF&%YW66Lvc ztiS2+L6t|2bDgjz4}e$pLtJBxJIZHszStU_5vOg1@mjX`IMwqfrEV|o6Df;S`=-k$8IN+w}9u9aB zB1w3T(i-9W9LE)6xzyR7>%)6?U1f}TcQqV@$}rFPvBt#L(4qt6}U(W8!M zH9x0l*BqrsZpHrTYOs+u+SX5VE4KD}%xc|=Q^MtmTj5mdBAzZzjaio^zAT-~xsWzz z1|E}Kdnm5|o;-%$+!t*vsvQ$)VS6evJ<6zN98y0nTtqt_GBKexTcO_vjR!q4uf)A< zT*X>_FW6D2_Dcu|yove##uL#8%yCB|XP#;FYp0Z60k}AaBTqfp5q$x_jcn za}E3CjbMAMLAAV}!+rm(8TUQX5lh+)wc%d=_CP(Z=|1*UUl4uzIF@j{MC7&6`oE#dt@%WvIx9Yt2;9NWH!<-&c9MD)oWhU2NKj}@a{ z8L=P7yXJVT3t!^+!PW)6{UH8WKF8k_-&2p${(vUGC+?0CARS3OGcUiVe*R7Gu_j#g zE=V5UD(CXJnjaS)ufJQH54&xRN6pKu;;wMRJe0Jne!Ps1!2W~IS*L0UzR0+mlz7`T z6=g0MpL~0~HkQ9Ir0S?+a_dc34rkjy-Lz=#AR{dB;7iLcfH2 zJ67O{?m}fZpR;uc%R^dS9_w?;dPyr#8<8F!Hy@t2XODg#-uA4_%iG;+IXr()Bh75F zUk7_QA$PD|C+=Yma#@hZyE!M#89DAN{=8$e>av8K`I-KdqL(UZkjD^sFUBNJ5Uo7ex_M zs41lER%W#Y@7C<9!~mWbqB52#X=eTWUTr%RmD%uaq#E2M z9CdtaCoNYu>q|pjF$j*BUlTUl7R9vbXEsfE=bbvj?ne`7UQa^gkdaP_1|o7BRxE4> zLuI)?NoJ)V>{Z6cSZvyS!ns+}FSU+VuUVce;yBaZujyLzS~{1@-#g+jXH$_kRO2ox zkUPo%dl;?yNnG1Y+z;LlY*6gd#LDhieC}D|FJp_-&Ru9@uVgnQ8W#Hyt5fs=BFmfc zojcO?ehqVEzYO{<(cPe~UH#29pr|EL+0jwpX}@plVr%L#`lj}jyI~JCK9%TIi}%P| zqFLJ`%Zx|d)22>SXsXfp7hwhJ;!>mDUN>iGfC#CbY8?1dmXEPCQgS!O(o^3CIc<9< zG3U>M&g#k$=-=$S>!>pBh833Gra+YRLS+de8OFMbeW#*+jQ-A;KMnB>R6H81HFvWe z7e69)dRK3HGWxq(yOn$9uJ80L#DiGlCc77n8g2WDA|{ATbzCY(i5@}Yb}v-DaK%Aa z9*lii4=3(|xJ&NOXJVqcFIXC@EmxjZ^~-a?lgK)nd%8XHWIW^fBohZIl}iXbgwW7K zopYVd9F?(4I+gd>tpKk#J4*kc`;=tF8KJhb!R}0pX2vKKVs3BC33f$e9=tEmuo4sB zmRGyTV@$CBlFJ_BAQ)T5yB=F|OIPg5`sCFBPr;)grOnyX9Gi1(JJN!)@#lu*M69iv z)7DM(v#*r^pEweVXKW+>Y1*pCoA#$I1;5(L^@sS!a>OGyy{q|v{Z4PbeINR- zV?f`9_=L6$p5`}V6xwZaUb0T-6+C6^?9IVb?1lu5FGpT)iEF&3HuH1_xqY1Ewl$76 zy)8nMoAM`!-T1~n3+ir1R^%!3{_g~^PFuq(a{HXW`l=^Dpbo;R}w zSnZA1oUzwD7gyKX`RS|z#trNRM*dObMpoX(;B|a1JREIgW8uGxqko;u@3N~ytOlA{ z7FSqOgQgt`h)y;S`XLj37H{;+e72lM>tHtx=W1;R#N%E3kmBeQL`^W`r-5s(NM7-t zC5|bb%em$6A*!Z1n*%4z(k;@?6i^-Emcu0-wSvpKz|R?GD$Aazzl<{P2cHCPZ1xn8!f-PD+RKO9@&W>YlzI8a&% zclYyDfO6hTTBEN%klk9dieYv*%n-V7^YvemuQNTS0a8^-#59^&zc z&U-3EWQ}Iobbs1>D66MOgUiw`N9-w_cKKmPqEcRTzFM)*aNSSh)oOknX4*rJ~WpnYWtX|z33oVj&u&v`aeTvi8rkFjn z7bvj3(mDSPKL9=fc1x&Rn26(uQS9?*^jpERL2V&R;F2aEV+b8VaGav%o3xD{PqoI| zvXAfUZ`=>9&I8?Tb8grZH{c1w=XG2B@NFt}uve;k;n`Qm)3_D9h7uH2R$==}V1ohPnZyr`*>S>=B|IV@y*SXg#VB4GbXUUs0 zk_pit>L*F7S)HekvvqFS>&y2*iP3wX=gIPJ&J5+#e7iVqE6RO4=y+U-oNLAwbB_Hd zx5JrKmEil-%IWgEtO<{l?AmP}qK4%n+Fpr=)+DE~+JS_Kg4opG7drA(k^}i#S`m*k z_vC9L{sgQ_#JBLi&GQk5%xaUbfmD2aCRzKfbh_h%2Ca>CQG9nq$TaOQ({?vRgs?k? zV>yVaK2Dy+E_+&cM94I4b!pmt{DsrH_7D;BShBW1#eENq65|ltl{^+fDE+a<>RGaQ zZ}J$C5wjyga-NFy=1P|7{=SwiX+A?l$YNY{(y#+ngtrw5%&HZ2Qs;O?+XaX&qBwg3 zr>V1UO#Yq4Xfd^>$ZB%hjSP6 z1mx6uc}B+n;0cH|s%lk4n!26sp?4)6q7SBdHp+~_6VR-A2c?_ZmlI_=PefnN0aOV_ zs@KuP{JyZ$8qar2QhFBE*Usm*j$5*FI1lDQuyAA7_KJHlBbDZp^r?1^D+WfC(v3x^ z_Ex9uMX`we32VWk$L<+ESUF zg|E(_+`d^_?WmZOMx{-h0sW=8x8|9MK+0!xNt{|81rA>i(*8ga<(;I-Ye^D+LeIg= z=@=&}M^WvKSTfEW1PWQTtg${ZcWtUiTF;W#eX0I*$K5%ym%LG8c&XLE3RU8W?v^=s zP?rg>7FocqKo~v7LsVX0Z|h00WyT#s?R)CNWmlzf%;Ix!+59?3l%2Cv zT;IxPa}KtR?kLtai+v= zEk9X#%+1vXWJU!VDizdLqT2bKdM{`e*pY}_C`+5=>c@k3tYoE%mZ}uX){aPx=W{)% z-KrMT+CJW_hH)drlMw5@5h9Q5G4ooJ-`l=LTIMmacSLzyxv6uendzNar~=FG&cwvl z^{;wNU8zFG9Z{)kNp9`G+T`ma`-Hi63Y7d&l=tN;cdbhoT$WF9x$9)I8@6#N-y-H}+ZVq_ zB*~2ica<%RaA4xGejG?d$OHYRTt(_Wm9jXdxv;)s!KHot?(0FGG;mPG#pLdGLf)3)`$B38q`geIo z{uHZJKfDOlkdI=&((HY-PtqFrNK$WKh=V;+VztW>|8=#?`S6isS+ho<=W~_y#bF&} z2j+Fr!92WoDYT0C0LxCyTIsjur5;Zj+H9k#!$*=W7xl%c;PzAIm@l?Eeuxh=n5+_Q zG}yFk^7WfS~1=SyBJjDD$}-cypvav9@!^}=lraz#-sM#qD{ta@cUqq z!D2w{lq-br7Gr(G29qiVnu?YiqwLRlM3{I{$f zF1AD`#Q7jM;X@?bY2iNmkf?Z$?3-t;oE&*z{GeqX!H_r}Q7(I5_< zIsi~aNt^tB7}>EJr+t@=$f-??%)VqGG-|$q|3G6;HGgaR#AjD>A%?ySKR0nOzQBa% zz=KaGND?{B{Xa-16X}-l<7f5GHN{=r$OJYu(5ZF0;oAopfY zN2+wjSVy~D|0vdP>qqT_+#~<7KTeJR`mO#0a<3!u$bnr5Kj*>$xwm5q@Y9)ngv|aV+_kUxYbqwI&0nuO-{o^#P@0qi9;PiPJsM&&#j17w-hb zi6z=rXzYny_4BQMFr?y(EzG%D#YNQV${)|Gg)|OB`SF(J#@5H)9R7YdL`g*sGI@5zHP(ev{A#RlOhw4+ zid-d*wQdFO=NF~Q#vA=M_Y1-r3hDFvu8_iLDmCjN}8+q~`b zRqjV|&s$OAw8r8&A1qbh*HYCLL4;?P_`R*U`LtepudmKm=2R*&$DJ!r2d{#Rh+&@1 zp|e}%d`b0XtTv9NWsOUtYUw`k=PE?_)$nNhIQ47GStK_m5}vB$afi+}Hknm-J!P|b zzt-&;xhB+J_nxd*7ScPB4$xe?g7a=bHnxgl1;c*!K(tFtq3w#Z<1vnJCW`am$yh*7 zO6Z?mykqq;&QR_CtVaH)5EnxvC%ObbK5}gmCxc$Hs~_U8jkJ=*iPe2sar8*-SmXVk zBn-R5VCiLUkFFLwRI(vNv#AU#G7Y>7xa}dlfBDS4!WmN#u|AI~XWRTV9n12guq$?qNSMs$dk?jV+Kl|Afg=lMAI{?XdB$G6a#gV zq9({Z+Lh!)D)GK0Y&@$?zV2dQJkVN;{x#Np(?~9p2YJ$H5jM6r{d>laHg}noDyQQT z$K-AD_`!O$nVj0MNLpSBvsI>cWa<3*_@}GZ%#PNF`aW&tvpJ2YRs&79ceTxF+PHpO z(ghiVq=7@D&P{ekiMH!j{r=T*!B>_=)cLo4$5P^CW$q7@*iDIj_aU36%~|m#i_zv^ zg_iKRJDPS)+_9?_6hwIvskDhM!1nBN%zHMEC+RSKE|+pZQi=a!G1j z)+k-kPVeyN>?#ml`a`#_9JzDW{(*z8oUBI8Rf*MVmL42etyyjI)oaj|r`IA|m%3Hm8i99MT;HoS;&SI`f-sJDeJr$)r8V~CwGnX94Sg(0?0DNRugkB|2q>88Efu4{ZJW==&sw+031VR-^%ya156bqJs{G{v>$C5-m;P&a6xkh_Wmi1 z6rZmxMJt#ODZ#JMOxNCjgIB?n+r&d?1_|Xf&kRWEPvZLY(`}`i5B<-h>3Rh^Kd~>C z@zLNF#_G%p-K5$<(|r^oQk*H%wv9Yf70GaL7kRNY+_jjO zV$cc(tzcJj;up~?)r=fpWxWTl0+pIK8vX^SFScrXdBHhYs%PzDR@V)TzS{nQnD;?; zs`Fx9!I9hpo}(1OX<2KiJi^k4^_DN#A5~uT_RkKC_(q?~Lw_ z)&hkkO1~Izw6lZB@wh6@i+IJFG_F&+y;f~Xr@A&?&N!0Jn#aRJt~Gv8XX-rD=Y45f zXTE-~@qzj2IP$@=t%=+(RVpC1k}-Rz*h8xMQn?KY2ft9g$V6yvP z>X$o~WcP87zFaodF1LbI%nffpCEw!Aq8lXtSa)s5c*kYszV@N=*)_d=ryQZ@{7|!= zSk5=O04Z-&5yuL26f2wd^{Sq>n9Q}AF`1>a(|)Gds!hFzuk&~`*8P5zozrb?J;>YT z^c{UpPpmKJD*d3jB6c`iBZnEAUDArc_gyWLsveV;}z;@K#x13#?-2`E<_s)bv5vZ z*ZHCD1uEHKYEu9BMj0=YD({6=^JS0_;}LnGZ{)=$ry*8QWc-fK<~J5<*b$xWNrNRn zrPZyx~coc_I?uY?iQcVwcDJ<21ar%-7P)i{m=_D3YR!%p1%0J8{W?r zt9S>lh*eSiJYY&}K=*V8yZ*lTjK4f5cd|vS$%oU|rFW|7C0%x^*>OsB`7tY#!Dtz& z(dDfmKANccYmyxJ()hlJ4R=}I3VZX8qm_2v=%q`FUShPdCxTxXfAma8G~q9T_Kqrl zf%qKn$8cJF6?o+?Y22~sGSVd**5Iq4qVLBrKGE(9(!&d|6{;}Q>YXlgg=KcQUy{m$ zuL57i9nHkHtccj8U41_4sGx76+oH7=M2KXubS_bP-#SisEzaql)U-3sJZYNIvMD(7 zPustlJPY;wNi3G&N9uK98A@2ge37cW@XT>`X_f9i_$}RxOFr+1xge?y+nhJL%S8WXHUIi%9A9dhLVXqF%3kpiJyiwoT3BQS0NkFlEk) zydV4)-3U9E%r(D6#Dul;xpW@<7BvgbJbci^Y2b5wczXeB#Ca%Tb9{KVrbhI}N`^g) zHFq&NovN&`Z%T}k`w*Vxq+#`(7i@8i@oP~}0`WCl#u=Hfbir_#6c!@x4s zF~cwA74)b)fOkWr(~C)z-_gY@O!Lb;nWiZcCXbhyouh=0)xK78^gt*jjjJDh4>?N7 z&#-Jghx-kFzjlsN)Msb={fH`{X7(lhw@Zk45m(^ZvYDFn8Z+aat zZkosHHm_f#$rInEUcivgF~8=->U+{XcJum0nv_}h*bRP>!7mauZSpjC#Xn@R5sORw zc+$kn(sVve)UD>(;bT4t-JMaIsDFsecqHr7JP~%ew zvpH8-dm3oo^nUJ$yn9$Ly>m8Pfy9q;uo*8hWcMvE*Lv{oC7puILh3*b*?n=Pi~r~G iSkz@iE%T4#G4em(>6dfsAp-<-gI1g}i~ni#r~d;TcP|bA literal 0 HcmV?d00001 From fee535835072e93cc2ffb1ab4643f7e6bcc3de33 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:56:27 -0600 Subject: [PATCH 02/42] Fixed STR Mod showing total instead of adds for custom martial maneuvers. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 388e206f0b..212bc5687a 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -628,7 +628,7 @@ tempPosition = tempString.indexOf("str"); diceString = tempString.slice(0, tempPosition); diceString = diceString.slice(-3).replace(/\D/g,""); - importedManeuvers["martialManeuverStrMod"+ID] = parseInt(diceString)||0; + importedManeuvers["martialManeuverStrMod"+ID] = (parseInt(diceString)||0) - (parseInt(character.strength)||0); } } From dd361b5e48870eb8eaed1c8cfbbe2ff7cb20f9e6 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:52:17 -0600 Subject: [PATCH 03/42] Non-standard weapons set permanently to weaponType overide now at least temporary. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 212bc5687a..fb556d3eed 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -1,6 +1,6 @@ /* HeroSystem6eHeroic_HDImporter.js * Hero Designer Importer for the Roll20 Hero System 6e Heroic character sheet -* Version: 2.2 +* Version: 2.3 * By Villain in Glasses * villaininglasses@icloud.com * Discord: Villain#0604 @@ -736,6 +736,9 @@ let multipowerArray = new Array(); let multipowerArrayIndex = 0; + // Weapon States + let weaponStates = "KKKKKNOOOOOO"; + // Read equipment const maxEquipment = 16; let importCount = 0; @@ -910,9 +913,11 @@ // Check for Killing Attack. if (tempString.includes("Killing Attack") || tempString.includes("RKA") || tempString.includes("HKA")) { - // importedWeapons.weaponNormalDamage01= "off"; + // importedWeapons["weaponNormalDamage"+ID] = "off"; } else { importedWeapons["weaponNormalDamage"+ID]= "on"; + weaponStates = setCharAt(weaponStates, importCount, 'N'); + importedWeapons["weaponType"+ID]= "normal"; } // Get OCV bonus or penalty. @@ -966,7 +971,8 @@ } // Import weapons. - + importedWeapons.weaponStates = weaponStates; + setAttrs(object.id, importedWeapons); if(verbose) { @@ -4499,11 +4505,17 @@ } + function setCharAt(str,index,chr) { + // Replace a single character in a string. + if(index > str.length-1) return str; + return str.substring(0,index) + chr + str.substring(index+1); + } + + /* **************************************** */ /* *** END Importing Functions *** */ /* **************************************** */ - // TEST const createSingleWriteQueue = (attributes) => { // this is the list of trigger attributes that will trigger class recalculation, as of 5e OGL 2.5 October 2018 // (see on... handler that calls update_class in sheet html) From 7a68c8f55cab4a5dd1eb79c2a639b3d8cbb99493 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:47:54 -0600 Subject: [PATCH 04/42] Looks for Max Weapon Range in weapon notes. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 98 +++++++++++++------ HeroSystem6eHeroic_HDImporter/README.MD | 14 ++- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index fb556d3eed..eec08b969b 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -42,7 +42,7 @@ (function() { // Constants const versionMod = "2.3"; - const versionSheet = "3.41"; // Note that a newer sheet will make upgrades as well as it can. + const versionSheet = "3.51"; // Note that a newer sheet will make upgrades as well as it can. const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2"]); // HeroSystem6eHeroic.hde versions allowed. const defaultAttributes = { @@ -956,6 +956,17 @@ } } + // Calculate thrown weapon range or assign range without units. + importedWeapons["weaponRange"+ID] = getWeaponRange(weaponsArray[importCount].range, character.strength, importedWeapons["weaponMass"+ID], script_name); + + // Check for max range in notes. + if (weaponsArray[importCount].notes !== "") { + tempString = weaponsArray[importCount].notes; + importedWeapons["weaponRange"+ID] = getWeaponMaxRange(tempString, script_name); + } else { + importedWeapons["weaponRange"+ID] = 0; + } + // Get weapon mass. if (weaponsArray[importCount].mass !== "") { tempString = weaponsArray[importCount].mass; @@ -963,9 +974,6 @@ } else { importedWeapons["weaponMass"+ID] = 0; } - - // Calculate thrown weapon range or assign range without units. - importedWeapons["weaponRange"+ID] = getWeaponRange(weaponsArray[importCount].range, character.strength, importedWeapons["weaponMass"+ID], script_name); } } @@ -4180,8 +4188,38 @@ } + var getWeaponMaxRange = function (inputString, script_name) { + let outcome = ""; + let startPosition = 0; + let endPosition = 0; + + inputString = inputString.toLowerCase(); + + if (inputString.includes("max range")) { + startPosition = inputString.indexOf("max range"); + outcome = inputString.slice(startPosition); + if (outcome.includes(';')) { + endPosition = outcome.indexOf(';'); + outcome = outcome.slice(0, endPosition); + } else if (outcome.includes(')')) { + endPosition = outcome.indexOf(')'); + outcome = outcome.slice(0, endPosition); + } else { + endPosition = Math.min(16, outcome.length); + outcome = outcome.slice(0, endPosition); + } + outcome = outcome.replace(/[^\d,-]/g, ""); + if (outcome.includes(',')) { + outcome = outcome.replace(',', ", "); + } + } + + return outcome.trim(); + } + + var getArmorLocations = function (inputString, script_name) { - let locations = ""; + let outcome = ""; let startPosition = 0; let endPosition = 0; @@ -4189,41 +4227,41 @@ if (inputString.includes("location")) { startPosition = inputString.indexOf("location"); - locations = inputString.slice(startPosition); - if (locations.includes(';')) { - endPosition = locations.indexOf(';'); - locations = locations.slice(0,endPosition); - } else if (locations.includes(')')) { - endPosition = locations.indexOf(')'); - locations = locations.slice(0,endPosition); + outcome = inputString.slice(startPosition); + if (outcome.includes(';')) { + endPosition = outcome.indexOf(';'); + outcome = outcome.slice(0, endPosition); + } else if (outcome.includes(')')) { + endPosition = outcome.indexOf(')'); + outcome = outcome.slice(0, endPosition); } else { - endPosition = Math.min(28, locations.length); - locations = locations.slice(0,endPosition); + endPosition = Math.min(28, outcome.length); + outcome = outcome.slice(0, endPosition); } - locations = locations.replace(/[^\d,-]/g, ""); - if (locations.includes(',')) { - locations = locations.replace(',', ", "); + outcome = outcome.replace(/[^\d,-]/g, ""); + if (outcome.includes(',')) { + outcome = outcome.replace(',', ", "); } } else if (inputString.includes("loc")) { startPosition = inputString.indexOf("loc"); - locations = inputString.slice(startPosition); - if (locations.includes(';')) { - endPosition = locations.indexOf(';'); - locations = locations.slice(0,endPosition); - } else if (locations.includes(')')) { - endPosition = locations.indexOf(')'); - locations = locations.slice(0,endPosition); + outcome = inputString.slice(startPosition); + if (outcome.includes(';')) { + endPosition = outcome.indexOf(';'); + outcome = outcome.slice(0, endPosition); + } else if (outcome.includes(')')) { + endPosition = outcome.indexOf(')'); + outcome = outcome.slice(0, endPosition); } else { - endPosition = Math.min(11, locations.length); - locations = locations.slice(0,endPosition); + endPosition = Math.min(11, outcome.length); + outcome = outcome.slice(0, endPosition); } - locations = locations.replace(/[^\d,-]/g, ""); - if (locations.includes(',')) { - locations = locations.replace(',', ", "); + outcome = outcome.replace(/[^\d,-]/g, ""); + if (outcome.includes(',')) { + outcome = outcome.replace(',', ", "); } } - return locations.trim(); + return outcome.trim(); } diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index 9e20d00e85..a450cbdf30 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -65,7 +65,17 @@ At present, the character sheet has twenty power, ten talent, and ten complicati Powers that add (or subtract) characteristics, movement, and perception abilities should be applied in the appropriate modifier fields. -If your campaign uses the optional rule of applying END costs to armor, HD Importer will recognize (X END/Turn) or (END/Turn: X) in equipment notes. +> [!TIP] +> HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempt to automatically apply the value given. + +### (Max Range: X) +A weapon's maximum range. + +### (locations: U-V, X-Y, Z) or (loc U-V) +Armor locations + +### (X END/Turn) or (END/Turn: X) +If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help @@ -114,3 +124,5 @@ Version 2.0 -- Updated to support additional character sheet features as of shee Version 2.1 -- Updated to support additional character sheet features as of sheet version 3.16. Updated version of Hero Designer export format (HeroSystem6eHeroic.hde to version 2.1). Expands support for martial arts maneuvers. Recognizes power notes and scans powers for damage advantages. Adds the everyman PS skill. Added a second sample character with martial arts skills. Bug fixes (August 9, 2024). Version 2.2 -- Fix for compound power import failure. Recognizes Mental Combat Skill Levels and assigns levels and costs. Minor update to HeroSystem6eHeroic.hde (version 2.2). Improved formatting of text in powers, talents, and complications (September 14, 2024). + +Version 2.3 -- From 65317f6a7b1f4cbc1bd56213a88130f56edc6e09 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:09:00 -0700 Subject: [PATCH 05/42] Recognizes extra copies of equipment and weapons. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 116 ++++++++++++++++-- HeroSystem6eHeroic_HDImporter/README.MD | 7 +- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index eec08b969b..a96766e003 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -712,6 +712,7 @@ let subStringA; let subStringB; let sampleSize; + let itemNumber; // Needed for adjusted damage. let advantage = 0; @@ -745,7 +746,7 @@ let imported = 0; let ID = "01"; - // Imports sixteen martial arts maneuvers, skipping empty slots. + // Imports sixteen piece of equipment, skipping empty slots. for (importCount = 1; importCount <= maxEquipment; importCount++) { @@ -860,8 +861,25 @@ // Get item mass. if (equipmentListArray[importCount].mass !== "") { + + // Check for additional copies in notes. + if (equipmentListArray[importCount].notes !== "") { + tempString = equipmentListArray[importCount].notes; + + if (tempString.includes("number of items")) { + // Get the number of copies and amend the equipment text. + itemNumber = getItemNumber(tempString); + importedEquipment["equipText"+ID] += " (" + (itemNumber).toString() + ")"; + } else { + itemNumber = 1; + } + } else { + itemNumber = 1; + } + + // Multiply the base mass by the number of copies. tempString = equipmentListArray[importCount].mass; - importedEquipment["equipMass"+ID] = getItemMass(tempString, script_name); + importedEquipment["equipMass"+ID] = itemNumber * getItemMass(tempString, script_name); } else { importedEquipment["equipMass"+ID] = 0; } @@ -970,14 +988,40 @@ // Get weapon mass. if (weaponsArray[importCount].mass !== "") { tempString = weaponsArray[importCount].mass; - importedWeapons["weaponMass"+ID] = getItemMass(tempString, script_name); + tempValue = getItemMass(tempString, script_name); + importedWeapons["weaponMass"+ID] = tempValue; + + // Check for a note on additional copies. + if (weaponsArray[importCount].notes !== "") { + tempString = weaponsArray[importCount].notes; + + if (tempString.includes("number of items")) { + if (equipmentListArrayIndex < maxEquipment) { + itemNumber = getItemNumber(tempString); + + // Fill in ammunition quantity if it hasn't already been assigned. + if (importedWeapons["weaponShots"+ID] === 0) { + importedWeapons["weaponShots"+ID] = itemNumber; + } + + // Assign copies to an open equipment slot. ID changes to the equipment slot. + equipmentListArrayIndex++; + ID = String(equipmentListArrayIndex).padStart(2,'0'); + + tempString = weaponsArray[importCount].name; + tempString += " (" + (itemNumber-1).toString() + ")"; + + importedWeapons["equipText"+ID] = tempString; + importedWeapons["equipMass"+ID] = (itemNumber-1) * tempValue; + } + } + } } else { importedWeapons["weaponMass"+ID] = 0; } - } - + } } - + // Import weapons. importedWeapons.weaponStates = weaponStates; @@ -2402,11 +2446,19 @@ } else if (theSkill.display === ("Weapon Familiarity")) { // Weapon familiarity skill line. - // There should be only one line since Hero Designer lumps them together. + // There will probably be only one line since Hero Designer lumps them together. // We need to break them up. let tempString = theSkill.text; tempString = tempString.replace(/\s\s+/g, " "); + + // Special skillsets to rename. + tempString = tempString.replace("Thrown Knives, Axes, and Darts", "Thrown Weapons"); + tempString = tempString.replace("Javelins and Thrown Spears", "Thrown Spears"); + tempString = tempString.replace("Axes, Maces, Hammers, and Picks", "Hafted Weapons"); + + // Drop WF: tempString = tempString.replace("WF: ", ""); + let weaponFamArrayLength = (tempString.split(",").length - 1); let weaponFamArray = new Array(weaponFamArrayLength); @@ -2939,8 +2991,8 @@ let attribute = skillObject.attribute; let text = skillObject.text; let type = "none"; - let base = skillObject.base; - let levels = skillObject.levels; + let base = parseInt(skillObject.base); + let levels = parseInt(skillObject.levels); let cost = skillObject.cost; if (skillObject.display === ("Skill Levels")) { @@ -2991,6 +3043,33 @@ } else if (text.startsWith("PS")) { // Professional skill. type = "ps"; + } else if (text.includes("Survival") || text.includes("Gambling")) { + // Special skills. + if (text.includes("8-")) { + // It is not clear if a familiarity skill should be odd or even from the costs. + if (base === 1) { + type = "sp2"; + } else if (base === 2) { + type = "sp4"; + } else { + type = "sp6"; + } + } else { + if ((base-levels*2) === 1) { + type = "sp1"; + } else if ((base-levels*2) === 2) { + type = "sp2"; + } else if ((base-levels*2) === 3) { + type = "sp3"; + } else if ((base-levels*2) === 4) { + type = "sp4"; + } else if ((base-levels*2) === 5) { + type = "sp5"; + } else { + // Default of six-point skills unless there is call for higher. + type = "sp6"; + } + } } else if (attribute === "INT") { // Intellect skill. type = "int"; @@ -4218,6 +4297,25 @@ } + var getItemNumber = function (inputString, script_name) { + let outcome = ""; + let startPosition = 0; + let endPosition = 0; + + inputString = inputString.toLowerCase(); + + if (inputString.includes("number of items")) { + endPosition = inputString.indexOf("number of items"); + startPosition = Math.max(endPosition - 3, 1); + outcome = inputString.slice(startPosition, endPosition); + outcome = outcome.replace(/[^\d,-]/g, ""); + outcome = parseInt(outcome)||0; + } + + return outcome; + } + + var getArmorLocations = function (inputString, script_name) { let outcome = ""; let startPosition = 0; diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index a450cbdf30..e5a0a0287a 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -66,14 +66,11 @@ At present, the character sheet has twenty power, ten talent, and ten complicati Powers that add (or subtract) characteristics, movement, and perception abilities should be applied in the appropriate modifier fields. > [!TIP] -> HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempt to automatically apply the value given. +> HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the value given (parentheses are not strictly required). -### (Max Range: X) +### (Max Range: Xm) or (max range X) A weapon's maximum range. -### (locations: U-V, X-Y, Z) or (loc U-V) -Armor locations - ### (X END/Turn) or (END/Turn: X) If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. From 0cee75db112a53c28abafcb7a83c2d7c5a67f3f9 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:40:47 -0700 Subject: [PATCH 06/42] Fixed knowledge skill type assignment at familiarity level. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index a96766e003..3b0b049432 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -3007,31 +3007,31 @@ } else if ((base === "0") && (cost === "0")) { // Everyman skill. type = "everyman"; - } else if (text.startsWith("KS") && ((base-levels) === 2)) { + } else if (text.startsWith("KS") && ((base-levels) <= 2)) { // Knowledge Skill type = "ks"; } else if (text.startsWith("KS") && ((base-levels) === 3)) { // Knowledge Skill based on INT. type = "intKS"; - } else if (text.startsWith("CK") && ((base-levels) === 2)) { + } else if (text.startsWith("CK") && ((base-levels) <= 2)) { // City Knowledge Skill type = "ck"; } else if (text.startsWith("CK") && ((base-levels) === 3)) { // City Knowledge Skill based on INT. type = "intCK"; - } else if (text.startsWith("CuK") && ((base-levels) === 2)) { + } else if (text.startsWith("CuK") && ((base-levels) <= 2)) { // Culture Knowledge Skill type = "cuk"; } else if (text.startsWith("CuK") && ((base-levels) === 3)) { // Culture Knowledge Skill based on INT. type = "intCuK"; - } else if (text.startsWith("Science Skill") && ((base-levels) === 2)) { + } else if (text.startsWith("Science Skill") && ((base-levels) <= 2)) { // Science Skill type = "ss"; } else if (text.startsWith("Science Skill") && ((base-levels) === 3)) { // Science Skill based on INT. type = "intSS"; - } else if (text.startsWith("AK") && ((base-levels) === 2)) { + } else if (text.startsWith("AK") && ((base-levels) <= 2)) { // Area Knowledge. type = "ak"; } else if (text.startsWith("AK") && ((base-levels) === 3)) { From 348cafdafaac6c622ed551719ed99ba01c4f3b42 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:30:16 -0700 Subject: [PATCH 07/42] Removes Active Points in skill names due to skill enhancers. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 3b0b049432..46c76481dd 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -42,7 +42,7 @@ (function() { // Constants const versionMod = "2.3"; - const versionSheet = "3.51"; // Note that a newer sheet will make upgrades as well as it can. + const versionSheet = "3.71"; // Note that a newer sheet will make upgrades as well as it can. const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2"]); // HeroSystem6eHeroic.hde versions allowed. const defaultAttributes = { @@ -561,12 +561,18 @@ while ( (importCount < maneuverSlots) && (importCount < maneuverArrayIndex) ) { if (importCount < maneuverArrayIndex) { ID = String(importCount+1).padStart(2,'0'); + tempString = maneuverArray[importCount].name; - if ( maneuverArray[importCount].name.length > nameMax) { - importedManeuvers["martialManeuverName"+ID] = maneuverArray[importCount].name.slice(0, nameMax); + if ( tempString.includes("Weapon Element") ) { + tempString = tempString.replace("Weapon Element", "Element"); + } + + if ( tempString.length > nameMax) { + tempString = tempString.slice(0, nameMax); + importedManeuvers["martialManeuverName"+ID] = tempString; importedManeuvers["martialManeuverEffect"+ID] = maneuverArray[importCount].name + '\n' + maneuverArray[importCount].effect; } else { - importedManeuvers["martialManeuverName"+ID] = maneuverArray[importCount].name; + importedManeuvers["martialManeuverName"+ID] = tempString; importedManeuvers["martialManeuverEffect"+ID] = maneuverArray[importCount].effect; } @@ -3099,10 +3105,16 @@ type = "combat"; } + // Remove unwanted text from skills influenced by enhancers. + if (text.includes("Active Points")) { + text = removeEnhancerText(text); + } + // Try to find the best name of the skill. // It may be in .name, .text, or .display. let name = skillObject.name; + if (name === "") { if ((text !== "") && text.includes("AK: ")) { name = text.replace("AK: ", ""); @@ -3152,6 +3164,35 @@ } + var removeEnhancerText = function(name) { + let startPosition; + let endPosition; + let subStringA; + let subStringB; + + if (name.includes("Active Points")) { + // Split the name into two parts on each side of Active Points. + startPosition = name.indexOf("Active Points"); + endPosition = startPosition+13; + subStringA = name.slice(0, startPosition); + subStringB = name.slice(endPosition); + + // Remove text "XY- (Z " at the end of subStringA. + endPosition = subStringA.lastIndexOf('(') - 3; + subStringA = subStringA.slice(0,endPosition); + + // Remove the first ')' in subStringB. + startPosition = subStringB.indexOf(')'); + subStringB = subStringB.slice(startPosition); + + // Join the subStrings to make a new name. + name = subStringA + subStringB; + } + + return name; + } + + var findEndurance = function(testObject) { // Determine endurance type, advantages, and limitations. // Remove advantage or limitation from tempString so that they aren't counted twice. From b1d00934ce6bc4081c32e8c70f20f28ad7962038 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:15:46 -0700 Subject: [PATCH 08/42] Limited mass of additional equipment copies to one decimal place. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 46c76481dd..330dfe9c00 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -533,6 +533,7 @@ let temp = 0; let tempString = ""; let diceString = ""; + let tempValue = 0; let tempPosition = 0; const maxManeuvers = 20; const maneuverSlots = 10; @@ -884,8 +885,9 @@ } // Multiply the base mass by the number of copies. - tempString = equipmentListArray[importCount].mass; - importedEquipment["equipMass"+ID] = itemNumber * getItemMass(tempString, script_name); + tempValue = itemNumber * getItemMass(equipmentListArray[importCount].mass, script_name); + tempValue = Math.round(10*tempValue)/10; + importedEquipment["equipMass"+ID] = tempValue; } else { importedEquipment["equipMass"+ID] = 0; } @@ -908,7 +910,6 @@ let importedWeapons = new Array(); const maxAdvantage = 1; const maxWeapons = 5; - let tempValue = 0; importCount = 0; imported = 0; @@ -1016,9 +1017,10 @@ tempString = weaponsArray[importCount].name; tempString += " (" + (itemNumber-1).toString() + ")"; - importedWeapons["equipText"+ID] = tempString; - importedWeapons["equipMass"+ID] = (itemNumber-1) * tempValue; + tempValue = (itemNumber-1) * tempValue; + tempValue = Math.round(10*tempValue)/10; + importedWeapons["equipMass"+ID] = tempValue; } } } @@ -3165,6 +3167,8 @@ var removeEnhancerText = function(name) { + // Here a .replace() function doesn't work because the text to be removed is + // of the form "xx- (x Active Points)." let startPosition; let endPosition; let subStringA; @@ -4339,7 +4343,7 @@ var getItemNumber = function (inputString, script_name) { - let outcome = ""; + let outcome = "1"; let startPosition = 0; let endPosition = 0; @@ -4350,10 +4354,9 @@ startPosition = Math.max(endPosition - 3, 1); outcome = inputString.slice(startPosition, endPosition); outcome = outcome.replace(/[^\d,-]/g, ""); - outcome = parseInt(outcome)||0; } - return outcome; + return parseInt(outcome)||1; } From 23e2e71a72cc0b8678dbf0aea08651342d10e9cb Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:26:12 -0700 Subject: [PATCH 09/42] Display quantity only for multiple extra weapons in equipment. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 330dfe9c00..5eb6e22392 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -1016,7 +1016,11 @@ ID = String(equipmentListArrayIndex).padStart(2,'0'); tempString = weaponsArray[importCount].name; - tempString += " (" + (itemNumber-1).toString() + ")"; + + if (itemNumber > 2) { + tempString += " (" + (itemNumber-1).toString() + ")"; + } + importedWeapons["equipText"+ID] = tempString; tempValue = (itemNumber-1) * tempValue; tempValue = Math.round(10*tempValue)/10; From ee1171d4140a179416119f360b3998d3d632463f Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:51:16 -0700 Subject: [PATCH 10/42] Fixed basic fluency being mislabeled as native fluency for linguists. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 5eb6e22392..b72926f424 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -2649,9 +2649,7 @@ // Determine fluency. if (tempString.includes("native")) { fluency = "native"; - } else if (cost == 0) { - fluency = "native"; - } else if ((cost == 1) && (tempString.includes("literate"))) { + } else if (cost === 0) { fluency = "native"; } else if (tempString.includes("basic")) { fluency = "basic"; @@ -2663,6 +2661,8 @@ fluency = "idiomatic"; } else if (tempString.includes("imitate")) { fluency = "imitate"; + } else if ((cost === 1) && (tempString.includes("literate"))) { + fluency = "native"; } else { fluency = "none"; } From 0d8e86b72420e68671a98f98c8e1a273b798a641 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Sun, 10 Nov 2024 00:51:31 -0700 Subject: [PATCH 11/42] Added cost to language skill import. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 180 ++++++------------ 1 file changed, 61 insertions(+), 119 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index b72926f424..ce039d0727 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -43,7 +43,7 @@ // Constants const versionMod = "2.3"; const versionSheet = "3.71"; // Note that a newer sheet will make upgrades as well as it can. - const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2"]); // HeroSystem6eHeroic.hde versions allowed. + const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2", "2.3"]); // HeroSystem6eHeroic.hde versions allowed. const defaultAttributes = { @@ -262,11 +262,13 @@ sendChat(script_name, '
Import of ' + character.character_name + ' started.
', null, {noarchive:true}); if (character.version === "1.0") { - sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v1.0. \n Version 2.2 supports additional content."); + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v1.0. \n Version 2.3 supports additional content."); } else if (character.version === "2.0") { - sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.0. \n Version 2.2 supports additional content."); + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.0. \n Version 2.3 supports additional content."); } else if (character.version === "2.1") { - sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.1. \n Version 2.2 is available."); + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.1. \n Version 2.3 supports additional content."); + } else if (character.version === "2.2") { + sendChat(script_name, "Exported from HERO Designer with \n HeroSystem6eHeroic.hde v2.2. \n Version 2.3 supports additional content."); } object = null; @@ -565,7 +567,7 @@ tempString = maneuverArray[importCount].name; if ( tempString.includes("Weapon Element") ) { - tempString = tempString.replace("Weapon Element", "Element"); + tempString = tempString.replace("Weapon Element", "WE"); } if ( tempString.length > nameMax) { @@ -748,14 +750,14 @@ let weaponStates = "KKKKKNOOOOOO"; // Read equipment - const maxEquipment = 16; + const maxGear = (character.version >= parseFloat("2.3")||0) ? 42 : 16; let importCount = 0; let imported = 0; let ID = "01"; - // Imports sixteen piece of equipment, skipping empty slots. + // Imports either 42 or 16 pieces of equipment (for version < 2.3), skipping empty slots. - for (importCount = 1; importCount <= maxEquipment; importCount++) { + for (importCount = 1; importCount <= maxGear; importCount++) { ID = String(importCount).padStart(2,'0'); @@ -835,7 +837,7 @@ setAttrs(object.id, {treasures: tempString}); - // Show the Treasures Gear Tab slide where the multipower equipment will appear. + // Show the Treasures Gear Tab slide where details of equipment will appear. setAttrs(object.id, {gearSlideSelection: 3}); } @@ -843,6 +845,7 @@ // Assign to character sheet Equipment List. let importedEquipment = new Array(); + const maxEquipment = 16; importCount = 0; imported = 0; @@ -909,14 +912,15 @@ let importedWeapons = new Array(); const maxAdvantage = 1; - const maxWeapons = 5; + const maxWeapons = 10; importCount = 0; imported = 0; for (importCount = 0; importCount < maxWeapons; importCount++) { - - ID = String(importCount+1).padStart(2,'0'); + + // Weapons 6-10 are numbered 11-15 within the Set A/B framework of the sheet. + ID = (importCount < 5) ? String(importCount+1).padStart(2,'0') : String(importCount+6).padStart(2,'0'); if (importCount < weaponsArrayIndex) { imported += 1; @@ -1003,7 +1007,7 @@ tempString = weaponsArray[importCount].notes; if (tempString.includes("number of items")) { - if (equipmentListArrayIndex < maxEquipment) { + if (equipmentListArrayIndex < maxGear) { itemNumber = getItemNumber(tempString); // Fill in ammunition quantity if it hasn't already been assigned. @@ -1048,16 +1052,17 @@ } // Prepare object of armor defenses. Assign to character sheet Armor List. - + let importedArmor = new Array(); - const maxArmor = 4; // The 4th may be overwritten if the character has resistant protection. + const maxArmor = 8; // The 8th piece will be overwritten if the character has resistant protection. importCount = 0; imported = 0; for (importCount = 0; importCount < maxArmor; importCount++) { - - ID = String(importCount+1).padStart(2,'0'); + + // Armor pieces 5-8 are numbered 11-14 within the Set A/B framework of the sheet. + ID = (importCount < 4) ? String(importCount+1).padStart(2,'0') : String(importCount+7).padStart(2,'0'); tempString = "none"; if (importCount < armorArrayIndex) { @@ -1156,7 +1161,7 @@ for (let i=0; i < equipmentMultipowers.length; i++) { // Get next multipower index. - shieldSearchIndex=equipmentMultipowers[i]; + shieldSearchIndex = equipmentMultipowers[i]; tempString = multipowerArray[shieldSearchIndex].name; tempString = tempString.toLowerCase(); @@ -1235,9 +1240,6 @@ if (i+2 > equipmentMultipowers.length) { // Shield is the last multipower in the list. for (let j = shieldSearchIndex; j 0) { - charMod.armorPD04 += tempValue; + charMod.armorPD14 += tempValue; if ( (specialArray.some(v => tempString.includes(v))) != true) { // We don't want to add overall modifications for special cases. charMod.pdMod += tempValue; } if (!pdAddedToTotal) { - charMod.totalPD04 = tempValue + parseInt(character.pd); + charMod.totalPD14 = tempValue + parseInt(character.pd); pdAddedToTotal = true; } else { - charMod.totalPD04 += tempValue; + charMod.totalPD14 += tempValue; } - charMod.armorName04 = importedPowers["powerName"+ID]; - charMod.armorLocations04 = "3-18"; + charMod.armorName14 = importedPowers["powerName"+ID]; + charMod.armorLocations14 = "3-18"; tempObject = (requiresRoll(powerArray[importCount].text)); if (tempObject.hasRoll) { - charMod.armorActivation04 = tempObject.skillRoll; + charMod.armorActivation14 = tempObject.skillRoll; } else { - charMod.armorActivation04 = 18; + charMod.armorActivation14 = 18; } } tempValue = getResistantED(powerArray[importCount].text, script_name); if (tempValue > 0) { - charMod.armorED04 += tempValue; + charMod.armorED14 += tempValue; if ( (specialArray.some(v => tempString.includes(v))) != true) { // We don't want to add overall modifications for special cases. charMod.edMod += tempValue; } if (!edAddedToTotal) { - charMod.totalED04 = tempValue + parseInt(character.ed); + charMod.totalED14 = tempValue + parseInt(character.ed); edAddedToTotal = true; } else { - charMod.totalED04 += tempValue; + charMod.totalED14 += tempValue; } - charMod.armorName04 = importedPowers["powerName"+ID]; - charMod.armorLocations04 = "3-18"; + charMod.armorName14 = importedPowers["powerName"+ID]; + charMod.armorLocations14 = "3-18"; tempObject = (requiresRoll(powerArray[importCount].text)); if (tempObject.hasRoll) { - charMod.armorActivation04 = tempObject.skillRoll; + charMod.armorActivation14 = tempObject.skillRoll; } else { - charMod.armorActivation04 = 18; + charMod.armorActivation14 = 18; } } } @@ -2033,18 +2035,18 @@ } if ( (powerArray[importCount].text).includes("Resistant")) { - charMod.armorPD04 += parseInt(character.pd); + charMod.armorPD14 += parseInt(character.pd); if (!pdAddedToTotal) { - charMod.totalPD04 += parseInt(character.pd); + charMod.totalPD14 += parseInt(character.pd); pdAddedToTotal = true; } - charMod.armorName04 = importedPowers["powerName"+ID]; - charMod.armorLocations04 = "3-18"; + charMod.armorName14 = importedPowers["powerName"+ID]; + charMod.armorLocations14 = "3-18"; tempObject = (requiresRoll(powerArray[importCount].text)); if (tempObject.hasRoll) { - charMod.armorActivation04 = tempObject.skillRoll; + charMod.armorActivation14 = tempObject.skillRoll; } else { - charMod.armorActivation04 = 18; + charMod.armorActivation14 = 18; } } } @@ -2055,18 +2057,18 @@ } if ( (powerArray[importCount].text).includes("Resistant") ) { - charMod.armorED04 += parseInt(character.ed); + charMod.armorED14 += parseInt(character.ed); if (!edAddedToTotal) { - charMod.totalED04 += parseInt(character.ed); + charMod.totalED14 += parseInt(character.ed); edAddedToTotal = true; } - charMod.armorName04 = importedPowers["powerName"+ID]; - charMod.armorLocations04 = "3-18"; + charMod.armorName14 = importedPowers["powerName"+ID]; + charMod.armorLocations14 = "3-18"; tempObject = (requiresRoll(powerArray[importCount].text)); if (tempObject.hasRoll) { - charMod.armorActivation04 = tempObject.skillRoll; + charMod.armorActivation14 = tempObject.skillRoll; } else { - charMod.armorActivation04 = 18; + charMod.armorActivation14 = 18; } } } @@ -2628,8 +2630,10 @@ // This function is called when a skill is identified as an enhancer. // The skills' text will determine which enhancer it is. // let languages; - - let language; + + var ID = String(languageIndex+41).padStart(2,'0'); + var language = new Object(); + let name = languageObject.name; let tempString = languageObject.text; if (name === "") { @@ -2649,8 +2653,10 @@ // Determine fluency. if (tempString.includes("native")) { fluency = "native"; - } else if (cost === 0) { + } else if ( tempString.includes("idiomatic") && (cost < 2)) { fluency = "native"; + } else if ( tempString.includes("imitate") && (cost < 3)) { + fluency = "nativePlus"; } else if (tempString.includes("basic")) { fluency = "basic"; } else if (tempString.includes("completely")) { @@ -2661,8 +2667,6 @@ fluency = "idiomatic"; } else if (tempString.includes("imitate")) { fluency = "imitate"; - } else if ((cost === 1) && (tempString.includes("literate"))) { - fluency = "native"; } else { fluency = "none"; } @@ -2676,72 +2680,10 @@ } // Assign this language to the character sheet. - - switch(languageIndex) { - case 0: - language = { - skillName41: name, - skillFluency41: fluency, - skillLiteracy41: literacy - } - break; - case 1: - language = { - skillName42: name, - skillFluency42: fluency, - skillLiteracy42: literacy - } - break; - case 2: - language = { - skillName43: name, - skillFluency43: fluency, - skillLiteracy43: literacy - } - break; - case 3: - language = { - skillName44: name, - skillFluency44: fluency, - skillLiteracy44: literacy - } - break; - case 4: - language = { - skillName45: name, - skillFluency45: fluency, - skillLiteracy45: literacy - } - break; - case 5: - language = { - skillName46: name, - skillFluency46: fluency, - skillLiteracy46: literacy - } - break; - case 6: - language = { - skillName47: name, - skillFluency47: fluency, - skillLiteracy47: literacy - } - break; - case 7: - language = { - skillName48: name, - skillFluency48: fluency, - skillLiteracy48: literacy - } - break; - default: - // Last language slot available. - language = { - skillName49: name, - skillFluency49: fluency, - skillLiteracy49: literacy - } - } + language["skillName"+ID] = name; + language["skillFluency"+ID] = fluency; + language["skillLiteracy"+ID] = literacy; + language["skillCP"+ID] = cost; setAttrs(object.id, language); From d5d1a16fc614550b4df803297e34517f4716874f Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Sun, 10 Nov 2024 01:05:08 -0700 Subject: [PATCH 12/42] Added Forgery to Special Cost skills. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index ce039d0727..22414bf780 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -2997,7 +2997,7 @@ } else if (text.startsWith("PS")) { // Professional skill. type = "ps"; - } else if (text.includes("Survival") || text.includes("Gambling")) { + } else if (text.includes("Survival") || text.includes("Gambling") || text.includes("Forgery")) { // Special skills. if (text.includes("8-")) { // It is not clear if a familiarity skill should be odd or even from the costs. From 7f7229bbcee02d93f5ff8c53a6bbd45c9ba00737 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Mon, 11 Nov 2024 01:34:02 -0700 Subject: [PATCH 13/42] Added equipment keyword and fixed everyman PS. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index 22414bf780..bb02e2421f 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -773,13 +773,13 @@ multipowerArray[multipowerArrayIndex]=equipmentArray[equipmentArrayIndex]; multipowerArrayIndex++; - } else if ((equipmentArray[equipmentArrayIndex].defense !== "") && (equipmentArray[equipmentArrayIndex].defense === "true")) { + } else if ((equipmentArray[equipmentArrayIndex].defense !== "") && (equipmentArray[equipmentArrayIndex].defense === "true") && (equipmentArray[equipmentArrayIndex].notes.toLowerCase().includes("equipment") !== true)) { // If the item is a defense add it to the armor list. // This will need to be updated for shields. armorArray[armorArrayIndex]=equipmentArray[equipmentArrayIndex]; armorArrayIndex++; - } else if ((equipmentArray[equipmentArrayIndex].attack !== "") && (equipmentArray[equipmentArrayIndex].damage !== "") && (equipmentArray[equipmentArrayIndex].attack === "true")) { + } else if ((equipmentArray[equipmentArrayIndex].attack !== "") && (equipmentArray[equipmentArrayIndex].damage !== "") && (equipmentArray[equipmentArrayIndex].attack === "true") && (equipmentArray[equipmentArrayIndex].notes.toLowerCase().includes("equipment") !== true) ) { // If the item is a damage attack add it to the weapon list. weaponsArray[weaponsArrayIndex]=equipmentArray[equipmentArrayIndex]; weaponsArrayIndex++; @@ -2955,9 +2955,9 @@ } else if (skillObject.text.includes("three pre-defined Skills")) { // Three-group skill. type = "group"; - } else if ((base === "0") && (cost === "0") && skillObject.text.includes("11-")) { - // Everyman professional skill. - type = "everymanPS"; + } else if (text.startsWith("PS")) { + // Professional skill. + type = (cost === "0") ? "everymanPS" : "ps"; } else if ((base === "0") && (cost === "0")) { // Everyman skill. type = "everyman"; @@ -2994,9 +2994,6 @@ } else if (text.startsWith("TF")) { // Transport familiarity. type = "tf"; - } else if (text.startsWith("PS")) { - // Professional skill. - type = "ps"; } else if (text.includes("Survival") || text.includes("Gambling") || text.includes("Forgery")) { // Special skills. if (text.includes("8-")) { From b2e0dd07a77f71229b55f615b3c38fbe8ede74b3 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:09:22 -0700 Subject: [PATCH 14/42] Cleanup to appearane of additional maneuvers in treasures. --- .../2.3/HeroSystem6eHeroic_HDImporter.js | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js index bb02e2421f..2000fb176f 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic_HDImporter.js @@ -42,7 +42,7 @@ (function() { // Constants const versionMod = "2.3"; - const versionSheet = "3.71"; // Note that a newer sheet will make upgrades as well as it can. + const versionSheet = "3.8"; // Note that a newer sheet will make upgrades as well as it can. const needsExportedVersion = new Set(["1.0", "2.0", "2.1", "2.2", "2.3"]); // HeroSystem6eHeroic.hde versions allowed. const defaultAttributes = { @@ -660,19 +660,22 @@ if (maneuverArrayIndex > maneuverSlots) { let extras = 0; + tempString = "Additional Maneuvers \n"; + for (let i = maneuverSlots; i < maneuverArrayIndex; i++) { - tempString = tempString + maneuverArray[i].name + "\n"; - tempString = tempString + "CP: " + maneuverArray[i].points + "\n"; + tempString += maneuverArray[i].name + '\n'; + tempString += '\t' + "CP: " + maneuverArray[i].points + " "; if (maneuverArray[i].ocv !== "") { - tempString = tempString + "OCV: " + maneuverArray[i].ocv + "\n"; + tempString += "OCV: " + maneuverArray[i].ocv + " "; } if (maneuverArray[i].dcv !== "") { - tempString = tempString + "DCV: " + maneuverArray[i].dcv + "\n"; + tempString += "DCV: " + maneuverArray[i].dcv + " "; } if (maneuverArray[i].phase !== "") { - tempString = tempString + "Phase: " + maneuverArray[i].phase + "\n"; + tempString += "Phase: " + maneuverArray[i].phase + '\n'; } - tempString = tempString + maneuverArray[i].effect + "\n" + "\n"; + tempString += '\t' + maneuverArray[i].effect; + tempString += "\n \n"; extras++; } @@ -684,8 +687,8 @@ } } - if ( (typeof character.treasures != "undefined") && (character.treasures !== "")) { - tempString = character.treasures + '\n' + '\n' + tempString.trim(); + if ( (typeof character.treasures !== "undefined") && (character.treasures !== "")) { + tempString = character.treasures + "\n \n" + tempString.trim(); } else { tempString = tempString.trim(); } @@ -750,7 +753,7 @@ let weaponStates = "KKKKKNOOOOOO"; // Read equipment - const maxGear = (character.version >= parseFloat("2.3")||0) ? 42 : 16; + const maxGear = (character.version >= parseFloat("2.3")||0) ? 50 : 16; let importCount = 0; let imported = 0; let ID = "01"; @@ -835,17 +838,17 @@ tempString = tempString.trim(); } - setAttrs(object.id, {treasures: tempString}); - - // Show the Treasures Gear Tab slide where details of equipment will appear. - setAttrs(object.id, {gearSlideSelection: 3}); + setAttrs(object.id, { + treasures: tempString, + gearSlideSelection: 3 + }); } // Prepare object of items that are not weapons or armor. // Assign to character sheet Equipment List. let importedEquipment = new Array(); - const maxEquipment = 16; + const maxEquipment = 32; importCount = 0; imported = 0; @@ -888,7 +891,7 @@ } // Multiply the base mass by the number of copies. - tempValue = itemNumber * getItemMass(equipmentListArray[importCount].mass, script_name); + tempValue = itemNumber * getItemMass(equipmentListArray[importCount].mass, script_name, 2); tempValue = Math.round(10*tempValue)/10; importedEquipment["equipMass"+ID] = tempValue; } else { @@ -999,7 +1002,7 @@ // Get weapon mass. if (weaponsArray[importCount].mass !== "") { tempString = weaponsArray[importCount].mass; - tempValue = getItemMass(tempString, script_name); + tempValue = getItemMass(tempString, script_name, 1); importedWeapons["weaponMass"+ID] = tempValue; // Check for a note on additional copies. @@ -1124,7 +1127,7 @@ // Get armor mass. if (armorArray[importCount].mass !== "") { tempString = armorArray[importCount].mass; - importedArmor["armorMass"+ID] = getItemMass(tempString, script_name); + importedArmor["armorMass"+ID] = getItemMass(tempString, script_name, 1); } else { importedArmor["armorMass"+ID] = 0; } @@ -1185,7 +1188,7 @@ // Get weapon mass. if (multipowerArray[shieldSearchIndex].mass !== "") { tempString = multipowerArray[shieldSearchIndex].mass; - importedShield.shieldMass = getItemMass(tempString, script_name); + importedShield.shieldMass = getItemMass(tempString, script_name, 1); } else { importedShield.shieldMass = 0; } @@ -1977,15 +1980,15 @@ tempString = (powerArray[importCount].text).toLowerCase(); if (theEffect === "Resistant Protection") { - if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if ( (typeof powerArray[importCount].text !== "undefined") && (powerArray[importCount].text !== "") ) { if(verbose) { sendChat(script_name, "Created Resistant Protection armor."); } tempValue = getResistantPD(powerArray[importCount].text, script_name); if (tempValue > 0) { - charMod.armorPD14 += tempValue; - if ( (specialArray.some(v => tempString.includes(v))) != true) { + charMod.armorPD14 = tempValue; + if ( (specialArray.some(v => tempString.includes(v))) !== true) { // We don't want to add overall modifications for special cases. charMod.pdMod += tempValue; } @@ -1993,7 +1996,7 @@ charMod.totalPD14 = tempValue + parseInt(character.pd); pdAddedToTotal = true; } else { - charMod.totalPD14 += tempValue; + charMod.totalPD14 = tempValue; } charMod.armorName14 = importedPowers["powerName"+ID]; charMod.armorLocations14 = "3-18"; @@ -2007,8 +2010,8 @@ tempValue = getResistantED(powerArray[importCount].text, script_name); if (tempValue > 0) { - charMod.armorED14 += tempValue; - if ( (specialArray.some(v => tempString.includes(v))) != true) { + charMod.armorED14 = tempValue; + if ( (specialArray.some(v => tempString.includes(v))) !== true) { // We don't want to add overall modifications for special cases. charMod.edMod += tempValue; } @@ -2016,7 +2019,7 @@ charMod.totalED14 = tempValue + parseInt(character.ed); edAddedToTotal = true; } else { - charMod.totalED14 += tempValue; + charMod.totalED14 = tempValue; } charMod.armorName14 = importedPowers["powerName"+ID]; charMod.armorLocations14 = "3-18"; @@ -2029,15 +2032,15 @@ } } } else if (theEffect === "Base PD Mod") { - if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if ( (typeof powerArray[importCount].text !== "undefined") && (powerArray[importCount].text !== "") ) { if(verbose) { sendChat(script_name, "Added Resistant PD to armor."); } if ( (powerArray[importCount].text).includes("Resistant")) { - charMod.armorPD14 += parseInt(character.pd); + charMod.armorPD14 = parseInt(character.pd); if (!pdAddedToTotal) { - charMod.totalPD14 += parseInt(character.pd); + charMod.totalPD14 = parseInt(character.pd); pdAddedToTotal = true; } charMod.armorName14 = importedPowers["powerName"+ID]; @@ -2051,15 +2054,15 @@ } } } else if (theEffect === "Base ED Mod") { - if ( (typeof powerArray[importCount].text != "undefined") && (powerArray[importCount].text != "") ) { + if ( (typeof powerArray[importCount].text !== "undefined") && (powerArray[importCount].text !== "") ) { if(verbose) { sendChat(script_name, "Added Resistant ED to armor."); } if ( (powerArray[importCount].text).includes("Resistant") ) { - charMod.armorED14 += parseInt(character.ed); + charMod.armorED14 = parseInt(character.ed); if (!edAddedToTotal) { - charMod.totalED14 += parseInt(character.ed); + charMod.totalED14 = parseInt(character.ed); edAddedToTotal = true; } charMod.armorName14 = importedPowers["powerName"+ID]; @@ -4532,13 +4535,14 @@ } - var getItemMass = function(massString, script_name) { - // Remove units from mass and round to one decimal. + var getItemMass = function(massString, script_name, decimalPlaces) { + // Remove units from mass and round to one or two decimal places. let mass = 0; + let roundingFactor = (decimalPlaces === 1) ? 10 : 100; if (massString !== "") { massString = parseFloat(massString.replace(/[^\d.-]/g, "")); - mass = Math.round(10*massString)/10; + mass = Math.round(roundingFactor*massString)/roundingFactor; } return mass; From db14b5559c3e59c2bc41729d5e273111a2e73631 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:46:32 -0700 Subject: [PATCH 15/42] Updated sample characters, README. --- .../2.3/HeroSystem6eHeroic.hde | 1400 ++++++++++++++++- .../2.3/Sample_Character.TXT | 2 +- .../2.3/Sample_Character.hdc | Bin 127314 -> 166000 bytes .../2.3/Sample_Character_MA.TXT | 2 +- .../2.3/Sample_Character_MA.hdc | Bin 108564 -> 148104 bytes HeroSystem6eHeroic_HDImporter/README.MD | 18 +- HeroSystem6eHeroic_HDImporter/script.json | 4 +- 7 files changed, 1413 insertions(+), 13 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde index 60e3a1e554..808d52151b 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde +++ b/HeroSystem6eHeroic_HDImporter/2.3/HeroSystem6eHeroic.hde @@ -1,4 +1,4 @@ -HeroSystem6eHeroic

HeroSystem6eHeroic

Version 2.2

Export format for the Roll20 API script HeroSystem6eHeroic_HDImporter, which imports Hero Designer characters into the HeroSystem6eHeroic character sheet.

For documentation see https://github.com/Roll20/roll20-api-scripts/tree/master/HeroSystem6eHeroic_HDImporter

By Villain In Glasses (Roll20 ID 633423)

+HeroSystem6eHeroic

HeroSystem6eHeroic

Version 2.3

Export format for the Roll20 API script HeroSystem6eHeroic_HDImporter, which imports Hero Designer characters into the HeroSystem6eHeroic character sheet.

For documentation see https://github.com/Roll20/roll20-api-scripts/tree/master/HeroSystem6eHeroic_HDImporter

By Villain In Glasses (Roll20 ID 633423)

txt !hero --import { "character":{ @@ -692,7 +692,1401 @@ "notes":"" - 16} + 16}, + "equipment17":{17 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 17}, + "equipment18":{18 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 18}, + "equipment19":{19 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 19}, + "equipment20":{20 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 20}, + "equipment21":{21 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 21}, + "equipment22":{22 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 22}, + "equipment23":{23 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 23}, + "equipment24":{24 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 24}, + "equipment25":{25 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 25}, + "equipment26":{26 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 26}, + "equipment27":{27 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 27}, + "equipment28":{28 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 28}, + "equipment29":{29 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 29}, + "equipment30":{30 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 30}, + "equipment31":{31 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 31}, + "equipment32":{32 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 32}, + "equipment33":{33 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 33}, + "equipment34":{34 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 34}, + "equipment35":{35 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 35}, + "equipment36":{36 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 36}, + "equipment37":{37 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 37}, + "equipment38":{38 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 38}, + "equipment39":{39 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 39}, + "equipment40":{40 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 40}, + "equipment41":{41 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 41}, + "equipment42":{42 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 42}, + "equipment43":{43 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 43}, + "equipment44":{44 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 44}, + "equipment45":{45 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 45}, + "equipment46":{46 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 46}, + "equipment47":{47 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 47}, + "equipment48":{48 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 48}, + "equipment49":{49 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 49}, + "equipment50":{50 + + + "name":"(Multipower) ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"(MPSlot ", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + + + "name":"", + "text":"", + "damage":"", + "end":"", + "range":"", + "mass":"", + "attack":"true", + "defense":"true", + "notes":"" + + + 50} }, "maneuvers":{ "maneuver01":{ @@ -3518,7 +4912,7 @@ "timeStamp":"", "genre":"", "campaign":"", - "version":"2.2", + "version":"2.3", "HeroSystem6eHeroic":"true" } } diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT index e427167b59..ec1d57c33e 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT +++ b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.TXT @@ -1 +1 @@ - !hero --import { "character":{ "character_name":"Darci", "character_title":"Fae-Cursed", "height":"1.66 m", "weight":"60.00 kg", "eyes":"Brown", "hair":"Brown", "backgroundText":"Darci grew up in a small highland village, the daughter of a village healer, with no ambition save to learn her mother's trade. Her life was turned upside down when she encountered a trol while out collection herbs in the woods. The troll promised to tell her secrets of Fae magic in return for her friendship. Darci has regretted her kindness ever since. Exiled and feard by common folk and given little help by the Fae, Darci has found safety in the service of a mercenary company.", "historyText":"", "appearance":"", "tactics":"", "campaignUse":"", "quote":"Village Herbalist", "experience":"0", "experienceBenefit":"0", "strength":"17", "dexterity":"13", "constitution":"18", "intelligence":"18", "ego":"13", "presence":"10", "ocv":"4", "dcv":"4", "omcv":"3", "dmcv":"3", "speed":"4", "pd":"4", "ed":"3", "body":"14", "stun":"28", "endurance":"40", "recovery":"9", "running":"12", "leaping":"4", "swimming":"6", "equipment":{ "equipment01":{ "name":"Bronze Maille", "text":"Resistant Protection (4 PD/4 ED) (12 Active Points); Normal Mass (-1), OIF (-1/2), Requires A Roll (11- roll; Locations 7-14; -1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"11.40kg", "attack":"", "defense":"true", "notes":"(2 END/turn)" }, "equipment02":{ "name":"Bronze Cap", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.83kg", "attack":"", "defense":"true", "notes":"(Locations 5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"1.20kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"Bronze Battle Axe", "text":"(Total: 46 Active Cost, 16 Real Cost) Killing Attack - Hand-To-Hand 2d6 (3d6 w/STR), Reduced Endurance (0 END; +1/2) (45 Active Points); OAF (-1), STR Min: 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 15) plus (1 Active Points) (Real Cost: 1)", "damage":"2d6 (3d6 w/STR)", "end":"0", "range":"", "mass":"1.60kg", "attack":"true", "defense":"", "notes":"" }, "equipment05":{ "name":"Bronze Dagger", "text":"Killing Attack - Hand-To-Hand 1d6-1 (1d6 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 6 (-1/4)", "damage":"1d6-1 (1d6 w/STR)", "end":"0", "range":"var.", "mass":"0.80kg", "attack":"true", "defense":"", "notes":"" }, "equipment06":{ "name":"Winter Coat", "text":"Life Support (Safe in Intense Cold) (2 Active Points); OIF (-1/2)", "damage":"", "end":"0", "range":"", "mass":"3.30kg", "attack":"", "defense":"", "notes":"" }, "equipment07":{ "name":"(Multipower) Small Shield", "text":"Multipower, 5-point reserve, (5 Active Points); all slots OAF (-1), STR Min 6 (-1/4)", "damage":"", "end":"", "range":"", "mass":"3.00kg", "attack":"", "defense":"", "notes":"" }, "equipment08":{ "name":"(MPSlot1) ", "text":"+1 DCV (5 Active Points); OAF (-1), Real Armor (-1/4), STR Min 6 (-1/4)", "damage":"", "end":"", "range":"", "mass":"", "attack":"", "defense":"", "notes":"" }, "equipment09":{ "name":"(MPSlot2) Bash", "text":"Hand-To-Hand Attack +1d6 (5 Active Points); OAF (-1), Hand-To-Hand Attack (-1/2), Side Effects -1 OCV, Side Effect occurs automatically whenever Power is used (-1/2), Real Weapon (-1/4), STR Min 6 (-1/4)", "damage":"1d6", "end":"1", "range":"", "mass":"", "attack":"true", "defense":"", "notes":"" }, "equipment10":{ "name":"Healing Potion", "text":"Healing BODY 4d6 (40 Active Points); 3 Charges which Never Recover (-3 1/4), OAF Fragile (-1 1/4), Extra Time (Full Phase, -1/2), Gestures (-1/4)", "damage":"4d6", "end":"[3 nr]", "range":"", "mass":"1.30kg", "attack":"", "defense":"", "notes":"" }, "equipment11":{}, "equipment12":{}, "equipment13":{}, "equipment14":{}, "equipment15":{}, "equipment16":{} }, "maneuvers":{ "maneuver01":{ }, "maneuver02":{ }, "maneuver03":{ }, "maneuver04":{ }, "maneuver05":{ }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"1", "text":"Member of a Mercenary Company Fringe Benefit (0 Active Points)", "notes":"Some Perks Notes." }, "perk02":{ "type":"Fringe Benefit", "points":"1", "text":"Low-ranking member of Fae Society Fringe Benefit (0 Active Points)", "notes":"" }, "perk03":{ }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Social Complication", "points":"10", "text":"Social Complication: Regarded as fae-touched and cursed. Frequently, Minor", "notes":"The mortal world tends to distrust anyone or anything touched by Fae." }, "complication02":{ "type":"Hunted", "points":"15", "text":"Hunted: Hunted by agents of Summer. Frequently (Mo Pow; Mildly Punish)", "notes":"While Darci hasn't reached the notoriety that would attract more dangerous agents, Summer won't hesitate to torment her and her companions." }, "complication03":{ "type":"Distinctive Features", "points":"5", "text":"Distinctive Features: Peculiar smell and hard-to-pin-down appearance. Not quite human. Trollish, to those who know of fae. (Easily Concealed; Noticed and Recognizable; Detectable By Commonly-Used Senses)", "notes":"" }, "complication04":{ "type":"Psychological Complication", "points":"20", "text":"Psychological Complication: Finds the touch of iron uncomfortable and won't wear iron armor or jewelry or use iron tools. (Very Common; Strong)", "notes":"" }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Bile and Acid", "base":"15", "text":"Killing Attack - Ranged 1d6, Area Of Effect (4 2m Areas; +1/2), Damage Over Time, Target's defenses only apply once (3 damage increments, damage occurs every four Segments, can be negated by Water; +2 1/2) (60 Active Points); 3 Recoverable Charges (-3/4), Extra Time (Full Phase, -1/2), No Range (-1/2), Gestures (Requires both hands; -1/2), Side Effects (1d6+1d3 drain STUN; -1/4), Concentration (1/2 DCV; -1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work in water; -1/4), Requires A Roll (Skill roll, -1 per 20 Active Points modifier; Magic Roll; -1/4)", "notes":"The effects of this spell aren't pretty, but they get the job done.", "cost":"14", "endurance":"[3 rc]", "damage":"1d6", "compound":"false" }, "power02":{ "name":"Pneuma", "base":"30", "text":"Killing Attack - Ranged 2d6, Invisible Power Effects (Inobvious to [one Sense Group]; +1/4) (37 Active Points); Requires A Roll (Skill roll; -1/2), Gestures (-1/4), Incantations (-1/4), Beam (-1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work under water; -1/4)", "notes":"A pneuma is an invisible dart, which Darci draws from her breath with an exaggerated motion and throws at her target.", "cost":"15", "endurance":"4", "damage":"2d6", "compound":"false" }, "power03":{ "name":"Self Renewal", "base":"55", "text":"Healing BODY 5d6, Can Heal Limbs (55 Active Points); Increased Endurance Cost (x6 END; -2 1/2), Extra Time (1 Turn (Post-Segment 12), Character May Take No Other Actions, -1 1/2), Concentration, Must Concentrate throughout use of Constant Power (0 DCV; Character is totally unaware of nearby events; -1 1/2), OAF (Eat a sprig of evergreen; -1), Gestures (Requires both hands; -1/2), Life Energy Modifier Power loses about a third of its effectiveness (-1/2), Self Only Power loses about a third of its effectiveness (-1/2), Incantations (-1/4), Requires A Roll (Characteristic roll, -1 per 20 Active Points modifier; -1/4)", "notes":"Darci can draw from the regenerative powers of trolls after an intense and painful bout of concentration.", "cost":"6", "endurance":"30", "damage":"5d6", "compound":"false" }, "power04":{ "name":"Underdark Eyes", "base":"5", "text":"Nightvision (5 Active Points); Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"Trolls may be unpleasant creatures, but they can see in the dark.", "cost":"2", "endurance":"0", "damage":"", "compound":"false" }, "power05":{ "name":"Winter's Shawl", "base":"12", "text":"Life Support (Immunity All terrestrial diseases; Immunity: All terrestrial poisons; Safe in Intense Cold) (12 Active Points); Costs Endurance (-1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"The trolls of the Winter Court can survive most any natural storm or plague.", "cost":"5", "endurance":"1", "damage":"", "compound":"false" }, "power06":{ "name":"Fae Sense", "base":"10", "text":"Detect Magic A Class Of Things 13- (no Sense Group), Range (10 Active Points); Increased Endurance Cost (x4 END; -3/4), Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4), Costs Endurance (Only Costs END to Activate; -1/4)", "notes":"The Fae have a knack for spotting ley lines and other magics in their enviornment.", "cost":"3", "endurance":"4", "damage":"13-", "compound":"false" }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"", "enhancer":"", "text":"PS: Soldier 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"", "enhancer":"", "text":"PS: Herbalist 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"", "enhancer":"", "text":"Language: Clan's Tongue (basic conversation; literate) (2 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"0" }, "skill04": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill05": { "name":"", "enhancer":"", "text":"Language: Fae (completely fluent; literate)", "display":"Language", "attribute":"GENERAL", "base":"4", "levels":"0", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"+3 Battleaxe", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill07": { "name":"Fae Society", "enhancer":"", "text":"KS 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"Clan Lands", "enhancer":"", "text":"AK 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill09": { "name":"Common Melee", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"Power Skill Fae Magic", "enhancer":"", "text":"Power 15-", "display":"Power", "attribute":"INT", "base":"7", "levels":"2", "cost":"7" }, "skill11": { "name":"", "enhancer":"", "text":"Stealth 12-", "display":"Stealth", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Concealment 13-", "display":"Concealment", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"", "text":"Science Skill: Herbal Medicine 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill15": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill16": { "name":"Survival", "enhancer":"", "text":"Survival 13-", "display":"Survival", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill17": { }, "skill18": { }, "skill19": { }, "skill20": { }, "skill21": { }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"Test PC", "gmName":"Villain In Glasses", "characterFile":"Sample_Character.hdc", "versionHD":"20220801", "timeStamp":"Sat, 14 Sep 2024 09:56:43", "genre":"Fantasy Hero", "campaign":"Coryn's Company", "version":"2.2", "HeroSystem6eHeroic":"true" } } \ No newline at end of file + !hero --import { "character":{ "character_name":"Darci", "character_title":"Fae-Cursed", "height":"1.66 m", "weight":"60.00 kg", "eyes":"Brown", "hair":"Brown", "backgroundText":"Darci grew up in a small highland village, the daughter of a village healer, with no ambition save to learn her mother's trade. Her life was turned upside down when she encountered a trol while out collection herbs in the woods. The troll promised to tell her secrets of Fae magic in return for her friendship. Darci has regretted her kindness ever since. Exiled and feard by common folk and given little help by the Fae, Darci has found safety in the service of a mercenary company.", "historyText":"", "appearance":"", "tactics":"", "campaignUse":"", "quote":"Village Herbalist", "experience":"0", "experienceBenefit":"0", "strength":"17", "dexterity":"13", "constitution":"18", "intelligence":"18", "ego":"13", "presence":"10", "ocv":"4", "dcv":"4", "omcv":"3", "dmcv":"3", "speed":"4", "pd":"4", "ed":"3", "body":"15", "stun":"26", "endurance":"40", "recovery":"9", "running":"12", "leaping":"6", "swimming":"6", "equipment":{ "equipment01":{ "name":"Bronze Maille", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4), Requires A Roll (12- roll; Locations 7-14; -1/4)", "damage":"", "end":"0", "range":"", "mass":"11.40kg", "attack":"", "defense":"true", "notes":"(1 END/turn)" }, "equipment02":{ "name":"Bronze Cap", "text":"Resistant Protection (6 PD/6 ED) (18 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.56kg", "attack":"", "defense":"true", "notes":"(Locations 5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.42kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"(Multipower) Small Shield", "text":"Multipower, 5-point reserve, (5 Active Points); all slots OAF (-1), STR Min 5 (-1/4)", "damage":"", "end":"", "range":"", "mass":"2.00kg", "attack":"", "defense":"", "notes":"" }, "equipment05":{ "name":"(MPSlot1) ", "text":"+1 DCV (5 Active Points); OAF (-1), Real Armor (-1/4), STR Min 5 (-1/4)", "damage":"", "end":"", "range":"", "mass":"", "attack":"", "defense":"", "notes":"" }, "equipment06":{ "name":"(MPSlot2) Bash", "text":"Hand-To-Hand Attack +1d6 (5 Active Points); OAF (-1), Hand-To-Hand Attack (-1/2), Side Effects -1 OCV, Side Effect occurs automatically whenever Power is used (-1/2), Real Weapon (-1/4), STR Min 5 (-1/4)", "damage":"1d6", "end":"1", "range":"", "mass":"", "attack":"true", "defense":"", "notes":"" }, "equipment07":{ "name":"Bronze Battle Axe", "text":"(Total: 38 Active Cost, 13 Real Cost) Killing Attack - Hand-To-Hand 1 1/2d6 (2 1/2d6 w/STR), Reduced Endurance (0 END; +1/2) (37 Active Points); OAF (-1), STR Min: 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 12) plus (1 Active Points) (Real Cost: 1)", "damage":"1 1/2d6 (2 1/2d6 w/STR)", "end":"0", "range":"", "mass":"1.60kg", "attack":"true", "defense":"", "notes":"" }, "equipment08":{ "name":"Bronze Dagger", "text":"Killing Attack - Hand-To-Hand 1d6-1 (1d6 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 6 (-1/4)", "damage":"1d6-1 (1d6 w/STR)", "end":"0", "range":"var.", "mass":"0.80kg", "attack":"true", "defense":"", "notes":"" }, "equipment09":{ "name":"Small Pack", "text":"+2 STR, Reduced Endurance (0 END; +1/2) (3 Active Points); Limited Power Power loses about two-thirds of its effectiveness (Only for determining pack capacity; -1 1/2), OAF (-1), Real Weapon (-1/4)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"true", "defense":"", "notes":"Holds up to 16 kg." }, "equipment10":{ "name":"Candle", "text":"Sight Group Images, +/-4 to PER Rolls, Reduced Endurance (0 END; +1/2), Area Of Effect (2m Radius; +3/4), Mobile (1m per Phase; +1/2) (49 Active Points); Only To Create Light (-1), OAF (-1), Extra Time (1 Turn (Post-Segment 12), Only to Activate, -3/4), No Range (-1/2), Real Weapon (-1/4), 1 Continuing Fuel Charge lasting 1 Hour (-0)", "damage":"", "end":"[1 cc]", "range":"", "mass":"0.10kg", "attack":"true", "defense":"", "notes":"(x6 number of items)" }, "equipment11":{ "name":"Hat", "text":"Resistant Protection (1 PD/1 ED) (3 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.25kg", "attack":"", "defense":"true", "notes":"(Locations 5)" }, "equipment12":{ "name":"Great Coat", "text":"Change Environment (+2 Temperature Level Adjustment), Reduced Endurance (0 END; +1/2) (9 Active Points); No Range (-1/2), Self Only (-1/2), OIF (-1/2)", "damage":"", "end":"0", "range":"", "mass":"2.00kg", "attack":"true", "defense":"", "notes":"" }, "equipment13":{ "name":"Herbalism Kit", "text":"+1 with Herbalism (2 Active Points); OAF (-1)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"", "defense":"", "notes":"" }, "equipment14":{ "name":"Survival Kit", "text":"+2 with Survival (4 Active Points); OAF (-1)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"", "defense":"", "notes":"" }, "equipment15":{ "name":"Traveling Clothes", "text":"Change Environment (+1 Temperature Level Adjustment), Reduced Endurance (0 END; +1/2) (4 Active Points); OAF (-1), No Range (-1/2), Self Only (-1/2)", "damage":"", "end":"0", "range":"", "mass":"2.00kg", "attack":"true", "defense":"", "notes":"" }, "equipment16":{ "name":"Warm Blanket", "text":"Change Environment (+2 Temperature Level Adjustment), Reduced Endurance (0 END; +1/2) (9 Active Points); OAF (-1), No Range (-1/2), Self Only (-1/2)", "damage":"", "end":"0", "range":"", "mass":"0.50kg", "attack":"true", "defense":"", "notes":"" }, "equipment17":{ "name":"Healing Potion", "text":"Healing BODY 4d6 (40 Active Points); 2 Charges which Never Recover (-3 1/2), OAF Fragile (-1 1/4), Extra Time (Full Phase, -1/2), Side Effects, Side Effect occurs automatically whenever Power is used (Side Effect only affects the recipient of the benefits of the Power; -1/2), Gestures (Drink; -1/4)", "damage":"4d6", "end":"[2 nr]", "range":"", "mass":"0.25kg", "attack":"", "defense":"", "notes":"" }, "equipment18":{}, "equipment19":{}, "equipment20":{}, "equipment21":{}, "equipment22":{}, "equipment23":{}, "equipment24":{}, "equipment25":{}, "equipment26":{}, "equipment27":{}, "equipment28":{}, "equipment29":{}, "equipment30":{}, "equipment31":{}, "equipment32":{}, "equipment33":{}, "equipment34":{}, "equipment35":{}, "equipment36":{}, "equipment37":{}, "equipment38":{}, "equipment39":{}, "equipment40":{}, "equipment41":{}, "equipment42":{}, "equipment43":{}, "equipment44":{}, "equipment45":{}, "equipment46":{}, "equipment47":{}, "equipment48":{}, "equipment49":{}, "equipment50":{} }, "maneuvers":{ "maneuver01":{ }, "maneuver02":{ }, "maneuver03":{ }, "maneuver04":{ }, "maneuver05":{ }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"1", "text":"Member of the Company Fringe Benefit: Membership", "notes":"" }, "perk02":{ "type":"Fringe Benefit", "points":"1", "text":"Low-ranking member of Fae Society Fringe Benefit (0 Active Points)", "notes":"" }, "perk03":{ }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Social Complication", "points":"10", "text":"Social Complication: Regarded as fae-touched and cursed. Frequently, Minor", "notes":"" }, "complication02":{ "type":"Hunted", "points":"15", "text":"Hunted: Hunted by agents of Summer. Frequently (Mo Pow; Mildly Punish)", "notes":"" }, "complication03":{ "type":"Distinctive Features", "points":"5", "text":"Distinctive Features: Peculiar smell and hard-to-pin-down appearance. Not quite human. Trollish, to those who know of fae. (Easily Concealed; Noticed and Recognizable; Detectable By Commonly-Used Senses)", "notes":"" }, "complication04":{ "type":"Psychological Complication", "points":"20", "text":"Psychological Complication: Finds the touch of iron uncomfortable and won't wear iron armor or jewelry or use iron tools. (Very Common; Strong)", "notes":"" }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Bile and Acid", "base":"15", "text":"Killing Attack - Ranged 1d6, Area Of Effect (4 2m Areas; +1/2), Damage Over Time, Target's defenses only apply once (3 damage increments, damage occurs every four Segments, can be negated by Water; +2 1/2) (60 Active Points); 3 Recoverable Charges (-3/4), Extra Time (Full Phase, -1/2), No Range (-1/2), Gestures (Requires both hands; -1/2), Side Effects (1d6+1d3 drain STUN; -1/4), Concentration (1/2 DCV; -1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work in water; -1/4), Requires A Roll (Skill roll, -1 per 20 Active Points modifier; Magic Roll; -1/4)", "notes":"", "cost":"14", "endurance":"[3 rc]", "damage":"1d6", "compound":"false" }, "power02":{ "name":"Pneuma", "base":"30", "text":"Killing Attack - Ranged 2d6, Invisible Power Effects (Inobvious to [one Sense Group]; +1/4) (37 Active Points); Requires A Roll (Skill roll; -1/2), Gestures (-1/4), Incantations (-1/4), Beam (-1/4), Limited Power Power loses about a fourth of its effectiveness (Does not work under water; -1/4)", "notes":"", "cost":"15", "endurance":"4", "damage":"2d6", "compound":"false" }, "power03":{ "name":"Self Renewal", "base":"55", "text":"Healing BODY 5d6, Can Heal Limbs (55 Active Points); Increased Endurance Cost (x6 END; -2 1/2), Extra Time (1 Turn (Post-Segment 12), Character May Take No Other Actions, -1 1/2), Concentration, Must Concentrate throughout use of Constant Power (0 DCV; Character is totally unaware of nearby events; -1 1/2), OAF (Eat a sprig of evergreen; -1), Gestures (Requires both hands; -1/2), Life Energy Modifier Power loses about a third of its effectiveness (-1/2), Self Only Power loses about a third of its effectiveness (-1/2), Incantations (-1/4), Requires A Roll (Characteristic roll, -1 per 20 Active Points modifier; -1/4)", "notes":"", "cost":"6", "endurance":"30", "damage":"5d6", "compound":"false" }, "power04":{ "name":"Underdark Eyes", "base":"5", "text":"Nightvision (5 Active Points); Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"", "cost":"2", "endurance":"0", "damage":"", "compound":"false" }, "power05":{ "name":"Winter's Shawl", "base":"12", "text":"Life Support (Immunity All terrestrial diseases; Immunity: All terrestrial poisons; Safe in Intense Cold) (12 Active Points); Costs Endurance (-1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4)", "notes":"", "cost":"5", "endurance":"1", "damage":"", "compound":"false" }, "power06":{ "name":"Fae Sense", "base":"10", "text":"Detect Magic A Class Of Things 13- (no Sense Group), Range (10 Active Points); Increased Endurance Cost (x4 END; -3/4), Gestures (Requires both hands; -1/2), Requires A Roll (11- roll; -1/2), Incantations (-1/4), Costs Endurance (Only Costs END to Activate; -1/4)", "notes":"", "cost":"3", "endurance":"4", "damage":"13-", "compound":"false" }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"Mercenary", "enhancer":"", "text":"PS 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"Herbalist", "enhancer":"", "text":"PS 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"", "enhancer":"", "text":"Language: Clan's Tongue (idiomatic; literate) (5 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"1" }, "skill04": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill05": { "name":"", "enhancer":"", "text":"Language: Fae (completely fluent; literate)", "display":"Language", "attribute":"GENERAL", "base":"4", "levels":"0", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"+3 Battle Axe", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill07": { "name":"Fae Society", "enhancer":"", "text":"KS 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"Clan Lands", "enhancer":"", "text":"AK 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill09": { "name":"Common Melee", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"Power Skill Fae Magic", "enhancer":"", "text":"Power 15-", "display":"Power", "attribute":"INT", "base":"7", "levels":"2", "cost":"7" }, "skill11": { "name":"", "enhancer":"", "text":"Stealth 12-", "display":"Stealth", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Concealment 13-", "display":"Concealment", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"", "text":"Science Skill: Herbal Medicine 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill15": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill16": { "name":"", "enhancer":"", "text":"Survival (Temperate/Subtropical) 13-", "display":"Survival", "attribute":"INT", "base":"2", "levels":"0", "cost":"2" }, "skill17": { }, "skill18": { }, "skill19": { }, "skill20": { }, "skill21": { }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"PC", "gmName":"Villain In Glasses", "characterFile":"Sample_Character.hdc", "versionHD":"20220801", "timeStamp":"Wed, 13 Nov 2024 13:14:50", "genre":"Fantasy Hero", "campaign":"Coryn's Company", "version":"2.3", "HeroSystem6eHeroic":"true" } } \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc index 165ad6c71153946202343844ff628a18c0be79b7..93ee772548bb55683af82c54698f08f8c5266d2d 100644 GIT binary patch delta 12500 zcmc&)dvui5wg2|S5P3)fAs|U6Gf5CJLYPcu@~{Yb0x?fW5NT_9MnnjS0cyEghFWFy ziUs8;XZb*==q;98QNMNX*ZKl#Z7md&b-8M(52#v{rQT@osxI3rxxanReDlhekhXtx zWhFED&N=(+_x|lY^Iz|#e0t9)SO58^leUo9L)qd4`Gni`GxM~ora=7smlMp=8C5PS z+5HerGVh&Pm?E3+rU~ZBl8vsEfKhDtj8eI8BV~!^+Cs5rR<`*;*C-m@ZLBofjrGQI zqg(h|9}*v3;}c)aD-)-_<`;X%j1jfvQ$&A;&s;mpOXi-dZzl?v-Z>r`S7+21bw;z% zXjB?)cvmf+s+;WawBoMZachiK#u}s3=)g4_@ouH)y=0NtdG%N^uJZGYQlrQS;5UfB z0iz6z;zzUUnNw(5^$)tt{+d~%<(1oMjJ#zWEfk|y<%qAYYO}dDZ(i_uR(h!tl4E}m?=Wcjbre>2j*k;s%_ZIwJs3fR0@(eHJs4=d_yCNe> zEeL8Qpa~TU?<}Ri%i5r{4x{sqj4K6-k>Xh`!mQMwfw+5=dKW zlXs%k0_m&7|-#z2Z0g!t3#v zOK-np;6m5YEWz|Dw{5lQX->+GfTAaQ&W#XzPfnMoKBTeMWoN0-C6;cVGJrAfERR@v zdx80f?QbO#Di@#j=34z9(W5R|dy=N62bJ2(;bnZ6@-=y;mvYU%d;b!9SgnYP=xLNWYkdP*0bFVDNgS8q*ASoO=a1Jr1S_B@hEv=|D;!oEyf zQ#OO8d}M*yzyA?!yuQ9EqUR^sAUtMd34(^5Gq5YwV|otWz-Py#grCZ9Rgv<3=_#X$ z;_x%$&C!P{)RxFW*k@LqS~m|rvn_rt7$fHMB%8<6`nXGjN^`6S%acxHTw>016B9OU zeI5D6^QGUkp37r_qZ})#Sl(=xx%kLMtVMgV6wwcx)SI49Tx7oRi)j~eI0VEOzn&tO zo}dw_?5sW&`N=_QFniv~BPTw};lGV$>zPb_Rf5`7%(~6J@4fDLiE`9jX2yK&>@f#PO2zEs z9=W%j3eEm=FFMlToY4p?PIO=Zp#t}Eo2x&aAI(XPGDr3ir6nU78D-{HInTxak4*`x z^vjBE6rNAUYZHZ6IE?)|VT4UHJA8_hHh$&SZ7@Dd!VYB}o=DrsI!?6ArP8c?{9?+H zOTUI|czc;qBDXeBo%P*hnhVDDb7V53T=-t8lX#qF6^*2_B+XhmR)Pjcuc{1I&(As8 ztSt!BOb&o%iP-v{8;&!Axv)MeAh2;Rjs6>EWAg09D17Fpy-QrOQPNm(_&i*lmr7Xx z+>A69C&3OC6Mi`}n=;djRT?g18raU|)UQhdV#0bnoAyPl!}oA%=A2P3;qi?sh1uQ{ zyIjWJ6IFIN@M3qAvtk=L7NGC}>!_ccxZ>GvBGL13f$S`xOxyjV6idZZ3s_5j*3j=9 zNd~3LC@2ipmosR7vNqtz0F)407gbH-YVFCA`Hz#&`d)yllAN}n$bkArT4R79R?%(!v$MG z9OW#hH)YG(o2gCSvVvYo0{oH7-o~MCtfc8A_iv!@P|@yo>Wn+jAOzO6faBhDjfj09eIoW@BF)R@vjIwnx-jzdg{lstGvxPF^xzraFsZe1yx54kfPnS9{QG|e0az5sg z_1h@VVT(%Ii>)`d(dQ(uK1@vjol!?91ylU1Q7rsfo;ys_rRTb^AXLnz%E}Eu4D3>( zu)CIneMZ!!N(>-l31fB0JCD+%&h(Y{+ofS2O1t&EeCQbEIPc~nF|HLo#6sZjMnKl} zcnw--w$VwF^L|ZDF~D9603miwtrst6=E(25H1pHs+pp4XlN`l`dAfS2Sae=~txP>e zKXGP5ff;s%E|=qmV)kvh^8~FE{prf`L*HrP&To(-DW6@fR6UR{oS@le&wsu};{JQ5 z##q(1m!EN%wf7|b)|s=S2>usYyCzzvU#EwiH>ygw*!tiU++BX~11gBQjKi0IN|l&% z{*_jU`ia`X37H9gVSWED`Yp*rdk_JAKca=`A4ibruNW^+@26*EXa`LfJ(ZbPw;*O0 zA!?hACdBW2G*=DAT-7W(w^;0G94)2nWWu+{%_%!Dawh)uqHiqW1}4KLJr7c$2;EsF zkME$ouv+b-T(yggB9&HqwiH_RJ5lo-_XHArQ+EE29+jOB(o}is$KlwIry5}Yw5~ru z`-pk?HW%n=yYEg!dV^XHIkVmUz=biLR`P_Y!=^#oFX*1kDlt>ie5kFpifL!9xscCS7 zID(vIwU_>R2D|*e^AzOS3jU%9i_YsuiO}K9^o_Yxff5qDXN>1Ki3(X z!S@YV@5bvYi~}~RkwAyauAE_4<9V0X2Hvg2JN@4p?A$DTx4sY!vjNPZ*W`YiqYMsn z=uh*-(lHa|={G4esm^E=v*%BWa~^JgxXP8QG^OxZN}V#w>Rn&B6w+Da2EZ zGVD`$NUvS1GN~R;VMV50Ot*$7ITIb2LeCLqiX&(Lrp+{(2VFe0Lw06;jg0&+&rGI# z{JR{!vc}jXmVJ0y%-QZuX#_h}MxE%|p5bsqVAsJ?RSrfrLb4S&xz60W?Q*gOYY}Uo z&Pi6xi?Ii@(pkVnA$s4Z-_&xYXfT#Q?&gTvc|6w;v1cBX=`@<-nAblgc4y$bEE)lA z;L8h=-SB>%JhtP%2Ji`YkS&GuhUt0MOY(s~Qg#ByV47v3E7vWb_>g8x%%0vC-~D$l z>^1wYd_dYWXXvX-*jP4Q~B?vE5gObJ^{E}Kqn^LRb%(flwyD>`4O5hw1;alBg%hwARX$Q7;Gy1~+}!0d$w zPde3FNVQX9tB}1fjO!!C@6exavs(b2^?16*y)w5p-dRC4QD^vHCANFQrMX05|9r;& zLHq!Qs8{awFo4F7E@ddw3O2*W@I;aJM5p}W2Aat6Ga?BYM(oR+BwyM<#~^Xy-5Rt+ zTAajXHA)NjH~t%h4w!eQsZjcW0FE!>xLXcM3}|KB#GYjJ(`0cfEp zOJGFbWx04kn(;lP4xrsA- z#0{9{_zP!i(R*h`3_j8wOWE$747*Y%D)R-eRKPNEfR8{6r-yb3(XQY^CDP$dh@K5F z*xl{9R_e1TRsTq)PIPE)3(HU#!`?c=^xR+QD&~sAka;-q6sEwFYMu{?*Up~RNo@dz z)4EWxfpHkOns>Qm&yE3I8B;{ds(@%1ORFlsLhN0dol*~NHy91#n@#SRY{y}uTX~-M z2USrJBVQULK5LqSDZ=xrPaZaA%NH{%(!`I}WXq$IDN{86&j|6vnhB!vE^pQ?a4!Zt zoE-V1mlg1hb$GVH=-`SzdB9WbtmiZ0o|Y#>=m%31v2B!d)kQ@T-){9GSY1bK&&t8= zV|!s97l%q;?kEszrfOGDLi^7dA z9T=qh6-k;e+$2 z+Kizv&ve?~rIw@t>+FMi&cUBQOMJ{wpgw@+Cg5z+Vxw6h^)`dwp&v(8<7Pm%en?XX z&|>h%pQ5_0gCA0k^Iau+^o!4^kJw`)2D6%3^+`?SV^(}tGoMD*!23CQVvO+&9TxK5 z=)^jDh$rzH5oGN;pHyjFBeI1@z0MYwCoMN_5;uxFSxfiT+c2XNkYg^OM>XM zoAgfXjPBqsGpxt&4P3_6*_d_H`+kw`H%-sG<4M-O?qCI_#9gWC+XnW8w9O^^Kc&F+ zUfeVb!Y%@;sfE~H+_fCZrCqh)t+1S1aKQwi1T$pEX@{?>_J)6;OL9YJ-L@ouB=J)9B35G(qG z8+*lPEXc0}H!K_9y##V?2;bfYjw(T?UU9TZ~Zb#=!|{jq40{Aq@=NJ2>5z5X`xF zSc+v&iQPFk(OP2x%*9F9ZpQ6_pq`uAk;!flVozYV9{kYXbI~IBOkE^dO&R4G$=i=Jy;?tY>_<(yk zM_NNz^*X&q)1OY1wK&(JqJ&wgK@#BD71?5aTsi>ha<`rD779V~f>W38aXRH?%w)&~>z3GXEtAssTdat3-G!x6jU z*hxEpttF+Bb1S7e^4!Q{8 z0dy00XsV&1$!d-9`B}kvNvnDjS?<)*lh3B){O2A46!#5acs>*Fn(_c7Uda^=2)YxR>Tv# zkGA(bV=3M-#$?rV{mN6Nt0wMwnR?1~g|mF@2^%$QCyrg=PYgp$j6r-8ess7cZs8s5 zEZ4=6<-?j04%XN+_Jz8@#jebU3jnVLaP zXp9?6$!4UPVX)IBHG^KSE2k4NPI>W~(6-ktiPH(}_r*z~C9Hzi*C1m$9hoVoe@0Wu F_S$cs%VA=(gi z#!|JJ#anaHW@xZJ@G%5+;7sPOu)xT&>-!b!ZJ*7=Kr5mD;0P4L&z$_1bEF z5S*zy@VP~6*P3y>6Q4Wyi{PJs53VZHHfWXni3LmeIXE?+=sJ+Xr9Z_stP)2bhcx|i z{yN4lIns3BYTrEWd3CYg9Sk!*^wT`uQB(n<`VLrlOQqI^o7ZbixItLYZAt+CPN8Q} zNb}*R5I=tWdhuJJ74hv|o8K$WlPlBo<#+S~?;CYLzhNaGNj@!HiI8&BI&CdV_1T@y z$)`m;r8H%JEy~}4OzL>~#$WQ^mfqll!^!jfT0jeF9x-kQCvBy#+4>UWr+((vht@l8 zh8q2YyKi^$h$DG^5%Le=*QZ0v0T87DmcsLzmh(q;z0aG&T}YF}Qy#O;BdI-FfaiXi z$)nkB{!&*!T&RK5+|yaTs77;X<#bN14u4yvst@oNI=#zEQ1xx7=mw;ykQ94RT$i>D zN2S^|x7b^cLPp-Q^KIQJd?36j&QssgwehBdCE5-&9<6WCcHyT1d34G>+VIVV&yV8q zY0nNcX9McI8K0H0TeaKBbU03?@5IjmEg#=&wS97&);8dnOKX(#YB%Dw8|ACT|N2CH zXvIk>=3V0c|G?r~eOeH$=O=xjR!Dn6@%4RBpwO|V=Vd0%Je?@01<}lz${J6svLYcQ zjHl#?=-04HKl7A3!EzffPu&)y+4idk=b{RR&4CH+o%P_lH5S;8w^F=Q~MaF=PXq?l_aqc=hWOR+PeMeu@8G_)Na$lBf>jf33tl2DoOqa6c{~$dh{z(*~^zpBg3jX&Z9LdK_<& z&eix9%!!nzJpY}IAAKiF>YRL~03%%y`U`4Y^NH=(AjEsU zX?n)HYZQXbG$EucLa-GO3GX_&Qor1;CE%ENOrB$?iIa=@BI0i6%^d-R_3!@!3= z+{Ak)R#-~rNB_&zFAVQs3kcmNq4@YKmHN|v&QoYJP0)$>e{Y)xj;sU;j2Tn{D$(J_ z(wIjdIi3vs)Y%kXaWIjOy^*A!I{QNcg2Zgvx|o&XCU#-CiyK%4E|)Yfg)WLv%7RS@ zBLYvmw3Nz!E__y@lGamzq;Oe-V`Omxip0YXi9#A`MFQG{hRO8p_(TZZg>jRjYb|c0 zBBX;=ddd(lc>?2lxzh+$b&U4d2HebKHz_niZbjUGrcTaTbm z&Y@twU%pWTM(QOc)=P~e_;G~VGIE44V&3d7s@9MD1*N|ONJR}sD_V_qqQpmKn5vX& zlcmuWaHevdcrpMPsqqX`B;!jE761FAPI2Wbc#Q7@ur9%Z;BdYyi;N?M;Gv+oQ}R~j zpt&|6gm$1zs=cA+rp6?df)lwuA&;~|O(-=#3Tmo$BMnhHCk@evO5D_3sR}TB5ErQ` zlnzhur3DJus_39TjQXic9B+|WC5Y3;anVRo)Si-A$P7w_>X$;0Libc58Lv2K-BQ|& zN(<`KX+l+`9WiGR(L12n6st+iRAXXVk5?2%cL}UCr%f}>v7?#dn+MPsY#n4+6ZxoP zGs-r=Xr`^AcNm@#6Lom*Z|Wc{9`Atgf_Mc@IzvX{;p>n~9jDvku*x(cSsEFxMXYD~In;Ea ztW0?1cp!eVen5Vz&Z`2{gxZObn;8bPJMTtdR5=#ZSQzW>SL`iI3lxBPu?EVzky* zAfijz8gcyTqP{Yz$QfqG}DBJ}Ilo?V@)H zrkO+o&d;2tomI-N>H~+d{xpQ`rn3=<=*|RNH+7dJ@C5gWs0~ceGd7)p&&^0xs{&&D z9u|wCT_3`mW{kur(4A!Aj2I!|2SB*KhQ3++1K4+L?~?lImD!3Qq++&mjo5a4`^Qe> z!Z|o@#i~#qVZ>n*?bje(3_2m(i2fbUGLwtN1*g0!FA$_s(!PLgOr-Qf1{H+@i^4o! zJ!)G(hNF6vei4{5!gyXy7w_93U%Z`RR#9#Uy;EI3`6OJZBK6=(dXqOZBJ~+)x!F`i zil1k(Pp~r=i4Mi@X2p-Zq=2`L=EQ5Uml>%eokza4-(n&`SGv>A%E>*04E-)!1P;MA zqC&yxHk0lMiGH%DqsAnkk1aBpHYbR3lOxcQAj(XaXtY?3gvkB&ir!~n&}!q!*2Km5 z*bBSG<=;SdOiYsiBKHL3VDID;k01mgXr!jIFYtCVGKaL=y9#*4V`<{s2<}##J|%6o z7&{DIODI;*$xJM8#BUsY9{d=9EY43pFy-TsYK)6NyZ}`ev_ZZ%OL0Hh4<-D;o&PU) zLEMdjBe2p^1X<_`+S3^N2rdI%#x1wvul!}Gf0J=}01lyDrO{1CmDd}1i_D|_i;eF6 zYzGtV2f!_^=m_80hYwP>xzmWg3=5f9@es?3(NK`3wx6zouv&df-It2Nhu8{!DQoJ~ z88SX+5;%gWbp>fkx#SX*-!Td-V0(??hgnJd(d=-HmF$n72;xrD_3tUYFI5A%Q7)E8UCsOBGLB ajIK*nE>`8&KvZnTgfM!5Wr>PIEa!h`-q{KO diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT index ea2ecdf407..6c1c4380ed 100644 --- a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT +++ b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.TXT @@ -1 +1 @@ - !hero --import { "character":{ "character_name":"Henkle", "character_title":"Clan Doctor", "height":"1.76 m", "weight":"87.00 kg", "eyes":"Green", "hair":"Sandy", "backgroundText":"Henkle is a learned man, trained as a physician by the best court instructors and an expert swordsman. He once had a promising future serving clan royalty. It all fell to pieces when he misinterpreted a joke and subsequently dug himself into a ever deepening hole. Lucky to be alive, he found himself banished. The Company scooped him up after a particularly self destructive drinking binge.", "historyText":"", "appearance":"Like many a clansman, Henkle is not small and his lack of social awareness makes for an intimidating block of a man.", "tactics":"", "campaignUse":"As part of his education, Henkle dabbled in Wizardy and can cast a couple of spells, including a minor healing spell and a light spell.", "quote":"Banished aristocrat", "experience":"0", "experienceBenefit":"0", "strength":"18", "dexterity":"15", "constitution":"13", "intelligence":"15", "ego":"14", "presence":"15", "ocv":"5", "dcv":"4", "omcv":"3", "dmcv":"4", "speed":"3", "pd":"4", "ed":"4", "body":"15", "stun":"36", "endurance":"40", "recovery":"7", "running":"12", "leaping":"2", "swimming":"0", "equipment":{ "equipment01":{ "name":"Light Maille", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4), Requires A Roll (12- roll; Locations 9-14; -1/4)", "damage":"", "end":"0", "range":"", "mass":"10.20kg", "attack":"", "defense":"true", "notes":"(1 END/turn)" }, "equipment02":{ "name":"Open-face Helm", "text":"Resistant Protection (6 PD/6 ED) (18 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"0.83kg", "attack":"", "defense":"true", "notes":"(Locations 4-5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"1.20kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"Arming Sword", "text":"(Total: 31 Active Cost, 12 Real Cost) Killing Attack - Hand-To-Hand 1d6+1 (1 1/2d6 w/STR), Reduced Endurance (0 END; +1/2) (30 Active Points); OAF (-1), STR Minimum 12 (-1/2), Real Weapon (-1/4) (Real Cost: 11) plus (1 Active Points) (Real Cost: 1)", "damage":"1d6+1 (1 1/2d6 w/STR)", "end":"0", "range":"", "mass":"1.20kg", "attack":"true", "defense":"", "notes":"" }, "equipment05":{ "name":"Long Sword", "text":"(Total: 38 Active Cost, 13 Real Cost) Killing Attack - Hand-To-Hand 1 1/2d6 (2d6 w/STR), Reduced Endurance (0 END; +1/2) (37 Active Points); OAF (-1), STR Minimum 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 12) plus (1 Active Points) (Real Cost: 1)", "damage":"1 1/2d6 (2d6 w/STR)", "end":"0", "range":"", "mass":"1.70kg", "attack":"true", "defense":"", "notes":"" }, "equipment06":{ "name":"Knife", "text":"(Total: 18 Active Cost, 8 Real Cost) Killing Attack - Hand-To-Hand 1/2d6 (1d6+1 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 4 (-1/4) (Real Cost: 7) plus (1 Active Points) (Real Cost: 1)", "damage":"1/2d6 (1d6+1 w/STR)", "end":"0", "range":"", "mass":"0.40kg", "attack":"true", "defense":"", "notes":"" }, "equipment07":{}, "equipment08":{}, "equipment09":{}, "equipment10":{}, "equipment11":{}, "equipment12":{}, "equipment13":{}, "equipment14":{}, "equipment15":{}, "equipment16":{} }, "maneuvers":{ "maneuver01":{ "name":"Slash", "points":"4", "phase":"1/2", "ocv":"+0", "dcv":"+2", "effect":"Weapon +2 DC Strike", "notes":"" }, "maneuver02":{ "name":"Parry", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Block, Abort", "notes":"" }, "maneuver03":{ "name":"Counterstrike", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Weapon +2 DC Strike, Must Follow Block", "notes":"" }, "maneuver04":{ "name":"Half-Sword Disarm", "points":"4", "phase":"1/2", "ocv":"-1", "dcv":"+1", "effect":"Disarm, 28 STR to Disarm roll, Requires Both Hands", "notes":"" }, "maneuver05":{ "name":"Half-Sword Trip", "points":"3", "phase":"1/2", "ocv":"+2", "dcv":"+0", "effect":"Weapon Strike, Target Falls, Requires Both Hands", "notes":"" }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ "name":"Weapon Element: Blades", "points":"0", "phase":"", "ocv":"", "dcv":"", "effect":"", "notes":"" }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"2", "text":"Fringe Benefit: Sergeant", "notes":"" }, "perk02":{ "type":"Positive Reputation", "points":"3", "text":"Positive Reputation: Brillaint Doctor (A medium-sized group) 11-, +3/+3d6", "notes":"" }, "perk03":{ "type":"Fringe Benefit", "points":"1", "text":"Company Soldier Fringe Benefit: Membership", "notes":"" }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Hunted", "points":"15", "text":"Hunted: King's Church Frequently (Mo Pow; NCI; Watching)", "notes":"Henkle's overt interest in science and medicine as well as magic as it relates to the healing arts makes the Church unhappy." }, "complication02":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Airhead (Common; Moderate)", "notes":"Everything seems to go over Henkle's head. He has trouble understanding jokes, ruins the punchlines of his own jokes, and is generally the last to catch on. He is far from stupid, but sometimes you wonder." }, "complication03":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Aristocratic Attitude (Common; Moderate)", "notes":"Henkle is your classic snob. He was educated by elite teachers to respect every rule of court society. He knows how to behave and how to address each person according to their rank. Obviously, this doesn't work well with commoners and he frequently turns people off." }, "complication04":{ "type":"Physical Complication", "points":"15", "text":"Physical Complication: Horrible Hangovers (Infrequently; Greatly Impairing)", "notes":"After a night of drinking, Henkle is a mess. He suffers a -4 to all rolls on the following morning." }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Reknit Flesh", "base":"20", "text":"Healing BODY 2d6 (20 Active Points); Increased Endurance Cost (x5 END; -2), Extra Time (1 Turn (Post-Segment 12), -1 1/4), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), Incantations (-1/4), IIF Expendable (Herbal Ointment; Easy to obtain new Focus; -1/4)", "notes":"Magical energies, if one understands them well enough, can be set to stitching a wound or coaxing the body to more rapidly repair a hematoma or other moderate trauma.", "cost":"3", "endurance":"10", "damage":"2d6", "compound":"false" }, "power02":{ "name":"Light", "base":"22", "text":"Sight Group Images, +/-4 to PER Rolls, Area Of Effect (4m Radius; +1/4) (27 Active Points); Only To Create Light (-1), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), IIF Expendable (Difficult to obtain new Focus; Charcoal coated in saltpeter; -1/2), Incantations (-1/4), Extra Time (Full Phase, Only to Activate, -1/4), 2 Continuing Charges lasting 1 Hour each (-0)", "notes":"If magic ever had a use it would be to enable one continue study late into the night.", "cost":"7", "endurance":"[2 cc]", "damage":"", "compound":"false" }, "power03":{ }, "power04":{ }, "power05":{ }, "power06":{ }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"", "enhancer":"", "text":"PS: Soldier 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"", "enhancer":"", "text":"PS: Doctor 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"Power Skill Wizardry", "enhancer":"", "text":": Wizardry 12-", "display":"Power", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill04": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"5", "levels":"1", "cost":"5" }, "skill05": { "name":"", "enhancer":"", "text":"Science Skill: Medicine 13-", "display":"Science Skill", "attribute":"GENERAL", "base":"4", "levels":"2", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"Science Skill: Anatomy 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill07": { "name":"", "enhancer":"", "text":"KS: Herbalism 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"", "enhancer":"", "text":"High Society 12-", "display":"High Society", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill09": { "name":"", "enhancer":"", "text":"KS: Popular Literature 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"", "enhancer":"", "text":"CuK: Popular Entertainment 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill11": { "name":"", "enhancer":"", "text":"Conversation 12-", "display":"Conversation", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Tactics 12-", "display":"Tactics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"true", "text":"Linguist", "display":"Linguist", "attribute":"", "base":"3", "levels":"0", "cost":"3" }, "skill15": { "name":"", "enhancer":"", "text":"Language: Ancient Elven (basic conversation; literate) (2 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"1" }, "skill16": { "name":"", "enhancer":"", "text":"Language: Clans' Tongue (idiomatic; literate) (5 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"0" }, "skill17": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill18": { "name":"", "enhancer":"", "text":"Language: Southern Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill19": { "name":"", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill20": { "name":"", "enhancer":"", "text":"+3 Long Sword", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill21": { "name":"", "enhancer":"", "text":"Defense Maneuver I-II ", "display":"Defense Maneuver", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"5" }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"Test PC #2", "gmName":"Villain in Glasses", "characterFile":"Sample_Character_MA.hdc", "versionHD":"20220801", "timeStamp":"Sat, 14 Sep 2024 09:56:01", "genre":"Fantasy HERO", "campaign":"Coryn's Company", "version":"2.2", "HeroSystem6eHeroic":"true" } } \ No newline at end of file + !hero --import { "character":{ "character_name":"Henkle", "character_title":"Clan Doctor", "height":"1.76 m", "weight":"87.00 kg", "eyes":"Green", "hair":"Sandy", "backgroundText":"Henkle is a learned man, trained as a physician by the best court instructors and an expert swordsman. He once had a promising future serving clan royalty. It all fell to pieces when he misinterpreted a joke and subsequently dug himself into a ever deepening hole. Lucky to be alive, he found himself banished. The Company scooped him up after a particularly self destructive drinking binge.", "historyText":"", "appearance":"Like many a clansman, Henkle is not small and his lack of social awareness makes for an intimidating block of a man.", "tactics":"", "campaignUse":"As part of his education, Henkle dabbled in Wizardy and can cast a couple of spells, including a minor healing spell and a light spell.", "quote":"Banished aristocrat", "experience":"0", "experienceBenefit":"0", "strength":"18", "dexterity":"15", "constitution":"13", "intelligence":"15", "ego":"14", "presence":"15", "ocv":"5", "dcv":"4", "omcv":"3", "dmcv":"4", "speed":"3", "pd":"4", "ed":"4", "body":"15", "stun":"36", "endurance":"40", "recovery":"7", "running":"12", "leaping":"4", "swimming":"0", "equipment":{ "equipment01":{ "name":"Light Maille", "text":"Resistant Protection (5 PD/5 ED) (15 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4), Requires A Roll (12- roll; Locations 7-14; -1/4)", "damage":"", "end":"0", "range":"", "mass":"11.40kg", "attack":"", "defense":"true", "notes":"(1 END/turn)" }, "equipment02":{ "name":"Open-face Helm", "text":"Resistant Protection (7 PD/7 ED) (21 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"1.17kg", "attack":"", "defense":"true", "notes":"(Locations 4-5)" }, "equipment03":{ "name":"High Boots, Gloves", "text":"Resistant Protection (2 PD/2 ED) (6 Active Points); Normal Mass (-1), OIF (-1/2), Real Armor (-1/4)", "damage":"", "end":"0", "range":"", "mass":"2.60kg", "attack":"", "defense":"true", "notes":"(Locations 16-18, 6-7)" }, "equipment04":{ "name":"Arming Sword", "text":"(Total: 31 Active Cost, 12 Real Cost) Killing Attack - Hand-To-Hand 1d6+1 (1 1/2d6 w/STR), Reduced Endurance (0 END; +1/2) (30 Active Points); OAF (-1), STR Minimum 12 (-1/2), Real Weapon (-1/4) (Real Cost: 11) plus (1 Active Points) (Real Cost: 1)", "damage":"1d6+1 (1 1/2d6 w/STR)", "end":"0", "range":"", "mass":"1.20kg", "attack":"true", "defense":"", "notes":"" }, "equipment05":{ "name":"Long Sword", "text":"(Total: 38 Active Cost, 13 Real Cost) Killing Attack - Hand-To-Hand 1 1/2d6 (2d6 w/STR), Reduced Endurance (0 END; +1/2) (37 Active Points); OAF (-1), STR Minimum 13 (-1/2), Real Weapon (-1/4), Required Hands One-And-A-Half-Handed (-1/4) (Real Cost: 12) plus (1 Active Points) (Real Cost: 1)", "damage":"1 1/2d6 (2d6 w/STR)", "end":"0", "range":"", "mass":"1.70kg", "attack":"true", "defense":"", "notes":"" }, "equipment06":{ "name":"Knife", "text":"Killing Attack - Hand-To-Hand 1/2d6 (1d6+1 w/STR), Range Based On STR (+1/4), Reduced Endurance (0 END; +1/2) (17 Active Points); OAF (-1), Real Weapon (-1/4), STR Minimum 4 (-1/4)", "damage":"1/2d6 (1d6+1 w/STR)", "end":"0", "range":"var.", "mass":"0.40kg", "attack":"true", "defense":"", "notes":"(x2 number of items)" }, "equipment07":{ "name":"Small Pack", "text":"+2 STR, Reduced Endurance (0 END; +1/2) (3 Active Points); Limited Power Power loses about two-thirds of its effectiveness (Only for determining pack capacity; -1 1/2), OAF (-1), Real Weapon (-1/4)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"true", "defense":"", "notes":"Holds up to 16 kg." }, "equipment08":{ "name":"Cook Set", "text":"+1 with PS Cook (2 Active Points); OAF (-1)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"", "defense":"", "notes":"" }, "equipment09":{ "name":"Healer's Bag", "text":"+1 Paramedics (2 Active Points); OAF (-1)", "damage":"", "end":"", "range":"", "mass":"0.50kg", "attack":"", "defense":"", "notes":"" }, "equipment10":{ "name":"Magnifying Glass", "text":"Microscopic ( x10) with Sight Group (5 Active Points); OAF (-1)", "damage":" x10", "end":"0", "range":"", "mass":"0.25kg", "attack":"", "defense":"", "notes":"" }, "equipment11":{ "name":"Lantern", "text":"Sight Group Images, +/-4 to PER Rolls, Reduced Endurance (0 END; +1/2), Area Of Effect (8m Radius; +1), Mobile (1m per Phase; +1/2) (55 Active Points); Only To Create Light (-1), OAF (-1), Extra Time (1 Minute, Only to Activate, -3/4), No Range (-1/2), Real Weapon (-1/4), 1 Continuing Fuel Charge lasting 6 Hours (-0), Required Hands One-Handed (-0)", "damage":"", "end":"[1 cc]", "range":"", "mass":"1.00kg", "attack":"true", "defense":"", "notes":"" }, "equipment12":{ "name":"Tinder Box", "text":"Major Transform 1d6 (Dry kindling into kindling on fire, Dowse with water or smother), Sticky (Can spread to flammables; +1/2) (15 Active Points); OAF (-1), 8 Charges (Recovers Under Limited Circumstances; -1/2), No Range (-1/2), Gestures (Requires both hands; -1/2), Requires A Roll (Characteristic roll; INT or DEX; -1/2), Real Weapon (-1/4)", "damage":"1d6", "end":"[8]", "range":"", "mass":"0.20kg", "attack":"true", "defense":"", "notes":"Equipment" }, "equipment13":{ "name":"Warm Blanket", "text":"Change Environment (+2 Temperature Level Adjustment), Reduced Endurance (0 END; +1/2) (9 Active Points); OAF (-1), No Range (-1/2), Self Only (-1/2)", "damage":"", "end":"0", "range":"", "mass":"0.50kg", "attack":"true", "defense":"", "notes":"" }, "equipment14":{ "name":"Water Flask", "text":"+1 with Survival (2 Active Points); OAF (-1), 1 Continuing Fuel Charge lasting 1 Day (-0)", "damage":"", "end":"", "range":"", "mass":"1.00kg", "attack":"", "defense":"", "notes":"" }, "equipment15":{}, "equipment16":{}, "equipment17":{}, "equipment18":{}, "equipment19":{}, "equipment20":{}, "equipment21":{}, "equipment22":{}, "equipment23":{}, "equipment24":{}, "equipment25":{}, "equipment26":{}, "equipment27":{}, "equipment28":{}, "equipment29":{}, "equipment30":{}, "equipment31":{}, "equipment32":{}, "equipment33":{}, "equipment34":{}, "equipment35":{}, "equipment36":{}, "equipment37":{}, "equipment38":{}, "equipment39":{}, "equipment40":{}, "equipment41":{}, "equipment42":{}, "equipment43":{}, "equipment44":{}, "equipment45":{}, "equipment46":{}, "equipment47":{}, "equipment48":{}, "equipment49":{}, "equipment50":{} }, "maneuvers":{ "maneuver01":{ "name":"Slash", "points":"4", "phase":"1/2", "ocv":"+0", "dcv":"+2", "effect":"Weapon +2 DC Strike", "notes":"" }, "maneuver02":{ "name":"Parry", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Block, Abort", "notes":"" }, "maneuver03":{ "name":"Counterstrike", "points":"4", "phase":"1/2", "ocv":"+2", "dcv":"+2", "effect":"Weapon +2 DC Strike, Must Follow Block", "notes":"" }, "maneuver04":{ "name":"Half-Sword Disarm", "points":"4", "phase":"1/2", "ocv":"-1", "dcv":"+1", "effect":"Disarm, 28 STR to Disarm roll, Requires Both Hands", "notes":"" }, "maneuver05":{ "name":"Half-Sword Trip", "points":"3", "phase":"1/2", "ocv":"+2", "dcv":"+0", "effect":"Weapon Strike, Target Falls, Requires Both Hands", "notes":"" }, "maneuver06":{ }, "maneuver07":{ }, "maneuver08":{ }, "maneuver09":{ }, "maneuver10":{ }, "maneuver11":{ }, "maneuver12":{ }, "maneuver13":{ }, "maneuver14":{ }, "maneuver15":{ }, "maneuver16":{ }, "maneuver17":{ }, "maneuver18":{ "name":"Weapon Element: Blades", "points":"0", "phase":"", "ocv":"", "dcv":"", "effect":"", "notes":"" }, "maneuver19":{ }, "maneuver20":{ } }, "perks":{ "perk01":{ "type":"Fringe Benefit", "points":"2", "text":"Fringe Benefit: Sergeant", "notes":"" }, "perk02":{ "type":"Positive Reputation", "points":"3", "text":"Positive Reputation: Brillaint Doctor (A medium-sized group) 11-, +3/+3d6", "notes":"" }, "perk03":{ "type":"Fringe Benefit", "points":"1", "text":"Company Soldier Fringe Benefit: Membership", "notes":"" }, "perk04":{ }, "perk05":{ }, "perk06":{ }, "perk07":{ }, "perk08":{ }, "perk09":{ }, "perk10":{ } }, "talents":{}, "complications":{ "complication01":{ "type":"Hunted", "points":"15", "text":"Hunted: King's Church Frequently (Mo Pow; NCI; Watching)", "notes":"" }, "complication02":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Airhead (Common; Moderate)", "notes":"This character is not truly stupid, but rather a bit slower on the uptake than the average person. The character needs to have jokes explained, doesn't understand situations that call for subtlety and wit, and tends to take sarcasm literally. While this condition is hardly debilitating, it does frequently cause the character to be the target of jokes, and causes a certain skepticism regarding the character's intelligence." }, "complication03":{ "type":"Psychological Complication", "points":"10", "text":"Psychological Complication: Aristocratic Attitude (Common; Moderate)", "notes":"This character speaks and acts as if he were royalty. He is stand-offish but polite, proper at all times, speaks impeccably, and expects his commands to be followed. To other characters, he is obviously stuck up and feels is 'too good' for other people." }, "complication04":{ "type":"Physical Complication", "points":"15", "text":"Physical Complication: Horrible Hangovers (Infrequently; Greatly Impairing)", "notes":"Pounding headaches, nausea, and light sensitivity. After waking up from a night of drinking the character suffers a -4 penalty to all rolls for 6 hours." }, "complication05":{}, "complication06":{}, "complication07":{}, "complication08":{}, "complication09":{}, "complication10":{}, "complication11":{}, "complication12":{}, "complication13":{}, "complication14":{}, "complication15":{}, "complication16":{}, "complication17":{}, "complication18":{}, "complication19":{}, "complication20":{} }, "powers":{ "power01":{ "name":"Reknit Flesh", "base":"20", "text":"Healing BODY 2d6 (20 Active Points); Increased Endurance Cost (x5 END; -2), Extra Time (1 Turn (Post-Segment 12), -1 1/4), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), Incantations (-1/4), IIF Expendable (Herbal Ointment; Easy to obtain new Focus; -1/4)", "notes":"Magical energies, if one understands them well enough, can be set to stitching a wound or coaxing the body to more rapidly repair the trauma of a hematoma.", "cost":"3", "endurance":"10", "damage":"2d6", "compound":"false" }, "power02":{ "name":"Light", "base":"22", "text":"Sight Group Images, +/-4 to PER Rolls, Area Of Effect (4m Radius; +1/4) (27 Active Points); Only To Create Light (-1), Gestures (Requires both hands; -1/2), Requires A Roll (Wizardry; -1/2), IIF Expendable (Difficult to obtain new Focus; Charcoal coated in saltpeter; -1/2), Incantations (-1/4), Extra Time (Full Phase, Only to Activate, -1/4), 2 Continuing Charges lasting 1 Hour each (-0)", "notes":"If magic ever had a use it would be to enable one continue study late into the night.", "cost":"7", "endurance":"[2 cc]", "damage":"", "compound":"false" }, "power03":{ }, "power04":{ }, "power05":{ }, "power06":{ }, "power07":{ }, "power08":{ }, "power09":{ }, "power10":{ }, "power11":{ }, "power12":{ }, "power13":{ }, "power14":{ }, "power15":{ }, "power16":{ }, "power17":{ }, "power18":{ }, "power19":{ }, "power20":{ }, "power21":{ }, "power22":{ }, "power23":{ }, "power24":{ }, "power25":{ }, "power26":{ }, "power27":{ }, "power28":{ }, "power29":{ }, "power30":{ } }, "skills": { "skill01": { "name":"", "enhancer":"", "text":"PS: Soldier 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill02": { "name":"", "enhancer":"", "text":"PS: Doctor 11-", "display":"Professional Skill", "attribute":"GENERAL", "base":"0", "levels":"0", "cost":"0" }, "skill03": { "name":"Power Skill Wizardry", "enhancer":"", "text":": Wizardry 12-", "display":"Power", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill04": { "name":"", "enhancer":"", "text":"Paramedics 13-", "display":"Paramedics", "attribute":"INT", "base":"5", "levels":"1", "cost":"5" }, "skill05": { "name":"", "enhancer":"", "text":"Science Skill: Medicine 13-", "display":"Science Skill", "attribute":"GENERAL", "base":"4", "levels":"2", "cost":"4" }, "skill06": { "name":"", "enhancer":"", "text":"Science Skill: Anatomy 11-", "display":"Science Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill07": { "name":"", "enhancer":"", "text":"KS: Herbalism 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill08": { "name":"", "enhancer":"", "text":"High Society 12-", "display":"High Society", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill09": { "name":"", "enhancer":"", "text":"KS: Popular Literature 11-", "display":"KS", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill10": { "name":"", "enhancer":"", "text":"CuK: Popular Entertainment 11-", "display":"Knowledge Skill", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill11": { "name":"", "enhancer":"", "text":"Conversation 12-", "display":"Conversation", "attribute":"PRE", "base":"3", "levels":"0", "cost":"3" }, "skill12": { "name":"", "enhancer":"", "text":"Tactics 12-", "display":"Tactics", "attribute":"INT", "base":"3", "levels":"0", "cost":"3" }, "skill13": { "name":"", "enhancer":"", "text":"Teamwork 12-", "display":"Teamwork", "attribute":"DEX", "base":"3", "levels":"0", "cost":"3" }, "skill14": { "name":"", "enhancer":"true", "text":"Linguist", "display":"Linguist", "attribute":"", "base":"3", "levels":"0", "cost":"3" }, "skill15": { "name":"", "enhancer":"", "text":"Language: Ancient Elven (basic conversation; literate) (2 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"1" }, "skill16": { "name":"", "enhancer":"", "text":"Language: Clans' Tongue (idiomatic; literate) (5 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"0" }, "skill17": { "name":"", "enhancer":"", "text":"Language: King's Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill18": { "name":"", "enhancer":"", "text":"Language: Southern Tongue (fluent conversation; literate) (3 Active Points)", "display":"Language", "attribute":"GENERAL", "base":"3", "levels":"0", "cost":"2" }, "skill19": { "name":"", "enhancer":"", "text":"WF: Common Melee Weapons", "display":"Weapon Familiarity", "attribute":"GENERAL", "base":"2", "levels":"0", "cost":"2" }, "skill20": { "name":"", "enhancer":"", "text":"+3 Long Sword", "display":"Combat Skill Levels", "attribute":"GENERAL", "base":"6", "levels":"3", "cost":"6" }, "skill21": { "name":"", "enhancer":"", "text":"Defense Maneuver I-II ", "display":"Defense Maneuver", "attribute":"GENERAL", "base":"5", "levels":"0", "cost":"5" }, "skill22": { }, "skill23": { }, "skill24": { }, "skill25": { }, "skill26": { }, "skill27": { }, "skill28": { }, "skill29": { }, "skill30": { }, "skill31": { }, "skill32": { }, "skill33": { }, "skill34": { }, "skill35": { }, "skill36": { }, "skill37": { }, "skill38": { }, "skill39": { }, "skill40": { }, "skill41": { }, "skill42": { }, "skill43": { }, "skill44": { }, "skill45": { }, "skill46": { }, "skill47": { }, "skill48": { }, "skill49": { }, "skill50": { } }, "playerName":"Test PC #2", "gmName":"Villain in Glasses", "characterFile":"Sample_Character_MA.hdc", "versionHD":"20220801", "timeStamp":"Wed, 13 Nov 2024 13:15:05", "genre":"Fantasy HERO", "campaign":"Sample", "version":"2.3", "HeroSystem6eHeroic":"true" } } \ No newline at end of file diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.hdc b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character_MA.hdc index 77acf99690b651559ecdba8cc09f19a374aea3f2..5699a64413e60eeee4994bbf8d91697b8796cebe 100644 GIT binary patch delta 11442 zcmbVR32>CxegEFd2!o8oAsNIXX(d_4#-L+YE3IN;^P(Q`_y-?(g^B ze%c$6jz+uRzW0Cs_rI_2n^)4mdUS!~^Ur>o_9Y=tiBs9X z$IrKv?YDMW!pn-8k!Jd zO&WI>%-A#%^4R=S@Ypg2hqDIaXIW?{iGsMsC@^(Yoqt+CpK8U9Q zt&u@Uei%=#@Xk2&uphsuuLCfJY4zC>;S5lDMwJAm;&$Kw0jB^oVI2x1nXtz7VWac| zu%`Wh-V-(fUK+hWU>(%w2CSVLVp9K_0={vp6nIE5jb|{Bgx+y%4O>H+A6gG>$slxD zyYZWslGkDDi`u9MKrqh?S!=O61PJbrTdOr22k$v$^1p;$m9VlK<-C1T|`Mq zaD{>#0RMaOG=g2qW(wdtum>-~=QLJqWQQ~|UT&{Y^t{O6BnuH~O#y13#y@3^1+cYO zA18(8%4sd49*{DsFEeD>`lSem^a(?@tzV<}A)Fv-NRo-kirpM zJ#0M%{zx@BBnnF405~vC9@bY9br}`yh=C)w`?T&UOA{0#s*93TAnF>DLIWHGs(o5u z)GGx~jnm*pu}3+KVU;BB)YniNxu#M}HpM4PFG|uh?tg(*Gz-4PbY}WxOeVvx|h!)Z=0PKJU zsewtX+h#`TPimCLD=Bos*!E5_j{*IZHkMejxx-U~_!|IrT8gdB0R77NBmoRpk+{Sx zzVPZQy@VRjt9jaLrL zN}hr@2E7Z^(_!exCd7?uY1n2{2lKMi!XaSV14(Ju$qWmIp#YjvCs&IHn{MY&CLWFe zp9!tTb=M))(Yknl*j%4|K;-4vF|-Q1yELZ=$0!LlsK0+etWw8H#69zUs6vn`<>a?@ zYEwU1EA9>DJ}9cv;`aNT3NkGZ0pJH|%RE*&st7OgB0no3<{IQ>Z|J+_B44CcSYGuz zzsM`}LPzv#w+2M=)=xgYQq6nS+sDLMXwWU55lL`XupW`^*ULjsbc%xx)!r{w%!QXbos7)VMXEH>dAzKFC@TsvLu_lIeS`>}%{Bz#WwH~pU##bJ? zxn1-ukAX8~uUF0Fie~jukEoQ#H@={bKP58fu^2a;hC)w?p_MA@xX6w%GgCLa5kOFw zs;iHSPC3zRPKyd!x>JRYiGNVn_lxbqE&JU$D)4o2P);0PsWv<*GS#OC#HtvKH4r6| zomCCT%L{EdDLNeL+{5C2)t)O>Xya66gQ948tg(mBK%S`HKNLJMu3jG&`6+Op_^~tO z91*n|_wHowLyQcWG>-OZQKTXFs=qmv;AjT2B6R+gI2OhiJR^#hB~s{?i(e{`eb>rm z**)o@_5UdTQ&ZXq1?J1M8K(cK)WM?~x~tOtPqmyV$;N?v`LI7MSvMtHP(Z7z1Qaz* za|gV(KL#E|FKv6vx^$K^rDKN<-g~qfF4qZPMH^yY5n^DS)rh}7tJP}9ceAw-|4mk> z)rGZgto2#-fa$WDya2d0`E~yvMAy9}p9$V3_bnk%kPnrgmh> zO88;q8)h7q7tCzTGr%Ml|87r1JDyFxx3dk4V3X~}be^*_>!eW&@Ue<9zj(v6bp7)M z_3Cigmg0Do83Btk7dr}vuaos2L~Rj ziXl&yt;*KC{dP;3H=TH$#T7d75|-A(2;!e>L%TK;M^~TC-8g>I*t{tLjz-Nd4P3LTeORk5TA|`^? z2u4;HZk-0SCUaV0RD-%+n!-0rLHw3SGIQnCZX*Lb59bz_LkeE}Q3CvCKf%aL?7C*L zsu(^BW!AsGSrnTj=x!DBX2jB%$s^jb8`&x}zaRkczX9^?1lVPrVHuX?n z)By57Cnvguk`=$cUoI#ND@^B)JBo57^Q{cl^2WW!HAze|a`4Ua+1zPgS*=_rMV`F4 zekF5y@Yb8}M-uQX426vmiAOr`$qstn_BrI~U%1K`AnUbjHR-^aN;l*Tg^)r{sHQ0{ zXoW&_0~|c^#eWg%c%R6Ze|l@F`o$@cDVP1`G5Pr~Ps&^GxfA45jZBESPO$yD-yv6j zoSy)IJi&QUo@{x!RDSQ{Kc=-?ZK}3dIOpL35xe78g`(o*i~lP(^rXwHpC%%J%~Jz1 ztGQJ6{P!nWUEps!LVUZml_@~Z?8sddvnKn0Ta3okrPIIxYhQ5kXWtMM&qEt=qzQ57 zc4lit!;w*ex_nBkoxc$Q7fnxSCPU;nD2MoTZ0iKojNvpUBmrr6jg}N}4F@OqP>JTMD*GnmI((_RIJ`%bx zD4tDTHbs#Tx}wArI%(6FeVFqZ&bv@BPztR{__wnb+f7Dww;i3AbR$-g)G34&j;hxUOKi(Pf4WAywUW&g&INy-z~s(ot_^XQ2ElmS2%0!}GEFHbS$4waw<&lM_sn3DBM(f3 z)Xfv(oSgW#%q2~Fo;8Y~%vlDGtF{NkdKq}`?uVLiQ@zzYr|b}r6ckV(ubo_*Yv&&J z+B+adGfmimnxXc}^^8`SVz+kTMoJ>)H!R)L2$l~)@EqDi@-;hfyBoJ>jAMY*BM%7f z`|{W~-sjM~tP9wgu3GMh>`JJ-KoyLLH5mPyso5S)=#53)+H)H5*=cD~5q)CUKrYto zNh!y8LnmJxeW*d34!eHNj?83&H5*-OL}w;5-oZ>r&b(VJ`+oksu7gY=YK#$~H7*_R zW`xzR8lvHReAS=_>j$&s@PM zmH@tEsZeM@o0G(%cLZLIxRS`?ORMvOw_Yf9B=7@t_I!YENM3Moih#fze<_5oFXd|r zx#9hp`3;aJ3_x8fK+@8W496u6D7<6e9dOoa?8nR?(bd2fmNANVLo~)_8zJ)Tx^iZp zsJpN@3go53d2vxs$Fn9wh>7G)K)Ma8W<}P9$j0B@NqjU?kFgO$^{E?ary-uREc#dz z6v`>1!fc0(8ixR#?GlN2be2R!3*ZJ1UUJa%ePmY~QHO2Cp&DtCYx#PmIW;**-H` znVloX5`-xwH4&-h69c8DTlKF+A=>3L}_%g_b7dPNAWQ4|GCpes~JI zSk8}`xmg&Pbi`Kxo3YCPQG{pq?aY*fD8or+SH9#c({lvQ5BQd?3`9@ho8ep60UCmD z#OXdR)Nkyp^j!<_*rS8m9?3fPm9+epGE`co&tdDu>cl32l_cMznn6Zf#zYOZOA|ms z?NN?Hl=OChcoROmG~r$tGhylwK1={%+_U>nv&Rh0z>)~z)n^%ITIGqcoPa%E(kl=N z!%io58RJcqtpha_F@@HvX*aibp!EeQ@HgW|PSgSo_+l{L1Ts3oU$To>#gY$>-ORD%r&3>7rzOL2 zPXve7>o)M%4WxH+lv6&6+^>1EO(N+@M2$jcGFC8O<{$S1WjaQhv7j9q@(W@O8P5@-CRshGal@Y0LlOp+vqomrR!bUMFJ7t6Gm z+H{7q=f0eyP-yHndT@ZThOD`@q0^*{O%i=Kkn%T~NPen9pW?;jpJqh13BL(<)RR)9 zsb0NE)W$iA;I~FX@uAVUL$tKx;AE{oCtzH}mQJFTvH!3$j2)z-aVEbt(pVcRJz=5P z80Z~TcYOfd*(tv!1rS|@k})Gds*TbnV!oI+-fcVCEaC*b@el@U;Pz;Z#CSPlmaS mGKRcZr5N3t79`mTz?a8xVmPb^_l<~{$&Qq%n`g!S!umgl((p3? delta 3187 zcmZuzeQZ=!7Qg4sSUYW$Rtri?=>t{@1?tRn+Gzxhf?tQPF z7V6Q(a^y7Hw4zt6jP!c3HU1 zE*ugMWcRdryLb;`K8+cX$+#b1on~ykbRiqWn#20j3%#Wcur9kK@YJgYmE=Q{RLR#) zQZzU5_Zhm4!kI^C1&_Z-8*?S^f9c$!(8y;Z&X4rW<$~P6KMNe5`3J2mjl*>QMoFGL zNmV?xnZmh=OaF2fEfim0>{RBiT%KBJKMAm|AFR+*7vs6_KhEANuIDp53G#L2dg{{s zxhtPMA6npIqlyWfd^|}}Zf~YAx83E;FZ z0GV(tX)uJ5opCYlfJ|0);NJzl13+ESN4h{F(g9y0Hb*e-!y}sVEiCpJJ$KBXI$+Gp zMzaY9MWI^$gw% z)OMx6zEG}9UN6^~n?KN}FNYT;R1>nJQALp_^{S4~yhIiJP!ZMUrf=4SZl|ITWh=Zb zA=+xBcg-`vEo-S#w|^7%J}RKcZl@sJd$9j?cHU+bpxRci=ng)0f8PbHP|! z-H=Pocbky<`I$vjQ9T5|B@MIiSKi|BsEJt8P|TQU_2(BJ5ZZaqa$4O}}?<&Nhg>qfG!_*uf|B#k=6HDme9XcE?8?*a}OC1PJf*G<&j`sle-j^4`h>^SS zuoF1iP-W~wOF)r2keU#V zB~rmzQw%|p^cpuuAeV#@@`A1QL03{pdc$13&(|4~jO+Bh5)*;fE^5wtF8Isx4m{Gl zrCHco&ad#NzzkGXiB< zBJ@a-$XOv>N)EjIm=Gb~tf9p#WC!UWHt!{dv^f9;0s*yqUKaUD52Tst*%#5S-pbWH8ptu-c46YPhZ))KqnywfxQXn55D-0pT5^A z)K8|%c+W%h?ww6)gE-DDtDN7x{`+!1J&J?&Vw7Cp8SEjCK3|rknB0my9L0rx^fAQV zh=`l;Uk8XAOyn^bloKc_UlM&k-_uH`se%8vjh5o|=T`cQ@1$>DI#&eb&{$)_^%a`4k=XmTaT914hiX z3L!MftwVM^>geWEVQx=Q*{rwLAYk{sJ$oxR)=mg1`MkD4>MZAXGgRaI7l(Ed zcH=?746|GK*srKSyNloB%mdEdvUGwU*-KZ8qlP$HgqxN<7#9rI7Ua z?t8prhpC2&8x8JqH>}q$d{W_|KU^}gub(IKL-xSR3F7JfG_2h(!n}Np*38rU{AX2O zW{iGoNL!gFYUCXo(SDE!wVwM+`qmoZ?zgC%$GfR0GV2I$A46KV{f*Xp*WRL5M|S&# z5y#{bq|;e6s*7>2 [!TIP] -> HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the value given (parentheses are not strictly required). +> HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -### (Max Range: Xm) or (max range X) -A weapon's maximum range. +### "(locations x-y, z)" or "(loc x, y-z)" +If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". -### (X END/Turn) or (END/Turn: X) +### "Equipment" +A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. + +### "Max Range: XYZm" or "max range XYZ" +HD Importer will fill in a weapon's maximum range with XYZ. + +### "X END/Turn" or "END/Turn: X" If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help @@ -122,4 +128,4 @@ Version 2.1 -- Updated to support additional character sheet features as of shee Version 2.2 -- Fix for compound power import failure. Recognizes Mental Combat Skill Levels and assigns levels and costs. Minor update to HeroSystem6eHeroic.hde (version 2.2). Improved formatting of text in powers, talents, and complications (September 14, 2024). -Version 2.3 -- +Version 2.3 -- This version is up to date with Sheet Version 3.80. Adds special intelligence skill types with variable base cost (SPx). Adds "Native +" language skill assignment for native languages updated imitate dialects. Doubles the number of armor, weapons, and equipment imported to account for the new "A/B" sets feature of the sheet. Recognizes additional copies of weapons purchased with the quantity feature of HD. Adds recognition of the keywords and key phrases "Max Range XYZ" and "Equipment." HeroSystem6eHeroic.hde (version 2.3) updated to 50 equipment slots (November 13, 2024). diff --git a/HeroSystem6eHeroic_HDImporter/script.json b/HeroSystem6eHeroic_HDImporter/script.json index b6c873839c..528a58a4a8 100644 --- a/HeroSystem6eHeroic_HDImporter/script.json +++ b/HeroSystem6eHeroic_HDImporter/script.json @@ -1,7 +1,7 @@ { "name":"HeroSystem6eHeroic HDImporter", "script":"HeroSystem6eHeroic_HDImporter.js", - "version":"2.2", + "version":"2.3", "description":"HDImporter imports HERO Designer-created heroes, villains, monsters, and other characters into a HeroSystem6eHeroic Roll20 campaign. The characters must be exported from Hero Designer using the format HeroSystem6eHeroic.hde, which is a companion file in the HD Importer repository. To use, open an exported character text file and paste the contents into chat and hit enter. Full sheet instructions in the [README](https://github.com/Roll20/roll20-api-scripts/tree/master/HeroSystem6eHeroic_HDImporter). Based on BeyondImporter Version O.4.0 by Robin Kuiper, Matt DeKok, and Ammo Goettsch", "authors": "Villain In Glasses", "roll20userid":"633423", @@ -13,6 +13,6 @@ }, "conflicts":[], "previousVersions":[ - "1.0, 1.1, 2.0, 2.1" + "1.0, 1.1, 2.0, 2.1, 2.2" ] } From 35ab41c78bd57ad14bbfcfe2d976afd13b9a0bad Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:54:42 -0700 Subject: [PATCH 16/42] README update. --- HeroSystem6eHeroic_HDImporter/README.MD | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index d00e05c27f..b71bd85182 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -68,16 +68,16 @@ Powers that add (or subtract) characteristics, movement, and perception abilitie > [!TIP] > HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -### "(locations x-y, z)" or "(loc x, y-z)" +*"(locations x-y, z)" or "(loc x, y-z)"* If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". -### "Equipment" +*"Equipment"* A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. -### "Max Range: XYZm" or "max range XYZ" +*"Max Range: XYZm" or "max range XYZ"* HD Importer will fill in a weapon's maximum range with XYZ. -### "X END/Turn" or "END/Turn: X" +*"X END/Turn" or "END/Turn: X"* If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help From 1f55b8852d14195c5994cbf0708ba64facaa1134 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:56:28 -0700 Subject: [PATCH 17/42] README update. --- HeroSystem6eHeroic_HDImporter/README.MD | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index b71bd85182..4b9f564131 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -68,16 +68,16 @@ Powers that add (or subtract) characteristics, movement, and perception abilitie > [!TIP] > HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -*"(locations x-y, z)" or "(loc x, y-z)"* +#### "(locations x-y, z)" or "(loc x, y-z)" If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". -*"Equipment"* +#### "Equipment" A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. -*"Max Range: XYZm" or "max range XYZ"* +#### "Max Range: XYZm" or "max range XYZ" HD Importer will fill in a weapon's maximum range with XYZ. -*"X END/Turn" or "END/Turn: X"* +#### "X END/Turn" or "END/Turn: X" If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help From 508701c447af8bdf6651ab6f64025a0d6816d0a7 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:35:41 -0700 Subject: [PATCH 18/42] README update. --- HeroSystem6eHeroic_HDImporter/README.MD | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index 4b9f564131..0dd63e86e9 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -68,17 +68,13 @@ Powers that add (or subtract) characteristics, movement, and perception abilitie > [!TIP] > HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -#### "(locations x-y, z)" or "(loc x, y-z)" -If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". +- `(locations x-y, z)" or "(loc x, y-z)` : `If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". -#### "Equipment" -A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. +- `Equipment` : A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. -#### "Max Range: XYZm" or "max range XYZ" -HD Importer will fill in a weapon's maximum range with XYZ. +- `Max Range: XYZm" or "max range XYZ` : HD Importer will fill in a weapon's maximum range with XYZ. -#### "X END/Turn" or "END/Turn: X" -If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. +- `X END/Turn" or "END/Turn: X` : If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help @@ -128,4 +124,4 @@ Version 2.1 -- Updated to support additional character sheet features as of shee Version 2.2 -- Fix for compound power import failure. Recognizes Mental Combat Skill Levels and assigns levels and costs. Minor update to HeroSystem6eHeroic.hde (version 2.2). Improved formatting of text in powers, talents, and complications (September 14, 2024). -Version 2.3 -- This version is up to date with Sheet Version 3.80. Adds special intelligence skill types with variable base cost (SPx). Adds "Native +" language skill assignment for native languages updated imitate dialects. Doubles the number of armor, weapons, and equipment imported to account for the new "A/B" sets feature of the sheet. Recognizes additional copies of weapons purchased with the quantity feature of HD. Adds recognition of the keywords and key phrases "Max Range XYZ" and "Equipment." HeroSystem6eHeroic.hde (version 2.3) updated to 50 equipment slots (November 13, 2024). +Version 2.3 -- This version is up to date with Sheet Version 3.80. Adds special intelligence skill types with variable base cost (SPx). Adds "Native +" language skill assignment for native languages updated to imitate dialects. Doubles the number of armor, weapons, and equipment imported to account for the new "A/B" sets feature of the sheet. Recognizes additional copies of weapons purchased with the quantity feature of HD. Adds recognition of the keywords and key phrases "Max Range XYZ" and "Equipment." HeroSystem6eHeroic.hde (version 2.3) updated to 50 equipment slots (November 13, 2024). From a441dac0140eca03049d51a6bf09b9b73772abc0 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:37:22 -0700 Subject: [PATCH 19/42] README update. --- HeroSystem6eHeroic_HDImporter/README.MD | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index 0dd63e86e9..fecd2e434a 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -68,13 +68,13 @@ Powers that add (or subtract) characteristics, movement, and perception abilitie > [!TIP] > HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -- `(locations x-y, z)" or "(loc x, y-z)` : `If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". +- `(locations x-y, z) or (loc x, y-z)` : `If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". - `Equipment` : A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. -- `Max Range: XYZm" or "max range XYZ` : HD Importer will fill in a weapon's maximum range with XYZ. +- `Max Range: XYZm or max range XYZ` : HD Importer will fill in a weapon's maximum range with XYZ. -- `X END/Turn" or "END/Turn: X` : If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. +- `X END/Turn or END/Turn: X` : If your campaign uses the optional rule of applying END costs to armor, HD Importer will automatically apply the value X. # Help From 8bb299d9c36d323ab51dedf886da16df6c7790fe Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:27:58 -0700 Subject: [PATCH 20/42] README update. --- HeroSystem6eHeroic_HDImporter/README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeroSystem6eHeroic_HDImporter/README.MD b/HeroSystem6eHeroic_HDImporter/README.MD index fecd2e434a..457c34347d 100644 --- a/HeroSystem6eHeroic_HDImporter/README.MD +++ b/HeroSystem6eHeroic_HDImporter/README.MD @@ -68,7 +68,7 @@ Powers that add (or subtract) characteristics, movement, and perception abilitie > [!TIP] > HD Importer will recognize the following key phrases if added to a piece of equipment's notes and attempts to automatically apply the values given. -- `(locations x-y, z) or (loc x, y-z)` : `If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". +- `(locations x-y, z) or (loc x, y-z)` : If HD Importer finds the keyword "locations" it will assign the list "x-y, z" to a piece of armor's locations. End the list with closed parentheses ")" or a semicolon ";". - `Equipment` : A weapon with the "equipment" keyword will be imported as a piece of equipment, not a weapon. Useful for ordinary items built with attack powers. From 60bca4e632638b4809299c056bfb52754e308640 Mon Sep 17 00:00:00 2001 From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:32:06 -0700 Subject: [PATCH 21/42] Sample Character Update --- .../2.3/Sample_Character.hdc | Bin 166000 -> 165894 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc b/HeroSystem6eHeroic_HDImporter/2.3/Sample_Character.hdc index 93ee772548bb55683af82c54698f08f8c5266d2d..1b41f82862279e34a618fe2e0eb5a8fc18b3afc0 100644 GIT binary patch delta 118 zcmew`fvar-7xTaW3>%qZ7&o6`oWjT*%#g^C%TU0O!;sos%e=joneo0PzbS(mgE@mK z5Lz-AGZ;?(C?Gw#;G5obtBZ_W?fYaIx9^i>YKh@R(#sG(x$vX#cAqMyQ@qo4I+$dp Oi*+!GP50s=Wl7y=ml8G;x>7~+B0mBAOt_hE1Zvuzoa7(9StK|mD> z3@!|*48;tY4CxGcKvofhK2Q!s=Q9K|R03s6fV^A=Gav~Q&tynu&|}D8NCAo|0Zq_n zuw&q5-~zG~7;G4v89X)@Fg7#FIs=U;0veIWpbj)f0Vt6VG^l_f5hzmG{Eun-KPJZe zlKdtN77QS4#$e81$Y3;C!BKj8KO3Xo^xYR2x!Ubz8MoWZGPT5Tx&Sp60d3CQ{-~1a YH1G72?M!0R*S0gsOuygGq|V3y01Ev%x&QzG From 74edb77ecbc63001348f84353fa7509ee67fbc9f Mon Sep 17 00:00:00 2001 From: nesuprachy <64099419+nesuprachy@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:23:19 +0100 Subject: [PATCH 22/42] DrD2StatusMarkers patch 0.1.3 Token markers for load (encumberance) --- DrD2StatusMarkers/0.1.3/DrD2StatusMarkers.js | 94 ++++++++++++++++++++ DrD2StatusMarkers/DrD2StatusMarkers.js | 54 ++++++----- DrD2StatusMarkers/script.json | 2 +- 3 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 DrD2StatusMarkers/0.1.3/DrD2StatusMarkers.js diff --git a/DrD2StatusMarkers/0.1.3/DrD2StatusMarkers.js b/DrD2StatusMarkers/0.1.3/DrD2StatusMarkers.js new file mode 100644 index 0000000000..3cc07d2599 --- /dev/null +++ b/DrD2StatusMarkers/0.1.3/DrD2StatusMarkers.js @@ -0,0 +1,94 @@ +// Github: TBD +// By: nesuprachy +// Contact: https://app.roll20.net/users/11071738/nesuprachy +// +// This script sets token markers based on relevant sheet attributes. +// Works with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK`, `GRY`, `load` followed by corresponding values +// Uses TokenMod to set token markers from chat https://wiki.roll20.net/Script:Token_Mod + +var DrD2StatusMarkers = DrD2StatusMarkers || (function() { + 'use strict'; + + const version = '0.1.3'; + const lastUpdate = 1731662174989; + const markerAttributes = ['body_scarred', 'spirit_scarred', 'influence_scarred', 'danger', 'advantages', 'companion_bond_scarred', 'load']; + + checkInstall = function () { + log(`-=> DrD2StatusMarkers v${version} <=- [${new Date(lastUpdate)}]`); + }, + + handleMarkerAttributes = function (obj, prev) { + var attr = obj.get('name'); + if(markerAttributes.includes(attr)) { + var prevVal, newVal; + if(attr === 'load') { + prevVal = prev.current; + newVal = obj.get('current'); + }else { + prevVal = parseInt(prev.current)||0; + newVal = parseInt(obj.get('current'))||0; + } + var charId = obj.get('_characterid'); + var marker = ''; + //log(`${obj.get('name')} changed`); + //log(`prevVal ${prevVal} -> newVal ${newVal}`); + switch (attr) { + case markerAttributes[0]: + marker = 'RED'; + break; + case markerAttributes[1]: + marker = 'BLU'; + break; + case markerAttributes[2]: + marker = 'GRN'; + break; + case markerAttributes[3]: + marker = 'VIO'; + break; + case markerAttributes[4]: + marker = 'BLK'; + break; + case markerAttributes[5]: + marker = 'GRY'; + break; + case markerAttributes[6]: + marker= 'load'; + break; + default: + break; + } + if(marker){ + if(attr === 'load'){ + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-loadL|-loadS|-loadT|load${newVal}`, null, {noarchive:true}); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-loadL|-loadS|-loadT|load${newVal}`); + } else if(newVal > 0 && newVal < 10) { + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus|${marker}${newVal}`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus|${marker}${newVal}`); + } else if(newVal >= 10) { + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|${marker}9plus`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|${marker}9plus`); + } else { + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus`); + } + } + } + }, + + registerEventHandlers = function () { + on('change:attribute:current', function(obj, prev){handleMarkerAttributes(obj, prev)}); + }; + + return { + CheckInstall: checkInstall, + RegisterEventHandlers: registerEventHandlers + }; + +}()); + +on('ready', () => { + 'use strict'; + + DrD2StatusMarkers.CheckInstall(); + DrD2StatusMarkers.RegisterEventHandlers(); +}); \ No newline at end of file diff --git a/DrD2StatusMarkers/DrD2StatusMarkers.js b/DrD2StatusMarkers/DrD2StatusMarkers.js index 6df9106a8a..3cc07d2599 100644 --- a/DrD2StatusMarkers/DrD2StatusMarkers.js +++ b/DrD2StatusMarkers/DrD2StatusMarkers.js @@ -3,15 +3,15 @@ // Contact: https://app.roll20.net/users/11071738/nesuprachy // // This script sets token markers based on relevant sheet attributes. -// Works with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK` +// Works with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK`, `GRY`, `load` followed by corresponding values // Uses TokenMod to set token markers from chat https://wiki.roll20.net/Script:Token_Mod var DrD2StatusMarkers = DrD2StatusMarkers || (function() { 'use strict'; - const version = '0.1.2'; - const lastUpdate = 1725975609706; - const markerAttributes = ['body_scarred', 'spirit_scarred', 'influence_scarred', 'danger', 'advantages', 'companion_bond_scarred']; + const version = '0.1.3'; + const lastUpdate = 1731662174989; + const markerAttributes = ['body_scarred', 'spirit_scarred', 'influence_scarred', 'danger', 'advantages', 'companion_bond_scarred', 'load']; checkInstall = function () { log(`-=> DrD2StatusMarkers v${version} <=- [${new Date(lastUpdate)}]`); @@ -20,44 +20,56 @@ var DrD2StatusMarkers = DrD2StatusMarkers || (function() { handleMarkerAttributes = function (obj, prev) { var attr = obj.get('name'); if(markerAttributes.includes(attr)) { - var prevVal = parseInt(prev.current)||0; - var newVal = parseInt(obj.get('current'))||0; + var prevVal, newVal; + if(attr === 'load') { + prevVal = prev.current; + newVal = obj.get('current'); + }else { + prevVal = parseInt(prev.current)||0; + newVal = parseInt(obj.get('current'))||0; + } var charId = obj.get('_characterid'); - var color = ''; + var marker = ''; //log(`${obj.get('name')} changed`); //log(`prevVal ${prevVal} -> newVal ${newVal}`); switch (attr) { case markerAttributes[0]: - color = 'RED'; + marker = 'RED'; break; case markerAttributes[1]: - color = 'BLU'; + marker = 'BLU'; break; case markerAttributes[2]: - color = 'GRN'; + marker = 'GRN'; break; case markerAttributes[3]: - color = 'VIO'; + marker = 'VIO'; break; case markerAttributes[4]: - color = 'BLK'; + marker = 'BLK'; break; case markerAttributes[5]: - color = 'GRY'; + marker = 'GRY'; + break; + case markerAttributes[6]: + marker= 'load'; break; default: break; } - if(color){ - if(newVal > 0 && newVal < 10) { - sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|-${color}9plus|${color}${newVal}`, null, {noarchive:true} ); - //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|-${color}9plus|${color}${newVal}`); + if(marker){ + if(attr === 'load'){ + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-loadL|-loadS|-loadT|load${newVal}`, null, {noarchive:true}); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-loadL|-loadS|-loadT|load${newVal}`); + } else if(newVal > 0 && newVal < 10) { + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus|${marker}${newVal}`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus|${marker}${newVal}`); } else if(newVal >= 10) { - sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|${color}9plus`, null, {noarchive:true} ); - //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|${color}9plus`); + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|${marker}9plus`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|${marker}9plus`); } else { - sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|-${color}9plus`, null, {noarchive:true} ); - //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${color}1|-${color}2|-${color}3|-${color}4|-${color}5|-${color}6|-${color}7|-${color}8|-${color}9|-${color}9plus`); + sendChat('API', `!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus`, null, {noarchive:true} ); + //log(`!token-mod --ignore-selected --ids ${charId} --set statusmarkers|-${marker}1|-${marker}2|-${marker}3|-${marker}4|-${marker}5|-${marker}6|-${marker}7|-${marker}8|-${marker}9|-${marker}9plus`); } } } diff --git a/DrD2StatusMarkers/script.json b/DrD2StatusMarkers/script.json index 8b1e65c42e..7dfba69fac 100644 --- a/DrD2StatusMarkers/script.json +++ b/DrD2StatusMarkers/script.json @@ -3,7 +3,7 @@ "script": "DrD2StatusMarkers.js", "version": "0.1.2", "previousversions": ["0.1.0", "0.1.1"], - "description": "Designed for use only with the Draci Doupe II sheet.\n\nThis script sets token markers based on relevant sheet attributes.\nWorks with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK`, `GRY`.\nYou can download the token marker set [here](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fnesuprachy%2Froll20-character-sheets%2Ftree%2FDraci-doupe-II%2FDraci%2520doupe%2520II%2Fassets%2FDrD2-token_markers).", + "description": "Designed for use only with the Draci Doupe II sheet.\n\nThis script sets token markers based on relevant sheet attributes.\nWorks with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK`, `GRY`, `load` followed by corresponding values.\nYou can download the token marker set [here](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fnesuprachy%2Froll20-character-sheets%2Ftree%2FDraci-doupe-II%2FDraci%2520doupe%2520II%2Fassets%2FDrD2-token_markers).", "authors": "nesuprachy", "roll20userid": "11071738", "useroptions": [], From 7cfeacf8872b724bbed1cc595437b8e2c3bbf19f Mon Sep 17 00:00:00 2001 From: nesuprachy <64099419+nesuprachy@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:25:54 +0100 Subject: [PATCH 23/42] Update script.json --- DrD2StatusMarkers/script.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DrD2StatusMarkers/script.json b/DrD2StatusMarkers/script.json index 7dfba69fac..6506756b51 100644 --- a/DrD2StatusMarkers/script.json +++ b/DrD2StatusMarkers/script.json @@ -1,8 +1,8 @@ { "name": "DrD2StatusMarkers", "script": "DrD2StatusMarkers.js", - "version": "0.1.2", - "previousversions": ["0.1.0", "0.1.1"], + "version": "0.1.3", + "previousversions": ["0.1.0", "0.1.1", "0.1.2"], "description": "Designed for use only with the Draci Doupe II sheet.\n\nThis script sets token markers based on relevant sheet attributes.\nWorks with the DrD2 token marker set, icons must be named `RED`, `BLU`, `GRN`, `VIO`, `BLK`, `GRY`, `load` followed by corresponding values.\nYou can download the token marker set [here](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fnesuprachy%2Froll20-character-sheets%2Ftree%2FDraci-doupe-II%2FDraci%2520doupe%2520II%2Fassets%2FDrD2-token_markers).", "authors": "nesuprachy", "roll20userid": "11071738", From 74869385f914e4fbae3e6db22372d503cb4fa6c0 Mon Sep 17 00:00:00 2001 From: Ulty Date: Sun, 17 Nov 2024 13:31:36 +0100 Subject: [PATCH 24/42] COFantay: implemented automatic teleportation. --- COFantasy/3.15/COFantasy.js | 1185 +++++++++++++++++++++++++++-------- COFantasy/3.15/doc.html | 12 +- COFantasy/COFantasy.js | 1185 +++++++++++++++++++++++++++-------- COFantasy/ChangeLog.md | 2 + COFantasy/doc.html | 12 +- 5 files changed, 1846 insertions(+), 550 deletions(-) diff --git a/COFantasy/3.15/COFantasy.js b/COFantasy/3.15/COFantasy.js index dbc2737c6f..fe81221f66 100644 --- a/COFantasy/3.15/COFantasy.js +++ b/COFantasy/3.15/COFantasy.js @@ -1,4 +1,4 @@ -//Derni\xE8re modification : dim. 08 sept. 2024, 05:59 +//Derni\xE8re modification : jeu. 07 nov. 2024, 07:13 // ------------------ generateRowID code from the Aaron --------------------- const generateUUID = (function() { "use strict"; @@ -65,6 +65,15 @@ let COF_loaded = false; // - chargeFantastique : tout ce dont on a besoin pour une charge fantastique en cours (TODO: passer sous combat) // - eventId : compteur d'events pour avoir une id unique // - tokensTemps : liste de tokens \xE0 dur\xE9e de vie limit\xE9e, effac\xE9s \xE0 la fin du combat +// - tid: id du token +// - name: le nom du token +// - duree: dur\xE9e restante en rounds +// - init: init \xE0 laquelle diminuer la dur\xE9e +// - intrusion: distance \xE0 laquelle le token s'active +// - tokensActifs : map de pageid vers liste de tokens qui font une action quand on passe \xE0 c\xF4t\xE9. +// - tid: id du token +// - name: le nom du token +// - distance: la distance d'activation (par rapport au centre). Si pas pr\xE9sent, il faut intersecter avec le bo\xEEte du token (sans tenir compte de la rotation) // - effetAuD20 : les effets qui se produisent \xE0 chaque jet de d\xE9. // chaque effet est d\xE9termin\xE9 par un champ, puis pour chaque champ, // - min: valeur minimale du d\xE9 pour d\xE9clencher @@ -73,6 +82,7 @@ let COF_loaded = false; // - nomFin: nom \xE0 afficher pour le statut et mettre fin aux \xE9v\xE9nements // par exemple, foudreDuTemps pour les foudres du temps // - tenebresMagiques : \xE9tat g\xE9n\xE9ral de t\xE9n\xE8bres magiques +// - aileForgeRunique: aile de la forge runique pour g\xE9rer les p\xE9ch\xE9s. // - jetsEnCours : pour laisser le MJ montrer ou non un jet qui lui a \xE9t\xE9 montr\xE9 \xE0 lui seul // - currentAttackDisplay : pour pouvoir remontrer des display aux joueurs // - pause : le jeu est en pause @@ -1010,6 +1020,22 @@ var COFantasy = COFantasy || function() { }; } + //Retourne un encodage des tailes : + // 1 : minuscule + // 2 : tr\xE8s petit + // 3 : petit + // 4 : moyen + // 5 : grand + // 6 : \xE9norme + // 7 : colossal + function taillePersonnage(perso, def) { + if (perso.taille) return perso.taille; + let taille = tailleNormale(perso, def); + if (attributeAsBool(perso, 'agrandissement')) taille++; + perso.taille = taille; + return taille; + } + //options peut contenir // msg: un message \xE0 afficher // maxVal: la valeur max de l'attribut @@ -2076,6 +2102,84 @@ var COFantasy = COFantasy || function() { etat_de_marker[marker] = etat; } stateCOF.jetsEnCours = undefined; + //V\xE9rification de la validit\xE9 des id des tokens actifs + if (stateCOF.tokensActifs) { + let ta = stateCOF.tokensActifs; + let kept = []; + let removed = []; + for (let pageId in ta) { + if (!ta[pageId] || ta[pageId].length === 0) { + delete ta[pageId]; + return; + } + let page = getObj('page', pageId); + if (page) kept.push(pageId); + else removed.push(pageId); + } + kept.forEach(function(pageId) { + let tokensToRemove = []; + ta[pageId].forEach(function(tt) { + let token = getObj('graphic', tt.tid); + if (token) return; + token = findObjs({ + _type: 'graphic', + name: tt.name, + _pageid: pageId, + layer: 'gmlayer' + }); + if (token.length === 0) { + tokensToRemove.push(tt.tid); + return; + } + if (token.length > 1) { + token = token.filter(function(tp) { + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + return s && s.sortieEscalier; + }); + if (token.length != 1) { + error("Il y a plus d'un token nomm\xE9 " + tt.name + ", impossible de savoir lequel \xE9tait automatiquement un TP", tt); + tokensToRemove.push(tt.tid); + return; + } + } + tt.tid = token[0].id; + }); + //Co\xFBt quadratique, mais \xE7a ne devrait pas arriver trop souvent (?) + tokensToRemove.forEach(function(tid) { + removeTokenActif(tid, pageId); + }); + }); + removed.forEach(function(pageId) { + ta[pageId].forEach(function(tt) { + let tokens = findObjs({ + _type: 'graphic', + name: tt.name, + layer: 'gmlayer' + }); + if (tokens.length === 0) { + return; + } + if (tokens.length > 1) { + tokens = tokens.filter(function(tp) { + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + return s && s.sortieEscalier; + }); + if (tokens.length != 1) { + error("Il y a plus d'un token nomm\xE9 " + tt.name + ", impossible de savoir lequel \xE9tait automatiquement un TP", tt); + return; + } + } + let token = tokens[0]; + let pid = token.get('pageid'); + ta[pid] = ta[pid] || []; + tt.tid = token.id; + ta[pid].push(tt); + }); + delete ta[pageId]; + }); + } } function etatRendInactif(etat) { @@ -3212,13 +3316,12 @@ var COFantasy = COFantasy || function() { //options: //fromTemp si on est en train de supprimer un effet temporaire //affectToken si on a d\xE9j\xE0 chang\xE9 le statusmarkers (on vient donc d'un changement \xE0 la main d'un marker - function setState(personnage, etat, value, evt, options) { + function setState(personnage, etat, value, evt, options = {}) { let token = personnage.token; if (value && predicateAsBool(personnage, 'immunite_' + etat)) { sendPerso(personnage, 'ne peut pas \xEAtre ' + stringOfEtat(etat, personnage)); return false; } - options = options || {}; let aff = options.affectToken || affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt); if (stateCOF.combat && value && etatRendInactif(etat) && @@ -4215,8 +4318,7 @@ var COFantasy = COFantasy || function() { // - oldTokenId // - newTokenId // - newToken - function finDEffet(attr, effet, attrName, charId, evt, options) { //L'effet arrive en fin de vie, doit \xEAtre supprim\xE9 - options = options || {}; + function finDEffet(attr, effet, attrName, charId, evt, options = {}) { //L'effet arrive en fin de vie, doit \xEAtre supprim\xE9 evt.deletedAttributes = evt.deletedAttributes || []; let res; let newInit = []; @@ -4664,9 +4766,9 @@ var COFantasy = COFantasy || function() { }; let resa = restoreTokenOfPerso(perso, evt); if (resa) { - setToken(res.newToken, 'width', token.get('width'), evt); - setToken(res.newToken, 'height', token.get('height'), evt); - token = res.newToken; + setToken(resa.newToken, 'width', token.get('width'), evt); + setToken(resa.newToken, 'height', token.get('height'), evt); + token = resa.newToken; res = resa; } let apv = tokenAttribute(perso, 'anciensPV'); @@ -5560,6 +5662,7 @@ var COFantasy = COFantasy || function() { if (persoEstPNJ(attaquant, options)) return computeArmeAtkPNJ(attaquant, x); let attDiv; let attCar; + let attBase = 1; switch (x) { case '@{ATKCAC}': attDiv = ficheAttributeAsInt(attaquant, 'ATKCAC_DIV', 0, options); @@ -5569,6 +5672,7 @@ var COFantasy = COFantasy || function() { } else { attCar = ficheAttribute(attaquant, 'ATKCAC_CARAC', '@{FOR}', options); } + attBase = ficheAttributeAsInt(attaquant, 'atkcac_base', 1, options); break; case '@{ATKTIR}': attDiv = ficheAttributeAsInt(attaquant, 'ATKTIR_DIV', 0, options); @@ -5578,6 +5682,7 @@ var COFantasy = COFantasy || function() { } else { attCar = ficheAttribute(attaquant, 'ATKTIR_CARAC', '@{DEX}', options); } + attBase = ficheAttributeAsInt(attaquant, 'atktir_base', 1, options); break; case '@{ATKMAG}': attDiv = ficheAttributeAsInt(attaquant, 'ATKMAG_DIV', 0, options); @@ -5588,13 +5693,14 @@ var COFantasy = COFantasy || function() { attCar = ficheAttribute(attaquant, 'ATKMAG_CARAC', '@{INT}', options); } attDiv += predicateAsInt(attaquant, 'bonusAttaqueMagique', 0); + attBase = ficheAttributeAsInt(attaquant, 'atkmag_base', 1, options); break; default: return x; } attCar = computeCarValue(attaquant, attCar, options); if (attCar === undefined) return x; - return attCar + ficheAttributeAsInt(attaquant, 'niveau', 1, options) + attDiv; + return attCar + attBase + attDiv; } function armeDeCreatureFeerique(perso, weaponStats, dice) { @@ -6212,7 +6318,7 @@ var COFantasy = COFantasy || function() { } }); } - if ((act.startsWith('!cof-lancer-sort') || act.startsWith('!cof-immunite-guerisseur')) && + if ((act.startsWith('!cof-lancer-sort ') || act.startsWith('!cof-immunite-guerisseur ') || act.startsWith('!cof-lumiere ')) && act.indexOf('--lanceur') == -1) { act += " --lanceur " + tid; } @@ -6949,6 +7055,11 @@ var COFantasy = COFantasy || function() { options.cacheBonusToutesCaracs.val = bonus; } } + let explications = []; + bonus += effetSalleForgeRunique(personnage, explications); + explications.forEach(function(msg) { + expliquer(msg); + }); return bonus; } @@ -8181,6 +8292,7 @@ var COFantasy = COFantasy || function() { } // callback(selected, playerId, aoe) + // selected est une liste d'objets avec un seul champ, _id qui est une id de token function getSelected(msg, callback, options) { options = options || {}; let playerId = getPlayerIdFromMsg(msg); @@ -8193,7 +8305,7 @@ var COFantasy = COFantasy || function() { let count = args.length - 1; let called; let actif = options.lanceur; - if (actif === undefined) { + if (actif === undefined && !options.pasDeLanceur) { if (msg.selected !== undefined && msg.selected.length == 1) { actif = persoOfId(msg.selected[0]._id, msg.selected[0]._id, pageId); } @@ -9620,52 +9732,101 @@ var COFantasy = COFantasy || function() { }; break; case 'effet': - if (cmd.length < 2) { - error("Il manque un argument \xE0 l'option --effet de !cof-attack", cmd); + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --effet de !cof-attack", cmd); + return; + } + let effet = cmd[1]; + if (cof_states[effet] && cmd.length > 2) { //remplacer par sa version effet temporaire + effet += 'Temp'; + } + if (estEffetTemp(effet)) { + let duree = 1; + if (cmd.length > 2) { + if (cmd[2] == 'fin') duree = 0; + else { + duree = parseInt(cmd[2]); + if (isNaN(duree) || duree < 1) { + error( + "Le deuxi\xE8me argument de --effet doit \xEAtre un nombre positif", + cmd); + return; + } + } + } + let m = messageOfEffetTemp(effet); + lastEtat = { + effet, + duree, + message: m, + typeDmg: lastType + }; + scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); + } else if (estEffetCombat(effet)) { + lastEtat = { + effet, + typeDmg: lastType, + message: messageEffetCombat[effet] + }; + } else if (estEffetIndetermine(effet)) { + lastEtat = { + effet, + effetIndetermine: true, + typeDmg: lastType, + message: messageEffetIndetermine[effet] + }; + } else { + error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + return; + } + scope.effets = scope.effets || []; + scope.effets.push(lastEtat); return; } - let effet = cmd[1]; - if (cof_states[effet] && cmd.length > 2) { //remplacer par sa version effet temporaire - effet += 'Temp'; - } - if (estEffetTemp(effet)) { - let duree = 1; - if (cmd.length > 2) { - duree = parseInt(cmd[2]); - if (isNaN(duree) || duree < 1) { - error( - "Le deuxi\xE8me argument de --effet doit \xEAtre un nombre positif", - cmd); - return; - } + case 'finEffet': + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --finEffet de !cof-attack", cmd); + return; } - let m = messageOfEffetTemp(effet); - lastEtat = { - effet: effet, - duree: duree, - message: m, - typeDmg: lastType - }; - scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); - } else if (estEffetCombat(effet)) { - lastEtat = { - effet: effet, - typeDmg: lastType, - message: messageEffetCombat[effet] - }; - } else if (estEffetIndetermine(effet)) { - lastEtat = { - effet: effet, - effetIndetermine: true, - typeDmg: lastType - }; - } else { - error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + let effet = cmd[1]; + if (cof_states[effet]) { //remplacer par sa version effet temporaire + effet += 'Temp'; + } + if (estEffetTemp(effet)) { + let m = messageOfEffetTemp(effet); + lastEtat = { + effet, + duree: 0, + finEffet: true, + message: m, + typeDmg: lastType + }; + scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); + } else if (estEffetCombat(effet)) { + lastEtat = { + effet, + finEffet: true, + typeDmg: lastType, + message: messageEffetCombat[effet] + }; + } else if (estEffetIndetermine(effet)) { + lastEtat = { + effet, + finEffet: true, + effetIndetermine: true, + typeDmg: lastType, + message: messageEffetIndetermine[effet] + }; + } else { + error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + return; + } + scope.effets = scope.effets || []; + scope.effets.push(lastEtat); return; } - scope.effets = scope.effets || []; - scope.effets.push(lastEtat); - return; case 'valeur': if (cmd.length < 2) { error("Il manque un argument \xE0 l'option --valeur de !cof-attack", cmd); @@ -9713,50 +9874,73 @@ var COFantasy = COFantasy || function() { return; case 'etatSi': case 'etat': - if (cmd.length < 3 && cmd[0] == 'etatSi') { - error("Il manque un argument \xE0 l'option --etatSi de !cof-attack", cmd); - return; - } else if (cmd.length < 2) { - error("Il manque un argument \xE0 l'option --etat de !cof-attack", cmd); - return; - } - let etat = cmd[1]; - if (!_.has(cof_states, etat)) { - error("Etat non reconnu", cmd); + { + if (cmd.length < 3 && cmd[0] == 'etatSi') { + error("Il manque un argument \xE0 l'option --etatSi de !cof-attack", cmd); + return; + } else if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --etat de !cof-attack", cmd); + return; + } + let etat = cmd[1]; + if (!_.has(cof_states, etat)) { + error("\xC9tat " + etat + " non reconnu", cmd); + return; + } + let condition = 'toujoursVrai'; + if (cmd[0] == 'etatSi') { + condition = parseCondition(cmd.slice(2)); + if (condition === undefined) return; + } + scope.etats = scope.etats || []; + lastEtat = { + etat, + condition, + typeDmg: lastType + }; + if (cmd[0] == 'etat' && cmd.length > 3) { + if (!isCarac(cmd[2]) && (cmd[2].length != 6 || + !isCarac(cmd[2].substring(0, 3)) || !isCarac(cmd[2].substring(3, 6)))) { + error("Caract\xE9ristique du jet de sauvegarde incorrecte", cmd); + return; + } + lastEtat.saveCarac = cmd[2]; + let opposition = persoOfId(cmd[3]); + if (opposition) { + lastEtat.saveDifficulte = cmd[3] + ' ' + nomPerso(opposition); + } else { + lastEtat.saveDifficulte = parseInt(cmd[3]); + if (isNaN(lastEtat.saveDifficulte)) { + error("Difficult\xE9 du jet de sauvegarde incorrecte", cmd); + delete lastEtat.saveCarac; + delete lastEtat.saveDifficulte; + } + } + } + scope.etats.push(lastEtat); return; } - let condition = 'toujoursVrai'; - if (cmd[0] == 'etatSi') { - condition = parseCondition(cmd.slice(2)); - if (condition === undefined) return; - } - scope.etats = scope.etats || []; - lastEtat = { - etat: etat, - condition: condition, - typeDmg: lastType - }; - if (cmd[0] == 'etat' && cmd.length > 3) { - if (!isCarac(cmd[2]) && (cmd[2].length != 6 || - !isCarac(cmd[2].substring(0, 3)) || !isCarac(cmd[2].substring(3, 6)))) { - error("Caract\xE9ristique du jet de sauvegarde incorrecte", cmd); + case 'finEtat': + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --finEtat de !cof-attack", cmd); return; } - lastEtat.saveCarac = cmd[2]; - let opposition = persoOfId(cmd[3]); - if (opposition) { - lastEtat.saveDifficulte = cmd[3] + ' ' + nomPerso(opposition); - } else { - lastEtat.saveDifficulte = parseInt(cmd[3]); - if (isNaN(lastEtat.saveDifficulte)) { - error("Difficult\xE9 du jet de sauvegarde incorrecte", cmd); - delete lastEtat.saveCarac; - delete lastEtat.saveDifficulte; - } + let etat = cmd[1]; + if (!_.has(cof_states, etat)) { + error("\xC9tat " + etat + " non reconnu", cmd); + return; } + let effet = etat + 'Temp'; + scope.effets = scope.effets || []; + lastEtat = { + effet, + duree: 0, + typeDmg: lastType + }; + scope.effets.push(lastEtat); + return; } - scope.etats.push(lastEtat); - return; case 'peur': if (cmd.length < 3) { error("Il manque un argument \xE0 l'option --peur de !cof-attack", cmd); @@ -10931,12 +11115,12 @@ var COFantasy = COFantasy || function() { if (options.effets) { options.effets.forEach(function(ef) { if (ef.effet) { - if (estEffetTemp(ef.effet)) { + if (estEffetTemp(ef.effet) && ef.effet.duree) { optMana.dm = optMana.dm || (ef.message && ef.message.dm); optMana.soins = optMana.soins || (ef.message && ef.message.soins); optMana.duree = true; } - } else if (estEffetCombat(ef.effet)) { + } else if (estEffetCombat(ef.effet) && !ef.effet.finEffet) { optMana.dm = optMana.dm || messageEffetCombat[ef.effet].dm; optMana.soins = optMana.soins || messageEffetCombat[ef.effet].soins; } @@ -12410,20 +12594,109 @@ var COFantasy = COFantasy || function() { return def; } - //Retourne un encodage des tailes : - // 1 : minuscule - // 2 : tr\xE8s petit - // 3 : petit - // 4 : moyen - // 5 : grand - // 6 : \xE9norme - // 7 : colossal - function taillePersonnage(perso, def) { - if (perso.taille) return perso.taille; - let taille = tailleNormale(perso, def); - if (attributeAsBool(perso, 'agrandissement')) taille++; - perso.taille = taille; - return taille; + function pechePositif(salle, explications) { + explications.push("se sent bien dans " + salle + " => +1"); + return 1; + } + + function pecheNegatif(salle, explications) { + explications.push("se sent mal dans " + salle + " => -2"); + return -2; + } + + //returns 0 if no effect, a positive number if positive and a negative number is negative + function effetSalleForgeRunique(perso, explications) { + if (!stateCOF.aileForgeRunique) return 0; + let peche = predicateAsBool(perso, 'pecheThassilion'); + if (!peche) return 0; + switch (stateCOF.aileForgeRunique) { + case 'paresse': + { + let salle = "le labyrinthe purulent"; + switch (peche) { + case 'paresse': + return pechePositif(salle, explications); + case 'colere': + case 'orgueil': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'avarice': + { + let salle = "le caveau de l'avarice"; + switch (peche) { + case 'avarice': + return pechePositif(salle, explications); + case 'luxure': + case 'orgueil': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'orgueil': + { + let salle = "les voiles scintillants"; + switch (peche) { + case 'orgueil': + return pechePositif(salle, explications); + case 'avarice': + case 'paresse': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'envie': + { + let salle = "les salles de l'envie"; + switch (peche) { + case 'envie': + return pechePositif(salle, explications); + case 'colere': + case 'gourmandise': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'colere': + { + let salle = "les salles de la col\xE8re"; + switch (peche) { + case 'colere': + return pechePositif(salle, explications); + case 'paresse': + case 'envie': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'gourmandise': + { + let salle = "les cryptes affam\xE9es"; + switch (peche) { + case 'gourmandise': + return pechePositif(salle, explications); + case 'envie': + case 'luxure': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'luxure': + { + let salle = "les cages de fer"; + switch (peche) { + case 'luxure': + return pechePositif(salle, explications); + case 'avarice': + case 'gourmandise': + return pecheNegatif(salle, explications); + } + return 0; + } + } + error("L'aile de forge runique est mal d\xE9finie " + stateCOF.aileForgeRunique, stateCOF); + return 0; } //tm doit \xEAtre stateCOF.tenebresMagiques, et bien d\xE9fini. @@ -13185,6 +13458,11 @@ var COFantasy = COFantasy || function() { messageAttaqueDM("Force drain\xE9e", explications, options, -2); } let energieImpie = attributeAsInt(attaquant, 'energieImpie', 0); + if (attributeAsBool(attaquant, 'malchance')) { + let malchance = getIntValeurOfEffet(attaquant, 'malchance', 1); + attBonus -= malchance; + messageAttaqueDM("Malchance", explications, options, -malchance); + } if (energieImpie) { attBonus += energieImpie; messageAttaqueDM("\xC9nergie impie", explications, options, energieImpie); @@ -13605,6 +13883,7 @@ var COFantasy = COFantasy || function() { explications.push(msgConditions); attBonus -= conditions; } + attBonus += effetSalleForgeRunique(attaquant, explications); return attBonus; } @@ -16223,6 +16502,9 @@ var COFantasy = COFantasy || function() { return true; } if (attributeAsBool(target, 'immuniteA' + dmgType)) return true; + if (options.tranchant && predicateAsBool(target, 'immunite_tranchant')) return true; + if (options.contondant && predicateAsBool(target, 'immunite_contondant')) return true; + if (options.percant && predicateAsBool(target, 'immunite_percant')) return true; switch (dmgType) { case 'acide': if (predicateAsBool(target, 'batonDesRunesMortes') && attributeAsBool(target, 'runeLizura')) return true; @@ -16292,8 +16574,7 @@ var COFantasy = COFantasy || function() { // - un texte affichant le jet de d\xE9g\xE2ts // - la valeur finale des d\xE9g\xE2ts inflig\xE9s // crit est un bool\xE9en, il augmente de 1 (ou options.critCoef) le coefficient (option.dmgCoef) et active certains effets - function dealDamage(target, dmg, otherDmg, evt, crit, options, explications, displayRes) { - if (options === undefined) options = {}; + function dealDamage(target, dmg, otherDmg, evt, crit, options = {}, explications = false, displayRes = false) { let expliquer = function(msg) { if (explications) explications.push(msg); else sendPerso(target, msg); @@ -19583,6 +19864,17 @@ var COFantasy = COFantasy || function() { save: true }); } + if (!options.redo && options.sortilege && options.dm && + predicateAsBool(target, 'immuniteMagieGolem') && + (weaponStats.name.includes('sint\xE9gration') || weaponStats.name.includes('sintegration')) + ) { + target.effets.push({ + effet: 'ralentiTemp', + duree: randomInteger(6), + message: messageOfEffetTemp('ralentiTemp') + }); + target.diviseDmg++; + } if (options.ecraser) { target.messages.push(nomPerso(attaquant) + " saisit " + nomPerso(target) + " entre ses bras puissants"); if (options.ecraser === true) { @@ -20327,6 +20619,29 @@ var COFantasy = COFantasy || function() { } if (effets) { effets.forEach(function(ef) { + if (ef.finEffet) { + let attr = tokenAttribute(target, ef.effet); + if (attr.length === 0) { + if (ef.effet.endsWith('Temp')) { + let etat = ef.effet.substring(0, ef.effet.length - 4); + if (!cof_states[etat]) return; + if (getState(target, etat)) { + setState(target, etat, false, evt); + target.messages.push(nomPerso(target) + ' ' + messageFin(target, ef.message)); + } + } + return; + } + attr = attr[0]; + let feOptions = { + print: function(msg) { + target.messages.push(nomPerso(target) + ' ' + msg); + } + }; + let f = finDEffet(attr, ef.effet, attr.get('name'), target.charId, evt, feOptions); + if (f && f.newToken) target.token = f.newToken; + return; + } if (((options.sortilege || options.mana !== undefined) && (predicateAsBool(target, 'liberteDAction') && ( ef.effet == 'apeureTemp' || @@ -20705,6 +21020,7 @@ var COFantasy = COFantasy || function() { chanceRollId: options.chanceRollId, type: ce.typeDmg, necromancie: estNecromancie(options), + bonus: predicateAsInt(target, 'bonusSaveContre_' + ce.etat, 0), }; let rollId = 'etat_' + ce.etat + index + '_' + target.token.id; save(ce.save, target, rollId, expliquer, saveOpts, evt, @@ -21570,6 +21886,7 @@ var COFantasy = COFantasy || function() { perso.dansAuraDeProfanation = res; return res; } + //s repr\xE9sente le save, avec une carac, une carac2 optionnelle et un seuil //expliquer est une fonction qui prend en argument un string et le publie // options peut contenir les champs : @@ -21624,6 +21941,11 @@ var COFantasy = COFantasy || function() { return; } } + if (options.magique && predicateAsBool(target, 'immuniteMagieGolem')) { + expliquer(nomPerso(target) + " n'est pas affect\xE9 par cette magie."); + afterSave(true, ''); + return; + } if (s.fauchage) { if (s.fauchage <= taillePersonnage(target, 4)) { expliquer(nomPerso(target) + " est trop grand pour \xEAtre fauch\xE9."); @@ -21673,6 +21995,11 @@ var COFantasy = COFantasy || function() { expliquer("Peau d'\xE9corce am\xE9lior\xE9e => +" + bonusPeau + " pour r\xE9sister au poison"); } } + let explications = []; + bonus += effetSalleForgeRunique(target, explications); + explications.forEach(function(msg) { + expliquer(nomPerso(target) + ' ' + msg); + }); let bonusAttrs = []; let bonusPreds = []; let seuil = s.seuil; @@ -22667,7 +22994,7 @@ var COFantasy = COFantasy || function() { current = 5 - n; if (current < 0) current = 0; } - attrPR = createObj("attribute", { + attrPR = createObj('attribute', { characterid: perso.charId, name: 'pr', current, @@ -23163,7 +23490,7 @@ var COFantasy = COFantasy || function() { let dmTemp; if (tmpHitAttr.length === 0) { dmTemp = - createObj("attribute", { + createObj('attribute', { characterid: charId, name: 'DMTEMP', current: 0, @@ -23645,7 +23972,7 @@ var COFantasy = COFantasy || function() { return toEvaluate.replace(/@{/g, "@{" + name + "|"); } - // Retourne le diam\xE8tre d'un disque inscrit dans un carr\xE9 de surface + // Retourne le diam\xE8tre en pixels d'un disque inscrit dans un carr\xE9 de surface // \xE9quivalente \xE0 celle du token function tokenSizeAsCircle(token) { const surface = token.get('width') * token.get('height'); @@ -24096,14 +24423,16 @@ var COFantasy = COFantasy || function() { sendChat('', action); } - function getTokenTemp(tt) { + function getTokenTemp(tt, pageId) { let token = getObj('graphic', tt.tid); if (!token) { if (!tt.name) return; - token = findObjs({ + let f = { _type: 'graphic', name: tt.name - }); + }; + if (pageId) f._pageid = pageId; + token = findObjs(f); if (token.length === 0) return; token = token[0]; } @@ -25162,6 +25491,9 @@ var COFantasy = COFantasy || function() { sendPlayer('GM', boutonSimple("!cof-effet-chaque-d20 " + ev + " fin", "Mettre fin") + stateCOF.effetAuD20[ev].nomFin + " ?"); } } + if (stateCOF.aileForgeRunique) { + sendPlayer('GM', boutonSimple("!cof-aile-forge-runique sortir", "Sortir") + "de la salle de " + stateCOF.aileForgeRunique + " ?"); + } let attrs = findObjs({ _type: 'attribute' }); @@ -29720,7 +30052,8 @@ var COFantasy = COFantasy || function() { msgRate: ", rat\xE9.", rolls: options.rolls, chanceRollId: options.chanceRollId, - type: options.type + type: options.type, + bonus: predicateAsInt(perso, 'bonusSaveContre_' + etat, 0), }; let expliquer = function(s) { sendPerso(perso, s); @@ -32992,7 +33325,7 @@ var COFantasy = COFantasy || function() { // si le consommable n'a pas \xE9t\xE9 trouv\xE9, on le cr\xE9e avec une valeur de nb if (!found) { let pref = 'repeating_equipement_' + generateRowID() + '_'; - let attre = createObj("attribute", { + let attre = createObj('attribute', { name: pref + 'equip_nom', current: nom, characterid: perso.charId @@ -34021,6 +34354,168 @@ var COFantasy = COFantasy || function() { const labelsEscalier = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]; + //esc est un token, le reste est optionnel + function trouveSortieEscalier(esc, versLeHaut, loop, escaliers, tmaps) { + let escName; //Contiendra le nom de l'escalier vers lequel aller + //On regarde d'abord le gmnote + let gmNotes = esc.get('gmnotes'); + try { + gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' '); + gmNotes = linesOfNote(gmNotes); + gmNotes.find(function(l) { + if (versLeHaut) { + if (l.startsWith('monte:')) { + escName = l.substring(6); + return true; + } + if (l.startsWith('monter:')) { + escName = l.substring(7); + return true; + } + if (l.startsWith('bas:')) { + escName = l.substring(4); + return true; + } + return false; + } else { + if (l.startsWith('descend:')) { + escName = l.substring(8); + return true; + } + if (l.startsWith('descendre:')) { + escName = l.substring(10); + return true; + } + if (l.startsWith('haut:')) { + escName = l.substring(5); + return true; + } + return false; + } + return false; + }); + } catch (uriError) { + log("Erreur de d\xE9codage URI dans la note GM de " + esc.get('name') + " : " + gmNotes); + } + let i; //index de label si on n'utilise pas gmnote + if (escName === undefined) { + //Si on n'a pas trouv\xE9, on regarde le nom + escName = esc.get('name'); + let l = escName.length; + if (l > 1) { + let label = escName.substr(l - 1, 1); + escName = escName.substr(0, l - 1); + i = labelsEscalier.indexOf(label); + if (versLeHaut) { + if (i == 11) { + if (loop) escName += labelsEscalier[0]; + } else escName += labelsEscalier[i + 1]; + } else { + if (i === 0) { + if (loop) escName += labelsEscalier[11]; + } else escName += labelsEscalier[i - 1]; + } + } + } + if (!escName) return; + //Ensuite on cherche l'escalier de nom escName + let escs = escaliers; + if (escName.startsWith('tmap_')) { + if (!tmaps) { + tmaps = findObjs({ + _type: 'graphic', + layer: 'gmlayer' + }); + tmaps = tmaps.filter(function(e) { + return e.get('name').startsWith('tmap_'); + }); + } + escs = tmaps; + } + if (!escs) { + let pageId = esc.get('pageid'); + escs = findObjs({ + _type: 'graphic', + _pageid: pageId, + layer: 'gmlayer' + }); + } + let sortieEscalier = escs.find(function(esc2) { + return esc2.get('name') == escName; + }); + if (sortieEscalier === undefined && i !== undefined && loop) { + if (i > 0) { //sortie par le plus petit + escName = escName.substr(-1) + 'A'; + sortieEscalier = escs.find(function(esc2) { + return esc2.get('name') == escName; + }); + } else { + sortieEscalier = findEsc(escs, escName.substr(-1), 10); + } + } + return { + sortieEscalier, + tmaps + }; + } + + function prendreEscalier(perso, pageId, sortieEscalier) { + let token = perso.token; + let left = sortieEscalier.get('left'); + let top = sortieEscalier.get('top'); + let newPageId = sortieEscalier.get('pageid'); + //D\xE9placement du token + if (newPageId == pageId) { + token.set('left', left); + token.set('top', top); + } else { + //On change de carte, il faut donc copier le token + let tokenObj = JSON.parse(JSON.stringify(token)); + tokenObj._pageid = newPageId; + //On met la taille du token \xE0 jour en fonction des \xE9chelles des cartes. + let ratio = computeScale(pageId) / computeScale(newPageId); + if (ratio < 0.9 || ratio > 1.1) { + if (ratio < 0.25) ratio = 0.25; + else if (ratio > 4) ratio = 4; + tokenObj.width *= ratio; + tokenObj.height *= ratio; + } + tokenObj.imgsrc = normalizeTokenImg(tokenObj.imgsrc); + tokenObj.left = left; + tokenObj.top = top; + let newToken = createObj('graphic', tokenObj); + if (newToken === undefined) { + error("Impossible de copier le token, et donc de faire le changement de carte", tokenObj); + return; + } + } + //On d\xE9place ensuite le joueur. + let character = getObj('character', perso.charId); + if (character === undefined) return; + let charControlledby = character.get('controlledby'); + if (charControlledby === '') { + //Seul le MJ contr\xF4le le personnage + let players = findObjs({ + _type: 'player', + online: true + }); + let gm = players.find(function(p) { + return playerIsGM(p.id); + }); + if (gm) { + if (newPageId != pageId) movePlayerToPage(gm.id, pageId, newPageId); + sendPing(left, top, newPageId, gm.id, true, gm.id); + } + } else { + charControlledby.split(",").forEach(function(pid) { + if (newPageId != pageId) movePlayerToPage(pid, pageId, newPageId); + sendPing(left, top, newPageId, pid, true, pid); + }); + } + //Enfin, on efface le token de d\xE9part si on a chang\xE9 de page + if (newPageId != pageId) token.remove(); + } + function escalier(msg) { getSelected(msg, function(selected, playerId) { if (selected.length === 0) { @@ -34061,151 +34556,15 @@ var COFantasy = COFantasy || function() { if (sortieEscalier) return; if (intersection(posX, sizeX, esc.get('left'), esc.get('width')) && intersection(posY, sizeY, esc.get('top'), esc.get('height'))) { - let escName; //Contiendra le nom de l'escalier vers lequel aller - //On regarde d'abord le gmnote - let gmNotes = esc.get('gmnotes'); - try { - gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' '); - gmNotes = linesOfNote(gmNotes); - gmNotes.find(function(l) { - if (versLeHaut) { - if (l.startsWith('monte:')) { - escName = l.substring(6); - return true; - } - if (l.startsWith('monter:')) { - escName = l.substring(7); - return true; - } - if (l.startsWith('bas:')) { - escName = l.substring(4); - return true; - } - return false; - } else { - if (l.startsWith('descend:')) { - escName = l.substring(8); - return true; - } - if (l.startsWith('descendre:')) { - escName = l.substring(10); - return true; - } - if (l.startsWith('haut:')) { - escName = l.substring(5); - return true; - } - return false; - } - return false; - }); - } catch (uriError) { - log("Erreur de d\xE9codage URI dans la note GM de " + esc.get('name') + " : " + gmNotes); - } - let i; //index de label si on n'utilise pas gmnote - if (escName === undefined) { - //Si on n'a pas trouv\xE9, on regarde le nom - escName = esc.get('name'); - let l = escName.length; - if (l > 1) { - let label = escName.substr(l - 1, 1); - escName = escName.substr(0, l - 1); - i = labelsEscalier.indexOf(label); - if (versLeHaut) { - if (i == 11) { - if (loop) escName += labelsEscalier[0]; - } else escName += labelsEscalier[i + 1]; - } else { - if (i === 0) { - if (loop) escName += labelsEscalier[11]; - } else escName += labelsEscalier[i - 1]; - } - } - } - if (!escName) return; - //Ensuite on cherche l'escalier de nom escName - let escs = escaliers; - if (escName.startsWith('tmap_')) { - if (!tmaps) { - tmaps = findObjs({ - _type: 'graphic', - layer: 'gmlayer' - }); - tmaps = tmaps.filter(function(e) { - return e.get('name').startsWith('tmap_'); - }); - } - escs = tmaps; - } - sortieEscalier = escs.find(function(esc2) { - return esc2.get('name') == escName; - }); - if (sortieEscalier === undefined && i !== undefined && loop) { - if (i > 0) { //sortie par le plus petit - escName = escName.substr(-1) + 'A'; - sortieEscalier = escs.find(function(esc2) { - return esc2.get('name') == escName; - }); - } else { - sortieEscalier = findEsc(escs, escName.substr(-1), 10); - } + let s = trouveSortieEscalier(esc, versLeHaut, loop, escaliers, tmaps); + if (s) { + sortieEscalier = s.sortieEscalier; + tmaps = s.tmaps; } } }); if (sortieEscalier) { - let left = sortieEscalier.get('left'); - let top = sortieEscalier.get('top'); - let newPageId = sortieEscalier.get('pageid'); - //D\xE9placement du token - if (newPageId == pageId) { - token.set('left', left); - token.set('top', top); - } else { - //On change de carte, il faut donc copier le token - let tokenObj = JSON.parse(JSON.stringify(token)); - tokenObj._pageid = newPageId; - //On met la taille du token \xE0 jour en fonction des \xE9chelles des cartes. - let ratio = computeScale(pageId) / computeScale(newPageId); - if (ratio < 0.9 || ratio > 1.1) { - if (ratio < 0.25) ratio = 0.25; - else if (ratio > 4) ratio = 4; - tokenObj.width *= ratio; - tokenObj.height *= ratio; - } - tokenObj.imgsrc = normalizeTokenImg(tokenObj.imgsrc); - tokenObj.left = left; - tokenObj.top = top; - let newToken = createObj('graphic', tokenObj); - if (newToken === undefined) { - error("Impossible de copier le token, et donc de faire le changement de carte", tokenObj); - return; - } - } - //On d\xE9place ensuite le joueur. - let character = getObj('character', perso.charId); - if (character === undefined) return; - let charControlledby = character.get('controlledby'); - if (charControlledby === '') { - //Seul le MJ contr\xF4le le personnage - let players = findObjs({ - _type: 'player', - online: true - }); - let gm = players.find(function(p) { - return playerIsGM(p.id); - }); - if (gm) { - if (newPageId != pageId) movePlayerToPage(gm.id, pageId, newPageId); - sendPing(left, top, newPageId, gm.id, true, gm.id); - } - } else { - charControlledby.split(",").forEach(function(pid) { - if (newPageId != pageId) movePlayerToPage(pid, pageId, newPageId); - sendPing(left, top, newPageId, pid, true, pid); - }); - } - //Enfin, on efface le token de d\xE9part si on a chang\xE9 de page - if (newPageId != pageId) token.remove(); + prendreEscalier(perso, pageId, sortieEscalier); return; } let err = nomPerso(perso) + " n'est pas sur un escalier"; @@ -34218,6 +34577,100 @@ var COFantasy = COFantasy || function() { }); //fin getSelected } + function removeTokenActif(tid, pageId) { + let ta = stateCOF.tokensActifs; + ta[pageId] = ta[pageId].filter(function(tt) { + return tt.tid != tid; + }); + if (ta[pageId] == []) delete ta[pageId]; + } + + function mettreFinATPAuto(msg, options) { + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Aucun token s\xE9lectionn\xE9 pour !cof-tp-auto off", playerId); + return; + } + let ta = stateCOF.tokensActifs; + if (ta) return; + selected.forEach(function(sel) { + let token = getObj('graphic', sel._id); + if (token === undefined) return; + let pageId = token.get('pageid'); + if (!ta[pageId]) { + sendPlayer(msg, token.get('name') + " n'\xE9tait pas en TP automatique", playerId); + return; + } + removeTokenActif(token.id, pageId); + sendPlayer(msg, token.get('name') + " n'est plus en TP automatique", playerId); + }); + }, { + pasDeLanceur: true + }); + } + + //!cof-tp-auto ['off'|rayon] + function setTPAuto(msg) { + let options = parseOptions(msg); + if (options === undefined) return; + let cmd = options.cmd; + if (cmd === undefined) { + error("Probl\xE8me de parse options", msg.content); + return; + } + let r; + if (cmd.length > 1) { + if (cmd[1] == 'off') { + mettreFinATPAuto(msg, options); + return; + } + r = parseFloat(cmd[1]); + if (isNaN(r) || r < 0) { + sendPlayer(msg, "Argument de !cof-tp-auto invalide ( " + cmd[1] + ")"); + return; + } + } + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Aucun token s\xE9lectionn\xE9 pour !cof-tp-auto", playerId); + return; + } + selected.forEach(function(sel) { + let token = getObj('graphic', sel._id); + if (token === undefined) return; + if (token.get('layer') != 'gmlayer') { + sendPlayer(msg, "TP ne peut concerner que des tokens du layer MJ", playerId); + return; + } + stateCOF.tokensActifs = stateCOF.tokensActifs || {}; + let ta = stateCOF.tokensActifs; + let pageId = token.get('pageid'); + ta[pageId] = ta[pageId] || []; + let present = ta[pageId].find(function(tt) { + return tt.tid == token.id; + }); + //On converti la distance d'intrusion en pixels + let scale = computeScale(pageId); + let rayon = (r / scale) * PIX_PER_UNIT; + if (present) { + present.rayon = rayon; + sendPlayer(msg, "Le rayon de " + token.get('name') + " devient " + r, playerId); + return; + } + //token pas d\xE9j\xE0 dans la liste des tokens actifs + let tt = { + tid: token.id, + name: token.get('name'), + rayon + }; + ta[pageId].push(tt); + sendPlayer(msg, token.get('name') + " devient actif, de rayon " + r, playerId); + }); + }, { + pasDeLanceur: true + }); + } + function defautDansLaCuirasse(msg) { let args = msg.content.split(' '); if (args.length < 3) { @@ -34842,10 +35295,11 @@ var COFantasy = COFantasy || function() { let d20roll = attackRoll.results.total; effetAuD20(lanceur, d20roll); let msg = buildinline(attackRoll); - let attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); - if (estAffaibli(lanceur) && predicateAsBool(lanceur, 'insensibleAffaibli')) attBonus -= 2; + let attBonus; switch (typeAttaque) { case 'distance': + attBonus = ficheAttributeAsInt(lanceur, 'atktir_base', 1); + attBonus = computeArmeAtk(lanceur, '@ATKTIR'); attBonus += ficheAttributeAsInt(lanceur, 'ATKTIR_DIV', 0); if (persoArran(lanceur)) { attBonus += ficheAttributeAsInt(lanceur, 'mod_atktir', 0); @@ -34853,6 +35307,7 @@ var COFantasy = COFantasy || function() { attBonus += modCarac(lanceur, carac); break; case 'magique': + attBonus = ficheAttributeAsInt(lanceur, 'atkmag_base', 1); attBonus += ficheAttributeAsInt(lanceur, 'ATKMAG_DIV', 0); if (persoArran(lanceur)) { attBonus += ficheAttributeAsInt(lanceur, 'mod_atkmag', 0); @@ -34863,16 +35318,19 @@ var COFantasy = COFantasy || function() { attBonus += predicateAsInt(lanceur, 'bonusAttaqueMagique', 0); break; case 'contact': + attBonus = ficheAttributeAsInt(lanceur, 'atkcaca_base', 1); attBonus += ficheAttributeAsInt(lanceur, 'ATKCAC_DIV', 0); attBonus += modCarac(lanceur, carac); break; case 'esquive': + attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); attBonus += predicateAsInt(lanceur, 'reflexesFelins', 0); attBonus += predicateAsInt(lanceur, 'esquiveVoleur', 0); attBonus += predicateAsInt(lanceur, 'esquive', 0); attBonus += modCarac(lanceur, carac); break; default: + attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); } let weaponStats; if (opt && opt.arme && lanceur.arme) { @@ -36175,7 +36633,7 @@ var COFantasy = COFantasy || function() { if (!found) { if (m1) { let pref = 'repeating_equipement_' + generateRowID() + '_'; - let attre = createObj("attribute", { + let attre = createObj('attribute', { name: pref + 'equip_nom', current: consName, characterid: perso2.charId @@ -36183,7 +36641,7 @@ var COFantasy = COFantasy || function() { evt.attributes.push({ attribute: attre, }); - attre = createObj("attribute", { + attre = createObj('attribute', { name: pref + 'equip_effet', current: effet, characterid: perso2.charId @@ -36192,7 +36650,7 @@ var COFantasy = COFantasy || function() { attribute: attre, }); } else { - let attr2 = createObj("attribute", { + let attr2 = createObj('attribute', { name: attrName, current: 1, max: effet, @@ -37475,7 +37933,7 @@ var COFantasy = COFantasy || function() { } let druide = persoOfId(cmd[1], cmd[1], options.pageId); if (druide === undefined) { - error("Le premier argument de !cof-animer-arbre n'est pas un token valie", cmd); + error("Le premier argument de !cof-animer-arbre n'est pas un token valide", cmd); return; } let tokenArbre = getObj('graphic', cmd[2]); @@ -37484,7 +37942,7 @@ var COFantasy = COFantasy || function() { return; } if (tokenArbre.get('represents') !== '') { - sendPerso(druide, "ne peut pas animer " + tokenArbre.get('name')); + sendPerso(druide, "ne peut pas animer " + tokenArbre.get('name')+", car il repr\xE9sente d\xE9j\xE0 un personnage."); return; } if (options.portee !== undefined) { @@ -42522,6 +42980,57 @@ var COFantasy = COFantasy || function() { } } + /* !cof-aile-forge-runique */ + function entrerAileForgeRunique(msg) { + let cmd = msg.content.split(' '); + cmd = cmd.filter(function(c) { + return c.trim() !== ''; + }); + if (cmd.length < 2) { + if (stateCOF.aileForgeRunique) { + sendPlayer('GM', "Les personnage sont dans la salle de " + stateCOF.aileForgeRunique); + } else { + sendPlayer('GM', "Les personnages ne sont pas dans les forges runiques."); + } + return; + } + let b; + switch (cmd[1]) { + case 'paresse': + case 'orgueil': + case 'gourmandise': + case 'colere': + case 'luxure': + case 'avarice': + case 'envie': + b = cmd[1]; + break; + case 'col\xE8re': + b = 'colere'; + break; + case 'non': + case 'sortir': + case 'false': + case 'fin': + b = false; + break; + default: + error("Option de !cof-aile-forge-runique non reconnue", cmd); + return; + } + if (b) { + if (stateCOF.aileForgeRunique == b) { + sendPlayer('GM', "Les personnages sont d\xE9j\xE0 dans la salle de " + b); + return; + } + sendPlayer('GM', "Les personnages entrent dans la salle de " + b); + stateCOF.aileForgeRunique = b; + } else { + sendPlayer('GM', "Les personnages sortent de la salle de " + stateCOF.aileForgeRunique); + delete stateCOF.aileForgeRunique; + } + } + function fioleDeLumiere(msg) { let cmd = msg.content.split(' '); let tm = stateCOF.tenebresMagiques; @@ -42792,6 +43301,12 @@ var COFantasy = COFantasy || function() { case 'rock catching': notes += d + '\n'; return; + case 'division': + predicats += 'divisionVase '; + return; + case 'evasion': + predicats += 'esquiveDeLaMagie '; + return; default: if (d.startsWith('channel resistance ')) { let resChannel = parseInt(d.substring(19)); @@ -42925,17 +43440,33 @@ var COFantasy = COFantasy || function() { case 'sleep': predicats += 'immunite_endormi '; return; + case 'stunning': + predicats += 'immunite_etourdi '; + return; case 'construct': predicats += 'sansEsprit creatureArtificielle immuniteSaignement '; return; case 'blindness': predicats += 'immunite_aveugle '; return; + case 'elemental': + predicats += 'nonVivant '; + return; + case 'ooze': + predicats += 'immunite_endormi immunite_paralyse immunite_etourdi immunite_percant immunite_tranchant '; + return; + case 'magic': + predicats += 'immuniteMagieGolem '; + return; + case 'critical': + predicats += 'immuniteAuxCritiques '; + return; case 'undead': case 'traits': case 'effects': case 'plants': case 'and': + case 'hits': return; default: log("Immunit\xE9 \xE0 " + i + " non trait\xE9e"); @@ -43024,7 +43555,7 @@ var COFantasy = COFantasy || function() { let rdn = attr.get('current'); if (rdn) { rdn = '' + rdn; - rdn = rdn.replace('bludgeoning', 'contondant').replace('slashing', 'tranchant').replace('piercing', 'percant').replace('silver', 'argent').replace('magic', 'magique').replace('adamantine', 'adamantium').replace('cold iron', 'ferFroid').replace('/-', ''); + rdn = rdn.replace('bludgeoning', 'contondant').replace('slashing', 'tranchant').replace('piercing', 'percant').replace('silver', 'argent').replace('magic', 'magique').replace('adamantine', 'adamantium').replace('cold iron', 'ferFroid').replace(' or ', '_').replace('good', 'beni').replace('/-', '').replace('/\xD1', ''); //\xD1 est le tiret long if (rd === '') rd = rdn; else rd += ', ' + rdn; } @@ -43064,6 +43595,9 @@ var COFantasy = COFantasy || function() { case 'plant': predicats += 'plante vegetatif '; break; + case 'ooze': + predicats += 'vegetatif '; + break; default: if (npcType.includes('humanoid')) { predicats += 'humanoide '; @@ -46229,6 +46763,7 @@ var COFantasy = COFantasy || function() { return copyOldTokenToNewToken(tokenMJ[0], perso, evt); } + //Change le token de perso en nouveauToken function copyOldTokenToNewToken(nouveauToken, perso, evt) { let token = perso.token; setToken(nouveauToken, 'layer', 'objects', evt); @@ -46428,6 +46963,48 @@ var COFantasy = COFantasy || function() { }, options); } + // !cof-division-vase + function divisionVase(msg) { + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Pas de vase \xE0 diviser", playerId); + return; + } + const evt = { + type: 'division de vase' + }; + addEvent(evt); + iterSelected(selected, function(perso) { + let pv = parseInt(perso.token.get('bar1_value')); + if (isNaN(pv) || pv < 11) { + sendPlayer(msg, "Pas assez de PV (" + pv + ") pour se diviser", playerId); + return; + } + let pv_max = parseInt(perso.token.get('bar1_max')); + if (isNaN(pv_max) || pv_max < pv) pv_max = pv; + pv = Math.floor(pv / 2); + pv_max = Math.floor(pv_max / 2); + updateCurrentBar(perso, 1, pv, evt, pv_max); + // on diminue la taille + let width = perso.token.get('width'); + let height = perso.token.get('height'); + width = Math.ceil(width * 0.9); + height = Math.ceil(height * 0.9); + setToken(perso.token, 'width', width, evt); + setToken(perso.token, 'height', height, evt); + let tokenFields = getTokenFields(perso.token); + tokenFields.imgsrc = thumbImage(tokenFields.imgsrc); + let token2 = createObj('graphic', tokenFields); + if (!token2) { + sendPlayer(msg, "Pas moyen de copier, le faire \xE0 la main", playerId); + } else { + sendPlayer(msg, "token cr\xE9\xE9. Le bouger et le renommer", playerId); + } + sendPerso(perso, "se divise en deux"); + }); + }); + } + //n'est appel\xE9 qui si msg.content commence par !cof- function apiCommand(msg) { msg.content = msg.content.replace(/\s+/g, ' '); //remove duplicate whites @@ -46444,6 +47021,9 @@ var COFantasy = COFantasy || function() { case '!cof-agrandir-page': agrandirPage(msg); return; + case '!cof-aile-forge-runique': + entrerAileForgeRunique(msg); + return; case '!cof-animation-des-objets': animationDesObjets(msg); return; @@ -46495,6 +47075,9 @@ var COFantasy = COFantasy || function() { case '!cof-degainer': parseDegainer(msg); return; + case '!cof-division-vase': + divisionVase(msg); + return; case '!cof-dmg': parseDmgDirects(msg); return; @@ -46504,6 +47087,9 @@ var COFantasy = COFantasy || function() { case '!cof-effet-chaque-d20': setEffetChaqueD20(msg); return; + case "!cof-effet-combat": + effetCombat(msg); + return; case '!cof-effet-temp': parseEffetTemporaire(msg); return; @@ -46521,6 +47107,9 @@ var COFantasy = COFantasy || function() { case '!cof-explosion': attaqueExplosion(msg); return; + case '!cof-fin-changement-de-forme': + finChangementDeForme(msg); + return; case '!cof-hors-combat': //ancienne syntaxe, plus document\xE9e case '!cof-fin-combat': sortirDuCombat(); @@ -46601,9 +47190,6 @@ var COFantasy = COFantasy || function() { case '!cof-retour-boomerang': retourBoomerang(msg); return; - case '!cof-fin-changement-de-forme': - finChangementDeForme(msg); - return; case "!cof-rune-protection": runeProtection(msg); return; @@ -46645,6 +47231,9 @@ var COFantasy = COFantasy || function() { case '!cof-tenebres-magiques': tenebresMagiques(msg); return; + case '!cof-tp-auto': + setTPAuto(msg); + return; case '!cof-undo': undoEvent(); return; @@ -46654,9 +47243,6 @@ var COFantasy = COFantasy || function() { case '!cof-zone-de-vie': lancerZoneDeVie(msg); return; - case "!cof-effet-combat": - effetCombat(msg); - return; case "!cof-effet": parseEffetIndetermine(msg); return; @@ -47169,6 +47755,14 @@ var COFantasy = COFantasy || function() { effetTempGenerique: { generic: true }, + endormiTemp: { + activation: "s'endort", + actif: "dort profond\xE9ment", + fin: "se r\xE9veille", + msgSave: "r\xE9sister au sommeil", + prejudiciable: true, + visible: true + }, etourdiTemp: { activation: "est \xE9tourdi : aucune action et -5 en DEF", activationF: "est \xE9tourdie : aucune action et -5 en DEF", @@ -48269,6 +48863,12 @@ var COFantasy = COFantasy || function() { actif: "est en furie draconide", fin: "retrouve son calme" }, + malchance: { + activation: "entre dans une aura de malchance", + actif: "est dans une aura de malchance", + fin: "n'est plus dans une aura de malchance", + prejudiciable: true, + }, protectionContreLeMal: { activation: "re\xE7oit une b\xE9n\xE9diction de protection contre le mal", actif: "est prot\xE9g\xE9 contre le mal", @@ -50146,8 +50746,9 @@ var COFantasy = COFantasy || function() { stateCOF.tokensTemps.forEach(function(tt) { if (!tt.intrusion) return; //tt.intrusion est exprim\xE9 en pixels - let bombe = getTokenTemp(tt); + let bombe = getTokenTemp(tt, pageId); if (!bombe) return; + if (bombe.get('pageid') != pageId) return; let pb = pointOfToken(bombe); let distance = distancePoints(pt_depart, pb); if (distance < tt.intrusion) return; //On est parti de la zone de d\xE9part @@ -50182,6 +50783,43 @@ var COFantasy = COFantasy || function() { }); } } + if (stateCOF.tokensActifs && stateCOF.tokensActifs[pageId]) { + let pt_arrivee = { + x, + y + }; + let pt_depart = { + x: prev.left, + y: prev.top + }; + let rayon = tokenSizeAsCircle(token) / 2; + let estTP; + stateCOF.tokensActifs[pageId].forEach(function(tt) { + if (estTP) return; + let tp = getTokenTemp(tt, pageId); + if (!tp) { + removeTokenActif(tt.id, pageId); + return; + } + if (tt.rayon === undefined) { + if (!(intersection(x, token.get('width'), tp.get('left'), tp.get('width')) && + intersection(y, token.get('height'), tp.get('top'), tp.get('height')))) return; + } else { + let pb = pointOfToken(tp); + let distance = distancePoints(pt_depart, pb); + if (distance < tt.rayon) return; //On est parti de la zone de d\xE9part + let distToTrajectory = + distancePixTokenSegment(tp, pt_depart, pt_arrivee); + if (distToTrajectory > tt.rayon + rayon) return; + } + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + if (!s || !s.sortieEscalier) return; + prendreEscalier(perso, pageId, s.sortieEscalier); + estTP = true; + }); + if (estTP) return; + } //Effets des auras, asynchrone if (stateCOF.combat && stateCOF.combat.auras) { const evt = { @@ -51232,8 +51870,7 @@ var COFantasy = COFantasy || function() { } token.bar1_link = attrPV[0].id; token.pageid = pageId; - token.imgsrc = token.imgsrc.replace('/med.png', '/thumb.png'); - token.imgsrc = token.imgsrc.replace('/max.png', '/thumb.png'); + token.imgsrc = thumbImage(token.imgsrc); let newToken = createObj('graphic', token); if (newToken) { setDefaultTokenForCharacter(character, newToken); diff --git a/COFantasy/3.15/doc.html b/COFantasy/3.15/doc.html index 5affba2677..6a9f0c23aa 100644 --- a/COFantasy/3.15/doc.html +++ b/COFantasy/3.15/doc.html @@ -397,7 +397,9 @@

Options pour l'attaque :

  • --decrLimitePredicatParTour nom : l'attaque n'est possible que si un prédicat nom existe et si elle n'a pas été utilisée plus de fois dans le tour que la valeur de ce prédicat. L'attaque augmente ce nombre de 1.
  • --tempsRecharge effet duree : l'attaque n'est possible que si l'effet est inactif sur l'attaquant, et de plus active l'effet sur l'attaquant pour la durée indiquée si l'attaque est possible. Il existe un effet temporaire générique, rechargeGen(desc) que vous pouvez utiliser si aucun effet existant ne correspond pour votre attaque.
  • --etat e: si l'attaque touche, la cible passe dans l'état e. Il est aussi possible de spécifier une caractéristique et un seuil (comme pour !cof-set-state) pour faire afficher à chaque tour une action permettant de se libérer de l'état.
  • +
  • --finEtat e: si l'attaque touche, la cible sort de l'état e.
  • --effet e duree : ajoute à la cible l'effet temporaire e pour la duree spécifiée. Pour que cela soit automatiquement mis à jour, il faut utiliser le turn tracker. Noter que l'argument de durée peut être omis pour certains effets, comme ceux qui par définition durent tout le combat.
  • +
  • --finEffet e : enlève à la cible l'effet temporaire e.
  • --valeur v : spécifie une valeur au dernier effet mentionné.
  • --optionEffet opt arg1 arg2 ... : spécifie une option au dernier effet mentionné. L'option opt doit être donnée sans le -- et sera passée telle quelle à l'effet, par exemple pour les dégâts périodiques, si on ne veut pas que la RD s'applique, on pourra ajouter --optionEffet ignoreRD.
  • --peur seuil duree : fait un effet de peur si l'attaque touche. Le seuil sert pour le test de sagesse de résistance à la peur (tient compte de la capacité sans peur du chevalier).
  • @@ -909,8 +911,9 @@

    Escaliers et portails

    Chaque escalier de la même colonne doit avoir le même nom, et différer seulement par la lettre finale. Ensuite, si un ensemble de tokens est sur un escalier, utilisez !cof-escalier pour téléporter les tokens à l'emplacement de l'escalier suivant dans l'ordre des lettres. Si vous utilisez l'argument haut, alors les tokens n'iront pas plus loin que le dernier étage, et avec l'argument bas, il iront dans l'ordre inverse.

    Dans l'exemple précédent, il serait téléporté à l'emplacement du token nommé EscalierB. Cela ne fonctionne que si les bouts de l'escalier sont sur la même carte. À vous de choisir sir vous préférez révéler cette fonctionalité à vos joueurs, ou si vous le faites vous-même.

    La limite au nombre d'escaliers est de 12 étages, donc pas de lettre après L.

    -

    Il est possible d'avoir des escaliers qui mènent à d'autres cartes (ce qui fait alors changer la carte vue par le joueur). Il suffit que le nom de l'escalier commence par tmap_. Attention, à cause de limitations de Roll20, cela ne peut fonctionner que si l'image du token est dans une library personnelle d'un joueur : si elle vient du marketplace, il fait d'abord la copier dans sa library, puis utiliser l'image qui est dans la library pour le token.

    +

    Il est possible d'avoir des escaliers qui mènent à d'autres cartes (ce qui fait alors changer la carte vue par le joueur). Il suffit que le nom de l'escalier commence par tmap_. Attention, à cause de limitations de Roll20, cela ne peut fonctionner que si l'image du token est dans une library personnelle d'un joueur : si elle vient du marketplace, il faut d'abord la copier dans sa library, puis utiliser l'image qui est dans la library pour le token.

    Pour des escaliers plus flexibles (voire des portails), on peut décrire dans le champ des Notes du MJ du token sur la couche MJ une destination quand on monte et une destination quand on descend. Il suffit de mettre sur une ligne, descend: directement suivi du nom du token vers lequel aller pour descendre, et/ou sur une autre ligne monte: suivi du nom du token vers lequel aller quand on monte. Par exemple, pour bloquer un escalier qui descend, il suffit de mettre descend: dans le champ de notes du MJ. On peut aussi faire des "escaliers" à sens unique.

    +

    Il est possible d'automatiser les escaliers, de façons à ce que les tokens qui passent sur le lieux de l'escalier soient automatiquement transportés. Pour cela, sélectionner le token de l'escalier et lancer la commande !cof-tp-auto. Dans sa version sans argument, le dépacement automatique aura lieu dès qu'un token sera dans le rectangle du token de l'escalier. Sinon, on peut préciser un rayon (en mètres), et le mouvement aura lieur dès que le token sera à moins de cette distance du centre de l'escalier. Le déplacement se fera vers le haut si c'est possible, et sinon vers le bas. À noter que Roll20 peut perdre parfois les données sur les tokens entre les parties. Quand c'est le cas, le script va s'efforce de rerouver les escaliers automatiques, mais ça a plus de chances de marcher si vous donnez un nom unique au token de votre escalier. Pour mettre fin au déplacement automatique sur un escalier, sélectionner le token de l'escalier et taper la commande !cof-tp-auto off.

    Montures

    @@ -2494,6 +2497,7 @@

    4.6 Capacités diverses

  • Faucheuse de géants: ajouter le modifcateur tueurDeGrands aux attaques.
  • Fièvre du chêne : pour ajouter encore une RD de 5 contre le feu, ajouter un prédicat fievreChene.
  • Fiévreux : !cof-effet fievreux donne -2 en attaque, au dégâts et aux tests.
  • +
  • Forge runique (l'éveil des seigneurs des runes) : pour entrer dans une salle associée à un péché, utiliser !cof-aile-forge-runique, suivi du nom du péché (sans majuscule ni accent). Pour associer un péché à un personnage, lui ajouter un prédicat pecheThassilion de valeur le péché, toujours en minusucles et sans accent.
  • Foudres du temps : !cof-effet-chaque-d20 foudresDuTemps permet d'activer les foudres du temps. On peut donner un argument pour remplacer la valeur par défaut (13) par un autre jet de dés. Un deuxième argument permet de faire sortir la foudre pour tout jet entre les deux valeurs. Par exemple !cof-effet-chaque-d20 foudresDuTemps 13 14 fait venir la foudre pour tout jet de 13 ou 14. Si votre jeu contient un son (ou playlist) nommé Foudre (avec la majuscule), le son sera joué à chaque fois que la foudre frappera. Finalement, !cof-effet-chaque-d20 foudresDuTemps fin met fin aux foudres du temps.
  • Frénésie : Ajouter un prédicat frenesie avec comme valeur le nombre de PV en-dessous duquel la créature devient frénétique (+2 aus jets d'attaque).
  • Général (pour la campagne Vengeance) : Ajouter au général un prédicat generalVengeance, et aux gardes d'élite un prédicat gardeEliteVengeance.
  • @@ -2502,6 +2506,12 @@

    4.6 Capacités diverses

  • Huile de fort assaut : !cof-effet-temp dmgArme(L) 10 --valeur 1d6 où L est le label de l'arme qu'on veut enduire d'huile de fort assaut.
  • Hurlement : Pour les chiens infernaux, !cof-peur [[10+?{Nombre de chiens?}]] 2d4 --titre Hurlement --immuniseSiResiste hurlement --disque 50 --saufAllies, et pour les fantômes, !cof-peur 12 1d6 --titre Hurlement terrible --immuniseSiResiste hurlement --disque 10 --saufAllies.
  • Illusion : Pour les illusions qui disparaissent et réapparaissent un peu plus loin quand on essaie de les toucher, ajouter un prédicat estUneIllusion.
  • +
  • Immunité à la magie : +
      +
    • Immunité totale : prédicat immunite_magique.
    • +
    • Immunité à la magie des golems : prédicat immuniteMagieGolem. +
    +
  • Immunité aux armes : ajouter un prédicat immuniteAuxArmes. À noter qu'il est possible de spécifier le niveau de magie d'une attaque en donnant un argument à l'option --magique. Ainsi une arme de niveau magique 3 aura en argument --magique 3.
  • Immunité aux attaques non magiques : ajouter un prédicat immunite_nonMagique
  • Immunité aux critiques : ajouter un prédicat immuniteAuxCritiques.
  • diff --git a/COFantasy/COFantasy.js b/COFantasy/COFantasy.js index dbc2737c6f..fe81221f66 100644 --- a/COFantasy/COFantasy.js +++ b/COFantasy/COFantasy.js @@ -1,4 +1,4 @@ -//Derni\xE8re modification : dim. 08 sept. 2024, 05:59 +//Derni\xE8re modification : jeu. 07 nov. 2024, 07:13 // ------------------ generateRowID code from the Aaron --------------------- const generateUUID = (function() { "use strict"; @@ -65,6 +65,15 @@ let COF_loaded = false; // - chargeFantastique : tout ce dont on a besoin pour une charge fantastique en cours (TODO: passer sous combat) // - eventId : compteur d'events pour avoir une id unique // - tokensTemps : liste de tokens \xE0 dur\xE9e de vie limit\xE9e, effac\xE9s \xE0 la fin du combat +// - tid: id du token +// - name: le nom du token +// - duree: dur\xE9e restante en rounds +// - init: init \xE0 laquelle diminuer la dur\xE9e +// - intrusion: distance \xE0 laquelle le token s'active +// - tokensActifs : map de pageid vers liste de tokens qui font une action quand on passe \xE0 c\xF4t\xE9. +// - tid: id du token +// - name: le nom du token +// - distance: la distance d'activation (par rapport au centre). Si pas pr\xE9sent, il faut intersecter avec le bo\xEEte du token (sans tenir compte de la rotation) // - effetAuD20 : les effets qui se produisent \xE0 chaque jet de d\xE9. // chaque effet est d\xE9termin\xE9 par un champ, puis pour chaque champ, // - min: valeur minimale du d\xE9 pour d\xE9clencher @@ -73,6 +82,7 @@ let COF_loaded = false; // - nomFin: nom \xE0 afficher pour le statut et mettre fin aux \xE9v\xE9nements // par exemple, foudreDuTemps pour les foudres du temps // - tenebresMagiques : \xE9tat g\xE9n\xE9ral de t\xE9n\xE8bres magiques +// - aileForgeRunique: aile de la forge runique pour g\xE9rer les p\xE9ch\xE9s. // - jetsEnCours : pour laisser le MJ montrer ou non un jet qui lui a \xE9t\xE9 montr\xE9 \xE0 lui seul // - currentAttackDisplay : pour pouvoir remontrer des display aux joueurs // - pause : le jeu est en pause @@ -1010,6 +1020,22 @@ var COFantasy = COFantasy || function() { }; } + //Retourne un encodage des tailes : + // 1 : minuscule + // 2 : tr\xE8s petit + // 3 : petit + // 4 : moyen + // 5 : grand + // 6 : \xE9norme + // 7 : colossal + function taillePersonnage(perso, def) { + if (perso.taille) return perso.taille; + let taille = tailleNormale(perso, def); + if (attributeAsBool(perso, 'agrandissement')) taille++; + perso.taille = taille; + return taille; + } + //options peut contenir // msg: un message \xE0 afficher // maxVal: la valeur max de l'attribut @@ -2076,6 +2102,84 @@ var COFantasy = COFantasy || function() { etat_de_marker[marker] = etat; } stateCOF.jetsEnCours = undefined; + //V\xE9rification de la validit\xE9 des id des tokens actifs + if (stateCOF.tokensActifs) { + let ta = stateCOF.tokensActifs; + let kept = []; + let removed = []; + for (let pageId in ta) { + if (!ta[pageId] || ta[pageId].length === 0) { + delete ta[pageId]; + return; + } + let page = getObj('page', pageId); + if (page) kept.push(pageId); + else removed.push(pageId); + } + kept.forEach(function(pageId) { + let tokensToRemove = []; + ta[pageId].forEach(function(tt) { + let token = getObj('graphic', tt.tid); + if (token) return; + token = findObjs({ + _type: 'graphic', + name: tt.name, + _pageid: pageId, + layer: 'gmlayer' + }); + if (token.length === 0) { + tokensToRemove.push(tt.tid); + return; + } + if (token.length > 1) { + token = token.filter(function(tp) { + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + return s && s.sortieEscalier; + }); + if (token.length != 1) { + error("Il y a plus d'un token nomm\xE9 " + tt.name + ", impossible de savoir lequel \xE9tait automatiquement un TP", tt); + tokensToRemove.push(tt.tid); + return; + } + } + tt.tid = token[0].id; + }); + //Co\xFBt quadratique, mais \xE7a ne devrait pas arriver trop souvent (?) + tokensToRemove.forEach(function(tid) { + removeTokenActif(tid, pageId); + }); + }); + removed.forEach(function(pageId) { + ta[pageId].forEach(function(tt) { + let tokens = findObjs({ + _type: 'graphic', + name: tt.name, + layer: 'gmlayer' + }); + if (tokens.length === 0) { + return; + } + if (tokens.length > 1) { + tokens = tokens.filter(function(tp) { + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + return s && s.sortieEscalier; + }); + if (tokens.length != 1) { + error("Il y a plus d'un token nomm\xE9 " + tt.name + ", impossible de savoir lequel \xE9tait automatiquement un TP", tt); + return; + } + } + let token = tokens[0]; + let pid = token.get('pageid'); + ta[pid] = ta[pid] || []; + tt.tid = token.id; + ta[pid].push(tt); + }); + delete ta[pageId]; + }); + } } function etatRendInactif(etat) { @@ -3212,13 +3316,12 @@ var COFantasy = COFantasy || function() { //options: //fromTemp si on est en train de supprimer un effet temporaire //affectToken si on a d\xE9j\xE0 chang\xE9 le statusmarkers (on vient donc d'un changement \xE0 la main d'un marker - function setState(personnage, etat, value, evt, options) { + function setState(personnage, etat, value, evt, options = {}) { let token = personnage.token; if (value && predicateAsBool(personnage, 'immunite_' + etat)) { sendPerso(personnage, 'ne peut pas \xEAtre ' + stringOfEtat(etat, personnage)); return false; } - options = options || {}; let aff = options.affectToken || affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt); if (stateCOF.combat && value && etatRendInactif(etat) && @@ -4215,8 +4318,7 @@ var COFantasy = COFantasy || function() { // - oldTokenId // - newTokenId // - newToken - function finDEffet(attr, effet, attrName, charId, evt, options) { //L'effet arrive en fin de vie, doit \xEAtre supprim\xE9 - options = options || {}; + function finDEffet(attr, effet, attrName, charId, evt, options = {}) { //L'effet arrive en fin de vie, doit \xEAtre supprim\xE9 evt.deletedAttributes = evt.deletedAttributes || []; let res; let newInit = []; @@ -4664,9 +4766,9 @@ var COFantasy = COFantasy || function() { }; let resa = restoreTokenOfPerso(perso, evt); if (resa) { - setToken(res.newToken, 'width', token.get('width'), evt); - setToken(res.newToken, 'height', token.get('height'), evt); - token = res.newToken; + setToken(resa.newToken, 'width', token.get('width'), evt); + setToken(resa.newToken, 'height', token.get('height'), evt); + token = resa.newToken; res = resa; } let apv = tokenAttribute(perso, 'anciensPV'); @@ -5560,6 +5662,7 @@ var COFantasy = COFantasy || function() { if (persoEstPNJ(attaquant, options)) return computeArmeAtkPNJ(attaquant, x); let attDiv; let attCar; + let attBase = 1; switch (x) { case '@{ATKCAC}': attDiv = ficheAttributeAsInt(attaquant, 'ATKCAC_DIV', 0, options); @@ -5569,6 +5672,7 @@ var COFantasy = COFantasy || function() { } else { attCar = ficheAttribute(attaquant, 'ATKCAC_CARAC', '@{FOR}', options); } + attBase = ficheAttributeAsInt(attaquant, 'atkcac_base', 1, options); break; case '@{ATKTIR}': attDiv = ficheAttributeAsInt(attaquant, 'ATKTIR_DIV', 0, options); @@ -5578,6 +5682,7 @@ var COFantasy = COFantasy || function() { } else { attCar = ficheAttribute(attaquant, 'ATKTIR_CARAC', '@{DEX}', options); } + attBase = ficheAttributeAsInt(attaquant, 'atktir_base', 1, options); break; case '@{ATKMAG}': attDiv = ficheAttributeAsInt(attaquant, 'ATKMAG_DIV', 0, options); @@ -5588,13 +5693,14 @@ var COFantasy = COFantasy || function() { attCar = ficheAttribute(attaquant, 'ATKMAG_CARAC', '@{INT}', options); } attDiv += predicateAsInt(attaquant, 'bonusAttaqueMagique', 0); + attBase = ficheAttributeAsInt(attaquant, 'atkmag_base', 1, options); break; default: return x; } attCar = computeCarValue(attaquant, attCar, options); if (attCar === undefined) return x; - return attCar + ficheAttributeAsInt(attaquant, 'niveau', 1, options) + attDiv; + return attCar + attBase + attDiv; } function armeDeCreatureFeerique(perso, weaponStats, dice) { @@ -6212,7 +6318,7 @@ var COFantasy = COFantasy || function() { } }); } - if ((act.startsWith('!cof-lancer-sort') || act.startsWith('!cof-immunite-guerisseur')) && + if ((act.startsWith('!cof-lancer-sort ') || act.startsWith('!cof-immunite-guerisseur ') || act.startsWith('!cof-lumiere ')) && act.indexOf('--lanceur') == -1) { act += " --lanceur " + tid; } @@ -6949,6 +7055,11 @@ var COFantasy = COFantasy || function() { options.cacheBonusToutesCaracs.val = bonus; } } + let explications = []; + bonus += effetSalleForgeRunique(personnage, explications); + explications.forEach(function(msg) { + expliquer(msg); + }); return bonus; } @@ -8181,6 +8292,7 @@ var COFantasy = COFantasy || function() { } // callback(selected, playerId, aoe) + // selected est une liste d'objets avec un seul champ, _id qui est une id de token function getSelected(msg, callback, options) { options = options || {}; let playerId = getPlayerIdFromMsg(msg); @@ -8193,7 +8305,7 @@ var COFantasy = COFantasy || function() { let count = args.length - 1; let called; let actif = options.lanceur; - if (actif === undefined) { + if (actif === undefined && !options.pasDeLanceur) { if (msg.selected !== undefined && msg.selected.length == 1) { actif = persoOfId(msg.selected[0]._id, msg.selected[0]._id, pageId); } @@ -9620,52 +9732,101 @@ var COFantasy = COFantasy || function() { }; break; case 'effet': - if (cmd.length < 2) { - error("Il manque un argument \xE0 l'option --effet de !cof-attack", cmd); + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --effet de !cof-attack", cmd); + return; + } + let effet = cmd[1]; + if (cof_states[effet] && cmd.length > 2) { //remplacer par sa version effet temporaire + effet += 'Temp'; + } + if (estEffetTemp(effet)) { + let duree = 1; + if (cmd.length > 2) { + if (cmd[2] == 'fin') duree = 0; + else { + duree = parseInt(cmd[2]); + if (isNaN(duree) || duree < 1) { + error( + "Le deuxi\xE8me argument de --effet doit \xEAtre un nombre positif", + cmd); + return; + } + } + } + let m = messageOfEffetTemp(effet); + lastEtat = { + effet, + duree, + message: m, + typeDmg: lastType + }; + scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); + } else if (estEffetCombat(effet)) { + lastEtat = { + effet, + typeDmg: lastType, + message: messageEffetCombat[effet] + }; + } else if (estEffetIndetermine(effet)) { + lastEtat = { + effet, + effetIndetermine: true, + typeDmg: lastType, + message: messageEffetIndetermine[effet] + }; + } else { + error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + return; + } + scope.effets = scope.effets || []; + scope.effets.push(lastEtat); return; } - let effet = cmd[1]; - if (cof_states[effet] && cmd.length > 2) { //remplacer par sa version effet temporaire - effet += 'Temp'; - } - if (estEffetTemp(effet)) { - let duree = 1; - if (cmd.length > 2) { - duree = parseInt(cmd[2]); - if (isNaN(duree) || duree < 1) { - error( - "Le deuxi\xE8me argument de --effet doit \xEAtre un nombre positif", - cmd); - return; - } + case 'finEffet': + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --finEffet de !cof-attack", cmd); + return; } - let m = messageOfEffetTemp(effet); - lastEtat = { - effet: effet, - duree: duree, - message: m, - typeDmg: lastType - }; - scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); - } else if (estEffetCombat(effet)) { - lastEtat = { - effet: effet, - typeDmg: lastType, - message: messageEffetCombat[effet] - }; - } else if (estEffetIndetermine(effet)) { - lastEtat = { - effet: effet, - effetIndetermine: true, - typeDmg: lastType - }; - } else { - error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + let effet = cmd[1]; + if (cof_states[effet]) { //remplacer par sa version effet temporaire + effet += 'Temp'; + } + if (estEffetTemp(effet)) { + let m = messageOfEffetTemp(effet); + lastEtat = { + effet, + duree: 0, + finEffet: true, + message: m, + typeDmg: lastType + }; + scope.seulementVivant = scope.seulementVivant || (m && m.seulementVivant); + } else if (estEffetCombat(effet)) { + lastEtat = { + effet, + finEffet: true, + typeDmg: lastType, + message: messageEffetCombat[effet] + }; + } else if (estEffetIndetermine(effet)) { + lastEtat = { + effet, + finEffet: true, + effetIndetermine: true, + typeDmg: lastType, + message: messageEffetIndetermine[effet] + }; + } else { + error(cmd[1] + " n'est pas un effet temporaire r\xE9pertori\xE9", cmd); + return; + } + scope.effets = scope.effets || []; + scope.effets.push(lastEtat); return; } - scope.effets = scope.effets || []; - scope.effets.push(lastEtat); - return; case 'valeur': if (cmd.length < 2) { error("Il manque un argument \xE0 l'option --valeur de !cof-attack", cmd); @@ -9713,50 +9874,73 @@ var COFantasy = COFantasy || function() { return; case 'etatSi': case 'etat': - if (cmd.length < 3 && cmd[0] == 'etatSi') { - error("Il manque un argument \xE0 l'option --etatSi de !cof-attack", cmd); - return; - } else if (cmd.length < 2) { - error("Il manque un argument \xE0 l'option --etat de !cof-attack", cmd); - return; - } - let etat = cmd[1]; - if (!_.has(cof_states, etat)) { - error("Etat non reconnu", cmd); + { + if (cmd.length < 3 && cmd[0] == 'etatSi') { + error("Il manque un argument \xE0 l'option --etatSi de !cof-attack", cmd); + return; + } else if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --etat de !cof-attack", cmd); + return; + } + let etat = cmd[1]; + if (!_.has(cof_states, etat)) { + error("\xC9tat " + etat + " non reconnu", cmd); + return; + } + let condition = 'toujoursVrai'; + if (cmd[0] == 'etatSi') { + condition = parseCondition(cmd.slice(2)); + if (condition === undefined) return; + } + scope.etats = scope.etats || []; + lastEtat = { + etat, + condition, + typeDmg: lastType + }; + if (cmd[0] == 'etat' && cmd.length > 3) { + if (!isCarac(cmd[2]) && (cmd[2].length != 6 || + !isCarac(cmd[2].substring(0, 3)) || !isCarac(cmd[2].substring(3, 6)))) { + error("Caract\xE9ristique du jet de sauvegarde incorrecte", cmd); + return; + } + lastEtat.saveCarac = cmd[2]; + let opposition = persoOfId(cmd[3]); + if (opposition) { + lastEtat.saveDifficulte = cmd[3] + ' ' + nomPerso(opposition); + } else { + lastEtat.saveDifficulte = parseInt(cmd[3]); + if (isNaN(lastEtat.saveDifficulte)) { + error("Difficult\xE9 du jet de sauvegarde incorrecte", cmd); + delete lastEtat.saveCarac; + delete lastEtat.saveDifficulte; + } + } + } + scope.etats.push(lastEtat); return; } - let condition = 'toujoursVrai'; - if (cmd[0] == 'etatSi') { - condition = parseCondition(cmd.slice(2)); - if (condition === undefined) return; - } - scope.etats = scope.etats || []; - lastEtat = { - etat: etat, - condition: condition, - typeDmg: lastType - }; - if (cmd[0] == 'etat' && cmd.length > 3) { - if (!isCarac(cmd[2]) && (cmd[2].length != 6 || - !isCarac(cmd[2].substring(0, 3)) || !isCarac(cmd[2].substring(3, 6)))) { - error("Caract\xE9ristique du jet de sauvegarde incorrecte", cmd); + case 'finEtat': + { + if (cmd.length < 2) { + error("Il manque un argument \xE0 l'option --finEtat de !cof-attack", cmd); return; } - lastEtat.saveCarac = cmd[2]; - let opposition = persoOfId(cmd[3]); - if (opposition) { - lastEtat.saveDifficulte = cmd[3] + ' ' + nomPerso(opposition); - } else { - lastEtat.saveDifficulte = parseInt(cmd[3]); - if (isNaN(lastEtat.saveDifficulte)) { - error("Difficult\xE9 du jet de sauvegarde incorrecte", cmd); - delete lastEtat.saveCarac; - delete lastEtat.saveDifficulte; - } + let etat = cmd[1]; + if (!_.has(cof_states, etat)) { + error("\xC9tat " + etat + " non reconnu", cmd); + return; } + let effet = etat + 'Temp'; + scope.effets = scope.effets || []; + lastEtat = { + effet, + duree: 0, + typeDmg: lastType + }; + scope.effets.push(lastEtat); + return; } - scope.etats.push(lastEtat); - return; case 'peur': if (cmd.length < 3) { error("Il manque un argument \xE0 l'option --peur de !cof-attack", cmd); @@ -10931,12 +11115,12 @@ var COFantasy = COFantasy || function() { if (options.effets) { options.effets.forEach(function(ef) { if (ef.effet) { - if (estEffetTemp(ef.effet)) { + if (estEffetTemp(ef.effet) && ef.effet.duree) { optMana.dm = optMana.dm || (ef.message && ef.message.dm); optMana.soins = optMana.soins || (ef.message && ef.message.soins); optMana.duree = true; } - } else if (estEffetCombat(ef.effet)) { + } else if (estEffetCombat(ef.effet) && !ef.effet.finEffet) { optMana.dm = optMana.dm || messageEffetCombat[ef.effet].dm; optMana.soins = optMana.soins || messageEffetCombat[ef.effet].soins; } @@ -12410,20 +12594,109 @@ var COFantasy = COFantasy || function() { return def; } - //Retourne un encodage des tailes : - // 1 : minuscule - // 2 : tr\xE8s petit - // 3 : petit - // 4 : moyen - // 5 : grand - // 6 : \xE9norme - // 7 : colossal - function taillePersonnage(perso, def) { - if (perso.taille) return perso.taille; - let taille = tailleNormale(perso, def); - if (attributeAsBool(perso, 'agrandissement')) taille++; - perso.taille = taille; - return taille; + function pechePositif(salle, explications) { + explications.push("se sent bien dans " + salle + " => +1"); + return 1; + } + + function pecheNegatif(salle, explications) { + explications.push("se sent mal dans " + salle + " => -2"); + return -2; + } + + //returns 0 if no effect, a positive number if positive and a negative number is negative + function effetSalleForgeRunique(perso, explications) { + if (!stateCOF.aileForgeRunique) return 0; + let peche = predicateAsBool(perso, 'pecheThassilion'); + if (!peche) return 0; + switch (stateCOF.aileForgeRunique) { + case 'paresse': + { + let salle = "le labyrinthe purulent"; + switch (peche) { + case 'paresse': + return pechePositif(salle, explications); + case 'colere': + case 'orgueil': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'avarice': + { + let salle = "le caveau de l'avarice"; + switch (peche) { + case 'avarice': + return pechePositif(salle, explications); + case 'luxure': + case 'orgueil': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'orgueil': + { + let salle = "les voiles scintillants"; + switch (peche) { + case 'orgueil': + return pechePositif(salle, explications); + case 'avarice': + case 'paresse': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'envie': + { + let salle = "les salles de l'envie"; + switch (peche) { + case 'envie': + return pechePositif(salle, explications); + case 'colere': + case 'gourmandise': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'colere': + { + let salle = "les salles de la col\xE8re"; + switch (peche) { + case 'colere': + return pechePositif(salle, explications); + case 'paresse': + case 'envie': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'gourmandise': + { + let salle = "les cryptes affam\xE9es"; + switch (peche) { + case 'gourmandise': + return pechePositif(salle, explications); + case 'envie': + case 'luxure': + return pecheNegatif(salle, explications); + } + return 0; + } + case 'luxure': + { + let salle = "les cages de fer"; + switch (peche) { + case 'luxure': + return pechePositif(salle, explications); + case 'avarice': + case 'gourmandise': + return pecheNegatif(salle, explications); + } + return 0; + } + } + error("L'aile de forge runique est mal d\xE9finie " + stateCOF.aileForgeRunique, stateCOF); + return 0; } //tm doit \xEAtre stateCOF.tenebresMagiques, et bien d\xE9fini. @@ -13185,6 +13458,11 @@ var COFantasy = COFantasy || function() { messageAttaqueDM("Force drain\xE9e", explications, options, -2); } let energieImpie = attributeAsInt(attaquant, 'energieImpie', 0); + if (attributeAsBool(attaquant, 'malchance')) { + let malchance = getIntValeurOfEffet(attaquant, 'malchance', 1); + attBonus -= malchance; + messageAttaqueDM("Malchance", explications, options, -malchance); + } if (energieImpie) { attBonus += energieImpie; messageAttaqueDM("\xC9nergie impie", explications, options, energieImpie); @@ -13605,6 +13883,7 @@ var COFantasy = COFantasy || function() { explications.push(msgConditions); attBonus -= conditions; } + attBonus += effetSalleForgeRunique(attaquant, explications); return attBonus; } @@ -16223,6 +16502,9 @@ var COFantasy = COFantasy || function() { return true; } if (attributeAsBool(target, 'immuniteA' + dmgType)) return true; + if (options.tranchant && predicateAsBool(target, 'immunite_tranchant')) return true; + if (options.contondant && predicateAsBool(target, 'immunite_contondant')) return true; + if (options.percant && predicateAsBool(target, 'immunite_percant')) return true; switch (dmgType) { case 'acide': if (predicateAsBool(target, 'batonDesRunesMortes') && attributeAsBool(target, 'runeLizura')) return true; @@ -16292,8 +16574,7 @@ var COFantasy = COFantasy || function() { // - un texte affichant le jet de d\xE9g\xE2ts // - la valeur finale des d\xE9g\xE2ts inflig\xE9s // crit est un bool\xE9en, il augmente de 1 (ou options.critCoef) le coefficient (option.dmgCoef) et active certains effets - function dealDamage(target, dmg, otherDmg, evt, crit, options, explications, displayRes) { - if (options === undefined) options = {}; + function dealDamage(target, dmg, otherDmg, evt, crit, options = {}, explications = false, displayRes = false) { let expliquer = function(msg) { if (explications) explications.push(msg); else sendPerso(target, msg); @@ -19583,6 +19864,17 @@ var COFantasy = COFantasy || function() { save: true }); } + if (!options.redo && options.sortilege && options.dm && + predicateAsBool(target, 'immuniteMagieGolem') && + (weaponStats.name.includes('sint\xE9gration') || weaponStats.name.includes('sintegration')) + ) { + target.effets.push({ + effet: 'ralentiTemp', + duree: randomInteger(6), + message: messageOfEffetTemp('ralentiTemp') + }); + target.diviseDmg++; + } if (options.ecraser) { target.messages.push(nomPerso(attaquant) + " saisit " + nomPerso(target) + " entre ses bras puissants"); if (options.ecraser === true) { @@ -20327,6 +20619,29 @@ var COFantasy = COFantasy || function() { } if (effets) { effets.forEach(function(ef) { + if (ef.finEffet) { + let attr = tokenAttribute(target, ef.effet); + if (attr.length === 0) { + if (ef.effet.endsWith('Temp')) { + let etat = ef.effet.substring(0, ef.effet.length - 4); + if (!cof_states[etat]) return; + if (getState(target, etat)) { + setState(target, etat, false, evt); + target.messages.push(nomPerso(target) + ' ' + messageFin(target, ef.message)); + } + } + return; + } + attr = attr[0]; + let feOptions = { + print: function(msg) { + target.messages.push(nomPerso(target) + ' ' + msg); + } + }; + let f = finDEffet(attr, ef.effet, attr.get('name'), target.charId, evt, feOptions); + if (f && f.newToken) target.token = f.newToken; + return; + } if (((options.sortilege || options.mana !== undefined) && (predicateAsBool(target, 'liberteDAction') && ( ef.effet == 'apeureTemp' || @@ -20705,6 +21020,7 @@ var COFantasy = COFantasy || function() { chanceRollId: options.chanceRollId, type: ce.typeDmg, necromancie: estNecromancie(options), + bonus: predicateAsInt(target, 'bonusSaveContre_' + ce.etat, 0), }; let rollId = 'etat_' + ce.etat + index + '_' + target.token.id; save(ce.save, target, rollId, expliquer, saveOpts, evt, @@ -21570,6 +21886,7 @@ var COFantasy = COFantasy || function() { perso.dansAuraDeProfanation = res; return res; } + //s repr\xE9sente le save, avec une carac, une carac2 optionnelle et un seuil //expliquer est une fonction qui prend en argument un string et le publie // options peut contenir les champs : @@ -21624,6 +21941,11 @@ var COFantasy = COFantasy || function() { return; } } + if (options.magique && predicateAsBool(target, 'immuniteMagieGolem')) { + expliquer(nomPerso(target) + " n'est pas affect\xE9 par cette magie."); + afterSave(true, ''); + return; + } if (s.fauchage) { if (s.fauchage <= taillePersonnage(target, 4)) { expliquer(nomPerso(target) + " est trop grand pour \xEAtre fauch\xE9."); @@ -21673,6 +21995,11 @@ var COFantasy = COFantasy || function() { expliquer("Peau d'\xE9corce am\xE9lior\xE9e => +" + bonusPeau + " pour r\xE9sister au poison"); } } + let explications = []; + bonus += effetSalleForgeRunique(target, explications); + explications.forEach(function(msg) { + expliquer(nomPerso(target) + ' ' + msg); + }); let bonusAttrs = []; let bonusPreds = []; let seuil = s.seuil; @@ -22667,7 +22994,7 @@ var COFantasy = COFantasy || function() { current = 5 - n; if (current < 0) current = 0; } - attrPR = createObj("attribute", { + attrPR = createObj('attribute', { characterid: perso.charId, name: 'pr', current, @@ -23163,7 +23490,7 @@ var COFantasy = COFantasy || function() { let dmTemp; if (tmpHitAttr.length === 0) { dmTemp = - createObj("attribute", { + createObj('attribute', { characterid: charId, name: 'DMTEMP', current: 0, @@ -23645,7 +23972,7 @@ var COFantasy = COFantasy || function() { return toEvaluate.replace(/@{/g, "@{" + name + "|"); } - // Retourne le diam\xE8tre d'un disque inscrit dans un carr\xE9 de surface + // Retourne le diam\xE8tre en pixels d'un disque inscrit dans un carr\xE9 de surface // \xE9quivalente \xE0 celle du token function tokenSizeAsCircle(token) { const surface = token.get('width') * token.get('height'); @@ -24096,14 +24423,16 @@ var COFantasy = COFantasy || function() { sendChat('', action); } - function getTokenTemp(tt) { + function getTokenTemp(tt, pageId) { let token = getObj('graphic', tt.tid); if (!token) { if (!tt.name) return; - token = findObjs({ + let f = { _type: 'graphic', name: tt.name - }); + }; + if (pageId) f._pageid = pageId; + token = findObjs(f); if (token.length === 0) return; token = token[0]; } @@ -25162,6 +25491,9 @@ var COFantasy = COFantasy || function() { sendPlayer('GM', boutonSimple("!cof-effet-chaque-d20 " + ev + " fin", "Mettre fin") + stateCOF.effetAuD20[ev].nomFin + " ?"); } } + if (stateCOF.aileForgeRunique) { + sendPlayer('GM', boutonSimple("!cof-aile-forge-runique sortir", "Sortir") + "de la salle de " + stateCOF.aileForgeRunique + " ?"); + } let attrs = findObjs({ _type: 'attribute' }); @@ -29720,7 +30052,8 @@ var COFantasy = COFantasy || function() { msgRate: ", rat\xE9.", rolls: options.rolls, chanceRollId: options.chanceRollId, - type: options.type + type: options.type, + bonus: predicateAsInt(perso, 'bonusSaveContre_' + etat, 0), }; let expliquer = function(s) { sendPerso(perso, s); @@ -32992,7 +33325,7 @@ var COFantasy = COFantasy || function() { // si le consommable n'a pas \xE9t\xE9 trouv\xE9, on le cr\xE9e avec une valeur de nb if (!found) { let pref = 'repeating_equipement_' + generateRowID() + '_'; - let attre = createObj("attribute", { + let attre = createObj('attribute', { name: pref + 'equip_nom', current: nom, characterid: perso.charId @@ -34021,6 +34354,168 @@ var COFantasy = COFantasy || function() { const labelsEscalier = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]; + //esc est un token, le reste est optionnel + function trouveSortieEscalier(esc, versLeHaut, loop, escaliers, tmaps) { + let escName; //Contiendra le nom de l'escalier vers lequel aller + //On regarde d'abord le gmnote + let gmNotes = esc.get('gmnotes'); + try { + gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' '); + gmNotes = linesOfNote(gmNotes); + gmNotes.find(function(l) { + if (versLeHaut) { + if (l.startsWith('monte:')) { + escName = l.substring(6); + return true; + } + if (l.startsWith('monter:')) { + escName = l.substring(7); + return true; + } + if (l.startsWith('bas:')) { + escName = l.substring(4); + return true; + } + return false; + } else { + if (l.startsWith('descend:')) { + escName = l.substring(8); + return true; + } + if (l.startsWith('descendre:')) { + escName = l.substring(10); + return true; + } + if (l.startsWith('haut:')) { + escName = l.substring(5); + return true; + } + return false; + } + return false; + }); + } catch (uriError) { + log("Erreur de d\xE9codage URI dans la note GM de " + esc.get('name') + " : " + gmNotes); + } + let i; //index de label si on n'utilise pas gmnote + if (escName === undefined) { + //Si on n'a pas trouv\xE9, on regarde le nom + escName = esc.get('name'); + let l = escName.length; + if (l > 1) { + let label = escName.substr(l - 1, 1); + escName = escName.substr(0, l - 1); + i = labelsEscalier.indexOf(label); + if (versLeHaut) { + if (i == 11) { + if (loop) escName += labelsEscalier[0]; + } else escName += labelsEscalier[i + 1]; + } else { + if (i === 0) { + if (loop) escName += labelsEscalier[11]; + } else escName += labelsEscalier[i - 1]; + } + } + } + if (!escName) return; + //Ensuite on cherche l'escalier de nom escName + let escs = escaliers; + if (escName.startsWith('tmap_')) { + if (!tmaps) { + tmaps = findObjs({ + _type: 'graphic', + layer: 'gmlayer' + }); + tmaps = tmaps.filter(function(e) { + return e.get('name').startsWith('tmap_'); + }); + } + escs = tmaps; + } + if (!escs) { + let pageId = esc.get('pageid'); + escs = findObjs({ + _type: 'graphic', + _pageid: pageId, + layer: 'gmlayer' + }); + } + let sortieEscalier = escs.find(function(esc2) { + return esc2.get('name') == escName; + }); + if (sortieEscalier === undefined && i !== undefined && loop) { + if (i > 0) { //sortie par le plus petit + escName = escName.substr(-1) + 'A'; + sortieEscalier = escs.find(function(esc2) { + return esc2.get('name') == escName; + }); + } else { + sortieEscalier = findEsc(escs, escName.substr(-1), 10); + } + } + return { + sortieEscalier, + tmaps + }; + } + + function prendreEscalier(perso, pageId, sortieEscalier) { + let token = perso.token; + let left = sortieEscalier.get('left'); + let top = sortieEscalier.get('top'); + let newPageId = sortieEscalier.get('pageid'); + //D\xE9placement du token + if (newPageId == pageId) { + token.set('left', left); + token.set('top', top); + } else { + //On change de carte, il faut donc copier le token + let tokenObj = JSON.parse(JSON.stringify(token)); + tokenObj._pageid = newPageId; + //On met la taille du token \xE0 jour en fonction des \xE9chelles des cartes. + let ratio = computeScale(pageId) / computeScale(newPageId); + if (ratio < 0.9 || ratio > 1.1) { + if (ratio < 0.25) ratio = 0.25; + else if (ratio > 4) ratio = 4; + tokenObj.width *= ratio; + tokenObj.height *= ratio; + } + tokenObj.imgsrc = normalizeTokenImg(tokenObj.imgsrc); + tokenObj.left = left; + tokenObj.top = top; + let newToken = createObj('graphic', tokenObj); + if (newToken === undefined) { + error("Impossible de copier le token, et donc de faire le changement de carte", tokenObj); + return; + } + } + //On d\xE9place ensuite le joueur. + let character = getObj('character', perso.charId); + if (character === undefined) return; + let charControlledby = character.get('controlledby'); + if (charControlledby === '') { + //Seul le MJ contr\xF4le le personnage + let players = findObjs({ + _type: 'player', + online: true + }); + let gm = players.find(function(p) { + return playerIsGM(p.id); + }); + if (gm) { + if (newPageId != pageId) movePlayerToPage(gm.id, pageId, newPageId); + sendPing(left, top, newPageId, gm.id, true, gm.id); + } + } else { + charControlledby.split(",").forEach(function(pid) { + if (newPageId != pageId) movePlayerToPage(pid, pageId, newPageId); + sendPing(left, top, newPageId, pid, true, pid); + }); + } + //Enfin, on efface le token de d\xE9part si on a chang\xE9 de page + if (newPageId != pageId) token.remove(); + } + function escalier(msg) { getSelected(msg, function(selected, playerId) { if (selected.length === 0) { @@ -34061,151 +34556,15 @@ var COFantasy = COFantasy || function() { if (sortieEscalier) return; if (intersection(posX, sizeX, esc.get('left'), esc.get('width')) && intersection(posY, sizeY, esc.get('top'), esc.get('height'))) { - let escName; //Contiendra le nom de l'escalier vers lequel aller - //On regarde d'abord le gmnote - let gmNotes = esc.get('gmnotes'); - try { - gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' '); - gmNotes = linesOfNote(gmNotes); - gmNotes.find(function(l) { - if (versLeHaut) { - if (l.startsWith('monte:')) { - escName = l.substring(6); - return true; - } - if (l.startsWith('monter:')) { - escName = l.substring(7); - return true; - } - if (l.startsWith('bas:')) { - escName = l.substring(4); - return true; - } - return false; - } else { - if (l.startsWith('descend:')) { - escName = l.substring(8); - return true; - } - if (l.startsWith('descendre:')) { - escName = l.substring(10); - return true; - } - if (l.startsWith('haut:')) { - escName = l.substring(5); - return true; - } - return false; - } - return false; - }); - } catch (uriError) { - log("Erreur de d\xE9codage URI dans la note GM de " + esc.get('name') + " : " + gmNotes); - } - let i; //index de label si on n'utilise pas gmnote - if (escName === undefined) { - //Si on n'a pas trouv\xE9, on regarde le nom - escName = esc.get('name'); - let l = escName.length; - if (l > 1) { - let label = escName.substr(l - 1, 1); - escName = escName.substr(0, l - 1); - i = labelsEscalier.indexOf(label); - if (versLeHaut) { - if (i == 11) { - if (loop) escName += labelsEscalier[0]; - } else escName += labelsEscalier[i + 1]; - } else { - if (i === 0) { - if (loop) escName += labelsEscalier[11]; - } else escName += labelsEscalier[i - 1]; - } - } - } - if (!escName) return; - //Ensuite on cherche l'escalier de nom escName - let escs = escaliers; - if (escName.startsWith('tmap_')) { - if (!tmaps) { - tmaps = findObjs({ - _type: 'graphic', - layer: 'gmlayer' - }); - tmaps = tmaps.filter(function(e) { - return e.get('name').startsWith('tmap_'); - }); - } - escs = tmaps; - } - sortieEscalier = escs.find(function(esc2) { - return esc2.get('name') == escName; - }); - if (sortieEscalier === undefined && i !== undefined && loop) { - if (i > 0) { //sortie par le plus petit - escName = escName.substr(-1) + 'A'; - sortieEscalier = escs.find(function(esc2) { - return esc2.get('name') == escName; - }); - } else { - sortieEscalier = findEsc(escs, escName.substr(-1), 10); - } + let s = trouveSortieEscalier(esc, versLeHaut, loop, escaliers, tmaps); + if (s) { + sortieEscalier = s.sortieEscalier; + tmaps = s.tmaps; } } }); if (sortieEscalier) { - let left = sortieEscalier.get('left'); - let top = sortieEscalier.get('top'); - let newPageId = sortieEscalier.get('pageid'); - //D\xE9placement du token - if (newPageId == pageId) { - token.set('left', left); - token.set('top', top); - } else { - //On change de carte, il faut donc copier le token - let tokenObj = JSON.parse(JSON.stringify(token)); - tokenObj._pageid = newPageId; - //On met la taille du token \xE0 jour en fonction des \xE9chelles des cartes. - let ratio = computeScale(pageId) / computeScale(newPageId); - if (ratio < 0.9 || ratio > 1.1) { - if (ratio < 0.25) ratio = 0.25; - else if (ratio > 4) ratio = 4; - tokenObj.width *= ratio; - tokenObj.height *= ratio; - } - tokenObj.imgsrc = normalizeTokenImg(tokenObj.imgsrc); - tokenObj.left = left; - tokenObj.top = top; - let newToken = createObj('graphic', tokenObj); - if (newToken === undefined) { - error("Impossible de copier le token, et donc de faire le changement de carte", tokenObj); - return; - } - } - //On d\xE9place ensuite le joueur. - let character = getObj('character', perso.charId); - if (character === undefined) return; - let charControlledby = character.get('controlledby'); - if (charControlledby === '') { - //Seul le MJ contr\xF4le le personnage - let players = findObjs({ - _type: 'player', - online: true - }); - let gm = players.find(function(p) { - return playerIsGM(p.id); - }); - if (gm) { - if (newPageId != pageId) movePlayerToPage(gm.id, pageId, newPageId); - sendPing(left, top, newPageId, gm.id, true, gm.id); - } - } else { - charControlledby.split(",").forEach(function(pid) { - if (newPageId != pageId) movePlayerToPage(pid, pageId, newPageId); - sendPing(left, top, newPageId, pid, true, pid); - }); - } - //Enfin, on efface le token de d\xE9part si on a chang\xE9 de page - if (newPageId != pageId) token.remove(); + prendreEscalier(perso, pageId, sortieEscalier); return; } let err = nomPerso(perso) + " n'est pas sur un escalier"; @@ -34218,6 +34577,100 @@ var COFantasy = COFantasy || function() { }); //fin getSelected } + function removeTokenActif(tid, pageId) { + let ta = stateCOF.tokensActifs; + ta[pageId] = ta[pageId].filter(function(tt) { + return tt.tid != tid; + }); + if (ta[pageId] == []) delete ta[pageId]; + } + + function mettreFinATPAuto(msg, options) { + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Aucun token s\xE9lectionn\xE9 pour !cof-tp-auto off", playerId); + return; + } + let ta = stateCOF.tokensActifs; + if (ta) return; + selected.forEach(function(sel) { + let token = getObj('graphic', sel._id); + if (token === undefined) return; + let pageId = token.get('pageid'); + if (!ta[pageId]) { + sendPlayer(msg, token.get('name') + " n'\xE9tait pas en TP automatique", playerId); + return; + } + removeTokenActif(token.id, pageId); + sendPlayer(msg, token.get('name') + " n'est plus en TP automatique", playerId); + }); + }, { + pasDeLanceur: true + }); + } + + //!cof-tp-auto ['off'|rayon] + function setTPAuto(msg) { + let options = parseOptions(msg); + if (options === undefined) return; + let cmd = options.cmd; + if (cmd === undefined) { + error("Probl\xE8me de parse options", msg.content); + return; + } + let r; + if (cmd.length > 1) { + if (cmd[1] == 'off') { + mettreFinATPAuto(msg, options); + return; + } + r = parseFloat(cmd[1]); + if (isNaN(r) || r < 0) { + sendPlayer(msg, "Argument de !cof-tp-auto invalide ( " + cmd[1] + ")"); + return; + } + } + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Aucun token s\xE9lectionn\xE9 pour !cof-tp-auto", playerId); + return; + } + selected.forEach(function(sel) { + let token = getObj('graphic', sel._id); + if (token === undefined) return; + if (token.get('layer') != 'gmlayer') { + sendPlayer(msg, "TP ne peut concerner que des tokens du layer MJ", playerId); + return; + } + stateCOF.tokensActifs = stateCOF.tokensActifs || {}; + let ta = stateCOF.tokensActifs; + let pageId = token.get('pageid'); + ta[pageId] = ta[pageId] || []; + let present = ta[pageId].find(function(tt) { + return tt.tid == token.id; + }); + //On converti la distance d'intrusion en pixels + let scale = computeScale(pageId); + let rayon = (r / scale) * PIX_PER_UNIT; + if (present) { + present.rayon = rayon; + sendPlayer(msg, "Le rayon de " + token.get('name') + " devient " + r, playerId); + return; + } + //token pas d\xE9j\xE0 dans la liste des tokens actifs + let tt = { + tid: token.id, + name: token.get('name'), + rayon + }; + ta[pageId].push(tt); + sendPlayer(msg, token.get('name') + " devient actif, de rayon " + r, playerId); + }); + }, { + pasDeLanceur: true + }); + } + function defautDansLaCuirasse(msg) { let args = msg.content.split(' '); if (args.length < 3) { @@ -34842,10 +35295,11 @@ var COFantasy = COFantasy || function() { let d20roll = attackRoll.results.total; effetAuD20(lanceur, d20roll); let msg = buildinline(attackRoll); - let attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); - if (estAffaibli(lanceur) && predicateAsBool(lanceur, 'insensibleAffaibli')) attBonus -= 2; + let attBonus; switch (typeAttaque) { case 'distance': + attBonus = ficheAttributeAsInt(lanceur, 'atktir_base', 1); + attBonus = computeArmeAtk(lanceur, '@ATKTIR'); attBonus += ficheAttributeAsInt(lanceur, 'ATKTIR_DIV', 0); if (persoArran(lanceur)) { attBonus += ficheAttributeAsInt(lanceur, 'mod_atktir', 0); @@ -34853,6 +35307,7 @@ var COFantasy = COFantasy || function() { attBonus += modCarac(lanceur, carac); break; case 'magique': + attBonus = ficheAttributeAsInt(lanceur, 'atkmag_base', 1); attBonus += ficheAttributeAsInt(lanceur, 'ATKMAG_DIV', 0); if (persoArran(lanceur)) { attBonus += ficheAttributeAsInt(lanceur, 'mod_atkmag', 0); @@ -34863,16 +35318,19 @@ var COFantasy = COFantasy || function() { attBonus += predicateAsInt(lanceur, 'bonusAttaqueMagique', 0); break; case 'contact': + attBonus = ficheAttributeAsInt(lanceur, 'atkcaca_base', 1); attBonus += ficheAttributeAsInt(lanceur, 'ATKCAC_DIV', 0); attBonus += modCarac(lanceur, carac); break; case 'esquive': + attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); attBonus += predicateAsInt(lanceur, 'reflexesFelins', 0); attBonus += predicateAsInt(lanceur, 'esquiveVoleur', 0); attBonus += predicateAsInt(lanceur, 'esquive', 0); attBonus += modCarac(lanceur, carac); break; default: + attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1); } let weaponStats; if (opt && opt.arme && lanceur.arme) { @@ -36175,7 +36633,7 @@ var COFantasy = COFantasy || function() { if (!found) { if (m1) { let pref = 'repeating_equipement_' + generateRowID() + '_'; - let attre = createObj("attribute", { + let attre = createObj('attribute', { name: pref + 'equip_nom', current: consName, characterid: perso2.charId @@ -36183,7 +36641,7 @@ var COFantasy = COFantasy || function() { evt.attributes.push({ attribute: attre, }); - attre = createObj("attribute", { + attre = createObj('attribute', { name: pref + 'equip_effet', current: effet, characterid: perso2.charId @@ -36192,7 +36650,7 @@ var COFantasy = COFantasy || function() { attribute: attre, }); } else { - let attr2 = createObj("attribute", { + let attr2 = createObj('attribute', { name: attrName, current: 1, max: effet, @@ -37475,7 +37933,7 @@ var COFantasy = COFantasy || function() { } let druide = persoOfId(cmd[1], cmd[1], options.pageId); if (druide === undefined) { - error("Le premier argument de !cof-animer-arbre n'est pas un token valie", cmd); + error("Le premier argument de !cof-animer-arbre n'est pas un token valide", cmd); return; } let tokenArbre = getObj('graphic', cmd[2]); @@ -37484,7 +37942,7 @@ var COFantasy = COFantasy || function() { return; } if (tokenArbre.get('represents') !== '') { - sendPerso(druide, "ne peut pas animer " + tokenArbre.get('name')); + sendPerso(druide, "ne peut pas animer " + tokenArbre.get('name')+", car il repr\xE9sente d\xE9j\xE0 un personnage."); return; } if (options.portee !== undefined) { @@ -42522,6 +42980,57 @@ var COFantasy = COFantasy || function() { } } + /* !cof-aile-forge-runique */ + function entrerAileForgeRunique(msg) { + let cmd = msg.content.split(' '); + cmd = cmd.filter(function(c) { + return c.trim() !== ''; + }); + if (cmd.length < 2) { + if (stateCOF.aileForgeRunique) { + sendPlayer('GM', "Les personnage sont dans la salle de " + stateCOF.aileForgeRunique); + } else { + sendPlayer('GM', "Les personnages ne sont pas dans les forges runiques."); + } + return; + } + let b; + switch (cmd[1]) { + case 'paresse': + case 'orgueil': + case 'gourmandise': + case 'colere': + case 'luxure': + case 'avarice': + case 'envie': + b = cmd[1]; + break; + case 'col\xE8re': + b = 'colere'; + break; + case 'non': + case 'sortir': + case 'false': + case 'fin': + b = false; + break; + default: + error("Option de !cof-aile-forge-runique non reconnue", cmd); + return; + } + if (b) { + if (stateCOF.aileForgeRunique == b) { + sendPlayer('GM', "Les personnages sont d\xE9j\xE0 dans la salle de " + b); + return; + } + sendPlayer('GM', "Les personnages entrent dans la salle de " + b); + stateCOF.aileForgeRunique = b; + } else { + sendPlayer('GM', "Les personnages sortent de la salle de " + stateCOF.aileForgeRunique); + delete stateCOF.aileForgeRunique; + } + } + function fioleDeLumiere(msg) { let cmd = msg.content.split(' '); let tm = stateCOF.tenebresMagiques; @@ -42792,6 +43301,12 @@ var COFantasy = COFantasy || function() { case 'rock catching': notes += d + '\n'; return; + case 'division': + predicats += 'divisionVase '; + return; + case 'evasion': + predicats += 'esquiveDeLaMagie '; + return; default: if (d.startsWith('channel resistance ')) { let resChannel = parseInt(d.substring(19)); @@ -42925,17 +43440,33 @@ var COFantasy = COFantasy || function() { case 'sleep': predicats += 'immunite_endormi '; return; + case 'stunning': + predicats += 'immunite_etourdi '; + return; case 'construct': predicats += 'sansEsprit creatureArtificielle immuniteSaignement '; return; case 'blindness': predicats += 'immunite_aveugle '; return; + case 'elemental': + predicats += 'nonVivant '; + return; + case 'ooze': + predicats += 'immunite_endormi immunite_paralyse immunite_etourdi immunite_percant immunite_tranchant '; + return; + case 'magic': + predicats += 'immuniteMagieGolem '; + return; + case 'critical': + predicats += 'immuniteAuxCritiques '; + return; case 'undead': case 'traits': case 'effects': case 'plants': case 'and': + case 'hits': return; default: log("Immunit\xE9 \xE0 " + i + " non trait\xE9e"); @@ -43024,7 +43555,7 @@ var COFantasy = COFantasy || function() { let rdn = attr.get('current'); if (rdn) { rdn = '' + rdn; - rdn = rdn.replace('bludgeoning', 'contondant').replace('slashing', 'tranchant').replace('piercing', 'percant').replace('silver', 'argent').replace('magic', 'magique').replace('adamantine', 'adamantium').replace('cold iron', 'ferFroid').replace('/-', ''); + rdn = rdn.replace('bludgeoning', 'contondant').replace('slashing', 'tranchant').replace('piercing', 'percant').replace('silver', 'argent').replace('magic', 'magique').replace('adamantine', 'adamantium').replace('cold iron', 'ferFroid').replace(' or ', '_').replace('good', 'beni').replace('/-', '').replace('/\xD1', ''); //\xD1 est le tiret long if (rd === '') rd = rdn; else rd += ', ' + rdn; } @@ -43064,6 +43595,9 @@ var COFantasy = COFantasy || function() { case 'plant': predicats += 'plante vegetatif '; break; + case 'ooze': + predicats += 'vegetatif '; + break; default: if (npcType.includes('humanoid')) { predicats += 'humanoide '; @@ -46229,6 +46763,7 @@ var COFantasy = COFantasy || function() { return copyOldTokenToNewToken(tokenMJ[0], perso, evt); } + //Change le token de perso en nouveauToken function copyOldTokenToNewToken(nouveauToken, perso, evt) { let token = perso.token; setToken(nouveauToken, 'layer', 'objects', evt); @@ -46428,6 +46963,48 @@ var COFantasy = COFantasy || function() { }, options); } + // !cof-division-vase + function divisionVase(msg) { + getSelected(msg, function(selected, playerId) { + if (selected.length === 0) { + sendPlayer(msg, "Pas de vase \xE0 diviser", playerId); + return; + } + const evt = { + type: 'division de vase' + }; + addEvent(evt); + iterSelected(selected, function(perso) { + let pv = parseInt(perso.token.get('bar1_value')); + if (isNaN(pv) || pv < 11) { + sendPlayer(msg, "Pas assez de PV (" + pv + ") pour se diviser", playerId); + return; + } + let pv_max = parseInt(perso.token.get('bar1_max')); + if (isNaN(pv_max) || pv_max < pv) pv_max = pv; + pv = Math.floor(pv / 2); + pv_max = Math.floor(pv_max / 2); + updateCurrentBar(perso, 1, pv, evt, pv_max); + // on diminue la taille + let width = perso.token.get('width'); + let height = perso.token.get('height'); + width = Math.ceil(width * 0.9); + height = Math.ceil(height * 0.9); + setToken(perso.token, 'width', width, evt); + setToken(perso.token, 'height', height, evt); + let tokenFields = getTokenFields(perso.token); + tokenFields.imgsrc = thumbImage(tokenFields.imgsrc); + let token2 = createObj('graphic', tokenFields); + if (!token2) { + sendPlayer(msg, "Pas moyen de copier, le faire \xE0 la main", playerId); + } else { + sendPlayer(msg, "token cr\xE9\xE9. Le bouger et le renommer", playerId); + } + sendPerso(perso, "se divise en deux"); + }); + }); + } + //n'est appel\xE9 qui si msg.content commence par !cof- function apiCommand(msg) { msg.content = msg.content.replace(/\s+/g, ' '); //remove duplicate whites @@ -46444,6 +47021,9 @@ var COFantasy = COFantasy || function() { case '!cof-agrandir-page': agrandirPage(msg); return; + case '!cof-aile-forge-runique': + entrerAileForgeRunique(msg); + return; case '!cof-animation-des-objets': animationDesObjets(msg); return; @@ -46495,6 +47075,9 @@ var COFantasy = COFantasy || function() { case '!cof-degainer': parseDegainer(msg); return; + case '!cof-division-vase': + divisionVase(msg); + return; case '!cof-dmg': parseDmgDirects(msg); return; @@ -46504,6 +47087,9 @@ var COFantasy = COFantasy || function() { case '!cof-effet-chaque-d20': setEffetChaqueD20(msg); return; + case "!cof-effet-combat": + effetCombat(msg); + return; case '!cof-effet-temp': parseEffetTemporaire(msg); return; @@ -46521,6 +47107,9 @@ var COFantasy = COFantasy || function() { case '!cof-explosion': attaqueExplosion(msg); return; + case '!cof-fin-changement-de-forme': + finChangementDeForme(msg); + return; case '!cof-hors-combat': //ancienne syntaxe, plus document\xE9e case '!cof-fin-combat': sortirDuCombat(); @@ -46601,9 +47190,6 @@ var COFantasy = COFantasy || function() { case '!cof-retour-boomerang': retourBoomerang(msg); return; - case '!cof-fin-changement-de-forme': - finChangementDeForme(msg); - return; case "!cof-rune-protection": runeProtection(msg); return; @@ -46645,6 +47231,9 @@ var COFantasy = COFantasy || function() { case '!cof-tenebres-magiques': tenebresMagiques(msg); return; + case '!cof-tp-auto': + setTPAuto(msg); + return; case '!cof-undo': undoEvent(); return; @@ -46654,9 +47243,6 @@ var COFantasy = COFantasy || function() { case '!cof-zone-de-vie': lancerZoneDeVie(msg); return; - case "!cof-effet-combat": - effetCombat(msg); - return; case "!cof-effet": parseEffetIndetermine(msg); return; @@ -47169,6 +47755,14 @@ var COFantasy = COFantasy || function() { effetTempGenerique: { generic: true }, + endormiTemp: { + activation: "s'endort", + actif: "dort profond\xE9ment", + fin: "se r\xE9veille", + msgSave: "r\xE9sister au sommeil", + prejudiciable: true, + visible: true + }, etourdiTemp: { activation: "est \xE9tourdi : aucune action et -5 en DEF", activationF: "est \xE9tourdie : aucune action et -5 en DEF", @@ -48269,6 +48863,12 @@ var COFantasy = COFantasy || function() { actif: "est en furie draconide", fin: "retrouve son calme" }, + malchance: { + activation: "entre dans une aura de malchance", + actif: "est dans une aura de malchance", + fin: "n'est plus dans une aura de malchance", + prejudiciable: true, + }, protectionContreLeMal: { activation: "re\xE7oit une b\xE9n\xE9diction de protection contre le mal", actif: "est prot\xE9g\xE9 contre le mal", @@ -50146,8 +50746,9 @@ var COFantasy = COFantasy || function() { stateCOF.tokensTemps.forEach(function(tt) { if (!tt.intrusion) return; //tt.intrusion est exprim\xE9 en pixels - let bombe = getTokenTemp(tt); + let bombe = getTokenTemp(tt, pageId); if (!bombe) return; + if (bombe.get('pageid') != pageId) return; let pb = pointOfToken(bombe); let distance = distancePoints(pt_depart, pb); if (distance < tt.intrusion) return; //On est parti de la zone de d\xE9part @@ -50182,6 +50783,43 @@ var COFantasy = COFantasy || function() { }); } } + if (stateCOF.tokensActifs && stateCOF.tokensActifs[pageId]) { + let pt_arrivee = { + x, + y + }; + let pt_depart = { + x: prev.left, + y: prev.top + }; + let rayon = tokenSizeAsCircle(token) / 2; + let estTP; + stateCOF.tokensActifs[pageId].forEach(function(tt) { + if (estTP) return; + let tp = getTokenTemp(tt, pageId); + if (!tp) { + removeTokenActif(tt.id, pageId); + return; + } + if (tt.rayon === undefined) { + if (!(intersection(x, token.get('width'), tp.get('left'), tp.get('width')) && + intersection(y, token.get('height'), tp.get('top'), tp.get('height')))) return; + } else { + let pb = pointOfToken(tp); + let distance = distancePoints(pt_depart, pb); + if (distance < tt.rayon) return; //On est parti de la zone de d\xE9part + let distToTrajectory = + distancePixTokenSegment(tp, pt_depart, pt_arrivee); + if (distToTrajectory > tt.rayon + rayon) return; + } + let s = trouveSortieEscalier(tp, true, false); + if (!s || !s.sortieEscalier) s = trouveSortieEscalier(tp, false, false); + if (!s || !s.sortieEscalier) return; + prendreEscalier(perso, pageId, s.sortieEscalier); + estTP = true; + }); + if (estTP) return; + } //Effets des auras, asynchrone if (stateCOF.combat && stateCOF.combat.auras) { const evt = { @@ -51232,8 +51870,7 @@ var COFantasy = COFantasy || function() { } token.bar1_link = attrPV[0].id; token.pageid = pageId; - token.imgsrc = token.imgsrc.replace('/med.png', '/thumb.png'); - token.imgsrc = token.imgsrc.replace('/max.png', '/thumb.png'); + token.imgsrc = thumbImage(token.imgsrc); let newToken = createObj('graphic', token); if (newToken) { setDefaultTokenForCharacter(character, newToken); diff --git a/COFantasy/ChangeLog.md b/COFantasy/ChangeLog.md index a8c6c63186..713767b3a9 100644 --- a/COFantasy/ChangeLog.md +++ b/COFantasy/ChangeLog.md @@ -16,6 +16,8 @@ * Implémentation de la version avancée du drain de force de Dominia. ### Autres améliorations +* Possibilité d'avoir des escaliers (ou portails) automatiques. +* Ajout d'options --finEtat et --finEffet pour les attaques. * Prise en compte de decrcAttribute pour les actions montrées. * Ajout d'un effet temporaire générique. * Les cadavres réanimés sont considérés comme chair à canon du nécromancien s'il possède cette capacité. diff --git a/COFantasy/doc.html b/COFantasy/doc.html index 5affba2677..6a9f0c23aa 100644 --- a/COFantasy/doc.html +++ b/COFantasy/doc.html @@ -397,7 +397,9 @@

    Options pour l'attaque :

  • --decrLimitePredicatParTour nom : l'attaque n'est possible que si un prédicat nom existe et si elle n'a pas été utilisée plus de fois dans le tour que la valeur de ce prédicat. L'attaque augmente ce nombre de 1.
  • --tempsRecharge effet duree : l'attaque n'est possible que si l'effet est inactif sur l'attaquant, et de plus active l'effet sur l'attaquant pour la durée indiquée si l'attaque est possible. Il existe un effet temporaire générique, rechargeGen(desc) que vous pouvez utiliser si aucun effet existant ne correspond pour votre attaque.
  • --etat e: si l'attaque touche, la cible passe dans l'état e. Il est aussi possible de spécifier une caractéristique et un seuil (comme pour !cof-set-state) pour faire afficher à chaque tour une action permettant de se libérer de l'état.
  • +
  • --finEtat e: si l'attaque touche, la cible sort de l'état e.
  • --effet e duree : ajoute à la cible l'effet temporaire e pour la duree spécifiée. Pour que cela soit automatiquement mis à jour, il faut utiliser le turn tracker. Noter que l'argument de durée peut être omis pour certains effets, comme ceux qui par définition durent tout le combat.
  • +
  • --finEffet e : enlève à la cible l'effet temporaire e.
  • --valeur v : spécifie une valeur au dernier effet mentionné.
  • --optionEffet opt arg1 arg2 ... : spécifie une option au dernier effet mentionné. L'option opt doit être donnée sans le -- et sera passée telle quelle à l'effet, par exemple pour les dégâts périodiques, si on ne veut pas que la RD s'applique, on pourra ajouter --optionEffet ignoreRD.
  • --peur seuil duree : fait un effet de peur si l'attaque touche. Le seuil sert pour le test de sagesse de résistance à la peur (tient compte de la capacité sans peur du chevalier).
  • @@ -909,8 +911,9 @@

    Escaliers et portails

    Chaque escalier de la même colonne doit avoir le même nom, et différer seulement par la lettre finale. Ensuite, si un ensemble de tokens est sur un escalier, utilisez !cof-escalier pour téléporter les tokens à l'emplacement de l'escalier suivant dans l'ordre des lettres. Si vous utilisez l'argument haut, alors les tokens n'iront pas plus loin que le dernier étage, et avec l'argument bas, il iront dans l'ordre inverse.

    Dans l'exemple précédent, il serait téléporté à l'emplacement du token nommé EscalierB. Cela ne fonctionne que si les bouts de l'escalier sont sur la même carte. À vous de choisir sir vous préférez révéler cette fonctionalité à vos joueurs, ou si vous le faites vous-même.

    La limite au nombre d'escaliers est de 12 étages, donc pas de lettre après L.

    -

    Il est possible d'avoir des escaliers qui mènent à d'autres cartes (ce qui fait alors changer la carte vue par le joueur). Il suffit que le nom de l'escalier commence par tmap_. Attention, à cause de limitations de Roll20, cela ne peut fonctionner que si l'image du token est dans une library personnelle d'un joueur : si elle vient du marketplace, il fait d'abord la copier dans sa library, puis utiliser l'image qui est dans la library pour le token.

    +

    Il est possible d'avoir des escaliers qui mènent à d'autres cartes (ce qui fait alors changer la carte vue par le joueur). Il suffit que le nom de l'escalier commence par tmap_. Attention, à cause de limitations de Roll20, cela ne peut fonctionner que si l'image du token est dans une library personnelle d'un joueur : si elle vient du marketplace, il faut d'abord la copier dans sa library, puis utiliser l'image qui est dans la library pour le token.

    Pour des escaliers plus flexibles (voire des portails), on peut décrire dans le champ des Notes du MJ du token sur la couche MJ une destination quand on monte et une destination quand on descend. Il suffit de mettre sur une ligne, descend: directement suivi du nom du token vers lequel aller pour descendre, et/ou sur une autre ligne monte: suivi du nom du token vers lequel aller quand on monte. Par exemple, pour bloquer un escalier qui descend, il suffit de mettre descend: dans le champ de notes du MJ. On peut aussi faire des "escaliers" à sens unique.

    +

    Il est possible d'automatiser les escaliers, de façons à ce que les tokens qui passent sur le lieux de l'escalier soient automatiquement transportés. Pour cela, sélectionner le token de l'escalier et lancer la commande !cof-tp-auto. Dans sa version sans argument, le dépacement automatique aura lieu dès qu'un token sera dans le rectangle du token de l'escalier. Sinon, on peut préciser un rayon (en mètres), et le mouvement aura lieur dès que le token sera à moins de cette distance du centre de l'escalier. Le déplacement se fera vers le haut si c'est possible, et sinon vers le bas. À noter que Roll20 peut perdre parfois les données sur les tokens entre les parties. Quand c'est le cas, le script va s'efforce de rerouver les escaliers automatiques, mais ça a plus de chances de marcher si vous donnez un nom unique au token de votre escalier. Pour mettre fin au déplacement automatique sur un escalier, sélectionner le token de l'escalier et taper la commande !cof-tp-auto off.

    Montures

    @@ -2494,6 +2497,7 @@

    4.6 Capacités diverses

  • Faucheuse de géants: ajouter le modifcateur tueurDeGrands aux attaques.
  • Fièvre du chêne : pour ajouter encore une RD de 5 contre le feu, ajouter un prédicat fievreChene.
  • Fiévreux : !cof-effet fievreux donne -2 en attaque, au dégâts et aux tests.
  • +
  • Forge runique (l'éveil des seigneurs des runes) : pour entrer dans une salle associée à un péché, utiliser !cof-aile-forge-runique, suivi du nom du péché (sans majuscule ni accent). Pour associer un péché à un personnage, lui ajouter un prédicat pecheThassilion de valeur le péché, toujours en minusucles et sans accent.
  • Foudres du temps : !cof-effet-chaque-d20 foudresDuTemps permet d'activer les foudres du temps. On peut donner un argument pour remplacer la valeur par défaut (13) par un autre jet de dés. Un deuxième argument permet de faire sortir la foudre pour tout jet entre les deux valeurs. Par exemple !cof-effet-chaque-d20 foudresDuTemps 13 14 fait venir la foudre pour tout jet de 13 ou 14. Si votre jeu contient un son (ou playlist) nommé Foudre (avec la majuscule), le son sera joué à chaque fois que la foudre frappera. Finalement, !cof-effet-chaque-d20 foudresDuTemps fin met fin aux foudres du temps.
  • Frénésie : Ajouter un prédicat frenesie avec comme valeur le nombre de PV en-dessous duquel la créature devient frénétique (+2 aus jets d'attaque).
  • Général (pour la campagne Vengeance) : Ajouter au général un prédicat generalVengeance, et aux gardes d'élite un prédicat gardeEliteVengeance.
  • @@ -2502,6 +2506,12 @@

    4.6 Capacités diverses

  • Huile de fort assaut : !cof-effet-temp dmgArme(L) 10 --valeur 1d6 où L est le label de l'arme qu'on veut enduire d'huile de fort assaut.
  • Hurlement : Pour les chiens infernaux, !cof-peur [[10+?{Nombre de chiens?}]] 2d4 --titre Hurlement --immuniseSiResiste hurlement --disque 50 --saufAllies, et pour les fantômes, !cof-peur 12 1d6 --titre Hurlement terrible --immuniseSiResiste hurlement --disque 10 --saufAllies.
  • Illusion : Pour les illusions qui disparaissent et réapparaissent un peu plus loin quand on essaie de les toucher, ajouter un prédicat estUneIllusion.
  • +
  • Immunité à la magie : +
      +
    • Immunité totale : prédicat immunite_magique.
    • +
    • Immunité à la magie des golems : prédicat immuniteMagieGolem. +
    +
  • Immunité aux armes : ajouter un prédicat immuniteAuxArmes. À noter qu'il est possible de spécifier le niveau de magie d'une attaque en donnant un argument à l'option --magique. Ainsi une arme de niveau magique 3 aura en argument --magique 3.
  • Immunité aux attaques non magiques : ajouter un prédicat immunite_nonMagique
  • Immunité aux critiques : ajouter un prédicat immuniteAuxCritiques.
  • From e08ed905e72681d1e272a7bdc3987c6bad881dfd Mon Sep 17 00:00:00 2001 From: G-G-G <38135275+G-G-G@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:47:54 +0000 Subject: [PATCH 25/42] Hunters-Mark Now supports the ability to mark multiple characters at a tie. Some characters need this. --- HuntersMark/README.md | 20 ++-- HuntersMark/huntersmark.js | 203 ++++++++++++++++++++++++++----------- HuntersMark/script.json | 4 +- 3 files changed, 159 insertions(+), 68 deletions(-) diff --git a/HuntersMark/README.md b/HuntersMark/README.md index a2dc36f468..da608d8d0f 100644 --- a/HuntersMark/README.md +++ b/HuntersMark/README.md @@ -1,22 +1,28 @@ Hunter's Mark ============= -A script that lets each character have their own custom Status Marker, which they can use to mark other tokens. You can make only one target at a time. When you mark a new target, the old marker is removed. This is perfect for abilities like D&D's Hunter's Mark. +A script that lets each character have their own custom Status Marker, which they can use to mark other tokens. There are two settings. If you are a hunter-like character, you can mark only one target at a time. When you mark a new target, the old marker is removed frm all tokens but yourself and the target. This is perfect for abilities like D&D's Hunter's Mark. +The second setting lets you mark any number of characters. -Run it with !hunters-mark followed by one of the commands below. +Run this script with `!hunters-mark ` followed by one of the commands below. Available Commands ================== -* `help`: Shows a help file like this one. -* `add`: Select a character, and make sure they have one status marker applied. Then click Add, and that character will be added to the list of hunters, and the marker will be the one they use to mark targets. -* `delete`: Delete a character from the list of hunters. -* `show`: Show a list of hunters, their markers, and their current marked target, if any. +* `help`: Shows a detailed help file, and the menu buttons afterwards. +* `add`: Select a character who has exactly one token marker assigned. Then click Add or Hunter, and that character will be added to the list of hunter-like characters, and the marker will be the one they use to mark targets. +* `bard`: exactly as above. Select a character with one status mark assigned. But they rae added to rhe list of bard-like characters, and can assign marks to multiple targets simultaneously. Maybe all characters will be added here, depending on how you use marks. +* `delete`: Delete a character from all displayed lists. +* `show`: Show a list of characters with their markers, and the menu buttons afterwards. * `menu`: prints a set of buttons, to activate the scripts commands. Marking or Unmarking a Target ============================= -* `!hunters-mark @{selected|character_id} @{target|token_id}`: To mark or unmark a target, you need to supply your character id, and the token id of a target. The same command is used to mark or unmark a target. +* `!hunters-mark @{selected|token_id} @{target|token_id}`: To mark or unmark a target, you need to supply your own token id, and the token id of a target. The same command is used to mark or unmark a target. +Important: this has changed with version 0.4 to support bard-like characters. Previous versions used to use `@{selected|character_id}` - but you MUST now use `@{selected|token_id}`. Update any macros (this script's buttons are automatically updated). +Hunter and Bard +=============== +The two types of behaviour are classified as Hunter and Bard, because most people will be familiar with D&D. If you can think of alternate terms for these, send then to Gigs on the roll20 forums. diff --git a/HuntersMark/huntersmark.js b/HuntersMark/huntersmark.js index 9711d8f98e..c37a164d43 100644 --- a/HuntersMark/huntersmark.js +++ b/HuntersMark/huntersmark.js @@ -1,34 +1,51 @@ /* !hunters-mark !hunters-mark add + !hunters-mark bard !hunters-mark delete !hunters-mark help !hunters-mark show !hunters-mark menu - !hunters-mark @{selected|character_id} @{target|token_id} + !hunters-mark @{selected|token_id} @{target|token_id} + this used to be !hunters-mark @{selected|character_id} @{target|token_id} add adds selected character as a new hunter. must have exactly one marker - delete removes currently selected character from hunters list + bard as above, but adds a bard-like character + delete removes the currently selected character from all lists help shows help menu, and description of each feature show shows current hunters in state menu shows the menu of buttons if none of above - assumes arg[1] is a hunter character_id, and arg[2] is target token id. - if not, will send a warning and end script. + assumes arg[1] is a the acting character's token_id, and arg[2] is the target's token id. + (note: this is a change - earlier versions of the script used character_id for args[1]) + if not, will send a warning and end the script. */ +var API_Meta = API_Meta || {}; +API_Meta.Reporter = { + offset: Number.MAX_SAFE_INTEGER, + lineCount: -1 +}; { + try { + throw new Error(''); + } catch (e) { + API_Meta.Reporter.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (4)); + } +} + const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars const script_name = 'HUNTERSMARK'; - const version = '0.3.0'; + const version = '0.4.0'; const lastUpdate = 1593500895369; const tokenName = token => token.get('name') ? token.get('name') : (token.get('_id') ? token.get('_id') : 'Unknown'); const findHunter = (hunter, hunted = 'hunter') => state.HUNTERSMARK.hunters.findIndex(item => item[hunted] === hunter); + const findBard = (hunter, hunted = 'hunter') => state.HUNTERSMARK.bards.findIndex(item => item[hunted] === hunter); const getWho = who => who.split(' (GM)')[0]; - const mark = '@{selected|character_id} @{target|token_id}'; + const mark = '@{selected|token_id} @{target|token_id}'; const CSS = { container: 'border: 1pt solid green; background-color: white; font-size: 0.9em; border-radius: 10px;', table: '', @@ -46,8 +63,14 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars if(!state.hasOwnProperty('HUNTERSMARK')) { state.HUNTERSMARK = { schema: 0.0, - hunters: [] + hunters: [], + }; + } else { + if(!state.HUNTERSMARK.bards) { + state.HUNTERSMARK.bards = []; + state.HUNTERSMARK.schema = 0.1; + } } /* hunter: { hunter: id of character, @@ -74,8 +97,10 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars if(!command || command.toLowerCase() === 'help') { showHelp(getWho(msg.who)); - } else if(command.toLowerCase() === 'add') { + } else if(command.toLowerCase() === 'add' || command.toLowerCase() === 'hunter') { hunter(msg, 1); + } else if(command.toLowerCase() === 'bard') { + hunter(msg, 2); } else if(command.toLowerCase() === 'delete') { hunter(msg, -1); } else if(command.toLowerCase() === 'show') { @@ -89,18 +114,31 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars sendChat(script_name,`/w "${getWho(msg.who)}" You must have only one token selected.`); return; } - tokenMarker(args[1], args[2], getWho(msg.who)); + const target_id = args[2]; + const token_id = args[1]; + let character_id; + try { + const token = getObj('graphic', token_id); + const rep = token.get('represents'); + const character = getObj('character', rep); + character_id = character.id; + } catch(e) { + sendChat(script_name,`/w "${getWho(msg.who)}" Check the character's token - is the REPRESENTS field assigned?`); + return; + } + tokenMarker(character_id, token_id, target_id, getWho(msg.who)); } }; const showHelp = who => { const help = { - show: 'This shows the current list of hunters, and their marks.', - add: 'To add a new hunter, select a token representing the character and apply the status marker you want to use as their mark. Then click Add.', - 'delete': 'To remove a character from the list of hunters, select a token representing them and click Delete.', + show: 'This shows the current list of activated tokens, and their marks.', + hunter: 'To define a new hunter-like character, select a token representing the character and apply the status marker you want to use as their mark. Then click Hunter.', + bard: 'To define a new bard-like character, select a token representing the character and apply the status marker you want to use as their mark. Then click Bard.', + 'delete': 'To remove a character from their current list, select a token representing them and click Delete.', help: 'Show this description.', - menu: "Show a set of buttons to activate the script's features.", - 'mark a target': `

    To mark a target, use !hunters-mark [character id of hunter] [token id of target].

    A good way to do this is !hunters-mark ${mark}

    ` + menu: "Displays a button to activate each of the script's features. For convenience, this menu is always shown after Help and Show.", + 'mark a target': `

    To mark a target, use !hunters-mark [token id of hunter] [token id of target].

    For example: !hunters-mark ${mark}

    ` }; let output = `

    Hunter's Mark Instructions

    Use !hunters-mark followed by one of the commands below.

    ${CSS.table}`; Object.entries(help).forEach(([key, value]) => { @@ -114,7 +152,8 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars const showMenu = (who) => { const buttons = { Show: 'show', - Add: 'add', + Hunter: 'add', + Bard: 'bard', 'Delete': 'delete', Help: 'help' @@ -132,8 +171,12 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars const showState = (who) => { const tokenMarkers = JSON.parse(Campaign().get('token_markers')); const getIcon = tag => tokenMarkers.find(item => tag === item.tag).url; - const hunters = state.HUNTERSMARK.hunters.map(hunter => `
    `); - sendChat(script_name, `/w "${who}"

    Hunter Details

    **${getObj('character', hunter.hunter).get('name')}**${hunter.marked ? `

    Marked: ${getObj('graphic',hunter.marked).get('name')}` : ''}

    ${hunters.join('')}
    `); + //const hunters = state.HUNTERSMARK.hunters.map(hunter => `

    **${getObj('character', hunter.hunter).get('name')}**${hunter.marked ? `

    Marked: ${getObj('graphic',hunter.marked).get('name')}` : ''}

    `); + + const hunters = state.HUNTERSMARK.hunters.map(hunter => `

    **${getObj('character', hunter.hunter).get('name')}**

    `); + const bards = state.HUNTERSMARK.bards.map(hunter => `

    **${getObj('character', hunter.hunter).get('name')}**

    `); + sendChat(script_name, `/w "${who}"

    Hunter-like Characters

    Only one target can be marked.

    ${hunters.join('')}

    Bard-like Characters

    Multiple targets can be marked.

    ${bards.join('')}
    `); + showMenu(who); }; const hunter = (msg, addordelete) => { @@ -148,16 +191,26 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars if (token) { let character = getObj('character', token.get('represents')); if (character) { + const found_hunter = findHunter(character.get('_id')); + const found_bard = findBard(character.get('_id')); if(addordelete === -1) { // delete selected characters from state - const found = findHunter(character.get('_id')); - if(found === -1) { + if(found_hunter === -1) { excluded.push(tokenName(token)); } else { - state.HUNTERSMARK.hunters.splice(found, 1); + state.HUNTERSMARK.hunters.splice(found_hunter, 1); } - } else if (addordelete === 1) { + + if(found_bard === -1) { + excluded.push(tokenName(token)); + } else { + state.HUNTERSMARK.bards.splice(found_bard, 1); + } + } else if (addordelete >= 1) { // only need to check marker if adding. + + const colour_markers = ["red", "blue", "green", "brown", "purple", "pink", "yellow", "dead"]; + const marker = token.get('statusmarkers').split(','); if(marker.length === 0 || marker.length > 1 || marker[0] === '') { excluded.push(tokenName(token)); @@ -167,14 +220,23 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars marked: '', mark: marker[0] }; - const found = findHunter(newHunter.hunter); - if(found === -1) { + //problem with coloured status markers - they cause sandbox to crash + if (colour_markers.includes(newHunter.mark)) { + sendChat(script_name,`You cannot use the pure color or dead markers (the top row).`); + return; + } + + // need to delete the token if it already exists, then add it again. + if(found_hunter > -1) state.HUNTERSMARK.hunters.splice(found_hunter, 1) + if(found_bard > -1) state.HUNTERSMARK.bards.splice(found_bard, 1); + if(addordelete == 2) { + state.HUNTERSMARK.bards.push(newHunter); + } else if (addordelete == 1) { state.HUNTERSMARK.hunters.push(newHunter); - } else { - state.HUNTERSMARK.hunters.splice(found, 1, newHunter); } } - } + } + showstate = true; // report characters in state. } else { @@ -192,52 +254,39 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars } }; - const tokenMarker = (hunter_id, target_id, who) => { - const hunter_index = findHunter(hunter_id); - if(hunter_index === -1) { - sendChat(script_name, `/w "${who}" Hunter is not found. Check they are set up properly.`); + const tokenMarker = (character_id, token_id, target_id, who) => { + const hunter_index = findHunter(character_id); + const bard_index = findBard(character_id); + if(hunter_index === -1 && bard_index === -1) { + sendChat(script_name, `/w "${who}" Marker is not found. Check they are set up properly.`); return; } - const token = getObj('graphic', target_id); - if(!token) { + const target_token = getObj('graphic', target_id); + if(!target_token) { sendChat(script_name, `/w "${who}" Target token is not a valid target.`); return; } - /* here starts the actual work of the script */ - if(target_id == state.HUNTERSMARK.hunters[hunter_index].marked) { - // the target token matches the id stored in owner. - // This character is already marked, so unmark him and clear mark_id - state.HUNTERSMARK.hunters[hunter_index].marked = ''; - changeMarker(target_id, state.HUNTERSMARK.hunters[hunter_index].mark, 'remove'); - } else { - // marking a new target so: - // get old mark, and remove mark from previous character - // update mark_id and add marker - const oldmark = state.HUNTERSMARK.hunters[hunter_index].marked; - if(oldmark !== '') { - // find old character, remove mark from them, then: - changeMarker(oldmark, state.HUNTERSMARK.hunters[hunter_index].mark, 'remove'); - } - state.HUNTERSMARK.hunters[hunter_index].marked = target_id; - changeMarker(target_id, state.HUNTERSMARK.hunters[hunter_index].mark, 'add'); - + + const mark = (hunter_index > -1) ? state.HUNTERSMARK.hunters[hunter_index].mark : state.HUNTERSMARK.bards[bard_index].mark; + // ERROR: want to handle if undefined + if(hunter_index > -1) { + removeMarkers(mark, target_id, token_id); } + updateMarker(target_id, mark); }; - const changeMarker = (tid, marker, addorremove = 'add') => { + const updateMarker = (tid, marker) => { const token = getObj('graphic', tid); if(token) { let tokenMarkers = token.get('statusmarkers').split(','); - if(addorremove === 'add') { - if(!tokenMarkers.includes(marker)) { - tokenMarkers.push(marker); - } - } else if(addorremove === 'remove') { + //sendChat('', `marker: ${marker}; markers: ${tokenMarkers}`) + if(!tokenMarkers.includes(marker)) { + tokenMarkers.push(marker); + } else { tokenMarkers = tokenMarkers.filter(item => item !== marker); - } else { - return; - } + } token.set('statusmarkers', tokenMarkers.join(',')); + } }; @@ -250,4 +299,40 @@ const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars registerEventHandlers(); }); + const removeMarkers = (marker, ...token_ids) => { + // tokens is a rest array - it will be an array of tokens to ignore with this function + let tokens = findObjs({_subtype: "token"}); + // want to loop thtrough every token t + tokens.forEach(token => { + const tid = token.id; + if(!token_ids.includes(tid)) { + let tokenMarkers = token.get('statusmarkers').split(','); + if(tokenMarkers.includes(marker)) { + tokenMarkers = tokenMarkers.filter(item => item !== marker); + token.set('statusmarkers', tokenMarkers.join(',')); + } + } + }); + } })(); +/* CHANGES + make sure to use token_id at the start in place of character_id + Removes a crash when selecting any of the first row markers (coloutred and dead). + Adds bard-like characters that can assign marks to multiple characters. + Can change the assigned marker just by adding them again (before, you had to delete then re-add) + Can swap characters between lists just by adding them to relevant list + Can't add the same character multiple times to the same list. + The way last-marked character is recorded has changed, so it isn't displayted in the Show list any more. + + + Might want clear marks: + clear all tokens of all marks. (Clear All) + clear a single token of all marks (use target). (Clear Token) + Remove a specific mark from all tokens. (clear Mark) select a token with one or mor marks, and remove them from all tokens. + if a character can mark, add that on to their token (Show Mark). Marke select multiple tokens - could be useful before Clear Mark + these would be different functions. Culd add another row of buttons. + + What if a character has a token-marker that doesn't exist? For example, it was created with a custom set in one game, and that was changed. + + Show function: when displaying chsraacters, maybe don't show a list if it has no characters. If no characters in both, have a boilerplate message. +*/ \ No newline at end of file diff --git a/HuntersMark/script.json b/HuntersMark/script.json index 7562ac3d2f..6c4fc75dcf 100644 --- a/HuntersMark/script.json +++ b/HuntersMark/script.json @@ -1,8 +1,8 @@ { "name": "Hunter's Mark", "script": "huntersmark.js", - "version": "0.3.0", - "previousversions": [], + "version": "0.4.0", + "previousversions": ["0.3.0"], "description": "A script that lets each character have their own custom Status Marker, which they can use to mark other tokens. You can make only one target at a time. When you mark a new target, the old marker is removed. This is perfect for abilities like D&D's Hunter's Mark.\r\rFor instructions run `!hunters-mark --help`.", "authors": "GiGs", "roll20userid": "157788" From 103f1ed498197e8954055d59592a58f6007e0067 Mon Sep 17 00:00:00 2001 From: G-G-G <38135275+G-G-G@users.noreply.github.com> Date: Sat, 30 Nov 2024 07:26:35 +0000 Subject: [PATCH 26/42] Hunters Mark updated the version folder --- HuntersMark/0.4.0/huntersmark.js | 253 +++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 HuntersMark/0.4.0/huntersmark.js diff --git a/HuntersMark/0.4.0/huntersmark.js b/HuntersMark/0.4.0/huntersmark.js new file mode 100644 index 0000000000..9711d8f98e --- /dev/null +++ b/HuntersMark/0.4.0/huntersmark.js @@ -0,0 +1,253 @@ +/* + !hunters-mark + !hunters-mark add + !hunters-mark delete + !hunters-mark help + !hunters-mark show + !hunters-mark menu + !hunters-mark @{selected|character_id} @{target|token_id} + + add adds selected character as a new hunter. + must have exactly one marker + delete removes currently selected character from hunters list + help shows help menu, and description of each feature + show shows current hunters in state + menu shows the menu of buttons + + if none of above + assumes arg[1] is a hunter character_id, and arg[2] is target token id. + if not, will send a warning and end script. + +*/ +const HUNTERSMARK = (() => { // eslint-disable-line no-unused-vars + + const script_name = 'HUNTERSMARK'; + const version = '0.3.0'; + const lastUpdate = 1593500895369; + + const tokenName = token => token.get('name') ? token.get('name') : (token.get('_id') ? token.get('_id') : 'Unknown'); + const findHunter = (hunter, hunted = 'hunter') => state.HUNTERSMARK.hunters.findIndex(item => item[hunted] === hunter); + const getWho = who => who.split(' (GM)')[0]; + const mark = '@{selected|character_id} @{target|token_id}'; + const CSS = { + container: 'border: 1pt solid green; background-color: white; font-size: 0.9em; border-radius: 10px;', + table: '', + trow: '', + tend: '
    ', + tmiddle: '' , + trowend: '
    ', + button: 'border:0; margin-left: 3px; margin-right: 3px; padding-left: 5px; padding-right: 5px; border-radius: 7px; background:green;color:white;font-weight:bold;', + center: 'text-align: center;', + leftpad: 'padding-left: 10px;', + heading: 'text-align: center; text-decoration: underline; font-size: 16px; line-height: 24px;' + }; + + const checkState = () => { + if(!state.hasOwnProperty('HUNTERSMARK')) { + state.HUNTERSMARK = { + schema: 0.0, + hunters: [] + }; + } + /* hunter: { + hunter: id of character, + mark: tag of status marker to assign, + marked: id of last character marked, or '' + } + */ + }; + + const checkInstall = () => { + log(`-=> ${script_name.toUpperCase()} v${version} <=- [${new Date(lastUpdate)}]`); + // include state checking here + checkState(); + + }; + + const handleInput = (msg) => { + if (msg.type !== 'api' || !/!hunters-mark\b/.test(msg.content.toLowerCase())) { + return; + } + + const args = msg.content.split(/\s/); + const command = args[1] || ''; + + if(!command || command.toLowerCase() === 'help') { + showHelp(getWho(msg.who)); + } else if(command.toLowerCase() === 'add') { + hunter(msg, 1); + } else if(command.toLowerCase() === 'delete') { + hunter(msg, -1); + } else if(command.toLowerCase() === 'show') { + showState(getWho(msg.who)); + } else if(command.toLowerCase() === 'menu') { + showMenu(getWho(msg.who)); + /*} else if(command.toLowerCase() === 'mark') { + sendChat('player|' + getWho(msg.who),`!hunters-mark ${mark}`);*/ + } else { + if(msg.selected > 1) { + sendChat(script_name,`/w "${getWho(msg.who)}" You must have only one token selected.`); + return; + } + tokenMarker(args[1], args[2], getWho(msg.who)); + } + }; + + const showHelp = who => { + const help = { + show: 'This shows the current list of hunters, and their marks.', + add: 'To add a new hunter, select a token representing the character and apply the status marker you want to use as their mark. Then click Add.', + 'delete': 'To remove a character from the list of hunters, select a token representing them and click Delete.', + help: 'Show this description.', + menu: "Show a set of buttons to activate the script's features.", + 'mark a target': `

    To mark a target, use !hunters-mark [character id of hunter] [token id of target].

    A good way to do this is !hunters-mark ${mark}

    ` + }; + let output = `

    Hunter's Mark Instructions

    Use !hunters-mark followed by one of the commands below.

    ${CSS.table}`; + Object.entries(help).forEach(([key, value]) => { + output += `${CSS.trow}${key}${CSS.tmiddle}${value}${CSS.trowend}`; + }); + output += CSS.tend + '
    '; + sendChat(script_name, `/w "${who}" ${output}`); + showMenu(who); + }; + + const showMenu = (who) => { + const buttons = { + Show: 'show', + Add: 'add', + 'Delete': 'delete', + Help: 'help' + + }; + const output = `

    Hunters Mark Menu

    ` + + `

    ${makeButton('Mark / Unmark Target', mark, 'width: 192px; font-size: 1.1em; text-align:center;')}

    ` + + `

    ${Object.entries(buttons).reduce((list, [key, value]) => list + makeButton(key, value), '')}

    `; + sendChat(script_name, `/w "${who}" ${output}`); + }; + + const makeButton = (label, button, width='') => { + return `${label}`; + }; + + const showState = (who) => { + const tokenMarkers = JSON.parse(Campaign().get('token_markers')); + const getIcon = tag => tokenMarkers.find(item => tag === item.tag).url; + const hunters = state.HUNTERSMARK.hunters.map(hunter => `

    **${getObj('character', hunter.hunter).get('name')}**${hunter.marked ? `

    Marked: ${getObj('graphic',hunter.marked).get('name')}` : ''}

    `); + sendChat(script_name, `/w "${who}"

    Hunter Details

    ${hunters.join('')}
    `); + }; + + const hunter = (msg, addordelete) => { + if (!msg.selected) { + sendChat(script_name,`/w "${getWho(msg.who)}" You need to select at least one character's token, and each must have a single status marker assigned.`); + return; + } + let showstate = false; + let excluded = []; + (msg.selected||[]).forEach((obj) => { + let token = getObj('graphic', obj._id); + if (token) { + let character = getObj('character', token.get('represents')); + if (character) { + if(addordelete === -1) { + // delete selected characters from state + const found = findHunter(character.get('_id')); + if(found === -1) { + excluded.push(tokenName(token)); + } else { + state.HUNTERSMARK.hunters.splice(found, 1); + } + } else if (addordelete === 1) { + // only need to check marker if adding. + const marker = token.get('statusmarkers').split(','); + if(marker.length === 0 || marker.length > 1 || marker[0] === '') { + excluded.push(tokenName(token)); + } else { + const newHunter = { + hunter: character.get('_id'), + marked: '', + mark: marker[0] + }; + const found = findHunter(newHunter.hunter); + if(found === -1) { + state.HUNTERSMARK.hunters.push(newHunter); + } else { + state.HUNTERSMARK.hunters.splice(found, 1, newHunter); + } + } + } + showstate = true; + // report characters in state. + } else { + excluded.push(tokenName(token)); + } + } + + }); + + if(showstate) { + showState(getWho(msg.who)); + } + if(excluded.length > 0) { + sendChat(script_name, `/w "${getWho(msg.who)}" The following tokens were either missing elements or had too many markers, and were not updated: ${excluded.join(', ')}.`); + } + }; + + const tokenMarker = (hunter_id, target_id, who) => { + const hunter_index = findHunter(hunter_id); + if(hunter_index === -1) { + sendChat(script_name, `/w "${who}" Hunter is not found. Check they are set up properly.`); + return; + } + const token = getObj('graphic', target_id); + if(!token) { + sendChat(script_name, `/w "${who}" Target token is not a valid target.`); + return; + } + /* here starts the actual work of the script */ + if(target_id == state.HUNTERSMARK.hunters[hunter_index].marked) { + // the target token matches the id stored in owner. + // This character is already marked, so unmark him and clear mark_id + state.HUNTERSMARK.hunters[hunter_index].marked = ''; + changeMarker(target_id, state.HUNTERSMARK.hunters[hunter_index].mark, 'remove'); + } else { + // marking a new target so: + // get old mark, and remove mark from previous character + // update mark_id and add marker + const oldmark = state.HUNTERSMARK.hunters[hunter_index].marked; + if(oldmark !== '') { + // find old character, remove mark from them, then: + changeMarker(oldmark, state.HUNTERSMARK.hunters[hunter_index].mark, 'remove'); + } + state.HUNTERSMARK.hunters[hunter_index].marked = target_id; + changeMarker(target_id, state.HUNTERSMARK.hunters[hunter_index].mark, 'add'); + + } + }; + + const changeMarker = (tid, marker, addorremove = 'add') => { + const token = getObj('graphic', tid); + if(token) { + let tokenMarkers = token.get('statusmarkers').split(','); + if(addorremove === 'add') { + if(!tokenMarkers.includes(marker)) { + tokenMarkers.push(marker); + } + } else if(addorremove === 'remove') { + tokenMarkers = tokenMarkers.filter(item => item !== marker); + } else { + return; + } + token.set('statusmarkers', tokenMarkers.join(',')); + } + }; + + const registerEventHandlers = () => { + on('chat:message', handleInput); + }; + + on('ready', () => { + checkInstall(); + registerEventHandlers(); + }); + +})(); From 4c355d6ffcdd62a7fa0d68f0eddb8876c59d63f7 Mon Sep 17 00:00:00 2001 From: Nicole Brooks <83994164+NBrooks-Roll20@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:43:27 -0500 Subject: [PATCH 27/42] Adding 2024 support to StatusInfo script --- StatusInfo/0.3.12/StatusInfo.js | 793 ++++++++++++++++++++++++++++++++ StatusInfo/README.md | 2 +- StatusInfo/StatusInfo.js | 2 +- StatusInfo/script.json | 6 +- 4 files changed, 798 insertions(+), 5 deletions(-) create mode 100644 StatusInfo/0.3.12/StatusInfo.js diff --git a/StatusInfo/0.3.12/StatusInfo.js b/StatusInfo/0.3.12/StatusInfo.js new file mode 100644 index 0000000000..d3054aa866 --- /dev/null +++ b/StatusInfo/0.3.12/StatusInfo.js @@ -0,0 +1,793 @@ +/* + * Version: 0.3.11 + * Made By Robin Kuiper + * Skype: RobinKuiper.eu + * Discord: Atheos#1095 + * My Discord Server: https://discord.gg/AcC9VME + * Roll20: https://app.roll20.net/users/1226016/robin + * Roll20 Thread: https://app.roll20.net/forum/post/6252784/script-statusinfo + * Roll20 Wiki: https://wiki.roll20.net/Script:StatusInfo + * Github: https://github.com/RobinKuiper/Roll20APIScripts + * Reddit: https://www.reddit.com/user/robinkuiper/ + * Patreon: https://patreon.com/robinkuiper + * Paypal.me: https://www.paypal.me/robinkuiper + * + * COMMANDS (with default command): + * !condition [CONDITION] - Shows condition. + * !condtion help - Shows help menu. + * !condition config - Shows config menu. + * + * !condition add [condtion(s)] - Add condition(s) to selected tokens, eg. !condition add prone paralyzed + * !condition remove [condtion(s)] - Remove condition(s) from selected tokens, eg. !condition remove prone paralyzed +* !condition toggle [condtion(s)] - Toggles condition(s) of selected tokens, eg. !condition toggle prone paralyzed + * + * !condition config export - Exports the config (with conditions). + * !condition config import [json] - Import the given config (with conditions). + * + * TODO: + * Icon span + * whisper system + * stylings +*/ + +var StatusInfo = StatusInfo || (function() { + 'use strict'; + + let whisper, handled = [], + observers = { + tokenChange: [] + }; + + + const version = "0.3.11", + + // Styling for the chat responses. + style = "overflow: hidden; background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;", + buttonStyle = "background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center; float: right;", + conditionStyle = "background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;", + conditionButtonStyle = "text-decoration: underline; background-color: #fff; color: #000; padding: 0", + listStyle = 'list-style: none; padding: 0; margin: 0;', + + icon_image_positions = {red:"#C91010",blue:"#1076C9",green:"#2FC910",brown:"#C97310",purple:"#9510C9",pink:"#EB75E1",yellow:"#E5EB75",dead:"X",skull:0,sleepy:34,"half-heart":68,"half-haze":102,interdiction:136,snail:170,"lightning-helix":204,spanner:238,"chained-heart":272,"chemical-bolt":306,"death-zone":340,"drink-me":374,"edge-crack":408,"ninja-mask":442,stopwatch:476,"fishing-net":510,overdrive:544,strong:578,fist:612,padlock:646,"three-leaves":680,"fluffy-wing":714,pummeled:748,tread:782,arrowed:816,aura:850,"back-pain":884,"black-flag":918,"bleeding-eye":952,"bolt-shield":986,"broken-heart":1020,cobweb:1054,"broken-shield":1088,"flying-flag":1122,radioactive:1156,trophy:1190,"broken-skull":1224,"frozen-orb":1258,"rolling-bomb":1292,"white-tower":1326,grab:1360,screaming:1394,grenade:1428,"sentry-gun":1462,"all-for-one":1496,"angel-outfit":1530,"archery-target":1564}, + markers = ['blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-bomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch','strong', 'three-leaves', 'tread', 'trophy', 'white-tower'], + shaped_conditions = ['blinded', 'charmed', 'deafened', 'frightened', 'grappled', 'incapacitated', 'invisible', 'paralyzed', 'petrified', 'poisoned', 'prone', 'restrained', 'stunned', 'unconscious'], + + script_name = 'StatusInfo', + state_name = 'STATUSINFO', + + handleInput = (msg) => { + if (msg.type != 'api') return; + + // !condition BlindedBlinded + + // Split the message into command and argument(s) + let args = msg.content.split(' '); + let command = args.shift().substring(1); + let extracommand = args.shift(); + + if(command === state[state_name].config.command){ + switch(extracommand){ + case 'reset': + if(!playerIsGM(msg.playerid)) return; + + state[state_name] = {}; + setDefaults(true); + sendConfigMenu(); + break; + + case 'help': + if(!playerIsGM(msg.playerid)) return; + + sendHelpMenu(); + break; + + case 'config': + if(!playerIsGM(msg.playerid)) return; + + if(args.length > 0){ + if(args[0] === 'export' || args[0] === 'import'){ + if(args[0] === 'export'){ + makeAndSendMenu('
    '+HE(JSON.stringify(state[state_name]))+'

    Copy the entire content above and save it on your pc.

    '); + } + if(args[0] === 'import'){ + let json; + let config = msg.content.substring(('!'+state[state_name].config.command+' config import ').length); + try{ + json = JSON.parse(config); + } catch(e) { + makeAndSendMenu('This is not a valid JSON string.'); + return; + } + state[state_name] = json; + sendConfigMenu(); + } + + return; + } + + + let setting = args.shift().split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + if(key === 'prefix' && value.charAt(0) !== '_'){ value = '_' + value} + + state[state_name].config[key] = value; + + whisper = (state[state_name].config.sendOnlyToGM) ? '/w gm ' : ''; + } + + sendConfigMenu(); + break; + + // !s config-conditions + // !s config-conditions add + // !s config-conditions prone + // !s config-conditions prone name|blaat + case 'config-conditions': + if(!playerIsGM(msg.playerid)) return; + + let condition = args.shift(); + if(condition === 'add'){ + condition = args.shift(); + if(!condition){ + sendConditionsConfigMenu('You didn\'t give a condition name, eg. !'+state[state_name].config.command+' config-conditions add Prone.'); + return; + } + if(state[state_name].conditions[condition.toLowerCase()]){ + sendConditionsConfigMenu('The condition `'+condition+'` already exists.'); + return; + } + + state[state_name].conditions[condition.toLowerCase()] = { + name: condition, + icon: 'red', + description: '' + } + + sendSingleConditionConfigMenu(condition.toLowerCase()); + return; + } + + if(condition === 'remove'){ + let condition = args.shift(), + justDoIt = (args.shift() === 'yes'); + + if(!justDoIt) return; + + if(!condition){ + sendConditionsConfigMenu('You didn\'t give a condition name, eg. !'+state[state_name].config.command+' config-conditions remove Prone.'); + return; + } + if(!state[state_name].conditions[condition.toLowerCase()]){ + sendConditionsConfigMenu('The condition `'+condition+'` does\'t exist.'); + return; + } + + delete state[state_name].conditions[condition.toLowerCase()]; + sendConditionsConfigMenu('The condition `'+condition+'` is removed.'); + } + + if(state[state_name].conditions[condition]){ + if(args.length > 0){ + let setting = args.shift().split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + if(key === 'name' && value !== state[state_name].conditions[condition].name){ + state[state_name].conditions[value.toLowerCase()] = state[state_name].conditions[condition]; + delete state[state_name].conditions[condition]; + condition = value.toLowerCase(); + } + + // If we are editting the description, join the args all together in a string. + value = (key === 'description') ? value + ' ' + args.join(' ') : value; + + state[state_name].conditions[condition][key] = value; + } + + sendSingleConditionConfigMenu(condition); + return; + } + + sendConditionsConfigMenu(); + break; + + case 'add': case 'remove': case 'toggle': + if(!state[state_name].config.userToggle && !playerIsGM(msg.playerid)) return; + + if(!msg.selected || !msg.selected.length){ + makeAndSendMenu('No tokens are selected.'); + return; + } + if(!args.length){ + makeAndSendMenu('No condition(s) were given. Use: !'+state[state_name].config.command+' '+extracommand+' prone'); + return; + } + + let tokens = msg.selected.map(s => getObj(s._type, s._id)) + handleConditions(args, tokens, extracommand); + break; + + default: + if(!state[state_name].config.userAllowed && !playerIsGM(msg.playerid)) return; + + let condition_name = extracommand; + if(condition_name){ + let condition; + // Check if hte condition exists in the condition object. + if(condition = getConditionByName(condition_name)){ + // Send it to chat. + sendConditionToChat(condition); + }else{ + sendChat((whisper) ? script_name : '', whisper + 'Condition ' + condition_name + ' does not exist.', null, {noarchive:true}); + } + }else{ + if(!playerIsGM(msg.playerid)) return; + + sendMenu(msg.selected); + } + break; + } + } + }, + + handleConditions = (conditions, tokens, type='add', error=true) => { + conditions.forEach(condition_key => { + if(!state[state_name].conditions[condition_key.toLowerCase()]){ + if(error) makeAndSendMenu('The condition `'+condition_key+'` does not exist.'); + return; + } + + condition_key = condition_key.toLowerCase(); + + tokens.forEach(token => { + let prevSM = token.get('statusmarkers'); + let add = (type === 'add') ? true : (type === 'toggle') ? !token.get('status_'+getConditionByName(condition_key).icon) : false; + token.set('status_'+getConditionByName(condition_key).icon, add); + + let prev = token; + prev.attributes.statusmarkers = prevSM; + + notifyObservers('tokenChange', token, prev); + + if(add && !handled.includes(condition_key)){ + sendConditionToChat(getConditionByName(condition_key)); + doHandled(condition_key); + } + + handleShapedSheet(token.get('represents'), condition_key, add); + }); + }); + }, + + handleShapedSheet = (characterid, condition, add) => { + let character = getObj('character', characterid); + if(character){ + let sheet = character.get("charactersheetname"); + if(!sheet || !sheet.toLowerCase().includes('shaped')) return; + if(!shaped_conditions.includes(condition)) return; + + let attributes = {}; + attributes[condition] = (add) ? '1': '0'; + setAttrs(character.get('id'), attributes); + } + }, + + esRE = function (s) { + var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g; + return s.replace(escapeForRegexp,"\\$1"); + }, + + HE = (function(){ + var entities={ + //' ' : '&'+'nbsp'+';', + '<' : '&'+'lt'+';', + '>' : '&'+'gt'+';', + "'" : '&'+'#39'+';', + '@' : '&'+'#64'+';', + '{' : '&'+'#123'+';', + '|' : '&'+'#124'+';', + '}' : '&'+'#125'+';', + '[' : '&'+'#91'+';', + ']' : '&'+'#93'+';', + '"' : '&'+'quot'+';' + }, + re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g'); + return function(s){ + return s.replace(re, function(c){ return entities[c] || c; }); + }; + }()), + + handleStatusmarkerChange = (obj, prev) => { + if(handled.includes(obj.get('represents')) || !prev || !obj) return + + prev.statusmarkers = (typeof prev.get === 'function') ? prev.get('statusmarkers') : prev.statusmarkers; + + if(state[state_name].config.showDescOnStatusChange && typeof prev.statusmarkers === 'string'){ + // Check if the statusmarkers string is different from the previous statusmarkers string. + if(obj.get('statusmarkers') !== prev.statusmarkers){ + // Create arrays from the statusmarkers strings. + var prevstatusmarkers = prev.statusmarkers.split(","); + var statusmarkers = obj.get('statusmarkers').split(","); + + // Loop through the statusmarkers array. + statusmarkers.forEach(function(marker){ + let condition = getConditionByMarker(marker); + if(!condition) return; + // If it is a new statusmarkers, get the condition from the conditions object, and send it to chat. + if(marker !== "" && !prevstatusmarkers.includes(marker)){ + if(handled.includes(condition.name.toLowerCase())) return; + + //sendConditionToChat(condition); + handleConditions([condition.name], [obj], 'add', false) + doHandled(obj.get('represents')); + } + }); + + prevstatusmarkers.forEach((marker) => { + let condition = getConditionByMarker(marker); + if(!condition) return; + + if(marker !== '' && !statusmarkers.includes(marker)){ + handleConditions([condition.name], [obj], 'remove', false); + } + }) + } + } + }, + + handleAttributeChange = (obj, prev) => { + if(!shaped_conditions.includes(obj.get('name'))) return; + + let tokens = findObjs({ represents: obj.get('characterid') }); + + handleConditions([obj.get('name')], tokens, (obj.get('current') === '1') ? 'add' : 'remove') + }, + + doHandled = (what) => { + handled.push(what); + setTimeout(() => { + handled.splice(handled.indexOf(what), 1); + }, 1000); + }, + + getConditionByMarker = (marker) => { + return getObjects(state[state_name].conditions, 'icon', marker).shift() || false; + }, + + getConditionByName = (name) => { + return state[state_name].conditions[name.toLowerCase()] || false; + }, + + sendConditionToChat = (condition, w) => { + if(!condition.description || condition.description === '') return; + + let icon = (state[state_name].config.showIconInDescription) ? getIcon(condition.icon, 'margin-right: 5px; margin-top: 5px; display: inline-block;') || '' : ''; + + makeAndSendMenu(condition.description, icon+condition.name, { + title_tag: 'h2', + whisper: (state[state_name].config.sendOnlyToGM) ? 'gm' : '' + }); + }, + + getIcon = (icon, style='') => { + let X = ''; + let iconStyle = '' + + if(typeof icon_image_positions[icon] === 'undefined') return false; + //if(!icon_image_positions[icon]) return false; + + iconStyle += 'width: 24px; height: 24px;'; + + if(Number.isInteger(icon_image_positions[icon])){ + iconStyle += 'background-image: url(https://roll20.net/images/statussheet.png);' + iconStyle += 'background-repeat: no-repeat;' + iconStyle += 'background-position: -'+icon_image_positions[icon]+'px 0;' + }else if(icon_image_positions[icon] === 'X'){ + iconStyle += 'color: red; margin-right: 0px;'; + X = 'X'; + }else{ + iconStyle += 'background-color: ' + icon_image_positions[icon] + ';'; + iconStyle += 'border: 1px solid white; border-radius: 50%;' + } + + iconStyle += style; + + // TODO: Make span + return '
    '+X+'
    '; + }, + + ucFirst = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + //return an array of objects according to key, value, or key and value matching + getObjects = (obj, key, val) => { + var objects = []; + for (var i in obj) { + if (!obj.hasOwnProperty(i)) continue; + if (typeof obj[i] == 'object') { + objects = objects.concat(getObjects(obj[i], key, val)); + } else + //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) + if (i == key && obj[i] == val || i == key && val == '') { // + objects.push(obj); + } else if (obj[i] == val && key == ''){ + //only add if the object is not already in the array + if (objects.lastIndexOf(obj) == -1){ + objects.push(obj); + } + } + } + return objects; + }, + + sendConditionsConfigMenu = (message) => { + if(!state[state_name].conditions || typeof state[state_name].conditions === 'object') setDefaults(); + + let listItems = [], + icons = [], + check = true; + for(let key in state[state_name].conditions){ + let configButton = makeButton('Change', '!' + state[state_name].config.command + ' config-conditions '+key, buttonStyle); + listItems.push(''+getIcon(state[state_name].conditions[key].icon, 'display: inline-block;')+state[state_name].conditions[key].name+' ' + configButton); + + if(check && icons.includes(state[state_name].conditions[key].icon)){ + message = message || '' + '
    Multiple conditions use the same icon'; + check = false; + } + + icons.push(state[state_name].conditions[key].icon); + } + + let backButton = makeButton('Back', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%'); + let addButton = makeButton('Add Condition', '!' + state[state_name].config.command + ' config-conditions add ?{Name}', buttonStyle + 'float: none;'); + + message = (message) ? '

    '+message+'

    ' : ''; + let contents = makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
    '+message+addButton+'
    '+backButton; + makeAndSendMenu(contents, 'Conditions'); + }, + + sendSingleConditionConfigMenu = (conditionKey, message) => { + if(!conditionKey || !state[state_name].conditions[conditionKey]){ + sendConditionsConfigMenu('Condition '+conditionKey+' does not exist.'); + return; + } + + let condition = state[state_name].conditions[conditionKey]; + + let listItems = []; + let nameButton = makeButton(condition.name, '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' name|?{Name}', buttonStyle); + listItems.push('Name: ' + nameButton); + + let markerDropdown = '?{Marker'; + markers.forEach((marker) => { + markerDropdown += '|'+ucFirst(marker).replace(/-/g, ' ')+','+marker + }) + markerDropdown += '}'; + + let markerButton = makeButton(getIcon(condition.icon) || condition.icon, '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' icon|'+markerDropdown, buttonStyle); + listItems.push('Statusmarker: ' + markerButton); + + let backButton = makeButton('Back', '!' + state[state_name].config.command + ' config-conditions', buttonStyle + ' width: 100%'); + let removeButton = makeButton('Remove', '!' + state[state_name].config.command + ' config-conditions remove '+conditionKey+' ?{Are you sure?|Yes,yes|No,no}', buttonStyle + ' width: 100%'); + let changeButton = makeButton('Edit Description', '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' description|?{Description|'+condition.description+'}', buttonStyle); + + message = (message) ? '

    '+message+'

    ' : ''; + let contents = message+makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
    Description:'+condition.description+changeButton+'

    '+removeButton+backButton+'

    '; + makeAndSendMenu(contents, condition.name + ' - Config'); + }, + + sendMenu = (selected, show_names) => { + let contents = ''; + if(selected && selected.length){ + selected.forEach(s => { + let token = getObj(s._type, s._id); + if(token && token.get('statusmarkers') !== ''){ + let statusmarkers = token.get('statusmarkers').split(','); + let active_conditions = []; + statusmarkers.forEach(marker => { + let con; + if(con = getObjects(state[state_name].conditions, 'icon', marker)){ + if(con[0] && con[0].name) active_conditions.push(con[0].name); + } + }); + + if(active_conditions.length){ + contents += ''+token.get('name') + '\'s Conditions:
    ' + active_conditions.join(', ') + '
    '; + } + } + }); + } + + contents += 'Toggle Condition on Selected Token(s):
    ' + for(let condition_key in state[state_name].conditions){ + let condition = state[state_name].conditions[condition_key]; + contents += makeButton(getIcon(condition.icon) || condition.name, '!' + state[state_name].config.command + ' toggle '+condition_key, buttonStyle + 'float: none; margin-right: 5px;', condition.name); + } + //contents += (!show_names) ? '
    ' + makeButton('Show Names', '!' + state[state_name].config.command + ' names', buttonStyle + 'float: none;') : '
    ' + makeButton('Hide Names', '!' + state[state_name].config.command, buttonStyle + 'float: none;'); + + makeAndSendMenu(contents, script_name + ' Menu'); + }, + + sendConfigMenu = (first) => { + let commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', buttonStyle); + let userAllowedButton = makeButton(state[state_name].config.userAllowed, '!' + state[state_name].config.command + ' config userAllowed|'+!state[state_name].config.userAllowed, buttonStyle); + let userToggleButton = makeButton(state[state_name].config.userToggle, '!' + state[state_name].config.command + ' config userToggle|'+!state[state_name].config.userToggle, buttonStyle); + let toGMButton = makeButton(state[state_name].config.sendOnlyToGM, '!' + state[state_name].config.command + ' config sendOnlyToGM|'+!state[state_name].config.sendOnlyToGM, buttonStyle); + let statusChangeButton = makeButton(state[state_name].config.showDescOnStatusChange, '!' + state[state_name].config.command + ' config showDescOnStatusChange|'+!state[state_name].config.showDescOnStatusChange, buttonStyle); + let showIconButton = makeButton(state[state_name].config.showIconInDescription, '!' + state[state_name].config.command + ' config showIconInDescription|'+!state[state_name].config.showIconInDescription, buttonStyle); + + let listItems = [ + 'Command: ' + commandButton, + 'Only to GM: '+toGMButton, + 'Player Show: '+userAllowedButton, + 'Player Toggle: '+userToggleButton, + 'Show on Status Change: '+statusChangeButton, + 'Display icon in chat: '+showIconButton + ]; + + let configConditionsButton = makeButton('Conditions Config', '!' + state[state_name].config.command + ' config-conditions', buttonStyle + ' width: 100%'); + let resetButton = makeButton('Reset Config', '!' + state[state_name].config.command + ' reset', buttonStyle + ' width: 100%'); + + let exportButton = makeButton('Export Config', '!' + state[state_name].config.command + ' config export', buttonStyle + ' width: 100%'); + let importButton = makeButton('Import Config', '!' + state[state_name].config.command + ' config import ?{Config}', buttonStyle + ' width: 100%'); + + let title_text = (first) ? script_name+' First Time Setup' : script_name+' Config'; + let contents = makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
    '+configConditionsButton+'

    You can always come back to this config by typing `!'+state[state_name].config.command+' config`.


    '+exportButton+importButton+resetButton; + makeAndSendMenu(contents, title_text) + }, + + sendHelpMenu = (first) => { + let configButton = makeButton('Config', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%;') + + let listItems = [ + '!'+state[state_name].config.command+' help - Shows this menu.', + '!'+state[state_name].config.command+' config - Shows the configuration menu.', + '!'+state[state_name].config.command+' [CONDITION] - Shows the description of the condition entered.', + ' ', + '!'+state[state_name].config.command+' add [CONDITIONS] - Add the given condition(s) to the selected token(s).', + '!'+state[state_name].config.command+' remove [CONDITIONS] - Remove the given condition(s) from the selected token(s).', + ' ', + '!'+state[state_name].config.command+' config export - Exports the config (with conditions).', + '!'+state[state_name].config.command+' config import [JSON] - Imports the given config (with conditions).' + ] + + let contents = 'Commands:'+makeList(listItems, listStyle)+'
    '+configButton; + makeAndSendMenu(contents, script_name+' Help') + }, + + makeAndSendMenu = (contents, title, settings) => { + settings = settings || {}; + settings.whisper = (typeof settings.whisper === 'undefined' || settings.whisper === 'gm') ? '/w gm ' : ''; + title = (title && title != '') ? makeTitle(title, settings.title_tag || '') : ''; + sendChat(script_name, settings.whisper + '
    '+title+contents+'
    ', null, {noarchive:true}); + }, + + makeTitle = (title, title_tag) => { + title_tag = (title_tag && title_tag !== '') ? title_tag : 'h3'; + return '<'+title_tag+' style="margin-bottom: 10px;">'+title+''; + }, + + makeButton = (title, href, style, alt) => { + return ''+title+''; + }, + + makeList = (items, listStyle, itemStyle) => { + let list = '
      '; + items.forEach((item) => { + list += '
    • '+item+'
    • '; + }); + list += '
    '; + return list; + }, + + getConditions = () => { + return state[state_name].conditions; + }, + + checkInstall = () => { + if(!_.has(state, state_name)){ + state[state_name] = state[state_name] || {}; + } + setDefaults(); + + log(script_name + ' Ready! Command: !'+state[state_name].config.command); + }, + + observeTokenChange = function(handler){ + if(handler && _.isFunction(handler)){ + observers.tokenChange.push(handler); + } + }, + + notifyObservers = function(event,obj,prev){ + _.each(observers[event],function(handler){ + handler(obj,prev); + }); + }, + + registerEventHandlers = () => { + on('chat:message', handleInput); + on('change:graphic:statusmarkers', handleStatusmarkerChange); + on('change:attribute', handleAttributeChange); + + // Handle condition descriptions when tokenmod changes the statusmarkers on a token. + if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ + TokenMod.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof ApplyDamage && ApplyDamage.registerObserver){ + ApplyDamage.registerObserver("change",(obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof DeathTracker && DeathTracker.ObserveTokenChange){ + DeathTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof InspirationTracker && InspirationTracker.ObserveTokenChange){ + InspirationTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof CombatTracker && CombatTracker.ObserveTokenChange){ + CombatTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + }, + + setDefaults = (reset) => { + + // DEVELOPER NOTE: ON CHANGE! CHECK BITCH! DENK OM OLD IMPORTS! + + const defaults = { + config: { + command: 'condition', + userAllowed: false, + userToggle: false, + sendOnlyToGM: false, + showDescOnStatusChange: true, + showIconInDescription: true + }, + conditions: { + blinded: { + name: 'Blinded', + description: '

    A blinded creature can’t see and automatically fails any ability check that requires sight.

    Attack rolls against the creature have advantage, and the creature’s Attack rolls have disadvantage.

    ', + icon: 'bleeding-eye' + }, + charmed: { + name: 'Charmed', + description: '

    A charmed creature can’t Attack the charmer or target the charmer with harmful Abilities or magical effects.

    The charmer has advantage on any ability check to interact socially with the creature.

    ', + icon: 'broken-heart' + }, + deafened: { + name: 'Deafened', + description: '

    A deafened creature can’t hear and automatically fails any ability check that requires hearing.

    ', + icon: 'edge-crack' + }, + frightened: { + name: 'Frightened', + description: '

    A frightened creature has disadvantage on Ability Checks and Attack rolls while the source of its fear is within line of sight.

    The creature can’t willingly move closer to the source of its fear.

    ', + icon: 'screaming' + }, + grappled: { + name: 'Grappled', + description: '

    A grappled creature’s speed becomes 0, and it can’t benefit from any bonus to its speed.

    The condition ends if the Grappler is incapacitated.

    The condition also ends if an effect removes the grappled creature from the reach of the Grappler or Grappling effect, such as when a creature is hurled away by the Thunderwave spell.

    ', + icon: 'grab' + }, + incapacitated: { + name: 'Incapacitated', + description: '

    An incapacitated creature can’t take actions or reactions.

    ', + icon: 'interdiction' + }, + inspiration: { + name: 'Inspiration', + description: '

    If you have inspiration, you can expend it when you make an Attack roll, saving throw, or ability check. Spending your inspiration gives you advantage on that roll.

    Additionally, if you have inspiration, you can reward another player for good roleplaying, clever thinking, or simply doing something exciting in the game. When another player character does something that really contributes to the story in a fun and interesting way, you can give up your inspiration to give that character inspiration.

    ', + icon: 'black-flag' + }, + invisibility: { + name: 'Invisibility', + description: '

    An invisible creature is impossible to see without the aid of magic or a Special sense. For the purpose of Hiding, the creature is heavily obscured. The creature’s location can be detected by any noise it makes or any tracks it leaves.

    Attack rolls against the creature have disadvantage, and the creature’s Attack rolls have advantage.

    ', + icon: 'ninja-mask' + }, + paralyzed: { + name: 'Paralyzed', + description: '

    A paralyzed creature is incapacitated and can’t move or speak.

    The creature automatically fails Strength and Dexterity saving throws.

    Attack rolls against the creature have advantage.

    Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.

    ', + icon: 'pummeled' + }, + petrified: { + name: 'Petrified', + description: '

    A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.

    The creature is incapacitated, can’t move or speak, and is unaware of its surroundings.

    Attack rolls against the creature have advantage.

    The creature automatically fails Strength and Dexterity saving throws.

    The creature has Resistance to all damage.

    The creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.

    ', + icon: 'frozen-orb' + }, + poisoned: { + name: 'Poisoned', + description: '

    A poisoned creature has disadvantage on Attack rolls and Ability Checks.

    ', + icon: 'chemical-bolt' + }, + prone: { + name: 'Prone', + description: '

    A prone creature’s only Movement option is to crawl, unless it stands up and thereby ends the condition.

    The creature has disadvantage on Attack rolls.

    An Attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the Attack roll has disadvantage.

    ', + icon: 'back-pain' + }, + restrained: { + name: 'Restrained', + description: '

    A restrained creature’s speed becomes 0, and it can’t benefit from any bonus to its speed.

    Attack rolls against the creature have advantage, and the creature’s Attack rolls have disadvantage.

    The creature has disadvantage on Dexterity saving throws.

    ', + icon: 'fishing-net' + }, + stunned: { + name: 'Stunned', + description: '

    A stunned creature is incapacitated, can’t move, and can speak only falteringly.

    The creature automatically fails Strength and Dexterity saving throws.

    Attack rolls against the creature have advantage.

    ', + icon: 'fist' + }, + unconscious: { + name: 'Unconscious', + description: '

    An unconscious creature is incapacitated, can’t move or speak, and is unaware of its surroundings.

    The creature drops whatever it’s holding and falls prone.

    The creature automatically fails Strength and Dexterity saving throws.

    Attack rolls against the creature have advantage.

    Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.

    ', + icon: 'sleepy' + }, + }, + }; + + if(!state[state_name].config){ + state[state_name].config = defaults.config; + }else{ + if(!state[state_name].config.hasOwnProperty('command')){ + state[state_name].config.command = defaults.config.command; + } + if(!state[state_name].config.hasOwnProperty('userAllowed')){ + state[state_name].config.userAllowed = defaults.config.userAllowed; + } + if(!state[state_name].config.hasOwnProperty('userToggle')){ + state[state_name].config.userToggle = defaults.config.userToggle; + } + if(!state[state_name].config.hasOwnProperty('sendOnlyToGM')){ + state[state_name].config.sendOnlyToGM = defaults.config.sendOnlyToGM; + } + if(!state[state_name].config.hasOwnProperty('showDescOnStatusChange')){ + state[state_name].config.showDescOnStatusChange = defaults.config.showDescOnStatusChange; + } + if(!state[state_name].config.hasOwnProperty('showIconInDescription')){ + state[state_name].config.showIconInDescription = defaults.config.showIconInDescription; + } + } + + if(!state[state_name].conditions || typeof state[state_name].conditions !== 'object'){ + state[state_name].conditions = defaults.conditions; + } + + whisper = (state[state_name].config.sendOnlyToGM) ? '/w gm ' : ''; + + if(!state[state_name].config.hasOwnProperty('firsttime') && !reset){ + sendConfigMenu(true); + state[state_name].config.firsttime = false; + } + }; + + return { + checkInstall, + ObserveTokenChange: observeTokenChange, + registerEventHandlers, + getConditions, + getConditionByName, + handleConditions, + sendConditionToChat, + getIcon, + version + }; +})(); + +on('ready', () => { + 'use strict'; + + StatusInfo.checkInstall(); + StatusInfo.registerEventHandlers(); +}); diff --git a/StatusInfo/README.md b/StatusInfo/README.md index 6a36cff9b0..670ab3e04a 100644 --- a/StatusInfo/README.md +++ b/StatusInfo/README.md @@ -14,7 +14,7 @@ --- ``` -LATEST UPDATE: It now allows you to create and edit conditions, export/import the config, and add/remove/toggle condition(s) to/from token(s), see below. +LATEST UPDATE: Updated to work with the D&D 2024 sheet. ``` StatusInfo works nicely together with [Tokenmod](https://app.roll20.net/forum/post/4225825/script-update-tokenmod-an-interface-to-adjusting-properties-of-a-token-from-a-macro-or-the-chat-area/?pageforid=4225825#post-4225825) and my own [DeathTracker](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/DeathTracker) and [InspirationTracker](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/InspirationTracker) scripts. diff --git a/StatusInfo/StatusInfo.js b/StatusInfo/StatusInfo.js index dfcd1e31ff..d3054aa866 100644 --- a/StatusInfo/StatusInfo.js +++ b/StatusInfo/StatusInfo.js @@ -264,7 +264,7 @@ var StatusInfo = StatusInfo || (function() { handleShapedSheet = (characterid, condition, add) => { let character = getObj('character', characterid); if(character){ - let sheet = getAttrByName(character.get('id'), 'character_sheet', 'current'); + let sheet = character.get("charactersheetname"); if(!sheet || !sheet.toLowerCase().includes('shaped')) return; if(!shaped_conditions.includes(condition)) return; diff --git a/StatusInfo/script.json b/StatusInfo/script.json index 11f35ea60a..846b6f6797 100644 --- a/StatusInfo/script.json +++ b/StatusInfo/script.json @@ -1,9 +1,9 @@ { "name": "StatusInfo", "script": "StatusInfo.js", - "version": "0.3.11", - "previousversions": ["0.3.2", "0.3.4", "0.3.6", "0.3.8", "0.3.10"], - "description": "All info and latest version on \n\n https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo", + "version": "0.3.12", + "previousversions": ["0.3.2", "0.3.4", "0.3.6", "0.3.8", "0.3.10", "0.3.11"], + "description": "All info and latest version on \n\n https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo\n\nThis script is compatible with the D&D 2024 Character Sheet.", "authors": "Robin Kuiper", "roll20userid": "1226016", "patreon": "https://www.patreon.com/robinkuiper", From 3f3610a046a38ef3eed717fc6b10883c0bc227ca Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Tue, 7 Jan 2025 20:17:08 -0600 Subject: [PATCH 28/42] Updated TokenMod to v0.8.81 --- TokenMod/0.8.81/TokenMod.js | 4165 +++++++++++++++++++++++++++++++++++ TokenMod/TokenMod.js | 6 +- TokenMod/script.json | 5 +- 3 files changed, 4171 insertions(+), 5 deletions(-) create mode 100644 TokenMod/0.8.81/TokenMod.js diff --git a/TokenMod/0.8.81/TokenMod.js b/TokenMod/0.8.81/TokenMod.js new file mode 100644 index 0000000000..24bf6cd94f --- /dev/null +++ b/TokenMod/0.8.81/TokenMod.js @@ -0,0 +1,4165 @@ +// Github: https://github.com/shdwjk/Roll20API/blob/master/TokenMod/TokenMod.js +// By: The Aaron, Arcane Scriptomancer +// Contact: https://app.roll20.net/users/104025/the-aaron +var API_Meta = API_Meta||{}; // eslint-disable-line no-var +API_Meta.TokenMod={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.TokenMod.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}} + +const TokenMod = (() => { // eslint-disable-line no-unused-vars + + const scriptName = "TokenMod"; + const version = '0.8.81'; + API_Meta.TokenMod.version = version; + const lastUpdate = 1736302467; + const schemaVersion = 0.4; + + const fields = { + // booleans + showname: {type: 'boolean'}, + show_tooltip: {type: 'boolean'}, + showplayers_name: {type: 'boolean'}, + showplayers_bar1: {type: 'boolean'}, + showplayers_bar2: {type: 'boolean'}, + showplayers_bar3: {type: 'boolean'}, + showplayers_aura1: {type: 'boolean'}, + showplayers_aura2: {type: 'boolean'}, + playersedit_name: {type: 'boolean'}, + playersedit_bar1: {type: 'boolean'}, + playersedit_bar2: {type: 'boolean'}, + playersedit_bar3: {type: 'boolean'}, + playersedit_aura1: {type: 'boolean'}, + playersedit_aura2: {type: 'boolean'}, + light_otherplayers: {type: 'boolean'}, + light_hassight: {type: 'boolean'}, + isdrawing: {type: 'boolean'}, + flipv: {type: 'boolean'}, + fliph: {type: 'boolean'}, + aura1_square: {type: 'boolean'}, + aura2_square: {type: 'boolean'}, + lockMovement: {type: 'boolean'}, + + // UDL settings + has_bright_light_vision: {type: 'boolean'}, + has_night_vision: {type: 'boolean'}, + emits_bright_light: {type: 'boolean'}, + emits_low_light: {type: 'boolean'}, + has_limit_field_of_vision: {type: 'boolean'}, + has_limit_field_of_night_vision: {type: 'boolean'}, + has_directional_bright_light: {type: 'boolean'}, + has_directional_dim_light: {type: 'boolean'}, + light_sensitivity_multiplier: {type: 'number'}, + night_vision_effect: {type: 'option'}, + + + // bounded by screen size + left: {type: 'number', transform: 'screen'}, + top: {type: 'number', transform: 'screen'}, + width: {type: 'number', transform: 'screen'}, + height: {type: 'number', transform: 'screen'}, + scale: {type: 'number', transform: 'screen'}, + + // 360 degrees + rotation: {type: 'degrees'}, + light_angle: {type: 'circleSegment'}, + light_losangle: {type: 'circleSegment'}, + + limit_field_of_vision_center: {type: 'degrees'}, + limit_field_of_night_vision_center: {type: 'degrees'}, + directional_bright_light_center: {type: 'degrees'}, + directional_dim_light_center: {type: 'degrees'}, + + limit_field_of_vision_total: {type: 'circleSegment'}, + limit_field_of_night_vision_total: {type: 'circleSegment'}, + directional_bright_light_total: {type: 'circleSegment'}, + directional_dim_light_total: {type: 'circleSegment'}, + + + + // distance + light_radius: {type: 'numberBlank'}, + light_dimradius: {type: 'numberBlank'}, + light_multiplier: {type: 'numberBlank'}, + adv_fow_view_distance: {type: 'numberBlank'}, + aura1_radius: {type: 'numberBlank'}, + aura2_radius: {type: 'numberBlank'}, + + //UDL settings + night_vision_distance: {type: 'numberBlank'}, + bright_light_distance: {type: 'numberBlank'}, + low_light_distance: {type: 'numberBlank'}, + dim_light_opacity: {type: 'percentage'}, + + // text or numbers + bar1_value: {type: 'text'}, + bar2_value: {type: 'text'}, + bar3_value: {type: 'text'}, + bar1_max: {type: 'text'}, + bar2_max: {type: 'text'}, + bar3_max: {type: 'text'}, + bar1: {type: 'text'}, + bar2: {type: 'text'}, + bar3: {type: 'text'}, + bar1_reset: {type: 'text'}, + bar2_reset: {type: 'text'}, + bar3_reset: {type: 'text'}, + + bar_location: {type: 'option'}, + compact_bar: {type: 'option'}, + + + // colors + aura1_color: {type: 'color'}, + aura2_color: {type: 'color'}, + tint_color: {type: 'color'}, + night_vision_tint: {type: 'color'}, + lightColor: {type: 'color'}, + + // Text : special + name: {type: 'text'}, + tooltip: {type: 'text'}, + + statusmarkers: {type: 'status'}, + layer: {type: 'layer'}, + represents: {type: 'character_id'}, + bar1_link: {type: 'attribute'}, + bar2_link: {type: 'attribute'}, + bar3_link: {type: 'attribute'}, + currentSide: {type: 'sideNumber'}, + imgsrc: {type: 'image'}, + sides: {type: 'image' }, + + controlledby: {type: 'player'}, + + // : special + defaulttoken: {type: 'defaulttoken'} + }; + + const fieldAliases = { + bar1_current: "bar1_value", + bar2_current: "bar2_value", + bar3_current: "bar3_value", + bright_vision: "has_bright_light_vision", + night_vision: "has_night_vision", + emits_bright: "emits_bright_light", + emits_low: "emits_low_light", + night_distance: "night_vision_distance", + bright_distance: "bright_light_distance", + low_distance: "low_light_distance", + low_light_opacity: "dim_light_opacity", + has_directional_low_light: "has_directional_dim_light", + directional_low_light_total: "directional_dim_light_total", + directional_low_light_center: "directional_dim_light_center", + currentside: "currentSide", // fix for case issue + lightcolor: "lightColor", // fix for case issue + light_color: "lightColor", // fix for case issue + lockmovement: "lockMovement", // fix for case issue + lock_movement: "lockMovement" // fix for case issue + }; + + const reportTypes = [ + 'gm', 'player', 'all', 'control', 'token', 'character' + ]; + + const probBool = { + couldbe: ()=>(randomInteger(8)<=1), + sometimes: ()=>(randomInteger(8)<=2), + maybe: ()=>(randomInteger(8)<=4), + probably: ()=>(randomInteger(8)<=6), + likely: ()=>(randomInteger(8)<=7) + }; + + const unalias = (name) => fieldAliases.hasOwnProperty(name) ? fieldAliases[name] : name; + + const filters = { + hasArgument: (a) => a.match(/.+[|#]/) || 'defaulttoken'===a, + isBoolean: (a) => 'boolean' === (fields[a]||{type:'UNKNOWN'}).type, + isTruthyArgument: (a) => [1,'1','on','yes','true','sure','yup'].includes(a) + }; + + const getCleanImgsrc = (imgsrc) => { + let parts = (imgsrc||'').match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); + if(parts) { + let leader = parts[1].replace(/^https:\/\/s3.amazonaws.com\/files.d20.io\//,'https://files.d20.io/'); + return `${leader}thumb${parts[3]}${parts[4] ? parts[4] : `?${Math.round(Math.random()*9999999)}`}`; + } + }; + + const forceLightUpdateOnPage = (()=>{ + const forPage = (pid) => (getObj('page',pid)||{set:()=>{}}).set('force_lighting_refresh',true); + let pids = new Set(); + let t; + + return (pid) => { + pids.add(pid); + clearTimeout(t); + t = setTimeout(() => { + let activePages = getActivePages(); + [...pids].filter(p=>activePages.includes(p)).forEach(forPage); + pids.clear(); + },100); + }; + })(); + + const option_fields = { + night_vision_effect: { + __default__: ()=>()=>'None', + off: ()=>()=>'None', + ['none']: ()=>()=>'None', + ['dimming']: (amount='5ft')=>(token,mods)=>{ + const regexp = /^([=+\-/*])?(-?\d+\.?|\d*\.\d+)(u|g|s|ft|m|km|mi|in|cm|un|hex|sq|%)?$/i; // */ + let match = `${amount}`.match(regexp); + let factor; + let pnv; + if(mods.hasOwnProperty('night_vision_distance')){ + pnv = mods.night_vision_distance; + } else { + pnv = token.get('night_vision_distance'); + } + + let dp; + if(mods.hasOwnProperty('night_vision_effect') && /^Dimming_/.test(mods.night_vision_effect)){ + dp = (parseFloat(mods.night_vision_effect.replace(/^Dimming_/,''))||0)*pnv; + } else if(/^Dimming_/.test(token.get('night_vision_effect'))){ + dp = (parseFloat(token.get('night_vision_effect').replace(/^Dimming_/,''))||0)*pnv; + } + + if(match){ + let dist; + switch(match[3]){ + + // handle percentage + case '%': { + let p = parseFloat(match[2])||0; + if(p>1){ + p*=.01; + } + p = Math.min(1,p); + + dist = p*pnv; + } + break; + + // handle units + default: { + let page=getObj('page',token.get('pageid')); + if(page){ + dist = numberOp.ConvertUnitsRoll20(match[2],match[3],page); + } + else { + dist=5; + } + } + break; + } + switch(match[1]){ + default: + case '=': + factor=(dist/pnv); + break; + + case '+': + factor=((dist+dp)/pnv); + break; + case '-': + factor=((dp-dist)/pnv); + break; + case '*': + factor=((dist*dp)/pnv); + break; + case '/': + factor=((dp/dist)/pnv); + break; + } + } else { + factor=(5/pnv); + } + + return `Dimming_${Math.min(1,Math.max(0,factor))}`; + }, + ['nocturnal']: ()=>()=>'Nocturnal' + }, + bar_location: { + __default__ : ()=>null, + off : ()=>null, + none : ()=>null, + ['above'] : ()=>null, + ['overlap_top'] : ()=>'overlap_top', + ['overlap_bottom'] : ()=>'overlap_bottom', + ['below'] : ()=>'below' + }, + compact_bar: { + __default__ : ()=>null, + off : ()=>null, + none : ()=>null, + ['compact'] : ()=>'compact', + ['on'] : ()=>'compact' + } + }; + + const regex = { + moveAngle: /^(=)?([+-]?(?:0|[1-9][0-9]*))(!)?$/, + moveDistance: /^([+-]?\d+\.?|\d*\.\d+)(u|g|s|ft|m|km|mi|in|cm|un|hex|sq)?$/i, + numberString: /^[-+*/=]?[-+]?(0|[1-9][0-9]*)([.]+[0-9]*)?([eE][-+]?[0-9]+)?(!)?$/, + stripSingleQuotes: /'([^']+(?='))'/g, + stripDoubleQuotes: /"([^"]+(?="))"/g, + layers: /^(?:gmlayer|objects|map|walls)$/, + + imgsrc: /(.*\/images\/.*)(thumb|med|original|max)(.*)$/, + imageOp: /^(?:(-(?:\d*(?:\s*,\s*\d+)*|\*)$)|(\/(?:\d+@\d+(?:\s*,\s*\d+@\d+)*|\*)$)|([+^]))?(=?)(?:(https?:\/\/.*$)|([-\d\w]*))(?::(.*))?$/, + sideNumber: /^(\?)?([-+=*])?(\d*)$/, + color : { + ops: '([*=+\\-!])?', + transparent: '(transparent)', + html: '#?((?:[0-9a-f]{6})|(?:[0-9a-f]{3}))', + rgb: '(rgb\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))', + hsv: '(hsv\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))' + } + }; + + const colorOpReg = new RegExp(`^${regex.color.ops}(?:${regex.color.transparent}|${regex.color.html}|${regex.color.rgb}|${regex.color.hsv})$`,'i'); + const colorReg = new RegExp(`^(?:${regex.color.transparent}|${regex.color.html}|${regex.color.rgb}|${regex.color.hsv})$`,'i'); + const colorParams = /\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*\)/; + + + + //////////////////////////////////////////////////////////// + // Number Operations + //////////////////////////////////////////////////////////// + + class numberOp { + static parse(field, str, permitBlank=true) { + const regexp = /^([=+\-/*!])?(-?\d+\.?|-?\d*\.\d+)(u|g|s|ft|m|km|mi|in|cm|un|hex|sq)?(!)?$/i; // */ + + if(!str.length && permitBlank){ + return new numberOp(field, '','','' ); + } + + let m = `${str}`.match(regexp); + + if(m){ + let oper = m[1]||''; + let num = parseFloat(m[2]); + let scale = m[3]||''; + let enforceBounds = '!'===m[4]; + + return new numberOp(field, oper, num, scale.toLowerCase(),enforceBounds); + } + return {getMods:()=>({})}; + } + + constructor(field,op,num,units,enforce){ + this.field=field; + this.operation = op; + this.num = num; + this.units = units; + this.enforce = enforce; + } + + static ConvertUnitsPixel(num,unit,page){ + const unitSize = 70; + switch(unit){ + case 'u': + return num*unitSize; + + case 'g': + return num*(parseFloat(page.get('snapping_increment'))*unitSize); + + case 'ft': + case 'm': + case 'km': + case 'mi': + case 'in': + case 'cm': + case 'un': + case 'hex': + case 'sq': + case 's': + return (num/(parseFloat(page.get('scale_number'))||1))*unitSize; + default: + return num; + } + } + + static ConvertUnitsRoll20(num,unit,page){ + switch(unit){ + case 'u': + return num*(parseFloat(page.get('scale_number'))*(1/parseFloat(page.get('snapping_increment'))||1)); + + case 'g': + return num*parseFloat(page.get('scale_number')); + + default: + case 'ft': + case 'm': + case 'km': + case 'mi': + case 'in': + case 'cm': + case 'un': + case 'hex': + case 'sq': + case 's': + return num; + } + } + + getMods(token,mods){ + let num = this.num; + let page = getObj('page',token.get('pageid')); + switch(this.field){ + + case 'light_radius': + case 'light_dimradius': + case 'aura2_radius': + case 'aura1_radius': + case 'adv_fow_view_distance': + case 'night_vision_distance': + case 'bright_light_distance': + case 'low_light_distance': + case 'night_distance': + case 'bright_distance': + case 'low_distance': + num = numberOp.ConvertUnitsRoll20(num,this.units,page); + break; + + default: + case 'left': + case 'top': + case 'width': + case 'height': + num = numberOp.ConvertUnitsPixel(num,this.units,page); + break; + + case 'light_multiplier': + case 'light_sensitivity_multiplier': + break; + + } + + let current = parseFloat(token.get(this.field))||0; + const getValue = (k,m,t) => m.hasOwnProperty(k) ? m[k] : t.get(k); + + let adjuster = (a)=>a; + + switch(this.field){ + case 'bar1_value': + case 'bar2_value': + case 'bar3_value': + if(this.enforce){ + adjuster = (a,t)=>Math.max(0,Math.min(a,t.get(this.field.replace(/_value/,'_max')))); + } + break; + + case 'night_vision_distance': + adjuster = (a,token,mods) => { + let pnv; + if(mods.hasOwnProperty('night_vision_distance')){ + pnv = mods.night_vision_distance; + } else { + pnv = token.get('night_vision_distance'); + } + + let dp; + if(mods.hasOwnProperty('night_vision_effect') && /^Dimming_/.test(mods.night_vision_effect)){ + dp = parseFloat(mods.night_vision_effect.replace(/^Dimming_/,''))||undefined; + } else if(/^Dimming_/.test(token.get('night_vision_effect'))){ + dp = parseFloat(token.get('night_vision_effect').replace(/^Dimming_/,''))||undefined; + } + + if(undefined !== dp) { + let dd = 0; + if(dp>0){ + dd = parseFloat(pnv)*dp; + dp=Math.min(1,parseFloat(dd.toFixed(2))/a); + } + + mods.night_vision_effect=`Dimming_${dp}`; + } + return a; + }; + break; + } + + switch(this.operation){ + default: + case '=': { + switch(this.field){ + case 'bright_light_distance': + num=num||0; + return { + bright_light_distance: num, + low_light_distance: (parseFloat(getValue('low_light_distance',mods,token)||0)-(parseFloat(getValue('bright_light_distance',mods,token))||0)+num) + }; + case 'low_light_distance': + num=num||0; + return { + low_light_distance: ((parseFloat(getValue('bright_light_distance',mods,token))||0)+num) + }; + + default: + return {[this.field]:adjuster(num,token,mods)}; + } + } + case '!': return {[this.field]:adjuster((current===0 ? num : ''),token,mods)}; + case '+': return {[this.field]:adjuster((current+num),token,mods)}; + case '-': return {[this.field]:adjuster((current-num),token,mods)}; + case '/': return {[this.field]:adjuster((current/(num||1)),0,mods)}; + case '*': return {[this.field]:adjuster((current*num),0,mods)}; + } + } + + } + + //////////////////////////////////////////////////////////// + // Image Operations + //////////////////////////////////////////////////////////// + + + class imageOp { + static parseImage(input){ + const OP_REMOVE_BY_INDEX = 1; + const OP_REORDER = 2; + const OP_OPERATION = 3; + const OP_EXPLICIT_SET = 4; + const OP_IMAGE_URL = 5; + const OP_TOKEN_ID = 6; + const OP_TOKEN_SIDE_INDEX = 7; + + let parsed = input.match(regex.imageOp); + + if(parsed && parsed.length){ + if(parsed[ OP_REMOVE_BY_INDEX ]){ + let idxs=parsed[ OP_REMOVE_BY_INDEX ].slice(1); + return new imageOp('-',false, + '*'===idxs + ? ['*'] + : idxs.split(/\s*,\s*/).filter(s=>s.length).map((idx)=>parseInt(idx,10)-1) + ); + } else if(parsed[ OP_REORDER ]){ + let idxs=parsed[ OP_REORDER ].slice(1); + + return new imageOp('/',false, + idxs.split(/\s*,\s*/) + .filter(s=>s.length) + .map((idx)=>{ + let parts = idx.split(/@/); + return { + idx: (parseInt(parts[0])-1), + pos: (parseInt(parts[1])) + }; + }) + ); + } else { + let op = parsed[ OP_OPERATION ]||'_'; + let set = '='===parsed[ OP_EXPLICIT_SET ]; + if(parsed[ OP_IMAGE_URL ]){ + + let parts = parsed[ OP_IMAGE_URL ].split(/:@/); + let url=getCleanImgsrc(parts[0]); + if(url){ + return new imageOp(op,set,[],[{url,index:parseInt(parts[1])||undefined}]); + } + } else { + let id = parsed[ OP_TOKEN_ID ]; + let t = getObj('graphic',id); + + if(t){ + if(parsed[ OP_TOKEN_SIDE_INDEX ]){ + let sides = t.get('sides'); + if(sides.length){ + sides = sides.split(/\|/).map(decodeURIComponent).map(getCleanImgsrc); + } else { + sides = [getCleanImgsrc(t.get('imgsrc'))]; + } + let urls=[]; + let idxs; + if('*'===parsed[ OP_TOKEN_SIDE_INDEX ]){ + idxs=sides.reduce((m,v)=> ({ c:m.c+1, i:(v?[...m.i,m.c]:m.i) }), {c:0,i:[]}).i.map(id=>({idx:id})); + } else { + idxs=parsed[ OP_TOKEN_SIDE_INDEX ] + .split(/\s*,\s*/) + .filter(s=>s.length) + .map((idx)=>({ + idx: (parseInt(idx,10)||1)-1, + insert: parseInt(idx.split(/@/)[1])||undefined + })); + } + idxs.forEach((i)=>{ + if(sides[i.idx]){ + urls.push({url:sides[i.idx], index: i.insert }); + } + }); + + if(urls.length){ + return new imageOp(op,set,[],urls); + } + } else { + let url=getCleanImgsrc(t.get('imgsrc')); + if(url){ + return new imageOp(op,set,[],[{url}]); + } + } + } + } + } + } + return new imageOp(); + } + + constructor(op,set,indicies,urls){ + this.op = op||'/'; + this.set = set || false; + this.indicies=indicies||[]; + this.urls=urls||[]; + } + + getMods(token /* ,mods */){ + let sideText = token.get('sides'); + let sides; + + + if( sideText.length ){ + sides = sideText.split(/\|/).map(decodeURIComponent).map(getCleanImgsrc); + } else { + sides = [getCleanImgsrc(token.get('imgsrc'))]; + if('^' === this.op){ + this.op = '_'; + } + } + + switch(this.op) { + case '-': { + if('*'===this.indicies[0]){ + return { + currentSide: 0, + sides: '' + }; + } + let currentSide=token.get('currentSide'); + if(this.indicies.length){ + this.indicies.forEach((i)=>{ + if(currentSide===i){ + currentSide=0; + } + delete sides[i]; + }); + } else { + delete sides[currentSide]; + currentSide=0; + } + let idxs=sides.reduce((m,v)=> ({ c:m.c+1, i:(v?[...m.i,m.c]:m.i) }), {c:0,i:[]}).i; + sides=sides.reduce((m,s)=>m.concat( s ? [s] : []),[]); + currentSide=Math.max(_.indexOf(idxs,currentSide),0); + if(sides.length){ + return { + imgsrc: sides[currentSide], + currentSide: currentSide, + sides: sides.reduce((m,s)=>m.concat(s),[]).map(encodeURIComponent).join('|') + }; + } + return { + currentSide: 0, + sides: '' + }; + } + + case '/': { + let currentSide=token.get('currentSide'); + let imgsrc=token.get('imgsrc'); + let sidesOld=token.get('sides'); + + sides = this.indicies.reduce( (s,o) => { + let url = s[o.idx]; + s.splice(o.idx,1); + return [...s.slice(0,(o.pos||Number.MAX_SAFE_INTEGER)-1), url, ...s.slice((o.pos||Number.MAX_SAFE_INTEGER)-1)]; + },sides); + + + let retr = { + sides: sides.map(encodeURIComponent).join('|') + }; + if(retr.sides===sidesOld){ + delete retr.sides; + } + + if(imgsrc !== sides[currentSide]){ + retr.imgsrc=sides[currentSide]; + } + return retr; + } + + case '_': + return { + imgsrc: this.urls[0].url + }; + + case '^': { + // replacing + let currentSide=token.get('currentSide'); + let imgsrc=token.get('imgsrc'); + + sides = this.urls.reduce((s,u) => { + let replaceIdx =(u.index||Number.MAX_SAFE_INTEGER)-1; + if(sides.hasOwnProperty(replaceIdx)){ + sides[replaceIdx] = u.url; + } else { + sides.push(u.url); + } + return sides; + },sides); + + let retr = { + sides: sides.map(encodeURIComponent).join('|') + }; + if(this.set){ + retr.imgsrc=sides.slice(-1)[0]; + retr.currentSide=sides.length-1; + } + if(imgsrc !== sides[currentSide]){ + retr.imgsrc=sides[currentSide]; + } + return retr; + } + + case '+': { + + // appending + let currentSide=token.get('currentSide'); + let imgsrc=token.get('imgsrc'); + sides = this.urls.reduce((s,u) => + [...s.slice(0,(u.index||Number.MAX_SAFE_INTEGER)-1), u.url, ...s.slice((u.index||Number.MAX_SAFE_INTEGER)-1)] + ,sides); + let retr = { + sides: sides.map(encodeURIComponent).join('|') + }; + if(this.set){ + retr.imgsrc=sides.slice(-1)[0]; + retr.currentSide=sides.length-1; + } + if(imgsrc !== sides[currentSide]){ + retr.imgsrc=sides[currentSide]; + } + return retr; + } + } + return {}; + } + } + + //////////////////////////////////////////////////////////// + // Side Numbers + //////////////////////////////////////////////////////////// + + class sideNumberOp { + + static parseSideNumber(input){ + const OP_FLAG = 1; + const OP_OPERATION = 2; + const OP_COUNT = 3; + let parsed = input.toLowerCase().match(regex.sideNumber); + if(parsed && parsed.length){ + return new sideNumberOp( parsed[ OP_FLAG ], parsed[ OP_OPERATION ], parsed[ OP_COUNT ] ); + } + return new sideNumberOp(false,'/'); + } + + constructor(flag,op,count){ + this.flag=flag||false; + this.operation=op||'='; + this.count=(parseInt(`${count}`)||1); + } + + + getMods(token /*,mods */){ + // get sides + let sides = token.get('sides').split(/\|/).map(decodeURIComponent).map(getCleanImgsrc); + switch(this.operation){ + case '/': + return {}; + case '=': + if(sides[this.count-1]){ + return { + currentSide: this.count-1, + imgsrc: sides[this.count-1] + }; + } + return {}; + case '*': { + // get indexes that are valid + let idxs=sides.reduce((m,v)=> ({ c:m.c+1, i:(v?[...m.i,m.c]:m.i) }), {c:0,i:[]}).i; + if(idxs.length){ + let idx=_.sample(idxs); + return { + currentSide: idx, + imgsrc: sides[idx] + }; + } + return {}; + } + case '+': + case '-': { + let idx = token.get('currentSide')||0; + idx += ('-'===this.operation ? -1 : 1)*this.count; + if(this.flag){ + idx=Math.max(Math.min(idx,sides.length-1),0); + } else { + idx=(idx%sides.length)+(idx<0 ? sides.length : 0); + } + if(sides[idx]){ + return { + currentSide: idx, + imgsrc: sides[idx] + }; + } + return {}; + } + + } + + } + } + + + //////////////////////////////////////////////////////////// + // Colors + //////////////////////////////////////////////////////////// + + class Color { + static hsv2rgb(h, s, v) { + let r, g, b; + + let i = Math.floor(h * 6); + let f = h * 6 - i; + let p = v * (1 - s); + let q = v * (1 - f * s); + let t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return { r , g , b }; + } + + static rgb2hsv(r,g,b) { + let max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h, s, v = max; + + let d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) { + h = 0; // achromatic + } else { + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return { h, s, v }; + } + + static dec2hex (n){ + n = (Math.max(Math.min(Math.round(n*255),255), 0)||0); + return `${n<16?'0':''}${n.toString(16)}`; + } + + static hex2dec (n){ + return Math.max(Math.min(parseInt(n,16),255), 0)/255; + } + + static html2rgb(htmlstring){ + let s=htmlstring.toLowerCase().replace(/[^0-9a-f]/,''); + if(3===s.length){ + s=`${s[0]}${s[0]}${s[1]}${s[1]}${s[2]}${s[2]}`; + } + return { + r: this.hex2dec(s.substr(0,2)), + g: this.hex2dec(s.substr(2,2)), + b: this.hex2dec(s.substr(4,2)) + }; + } + + static parseRGBParam(p){ + if(/\./.test(p)){ + return parseFloat(p); + } + return parseInt(p,10)/255; + } + static parseHSVParam(p,f){ + if(/\./.test(p)){ + return parseFloat(p); + } + switch(f){ + case 'h': + return parseInt(p,10)/360; + case 's': + case 'v': + return parseInt(p,10)/100; + } + } + + static parseColor(input){ + return Color.buildColor(`${input}`.toLowerCase().match(colorReg)); + } + static buildColor(parsed){ + const idx = { + transparent: 1, + html: 2, + rgb: 3, + hsv: 4 + }; + + if(parsed){ + let c = new Color(); + if(parsed[idx.transparent]){ + c.type = 'transparent'; + } else if(parsed[idx.html]){ + c.type = 'rgb'; + _.each(Color.html2rgb(parsed[idx.html]),(v,k)=>{ + c[k]=v; + }); + } else if(parsed[idx.rgb]){ + c.type = 'rgb'; + let params = parsed[idx.rgb].match(colorParams); + c.r= Color.parseRGBParam(params[1]); + c.g= Color.parseRGBParam(params[2]); + c.b= Color.parseRGBParam(params[3]); + } else if(parsed[idx.hsv]){ + c.type = 'hsv'; + let params = parsed[idx.hsv].match(colorParams); + c.h= Color.parseHSVParam(params[1],'h'); + c.s= Color.parseHSVParam(params[2],'s'); + c.v= Color.parseHSVParam(params[3],'v'); + } + return c; + } + return new Color(); + } + + constructor(){ + this.type='transparent'; + } + + clone(){ + return Object.assign(new Color(), this); + } + + toRGB(){ + if('hsv'===this.type){ + _.each(Color.hsv2rgb(this.h,this.s,this.v),(v,k)=>{ + this[k]=v; + }); + this.type='rgb'; + } else if ('transparent' === this.type){ + this.type='rgb'; + this.r=0.0; + this.g=0.0; + this.b=0.0; + } + delete this.h; + delete this.s; + delete this.v; + return this; + } + + toHSV(){ + if('rgb'===this.type){ + _.each(Color.rgb2hsv(this.r,this.g,this.b),(v,k)=>{ + this[k]=v; + }); + this.type='hsv'; + } else if('transparent' === this.type){ + this.type='hsv'; + this.h=0.0; + this.s=0.0; + this.v=0.0; + } + + delete this.r; + delete this.g; + delete this.b; + + return this; + } + + toHTML(){ + switch(this.type){ + case 'transparent': + return 'transparent'; + case 'hsv': { + return this.clone().toRGB().toHTML(); + } + case 'rgb': + return `#${Color.dec2hex(this.r)}${Color.dec2hex(this.g)}${Color.dec2hex(this.b)}`; + } + } + } + + class ColorOp extends Color { + + constructor( op ) { + super(); + this.operation = op; + } + + static parseColor(input){ + const idx = { + ops: 1, + transparent: 2, + html: 3, + rgb: 4, + hsv: 5 + }; + + let parsed = `${input}`.toLowerCase().match(colorOpReg)||[]; + + if(parsed.length) { + return Object.assign(new ColorOp(parsed[idx.ops]||'='), Color.buildColor(parsed.slice(1))); + } else { + return Object.assign(new ColorOp(parsed[idx.ops]||(input.length ? '*':'=')), Color.parseColor('transparent')); + } + } + + applyTo(c){ + if( !(c instanceof Color) ){ + c = Color.parseColor(c); + } + switch(this.operation){ + case '=': + return this; + case '!': + return ('transparent'===c.type ? this : Color.parseColor('transparent')); + } + switch(this.type){ + case 'transparent': + return c; + case 'hsv': + c.toHSV(); + switch(this.operation){ + case '*': + c.h*=this.h; + c.s*=this.s; + c.v*=this.v; + c.toRGB(); + return c; + case '+': + c.h+=this.h; + c.s+=this.s; + c.v+=this.v; + c.toRGB(); + return c; + case '-': + c.h-=this.h; + c.s-=this.s; + c.v-=this.v; + c.toRGB(); + return c; + } + break; + case 'rgb': + c.toRGB(); + switch(this.operation){ + case '*': + c.r*=this.r; + c.g*=this.g; + c.b*=this.b; + return c; + case '+': + c.r+=this.r; + c.g+=this.g; + c.b+=this.b; + return c; + case '-': + c.r-=this.r; + c.g-=this.g; + c.b-=this.b; + return c; + } + } + + return c; + } + + + toString(){ + let extra =''; + switch (this.type){ + case 'transparent': + extra='(0.0, 0.0, 0.0, 1.0)'; + break; + case 'rgb': + extra=`(${this.r},${this.g},${this.b})`; + break; + case 'hsv': + extra=`(${this.h},${this.s},${this.v})`; + break; + } + return `${this.operation} ${this.type}${extra} ${this.toHTML()}`; + } + + } + + //////////////////////////////////////////////////////////// + // StatusMarkers + //////////////////////////////////////////////////////////// + + class TokenMarker { + constructor( name, tag, url ) { + this.name = name; + this.tag = tag; + this.url = url; + } + + getName() { + return this.name; + } + getTag() { + return this.tag; + } + + getHTML(scale = 1.4){ + return `
    `; + } + } + + class ColorDotTokenMarker extends TokenMarker { + constructor( name, color ) { + super(name,name); + this.color = color; + } + + getHTML(scale = 1.4){ + return `
    `; + } + } + + class ColorTextTokenMarker extends TokenMarker { + constructor( name, letter, color ) { + super(name,name); + this.color = color; + this.letter = letter; + } + + getHTML(scale = 1.4){ + return `
    ${this.letter}
    `; + } + } + + // legacy + class ImageStripTokenMarker extends TokenMarker { + constructor( name, offset){ + super(name, name); + this.offset = offset; + } + + getHTML(scale = 1.4){ + const ratio = 2.173913; + const statusSheet = 'https://app.roll20.net/images/statussheet.png'; + + return `
    `; + } + } + + class StatusMarkers { + + static init(){ + let tokenMarkers = {}; + let orderedLookup = new Set(); + let reverseLookup = {}; + + const insertTokenMarker = (tm) => { + tokenMarkers[tm.getTag()] = tm; + orderedLookup.add(tm.getTag()); + + reverseLookup[tm.getName()] = reverseLookup[tm.getName()]||[]; + reverseLookup[tm.getName()].push(tm.getTag()); + }; + + const buildStaticMarkers = () => { + insertTokenMarker(new ColorDotTokenMarker('red', '#C91010')); + insertTokenMarker(new ColorDotTokenMarker(`blue`, '#1076c9')); + insertTokenMarker(new ColorDotTokenMarker(`green`, '#2fc910')); + insertTokenMarker(new ColorDotTokenMarker(`brown`, '#c97310')); + insertTokenMarker(new ColorDotTokenMarker(`purple`, '#9510c9')); + insertTokenMarker(new ColorDotTokenMarker(`pink`, '#eb75e1')); + insertTokenMarker(new ColorDotTokenMarker(`yellow`, '#e5eb75')); + + insertTokenMarker(new ColorTextTokenMarker('dead', 'X', '#cc1010')); + }; + + const buildLegacyMarkers = () => { + const legacyNames = [ + 'skull', 'sleepy', 'half-heart', 'half-haze', 'interdiction', + 'snail', 'lightning-helix', 'spanner', 'chained-heart', + 'chemical-bolt', 'death-zone', 'drink-me', 'edge-crack', + 'ninja-mask', 'stopwatch', 'fishing-net', 'overdrive', 'strong', + 'fist', 'padlock', 'three-leaves', 'fluffy-wing', 'pummeled', + 'tread', 'arrowed', 'aura', 'back-pain', 'black-flag', + 'bleeding-eye', 'bolt-shield', 'broken-heart', 'cobweb', + 'broken-shield', 'flying-flag', 'radioactive', 'trophy', + 'broken-skull', 'frozen-orb', 'rolling-bomb', 'white-tower', + 'grab', 'screaming', 'grenade', 'sentry-gun', 'all-for-one', + 'angel-outfit', 'archery-target' + ]; + legacyNames.forEach( (n,i)=>insertTokenMarker(new ImageStripTokenMarker(n,i))); + }; + + const readTokenMarkers = () => { + JSON.parse(Campaign().get('_token_markers')||'[]').forEach( tm => insertTokenMarker(new TokenMarker(tm.name, tm.tag, tm.url))); + }; + + StatusMarkers.getStatus = (keyOrName) => { + if(tokenMarkers.hasOwnProperty(keyOrName)){ + return tokenMarkers[keyOrName]; + } + if(reverseLookup.hasOwnProperty(keyOrName)){ + return tokenMarkers[reverseLookup[keyOrName][0]]; // returning first one... + } + // maybe return a null status marker object? + }; + + StatusMarkers.getOrderedList = () => { + return [...orderedLookup].map( key => tokenMarkers[key]); + }; + + const simpleObj = o => JSON.parse(JSON.stringify(o||'{}')); + + + buildStaticMarkers(); + if(simpleObj(Campaign()).hasOwnProperty('_token_markers')){ + readTokenMarkers(); + } else { + buildLegacyMarkers(); + } + } + } + + class statusOp { + + static decomposeStatuses(statuses){ + return _.reduce(statuses.split(/,/),function(memo,st,idx){ + let parts=st.split(/@/), + entry = { + mark: parts[0], + num: parseInt(parts[1],10), + idx: idx + }; + if(isNaN(entry.num)){ + entry.num=''; + } + if(parts[0].length) { + memo[parts[0]] = ( memo[parts[0]] && memo[parts[0]].push(entry) && memo[parts[0]]) || [entry] ; + } + return memo; + },{}); + } + + static composeStatuses(statuses){ + return _.chain(statuses) + .reduce(function(m,s){ + _.each(s,function(sd){ + m.push(sd); + }); + return m; + },[]) + .sortBy(function(s){ + return s.idx; + }) + .map( (s) => ('dead'===s.mark ? 'dead' : ( s.mark+(s.num!=='' ? '@'+s.num : ''))) ) + .value() + .join(','); + } + + static parse(status) { + let s = status.split(/[:;]/); + if(s.hasOwnProperty(1) && 0 === s[1].length){ + s = [`${s[0]}::${s[2]}`,...s.slice(3)]; + } + let statparts = s.shift().match(/^(\S+?)(\[(\d*)\]|)$/)||[]; + let index = ( '[]' === statparts[2] ? statparts[2] : ( undefined !== statparts[3] ? Math.max(parseInt(statparts[3],10)-1,0) : 0 ) ); + + let stat=statparts[1]||''; + let op = (_.contains(['*','/','-','+','=','!','?'], stat[0]) ? stat[0] : false); + let numraw = s.shift() || ''; + let min = Math.min(Math.max(parseInt(s.shift(),10)||0, 0),9); + let max = Math.max(Math.min(parseInt(s.shift(),10)||9,9),0); + let numop = (_.contains(['*','/','-','+'],numraw[0]) ? numraw[0] : false); + let num = Math.max(0,Math.min(9,Math.abs(parseInt(numraw,10)))); + + if(isNaN(num)){ + num = ''; + } + + stat = ( op ? stat.substring(1) : stat); + + let tokenMarker = StatusMarkers.getStatus(stat); + + if(tokenMarker) { + return new statusOp( + tokenMarker, + { + status: tokenMarker.getTag(), + number: num, + index: index, + sign: numop, + min: (minmin?max:min), + operation: op || '+' + }); + } + if('=' === op && stat.length===0){ + return {getMods:(/*c*/)=>({statusmarkers:''})}; + } + + return {getMods:(c)=>({statusmarkers:c})}; + } + + constructor(tm, ops) { + this.tokenMarker = tm; + this.ops = ops; + } + + getMods(statuses='') { + let current = statusOp.decomposeStatuses(statuses); + let statusCount=(statuses).split(/,/).length; + let sm = this.ops; + + switch(sm.operation){ + case '!': + if('[]' !== sm.index && _.has(current,sm.status) ){ + if( _.has(current[sm.status],sm.index) ) { + current[sm.status]= _.filter(current[sm.status],function(e,idx){ + return idx !== sm.index; + }); + } else { + current[sm.status] = current[sm.status] || []; + current[sm.status].push({ + mark: sm.status, + num: (sm.number !=='' ? Math.max(sm.min,Math.min(sm.max,getRelativeChange(0, sm.sign+sm.number))):''), + index: statusCount++ + }); + } + } else { + current[sm.status] = current[sm.status] || []; + current[sm.status].push({ + mark: sm.status, + num: (sm.number!=='' ? Math.max(sm.min,Math.min(sm.max,getRelativeChange(0, sm.sign+sm.number))):''), + index: statusCount++ + }); + } + break; + case '?': + if('[]' !== sm.index && _.has(current,sm.status) && _.has(current[sm.status],sm.index)){ + current[sm.status][sm.index].num = (sm.number !== '') ? (Math.max(sm.min,Math.min(sm.max,getRelativeChange(current[sm.status][sm.index].num, sm.sign+sm.number)))) : ''; + + if([0,''].includes(current[sm.status][sm.index].num)) { + current[sm.status]= _.filter(current[sm.status],function(e,idx){ + return idx !== sm.index; + }); + } + } + break; + case '+': + if('[]' !== sm.index && _.has(current,sm.status) && _.has(current[sm.status],sm.index)){ + current[sm.status][sm.index].num = (sm.number !== '') ? (Math.max(sm.min,Math.min(sm.max,getRelativeChange(current[sm.status][sm.index].num, sm.sign+sm.number)))) : ''; + } else { + current[sm.status] = current[sm.status] || []; + current[sm.status].push({ + mark: sm.status, + num: (sm.number!=='' ? Math.max(sm.min,Math.min(sm.max,getRelativeChange(0, sm.sign+sm.number))):''), + index: statusCount++ + }); + } + break; + case '-': + if('[]' !== sm.index && _.has(current,sm.status)){ + if( _.has(current[sm.status],sm.index )) { + current[sm.status]= _.filter(current[sm.status],function(e,idx){ + return idx !== sm.index; + }); + } + } else { + current[sm.status] = current[sm.status] || []; + current[sm.status].pop(); + } + break; + case '=': + current = {}; + current[sm.status] = []; + current[sm.status].push({ + mark: sm.status, + num: (sm.number!=='' ? Math.max(sm.min,Math.min(sm.max,getRelativeChange(0, sm.sign+sm.number))):''), + index: statusCount++ + }); + break; + } + return { + statusmarkers: statusOp.composeStatuses(current) + }; + } + } + + + //////////////////////////////////////////////////////////// + // moveOp ////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + + class moveOp { + static parse(args){ + const identity = {getMods:() => ({})}; + let angle = 0; + let relativeAngle = true; + let updateAngle = false; + let distance = 0; + let units = ''; + + if(args.length>1){ + let match = args.shift().match(regex.moveAngle); + if(match) { + angle = transforms.degrees(match[2]); + relativeAngle = '='!==match[1]; + updateAngle = '!'===match[3]; + } else { + return identity; + } + } + + { + let match = args.shift().match(regex.moveDistance); + if(match){ + distance = match[1]; + units = match[2]; + } else { + return identity; + } + } + return new moveOp( + angle, + relativeAngle, + updateAngle, + distance, + units + ); + + } + + constructor(angle,relativeAngle,updateAngle,distance,units){ + this.angle = angle; + this.relativeAngle = relativeAngle; + this.updateAngle = updateAngle; + this.distance = distance; + this.units = units; + } + + getMods(token,mods){ + const getValue = (k) => mods.hasOwnProperty(k) ? mods[k] : token.get(k); + // find angle + // find postion from current by distance over angle. + // if current last move start with the token current position, update. + let angle = 0; + if(this.relativeAngle){ + angle = parseFloat(getValue('rotation')); + } + angle = (transforms.degrees(angle+this.angle)||0); + let radAngle = (angle-90) * (Math.PI/180); + + let page = getObj('page',token.get('pageid')); + if(page){ + let distance = numberOp.ConvertUnitsPixel(this.distance,this.units,page); + let cx = getValue('left'); + let cy = getValue('top'); + let lm = getValue('lastmove'); + if(mods.hasOwnProperty('lastmove')){ + lm +=`,${cx},${cy}`; + } else { + lm = `${cx},${cy}`; + } + + let x = cx+(distance*Math.cos(radAngle)); + let y = cy+(distance*Math.sin(radAngle)); + let props = { + lastmove: lm, + top: y, + left: x + }; + if(this.updateAngle){ + props.rotation = angle; + } + return props; + } + return {}; + } + } + + //////////////////////////////////////////////////////////// + // IsComputedAttr ////////////////////////////////////////// + //////////////////////////////////////////////////////////// + const getComputedProxy = ("undefined" !== typeof getComputed) + ? async (...a) => await getComputed(...a) + : async ()=>{} + ; + + class IsComputedAttr { + static #computedMap = new Map(); + static #sheetMap = new Map(); + + static async DoReady() { + let c = Campaign(); + Object.keys(c?.computedSummary||{}).forEach(k=>{ + IsComputedAttr.#computedMap.set(k,c.computedSummary[k]); + }); + + let cMap = findObjs({type:"character"}).reduce((m,c)=>({...m,[c.get('charactersheetname')]:c.id}),{}); + let promises = Object.keys(cMap).map(async c => { + let k = IsComputedAttr.#computedMap.keys().next().value; + if(k) { + let v = await getComputedProxy({characterId:cMap[c],property:k}); + IsComputedAttr.#sheetMap.set(c, undefined !== v); + } + }); + await Promise.all(promises); + } + + static Check(attrName) { + return IsComputedAttr.#computedMap.has(attrName); + } + + static Assignable(attrName) { + return IsComputedAttr.#computedMap.get(attrName)?.tokenBarValue ?? false; + } + + static Readonly(attrName) { + return IsComputedAttr.#computedMap.get(attrName)?.readonly ?? true; + } + + static IsComputed(sheet,attrName) { + let sheetName = sheet.get('charactersheetname'); + + if(IsComputedAttr.Check(attrName) && IsComputedAttr.#sheetMap.has(sheetName)){ + return IsComputedAttr.#sheetMap.get(sheetName); + } + return false; + } + + } + on('ready',IsComputedAttr.DoReady); + + //////////////////////////////////////////////////////////// + + + + + let observers = { + tokenChange: [] + }; + + const getActivePages = () => [...new Set([ + Campaign().get('playerpageid'), + ...Object.values(Campaign().get('playerspecificpages')), + ...findObjs({ + type: 'player', + online: true + }) + .filter((p)=>playerIsGM(p.id)) + .map((p)=>p.get('lastpage')) + ]) + ]; + + const getPageForPlayer = (playerid) => { + let player = getObj('player',playerid); + if(playerIsGM(playerid)){ + return player.get('lastpage') || Campaign().get('playerpageid'); + } + + let psp = Campaign().get('playerspecificpages'); + if(psp[playerid]){ + return psp[playerid]; + } + + return Campaign().get('playerpageid'); + }; + + + const transforms = { + percentage: (p)=>{ + let n = parseFloat(p); + if(!_.isNaN(n)){ + if(n > 1){ + n = Math.min(1,Math.max(n/100,0)); + } else { + n = Math.min(1,Math.max(n,0)); + } + } + return n; + }, + degrees: function(t){ + let n = parseFloat(t); + if(!_.isNaN(n)) { + n %= 360; + } + return n; + }, + circleSegment: function(t){ + let n = Math.abs(parseFloat(t)); + if(!_.isNaN(n)) { + n = Math.min(360,Math.max(0,n)); + } + return n; + }, + orderType: function(t){ + switch(t){ + case 'tofront': + case 'front': + case 'f': + case 'top': + return 'tofront'; + + case 'toback': + case 'back': + case 'b': + case 'bottom': + return 'toback'; + default: + return; + } + }, + keyHash: function(t){ + return (t && t.toLowerCase().replace(/\s+/,'_')) || undefined; + } + }; + + const checkGlobalConfig = function(){ + let s=state.TokenMod, + g=globalconfig && globalconfig.tokenmod; + + if(g && g.lastsaved && g.lastsaved > s.globalconfigCache.lastsaved){ + log(' > Updating from Global Config < ['+(new Date(g.lastsaved*1000))+']'); + + s.playersCanUse_ids = 'playersCanIDs' === g['Players can use --ids']; + state.TokenMod.globalconfigCache=globalconfig.tokenmod; + } + }; + + const assureHelpHandout = (create = false) => { + if(state.TheAaron && state.TheAaron.config && (false === state.TheAaron.config.makeHelpHandouts) ){ + return; + } + const helpIcon = "https://s3.amazonaws.com/files.d20.io/images/295769190/Abc99DVcre9JA2tKrVDCvA/thumb.png?1658515304"; + + // find handout + let props = {type:'handout', name:`Help: ${scriptName}`}; + let hh = findObjs(props)[0]; + if(!hh) { + hh = createObj('handout',Object.assign(props, {inplayerjournals: "all", avatar: helpIcon})); + create = true; + } + if(create || version !== state[scriptName].lastHelpVersion){ + hh.set({ + notes: helpParts.helpDoc({who:'handout',playerid:'handout'}) + }); + state[scriptName].lastHelpVersion = version; + log(' > Updating Help Handout to v'+version+' <'); + } + }; + + const checkInstall = function() { + log('-=> TokenMod v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); + + if( ! _.has(state,'TokenMod') || state.TokenMod.version !== schemaVersion) { + log(' > Updating Schema to v'+schemaVersion+' <'); + switch(state.TokenMod && state.TokenMod.version) { + + case 0.1: + case 0.2: + delete state.TokenMod.globalConfigCache; + state.TokenMod.globalconfigCache = {lastsaved:0}; + /* falls through */ + + case 0.3: + state.TokenMod.lastHelpVersion = version; + /* falls through */ + + case 'UpdateSchemaVersion': + state.TokenMod.version = schemaVersion; + break; + + default: + state.TokenMod = { + version: schemaVersion, + globalconfigCache: {lastsaved:0}, + playersCanUse_ids: false, + lastHelpVersion: version + }; + break; + } + } + checkGlobalConfig(); + StatusMarkers.init(); + assureHelpHandout(); + }; + + const observeTokenChange = function(handler){ + if(handler && _.isFunction(handler)){ + observers.tokenChange.push(handler); + } + }; + + const notifyObservers = function(event,obj,prev){ + _.each(observers[event],function(handler){ + try { + handler(obj,prev); + } catch(e) { + log(`TokenMod: An observer threw and exception in handler: ${handler}`); + } + }); + }; + + const getPlayerIDs = (function(){ + let age=0, + cache=[], + checkCache=function(){ + if(_.now()-60000>age){ + cache=_.chain(findObjs({type:'player'})) + .map((p)=>({ + id: p.id, + userid: p.get('d20userid'), + keyHash: transforms.keyHash(p.get('displayname')) + })) + .value(); + } + }, + findPlayer = function(data){ + checkCache(); + let pids=_.reduce(cache,(m,p)=>{ + if(p.id===data || p.userid===data || (-1 !== p.keyHash.indexOf(transforms.keyHash(data)))){ + m.push(p.id); + } + return m; + },[]); + if(!pids.length){ + let obj=filterObjs((o)=>(o.id===data && _.contains(['character','graphic'],o.get('type'))))[0]; + if(obj){ + let charObj = ('graphic'===obj.get('type') && getObj('character',obj.get('represents'))), + cb = (charObj ? charObj : obj).get('controlledby'); + pids = (cb.length ? cb.split(/,/) : []); + } + } + return pids; + }; + + return function(datum){ + return 'all'===datum ? ['all'] : findPlayer(datum); + }; + }()); + + const HE = (() => { + const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1'); + const e = (s) => `&${s};`; + const entities = { + '<' : e('lt'), + '>' : e('gt'), + "'" : e('#39'), + '@' : e('#64'), + '{' : e('#123'), + '|' : e('#124'), + '}' : e('#125'), + '[' : e('#91'), + ']' : e('#93'), + '"' : e('quot') + }; + const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g'); + return (s) => s.replace(re, (c) => (entities[c] || c) ); + })(); + + const ch = function (c) { + let entities = { + '<' : 'lt', + '>' : 'gt', + "'" : '#39', + '@' : '#64', + '{' : '#123', + '|' : '#124', + '}' : '#125', + '[' : '#91', + ']' : '#93', + '"' : 'quot', + '*' : 'ast', + '/' : 'sol', + ' ' : 'nbsp' + }; + + if(_.has(entities,c) ){ + return ('&'+entities[c]+';'); + } + return ''; + }; + + const getConfigOption_PlayersCanIDs = function() { + let text = ( state.TokenMod.playersCanUse_ids ? + 'ON' : + 'OFF' + ); + return '
    '+ + 'Players can IDs is currently '+ + text+ + ''+ + 'Toggle'+ + ''+ + '
    '; + + }; + + const _h = { + outer: (...o) => `
    ${o.join(' ')}
    `, + title: (t,v) => `
    ${t} v${v}
    `, + subhead: (...o) => `${o.join(' ')}`, + minorhead: (...o) => `${o.join(' ')}`, + optional: (...o) => `${ch('[')}${o.join(` ${ch('|')} `)}${ch(']')}`, + required: (...o) => `${ch('<')}${o.join(` ${ch('|')} `)}${ch('>')}`, + header: (...o) => `
    ${o.join(' ')}
    `, + section: (s,...o) => `${_h.subhead(s)}${_h.inset(...o)}`, + paragraph: (...o) => `

    ${o.join(' ')}

    `, + experimental: () => `
    Experimental
    `, + items: (o) => o.map(t=>`
  • ${t}
  • `).join(''), + ol: (...o) => `
      ${_h.items(o)}
    `, + ul: (...o) => `
      ${_h.items(o)}
    `, + grid: (...o) => `
    ${o.join('')}
    `, + cell: (o) => `
    ${o}
    `, + statusCell: (o) => { + let text = `${o.getName()}${o.getName()!==o.getTag()?` [${_h.code(o.getTag())}]`:''}`; + return `
    ${o.getHTML()}${text}
    `; + }, + helpHandoutLink: ()=>{ + let props = {type:'handout', name:`Help: ${scriptName}`}; + let hh = findObjs(props)[0]; + return `Help: ${scriptName}`; + }, + inset: (...o) => `
    ${o.join(' ')}
    `, + join: (...o) => o.join(' '), + pre: (...o) =>`
    ${o.join(' ')}
    `, + preformatted: (...o) =>_h.pre(o.join('
    ').replace(/\s/g,ch(' '))), + code: (...o) => `${o.join(' ')}`, + attr: { + bare: (o)=>`${ch('@')}${ch('{')}${o}${ch('}')}`, + selected: (o)=>`${ch('@')}${ch('{')}selected${ch('|')}${o}${ch('}')}`, + target: (o)=>`${ch('@')}${ch('{')}target${ch('|')}${o}${ch('}')}`, + char: (o,c)=>`${ch('@')}${ch('{')}${c||'CHARACTER NAME'}${ch('|')}${o}${ch('}')}` + }, + bold: (...o) => `${o.join(' ')}`, + italic: (...o) => `${o.join(' ')}`, + font: { + command: (...o)=>`${o.join(' ')}` + } + }; + + + const helpParts = { + commands: (/* context */) => _h.join( + _h.subhead('Commands'), + _h.inset( + _h.font.command( + `!token-mod `, + _h.required( + `--help`, + `--rebuild-help`, + `--help-statusmarkers`, + `--ignore-selected`, + `--current-page`, + `--active-pages`, + `--api-as`, + `--config`, + `--on`, + `--off`, + `--flip`, + `--set`, + `--move`, + `--report`, + `--order` + ), + _h.required(`parameter`), + _h.optional(`${_h.required(`parameter`)} ...`), + `...`, + _h.optional( + `--ids`, + _h.required(`token_id`), + _h.optional(`${_h.required(`token_id`)} ...`) + ) + ), + _h.paragraph('This command takes a list of modifications and applies them to the selected tokens (or tokens specified with --ids by a GM or Player depending on configuration).'), + _h.paragraph(`${_h.bold('Note:')} Each --option can be specified multiple times and in any order.`), + _h.paragraph(`${_h.bold('Note:')} If you are using multiple ${_h.attr.target('token_id')} calls in a macro, and need to adjust fewer than the supplied number of token ids, simply select the same token several times. The duplicates will be removed.`), + _h.paragraph(`${_h.bold('Note:')} Anywhere you use ${_h.code('|')}, you can use ${_h.code('#')} instead. Sometimes this makes macros easier.`), + _h.paragraph(`${_h.bold('Note:')} You can use the ${_h.code('{{')} and ${_h.code('}}')} to span multiple lines with your command for easier clarity and editing:`), + _h.inset( + _h.preformatted( + '!token-mod {{', + ' --on', + ' flipv', + ' fliph', + ' --set', + ' rotation|180', + ` bar1|${ch('[')+ch('[')}8d8+8${ch(']')+ch(']')}`, + ' light_radius|60', + ' light_dimradius|30', + ' name|"My bright token"', + '}}' + ) + ), + _h.ul( + `${_h.bold('--help')} -- Displays this help`, + `${_h.bold('--rebuild-help')} -- Recreated the help handout in the journal. Useful for showing updated custom status markers.`, + `${_h.bold('--help-statusmarkers')} -- Output just the list of known status markers into the chat.`, + `${_h.bold('--ignore-selected')} -- Prevents modifications to the selected tokens (only modifies tokens passed with --ids).`, + `${_h.bold('--current-page')} -- Only modifies tokens on the calling player${ch("'")}s current page. This is particularly useful when passing character_ids to ${_h.italic('--ids')}.`, + `${_h.bold('--active-pages')} -- Only modifies tokens on pages where there is a player or the GM. This is particularly useful when passing character_ids to ${_h.italic('--ids')}.`, + `${_h.bold('--api-as')} ${_h.required('playerid')} -- Sets the player id to use as the player when the API is calling the script.`, + `${_h.bold('--config')} -- Sets Config options. `, + `${_h.bold('--on')} -- Turns on any of the specified parameters (See ${_h.bold('Boolean Arguments')} below).`, + `${_h.bold('--off')} -- Turns off any of the specified parameters (See ${_h.bold('Boolean Arguments')} below).`, + `${_h.bold('--flip')} -- Flips the value of any of the specified parameters (See ${_h.bold('Boolean Arguments')} below).`, + `${_h.bold('--set')} -- Each parameter is treated as a key and value, divided by a ${_h.code('|')} character. Sets the key to the value. If the value has spaces, you must enclose it ${_h.code(ch("'"))} or ${_h.code(ch('"'))}. See below for specific value handling logic.`, + `${_h.bold('--move')} -- Moves each token in a direction and distance based on its facing.`, + `${_h.bold('--order')} -- Changes the ordering of tokens. Specify one of ${_h.code('tofront')}, ${_h.code('front')}, ${_h.code('f')}, ${_h.code('top')} to bring something to the front or ${_h.code('toback')}, ${_h.code('back')}, ${_h.code('b')}, ${_h.code('bottom')} to push it to the back.`, + `${_h.bold('--report')} -- Displays a report of what changed for each token. ${_h.experimental()}`, + `${_h.bold('--ids')} -- Each parameter is a Token ID, usually supplied with something like ${_h.attr.target(`Target 1${ch('|')}token_id`)}. By default, only a GM can use this argument. You can enable players to use it as well with ${_h.bold('--config players-can-ids|on')}.` + ) + ), + // SECTION: --ids, --ignore-selected, etc... + _h.section('Token Specification', + _h.paragraph(`By default, any selected token is adjusted when the command is executed. Note that there is a bug where using ${_h.attr.target('')} commands, they may cause them to get skipped.`), + _h.paragraph(`${_h.italic('--ids')} takes token ids to operate on, separated by spaces.`), + _h.inset(_h.pre( `!token-mod --ids -Jbz-mlHr1UXlfWnGaLh -JbjeTZycgyo0JqtFj-r -JbjYq5lqfXyPE89CJVs --on showname showplayers_name`)), + _h.paragraph(`Usually, you will want to specify these with the ${_h.attr.target('')} syntax:`), + _h.inset(_h.pre( `!token-mod --ids ${_h.attr.target('1|token_id')} ${_h.attr.target('2|token_id')} ${_h.attr.target('3|token_id')} --on showname showplayers_name`)), + _h.paragraph(`${_h.italic('--ignore-selected')} can be used when you want to be sure selected tokens are not affected. This is particularly useful when specifying the id of a known token, such as moving a graphic from the gm layer to the objects layer, or coloring an object on the map.`) + ) + ), + move: (/* context */) => _h.join( + _h.section('Move', + _h.paragraph(`Use ${_h.code('--move')} to supply a sequence of move operations to apply to a token. By default, moves are relative to the current facing of the token as defined by the rotation handle (generally, the "up" direction when the token is unrotated). Each operation can be either a distance, or a rotation followed by a distance, separated by a pipe ${_h.code('|')}. Distances can use the unit specifiers (${_h.code('g')},${_h.code('u')},${_h.code('ft')},etc -- see the ${_h.bold('Numbers')} section for more) and may be positive or negative. Rotations can be positive or negative. They can be prefaced by a ${_h.code('=')} to ignore the current rotation of the character and instead move based on up being 0. They can further be followed by a ${_h.code('!')} to also rotate the token to the new direction.`), + _h.paragraph(`Moving 3 grid spaces in the current facing.`), + _h.inset( + _h.preformatted( + '!token-mod --move 3g' + ) + ), + _h.paragraph(`Moving 3 grid spaces at 45 degrees to the current facing.`), + _h.inset( + _h.preformatted( + '!token-mod --move 45|3g' + ) + ), + _h.paragraph(`Moving 2 units to the right, ignoring the current facing.`), + _h.inset( + _h.preformatted( + '!token-mod --move =90|2u' + ) + ), + _h.paragraph(`Moving 10ft in the direction 90 degrees to the left of the current facing, and updating the facing to that new direction.`), + _h.inset( + _h.preformatted( + '!token-mod --move -90!|10ft' + ) + ), + _h.paragraph(`Moving forward 2 grid spaces, then right 10ft, then 3 units at 45 degrees to the current facing and updating to that face that direction. `), + _h.inset( + _h.preformatted( + '!token-mod --move 2g 90|10ft =45!|3u' + ) + ) + ) + ), + + booleans: (/* context */) => _h.join( + // SECTION: --on, --off, --flip, etc... + _h.section('Boolean Arguments', + _h.paragraph(`${_h.italic('--on')}, ${_h.italic('--off')} and ${_h.italic('--flip')} options only work on properties of a token that are either ${_h.code('true')} or ${_h.code('false')}, usually represented as checkboxes in the User Interface. Specified properties will only be changed once, priority is given to arguments to ${_h.italic('--on')} first, then ${_h.italic('--off')} and finally to ${_h.italic('--flip')}.`), + _h.inset( + _h.pre(`!token-mod --on showname light_hassight --off isdrawing --flip flipv fliph`) + ), + _h.minorhead('Available Boolean Properties:'), + _h.inset( + _h.grid( + _h.cell('showname'), + _h.cell('show_tooltip'), + _h.cell('showplayers_name'), + _h.cell('showplayers_bar1'), + _h.cell('showplayers_bar2'), + _h.cell('showplayers_bar3'), + _h.cell('showplayers_aura1'), + _h.cell('showplayers_aura2'), + _h.cell('playersedit_name'), + _h.cell('playersedit_bar1'), + _h.cell('playersedit_bar2'), + _h.cell('playersedit_bar3'), + _h.cell('playersedit_aura1'), + _h.cell('playersedit_aura2'), + _h.cell('light_otherplayers'), + _h.cell('light_hassight'), + _h.cell('isdrawing'), + _h.cell('flipv'), + _h.cell('fliph'), + _h.cell('aura1_square'), + _h.cell('aura2_square'), + + _h.cell("has_bright_light_vision"), + _h.cell("has_limit_field_of_vision"), + _h.cell("has_limit_field_of_night_vision"), + _h.cell("has_directional_bright_light"), + _h.cell("has_directional_dim_light"), + _h.cell("bright_vision"), + _h.cell("has_night_vision"), + _h.cell("night_vision"), + _h.cell("emits_bright_light"), + _h.cell("emits_bright"), + _h.cell("emits_low_light"), + _h.cell("emits_low"), + _h.cell('lockMovement') + ) + ), + _h.paragraph( `Any of the booleans can be set with the ${_h.italic('--set')} command by passing a true or false as the value`), + _h.inset( + _h.pre('!token-mod --set showname|yes isdrawing|no') + ), + _h.paragraph(`The following are considered true values: ${_h.code('1')}, ${_h.code('on')}, ${_h.code('yes')}, ${_h.code('true')}, ${_h.code('sure')}, ${_h.code('yup')}`), + + _h.subhead("Probabilistic Booleans"), + _h.paragraph(`TokenMod accepts the following probabilistic values which are true some of the time and false otherwise: ${_h.code('couldbe')} (true 1 in 8 times) , ${_h.code('sometimes')} (true 1 in 4 times) , ${_h.code('maybe')} (true 1 in 2 times), ${_h.code('probably')} (true 3 in 4 times), ${_h.code('likely')} (true 7 in 8 times)`), + + _h.paragraph(`Anything else is considered false.`), + + _h.subhead("Updated Dynamic Lighting"), + _h.paragraph(`${_h.code("has_bright_light_vision")} is the UDL version of ${_h.bold("light_hassight")}. It controls if a token can see at all, and must be turned on for a token to use UDL. You can also use the alias ${_h.code("bright_vision")}.`), + _h.paragraph(`${_h.code("has_night_vision")} controls if a token can see without emitted light around it. This was handled with ${_h.bold("light_otherplayers")} in the old light system. In the new light system, you don't need to be emitting light to see if you have night vision turned on. You can also use the alias ${_h.code("night_vision")}.`), + _h.paragraph(`${_h.code("emits_bright_light")} determines if the configured ${_h.bold("bright_light_distance")} is active or not. There wasn't a concept like this in the old system, it would be synonymous with setting the ${_h.bold("light_radius")} to 0, but now it's not necessary. You can also use the alias ${_h.code("emits_bright")}.`), + _h.paragraph(`${_h.code("emits_low_light")} determines if the configured ${_h.bold("low_light_distance")} is active or not. There wasn't a concept like this in the old system, it would be synonymous with setting the ${_h.bold("light_dimradius")} to 0 (kind of), but now it's not necessary. You can also use the alias ${_h.code("emits_low")}.`) + ) + ), + + setPercentage: (/* context*/) => _h.join( + _h.subhead('Percentage'), + _h.inset( + _h.paragraph(`Percentage values can be a floating point number between 0 and 1.0, such as ${_h.code('0.35')}, or an integer number between 1 and 100.`), + _h.minorhead('Available Percentage Properties:'), + _h.inset( + _h.grid( + _h.cell('dim_light_opacity') + ) + ), + _h.paragraph(`Setting the low light opacity to 30%:`), + _h.inset( + _h.pre( '!token-mod --set dim_light_opacity|30' ) + ), + _h.inset( + _h.pre( '!token-mod --set dim_light_opacity|0.3' ) + ) + ) + ), + + setNightVisionEffect: (/* context*/) => _h.join( + _h.subhead('Night Vision Effect'), + _h.inset( + _h.paragraph(`Night Vision Effect specifies how the region of night vision around a token looks. There are two effects that can be turned on: ${_h.code('dimming')} and ${_h.code('nocturnal')}. You can disable Night Vision Effects using ${_h.code('off')}, ${_h.code('none')}, or leave the field blank. Any other value is ignored.`), + _h.minorhead('Available Night Vision Effect Properties:'), + _h.inset( + _h.grid( + _h.cell('night_vision_effect') + ) + ), + _h.paragraph(`Enable the nocturnal Night Vision Effect on a token:`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|nocturnal' ) + ), + _h.paragraph(`Enable the dimming Night Vision Effect on a token, with dimming starting at 5ft from the token:`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|dimming' ) + ), + _h.paragraph(`Dimming can take an additional argument to set the distance from the token to begin dimming. The default is 5ft if not specified. Distances are provided by appending a another ${_h.code('|')} character and adding a number followed by either a unit or a ${_h.code('%')}:`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|dimming|5ft' ), + _h.pre( '!token-mod --set night_vision_effect|dimming|1u' ) + ), + _h.paragraph(`Using the ${_h.code('%')} allows you to specify the distance as a percentage of the Night Vision Distance. Numbers less than 1 are treated as a decimal percentage. Both of the following are the same:`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|dimming|20%' ), + _h.pre( '!token-mod --set night_vision_effect|dimming|0.2%' ) + ), + _h.paragraph(`You can also use operators to make relative changes. Operators are ${_h.code('+')}, ${_h.code('-')}, ${_h.code('*')}, and ${_h.code('/')}`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|dimming|+10%' ), + _h.pre( '!token-mod --set night_vision_effect|dimming|-5ft' ), + _h.pre( '!token-mod --set night_vision_effect|dimming|/2' ), + _h.pre( '!token-mod --set night_vision_effect|dimming|*10' ) + ), + _h.paragraph(`Disable any Night Vision Effects on a token:`), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|off' ) + ), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|none' ) + ), + _h.inset( + _h.pre( '!token-mod --set night_vision_effect|' ) + ) + ) + ), + + setCompactBar: (/* context*/) => _h.join( + _h.subhead('Compact Bar'), + _h.inset( + _h.paragraph(`Compact Bar specifes how the bar looks. A compact bar is much smaller than the normal presentation and does not have numbers overlaying it. To enable Compact Bar for a token, use ${_h.code('compact')} or ${_h.code('on')}. You can disable Compact Bar using ${_h.code('off')}, ${_h.code('none')}, or leave the field blank. Any other value is ignored.`), + _h.minorhead('Available Compact Bar Properties:'), + _h.inset( + _h.grid( + _h.cell('compact_bar') + ) + ), + _h.paragraph(`Enable Compact Bar on a token:`), + _h.inset( + _h.pre( '!token-mod --set compact_bar|compact' ) + ), + _h.inset( + _h.pre( '!token-mod --set compact_bar|on' ) + ), + _h.paragraph(`Disable Compact Bar on a token:`), + _h.inset( + _h.pre( '!token-mod --set compact_bar|off' ) + ), + _h.inset( + _h.pre( '!token-mod --set compact_bar|none' ) + ), + _h.inset( + _h.pre( '!token-mod --set compact_bar|' ) + ) + ) + ), + + setBarLocation: (/* context*/) => _h.join( + _h.subhead('Bar Location'), + _h.inset( + _h.paragraph(`Bar Location specifes where the bar on a token appears. There are 4 options: ${_h.code('above')}, ${_h.code('overlap_top')}, ${_h.code('overlap_bottom')}, and ${_h.code('below')}. You can also use ${_h.code('off')}, ${_h.code('none')}, or leave the field blank as an alias for ${_h.code('above')}. Any other value is ignored.`), + _h.minorhead('Available Bar Location Properties:'), + _h.inset( + _h.grid( + _h.cell('bar_location') + ) + ), + _h.paragraph(`Setting the bar location to below the token:`), + _h.inset( + _h.pre( '!token-mod --set bar_location|below' ) + ), + _h.paragraph(`Setting the bar location to overlap the top of the token:`), + _h.inset( + _h.pre( '!token-mod --set bar_location|overlap_top' ) + ), + _h.paragraph(`Setting the bar location to overlap the bottom of the token:`), + _h.inset( + _h.pre( '!token-mod --set bar_location|overlap_bottom' ) + ), + _h.paragraph(`Setting the bar location to above the token:`), + _h.inset( + _h.pre( '!token-mod --set bar_location|above' ) + ), + _h.inset( + _h.pre( '!token-mod --set dim_light_opacity|none' ) + ), + _h.inset( + _h.pre( '!token-mod --set dim_light_opacity|off' ) + ), + _h.inset( + _h.pre( '!token-mod --set dim_light_opacity|' ) + ) + ) + ), + + setNumbers: (/* context*/) => _h.join( + _h.subhead('Numbers'), + _h.inset( + _h.paragraph('Number values can be any floating point number (though most fields will drop the fractional part). Numbers must be given a numeric value. They cannot be blank or a non-numeric string.'), + _h.minorhead('Available Numbers Properties:'), + _h.inset( + _h.grid( + _h.cell('left'), + _h.cell('top'), + _h.cell('width'), + _h.cell('height'), + _h.cell('scale'), + _h.cell('light_sensitivity_multiplier') + ) + ), + _h.paragraph( `It${ch("'")}s probably a good idea not to set the location of a token off screen, or the width or height to 0.`), + _h.paragraph( `Placing a token in the top left corner of the map and making it take up a 2x2 grid section:`), + _h.inset( + _h.pre( '!token-mod --set top|0 left|0 width|140 height|140' ) + ), + _h.paragraph(`You can also apply relative change using ${_h.code('+')}, ${_h.code('-')}, ${_h.code(ch('*'))}, and ${_h.code(ch('/'))}. This will move each token one unit down, 2 units left, then make it 5 times as wide and half as tall.`), + _h.inset( + _h.pre( `!token-mod --set top|+70 left|-140 width|${ch('*')}5 height|/2` ) + ), + _h.paragraph(`You can use ${_h.code('=')} to explicity set a value. This is the default behavior, but you might need to use it to move something to a location off the edge using a negative number but not a relative number:`), + _h.inset( + _h.pre( '!token-mod --set top|=-140' ) + ), + _h.paragraph( `${_h.code('scale')} is a pseudo field which adjusts both ${_h.code('width')} and ${_h.code('height')} with the same operation. This will scale a token to twice it's current size.`), + _h.inset( + _h.pre( '!token-mod --set scale|*2' ) + ), + _h.paragraph(`You can follow a number by one of ${_h.code('u')}, ${_h.code('g')}, or ${_h.code('s')} to adjust the scale that the number is applied in.`), + _h.paragraph(`Use ${_h.code('u')} to use a number based on Roll20 Units, which are 70 pixels at 100% zoom. This will set a graphic to 280x140.`), + _h.inset( + _h.pre( '!token-mod --set width|4u height|2u' ) + ), + _h.paragraph(`Use ${_h.code('g')} to use a number based on the current grid size. This will set a token to the middle of the 8th column, 4rd row grid. (.5 offset for half the center)`), + _h.inset( + _h.pre( '!token-mod --set left|7.5g top|3.5g' ) + ), + _h.paragraph(`Use ${_h.code('s')} to use a number based on the current unit if measure. (ft, m, mi, etc) This will set a token to be 25ft by 35ft (assuming ft are the unit of measure)`), + _h.inset( + _h.pre( '!token-mod --set width|25s height|35s' ) + ), + _h.paragraph(`Currently, you can also use any of the default units of measure as alternatives to ${_h.code('s')}: ${_h.code('ft')}, ${_h.code('m')}, ${_h.code('km')}, ${_h.code('mi')}, ${_h.code('in')}, ${_h.code('cm')}, ${_h.code('un')}, ${_h.code('hex')}, ${_h.code('sq')}`), + _h.inset( + _h.pre( '!token-mod --set width|25ft height|35ft' ) + ) + ) + ), + + setNumbersOrBlank: ( /* context */) => _h.join( + _h.subhead('Numbers or Blank'), + _h.inset( + _h.paragraph('Just like the Numbers fields, except you can set them to blank as well.'), + _h.minorhead('Available Numbers or Blank Properties:'), + _h.inset( + _h.grid( + _h.cell('light_radius'), + _h.cell('light_dimradius'), + _h.cell('light_multiplier'), + _h.cell('aura1_radius'), + _h.cell('aura2_radius'), + _h.cell('adv_fow_view_distance'), + _h.cell("night_vision_distance"), + _h.cell("night_distance"), + _h.cell("bright_light_distance"), + _h.cell("bright_distance"), + _h.cell("low_light_distance"), + _h.cell("low_distance") + ) + ), + _h.paragraph(`Here is setting a standard DnD 5e torch, turning off aura1 and setting aura2 to 30. Note that the ${_h.code('|')} is still required for setting a blank value, such as aura1_radius below.`), + _h.inset( + _h.pre('!token-mod --set light_radius|40 light_dimradius|20 aura1_radius| aura2_radius|30') + ), + _h.paragraph(`Just as above, you can use ${_h.code('=')}, ${_h.code('+')}, ${_h.code('-')}, ${_h.code(ch('*'))}, and ${_h.code(ch('/'))} when setting these values.`), + _h.paragraph(`Here is setting a standard DnD 5e torch, with advanced fog of war revealed for 30.`), + _h.inset( + _h.pre('!token-mod --set light_radius|40 light_dimradius|20 adv_fow_view_distance|30') + ), + _h.paragraph(`Sometimes it is convenient to have a way to set a radius if there is none, but remove it if it is set. This allows toggling a known radius on and off, or setting a multiplier if there isn't one, but clearing it if there is. You can preface a number with ${_h.code('!')} to toggle it${ch("'")}s value on and off. Here is an example that will add or remove a 20${ch("'")} radius aura 1 from a token:`), + _h.inset( + _h.pre('!token-mod --set aura1_radius|!20') + ), + _h.paragraph(`These also support the relative scale operations that ${_h.bold('Numbers')} support: ${_h.code('u')}, ${_h.code('g')}, ${_h.code('s')}`), + _h.inset( + _h.pre('!token-mod --set aura1_radius|3g aura2_radius|10u light_radius|25s') + ), + _h.paragraph(`${_h.bold('Note:')} ${_h.code('light_multiplier')} ignores these modifiers. Additionally, the rest are already in the scale of measuring distance (${_h.code('s')}) so there is no difference between ${_h.code('25s')}, ${_h.code('25ft')}, and ${_h.code('25')}.`), + _h.subhead(`Updated Dynamic Lighting`), + _h.paragraph(`${_h.code("night_vision_distance")} lets you set how far away a token can see with no light. You need to have ${_h.bold("has_night_vision")} turned on for this to take effect. You can also use the alias ${_h.code("night_distance")}.`), + _h.paragraph(`${_h.code("bright_light_distance")} lets you set how far bright light is emitted from the token. You need to have ${_h.bold("has_bright_light_vision")} turned on for this to take effect. You can also use the alias ${_h.code("bright_distance")}.`), + _h.paragraph(`${_h.code("low_light_distance")} lets you set how far low light is emitted from the token. You need to have ${_h.bold("has_bright_light_vision")} turned on for this to take effect. You can also use the alias ${_h.code("low_distance")}.`) + ) + ), + + setDegrees: ( /* context */) => _h.join( + _h.subhead('Degrees'), + _h.inset( + _h.paragraph('Any positive or negative number. Values will be automatically adjusted to be in the 0-360 range, so if you add 120 to 270, it will wrap around to 90.'), + _h.minorhead('Available Degrees Properties:'), + _h.inset( + _h.grid( + _h.cell('rotation'), + _h.cell("limit_field_of_vision_center"), + _h.cell("limit_field_of_night_vision_center"), + _h.cell("directional_bright_light_center"), + _h.cell("directional_dim_light_center") + ) + ), + _h.paragraph('Rotating a token by 180 degrees.'), + _h.inset( + _h.pre('!token-mod --set rotation|+180') + ) + ) + ), + + setCircleSegment: ( /* context */) => _h.join( + _h.subhead('Circle Segment (Arc)'), + _h.inset( + _h.paragraph('Any Positive or negative number, with the final result being clamped to from 0-360. This is different from a degrees setting, where 0 and 360 are the same thing and subtracting 1 from 0 takes you to 359. Anything lower than 0 will become 0 and anything higher than 360 will become 360.'), + _h.minorhead('Available Circle Segment (Arc) Properties:'), + _h.inset( + _h.grid( + _h.cell('light_angle'), + _h.cell('light_losangle'), + _h.cell("limit_field_of_vision_total"), + _h.cell("limit_field_of_night_vision_total"), + _h.cell("directional_bright_light_total"), + _h.cell("directional_dim_light_total") + ) + ), + _h.paragraph('Setting line of sight angle to 90 degrees.'), + _h.inset( + _h.pre('!token-mod --set light_losangle|90') + ) + ) + ), + + setColors: ( /* context */) => _h.join( + _h.subhead('Colors'), + _h.inset( + _h.paragraph(`Colors can be specified in multiple formats:`), + _h.inset( + _h.ul( + `${_h.bold('Transparent')} -- This is the special literal ${_h.code('transparent')} and represents no color at all.`, + `${_h.bold('HTML Color')} -- This is 6 or 3 hexidecimal digits, optionally prefaced by ${_h.code('#')}. Digits in a 3 digit hexidecimal color are doubled. All of the following are the same: ${_h.code('#ff00aa')}, ${_h.code('#f0a')}, ${_h.code('ff00aa')}, ${_h.code('f0a')}`, + `${_h.bold('RGB Color')} -- This is an RGB color in the format ${_h.code('rgb(1.0,1.0,1.0)')} or ${_h.code('rgb(256,256,256)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 256. Note that numbers can be outside this range for the purpose of doing math.`, + `${_h.bold('HSV Color')} -- This is an HSV color in the format ${_h.code('hsv(1.0,1.0,1.0)')} or ${_h.code('hsv(360,100,100)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 360 for the hue and 0 to 100 for saturation and value. Note that numbers can be outside this range for the purpose of doing math.` + ) + ), + _h.minorhead('Available Colors Properties:'), + _h.inset( + _h.grid( + _h.cell('tint_color'), + _h.cell('aura1_color'), + _h.cell('aura2_color'), + _h.cell('night_vision_tint'), + _h.cell('lightColor'), + _h.cell('lightcolor') + ) + ), + _h.paragraph('Turning off the tint and setting aura1 to a reddish color. All of the following are the same:'), + _h.inset( + _h.pre('!token-mod --set tint_color|transparent aura1_color|ff3366'), + _h.pre('!token-mod --set tint_color| aura1_color|f36'), + _h.pre('!token-mod --set tint_color|transparent aura1_color|#f36'), + _h.pre('!token-mod --set tint_color| aura1_color|#ff3366') + ), + _h.paragraph('Setting the tint_color using an RGB Color using Integer and Decimal notations:'), + _h.inset( + _h.pre('!token-mod --set tint_color|rgb(127,0,256)'), + _h.pre('!token-mod --set tint_color|rgb(.5,0.0,1.0)') + ), + _h.paragraph('Setting the tint_color using an HSV Color using Integer and Decimal notations:'), + _h.inset( + _h.pre('!token-mod --set tint_color|hsv(0,50,100)'), + _h.pre('!token-mod --set tint_color|hsv(0.0,.5,1.0)') + ), + + _h.paragraph(`You can toggle a color on and off by prefacing it with ${_h.code('!')}. If the color is currently transparent, it will be set to the specified color, otherwise it will be set to transparent:`), + _h.inset( + _h.pre('!token-mod --set tint_color|!rgb(1.0,.0,.2)') + ), + + _h.minorhead('Color Math'), + + _h.paragraph(`You can perform math on colors using ${_h.code('+')}, ${_h.code('-')}, and ${_h.code(ch('*'))}.`), + _h.paragraph(`Making the aura just a little more red:`), + _h.inset( + _h.pre('!token-mod --set aura1_color|+#330000') + ), + _h.paragraph(`Making the aura just a little less blue:`), + _h.inset( + _h.pre('!token-mod --set aura1_color|-rgb(0.0,0.0,0.1)') + ), + _h.paragraph(`HSV colors are especially good for color math. Making the aura twice as bright:`), + _h.inset( + _h.pre(`!token-mod --set aura1_color|${ch('*')}hsv(1.0,1.0,2.0)`) + ), + + _h.paragraph(`Performing math operations with a transparent color as the command argument does nothing:`), + _h.inset( + _h.pre(`!token-mod --set aura1_color|${ch('*')}transparent`) + ), + + _h.paragraph(`Performing math operations on a transparent color on a token treats the color as black. Assuming a token had a transparent aura1, this would set it to #330000.`), + _h.inset( + _h.pre('!token-mod --set aura1_color|+300') + ) + ) + ), + + setText: ( /* context */) => _h.join( + _h.subhead('Text'), + _h.inset( + _h.paragraph(`These can be pretty much anything. If your value has spaces in it, you need to enclose it in ${ch("'")} or ${ch('"')}.`), + _h.minorhead('Available Text Properties:'), + _h.inset( + _h.grid( + _h.cell('name'), + _h.cell('tooltip'), + _h.cell('bar1_value'), + _h.cell('bar2_value'), + _h.cell('bar3_value'), + _h.cell('bar1_current'), + _h.cell('bar2_current'), + _h.cell('bar3_current'), + _h.cell('bar1_max'), + _h.cell('bar2_max'), + _h.cell('bar3_max'), + _h.cell('bar1'), + _h.cell('bar2'), + _h.cell('bar3'), + _h.cell('bar1_reset'), + _h.cell('bar2_reset'), + _h.cell('bar3_reset') + ) + ), + _h.paragraph(`Setting a token${ch("'")}s name to ${ch('"')}Sir Thomas${ch('"')} and bar1 value to 23.`), + _h.inset( + _h.pre(`!token-mod --set name|${ch('"')}Sir Thomas${ch('"')} bar1_value|23`) + ), + _h.paragraph(`Setting a bar to a numeric value will be treated as a relative change if prefaced by ${_h.code('+')}, ${_h.code('-')}, ${_h.code(ch('*'))}, or ${_h.code('/')}, or will be explicitly set when prefaced with a ${_h.code('=')}. If you are setting a bar value, you can append a ${_h.code('!')} to the value to force it to be bounded between ${_h.code('0')} and ${_h.code('max')} for the bar.`), + _h.paragraph(`${_h.italic('bar1')}, ${_h.italic('bar2')} and ${_h.italic('bar3')} are special. Any value set on them will be set in both the ${_h.italic('_value')} and ${_h.italic('_max')} fields for that bar. This is most useful for setting hit points, particularly if the value comes from an inline roll.`), + _h.inset( + _h.pre(`!token-mod --set bar1|${ch('[')}${ch('[')}3d6+8${ch(']')}${ch(']')}`) + ), + _h.paragraph(`${_h.italic('bar1_reset')}, ${_h.italic('bar2_reset')} and ${_h.italic('bar3_reset')} are special. Any value set on them will be ignored, instead they will set the ${_h.italic('_value')} field for that bar to whatever the matching ${_h.italic('_max')} field is set to. This is most useful for resetting hit points or resource counts like spells. (The ${_h.code('|')} is currently still required.)`), + _h.inset( + _h.pre(`!token-mod --set bar1_reset| bar3_reset|`) + ) + ) + ), + + setLayer: ( /* context */) => _h.join( + _h.subhead('Layer'), + _h.inset( + _h.paragraph(`There is only one Layer property. It can be one of 4 values, listed below.`), + _h.minorhead('Available Layer Values:'), + _h.inset( + _h.grid( + _h.cell('gmlayer'), + _h.cell('objects'), + _h.cell('map'), + _h.cell('walls') + ) + ), + _h.paragraph('Moving something to the gmlayer.'), + _h.inset( + _h.pre('!token-mod --set layer|gmlayer') + ) + ) + ), + availableStatusMarkers: (/* context */) => _h.join( + _h.minorhead('Available Status Markers:'), + _h.inset( + _h.grid( + ...StatusMarkers.getOrderedList().map(tm=>_h.statusCell(tm)) + ) + ) + ), + setStatus: ( context ) => _h.join( + _h.subhead('Status'), + _h.inset( + _h.paragraph(`There is only one Status property. Status has a somewhat complicated syntax to support the greatest possible flexibility.`), + _h.minorhead('Available Status Property:'), + _h.inset( + _h.grid( + _h.cell('statusmarkers') + ) + ), + + _h.paragraph(`Status is the only property that supports multiple values, all separated by ${_h.code('|')} as seen below. This command adds the blue, red, green, padlock and broken-sheilds to a token, on top of any other status markers it already has:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue|red|green|padlock|broken-shield') + ), + + _h.paragraph(`You can optionally preface each status with a ${_h.code('+')} to remind you it is being added. This command is identical:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|+blue|+red|+green|+padlock|+broken-shield') + ), + + _h.paragraph(`Each value can be followed by a ${_h.code(':')} and a number between 0 and 9. (The number following the ${_h.italic('dead')} status is ignored as that status is special.) This will set the blue status with no number overlay, red with a 3 overlay, green with no overlay, padlock with a 2 overlay, and broken-shield with a 7 overlay:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:0|red:3|green|padlock:2|broken-shield:7') + ), + _h.paragraph(`${_h.bold('Note:')} TokenMod will now show 0 on status markers everywhere that makes sense to do.`), + + _h.paragraph(`You can use a semicolon (${_h.code(';')}) in place of a colon (${_h.code(':')}) to allow setting statuses with numbers from API Buttons.`), + _h.inset( + _h.pre(`${ch('[')}[Set some statuses](!token-mod --set statusmarkers|blue;0|red;3|green|padlock;2|broken-shield;7)`) + ), + + _h.paragraph(`The numbers following a status can be prefaced with a ${_h.code('+')} or ${_h.code('-')}, which causes their value to be applied to the current value. Here${ch("'")}s an example showing blue getting incremented by 2, and padlock getting decremented by 1. Values will be bounded between 0 and 9.`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:+2|padlock:-1') + ), + + _h.paragraph(`You can append two additional numbers separated by ${_h.code(':')}. These numbers will be used as the minimum and maximum value when setting or adjusting the number on a status marker. Specified minimum and maximum values will be kept between 0 and 9.`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:+1:2:5') + ), + + _h.paragraph(`Omitting either of the numbers will cause them to use their default value. Here is an example limiting the max to 5:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:+1::5') + ), + + _h.paragraph(`You can optionally preface each status with a ${_h.code('?')} to modify the way ${_h.code('+')} and ${_h.code('-')} on status numbers work. With ${_h.code('?')} on the front of the status, only selected tokens that have that status will be modified. Additionally, if the status reaches 0, it will be removed. Here${ch("'")}s an example showing blue getting decremented by 1. If it reaches 0, it will be removed and no status will be added if it is missing.`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|?blue:-1') + ), + + _h.paragraph(`By default, status markers will be added, retaining whichever status markers are already present. You can override this behavior by prefacing a status with a ${_h.code('-')} to cause the status to be removed. This will remove the blue and padlock status:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|-blue|-padlock') + ), + + _h.paragraph(`Sometimes it is convenient to have a way to add a status if it is not there, but remove it if it is. This allows marking tokens with markers and clearing them with the same command. You can preface a status with ${_h.code('!')} to toggle it${ch("'")}s state on and off. Here is an example that will add or remove the Rook piece from a token:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|!white-tower') + ), + + _h.paragraph(`Sometimes, you might want to clear all status marker as part of setting a new status marker. You can do this by prefacing a status marker with an ${_h.code('=')}. Note that this affects all status markers before as well, so you will want to do this only on the first status marker. This will remove all status markers and set only the dead marker:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|=dead') + ), + + _h.paragraph(`If you want to remove all status markers, just set an empty status marker with ${_h.code('=')}. This will clear all the status markers:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|=') + ), + + _h.paragraph(`You can also do this by setting a single status marker, then removing it:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|=blue|-blue') + ), + + _h.paragraph(`You can set multiple of the same status marker with a bracket syntax. Copies of a status are indexed starting at 1 from left to right. Leaving brackets off will be the same as specifying index 1. Using empty brackets is the same as specifying an index 1 greater than the highest index in use. When setting a status at an index that doesn${ch("'")}t exist (say, 8 when you only have 2 of that status) it will be appended to the right as the next index. When removing a status that doesn${ch("'")}t exist, it will be ignored. Removing the empty bracket status will remove all statues of that type.`), + _h.paragraph(`Adding 2 blue status markers with the numbers 7 and 5 in a few different ways:`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:7|blue[]:5'), + _h.pre('!token-mod --set statusmarkers|blue[]:7|blue[]:5'), + _h.pre('!token-mod --set statusmarkers|blue[1]:7|blue[2]:5') + ), + _h.paragraph('Removing the second blue status marker:'), + _h.inset( + _h.pre('!token-mod --set statusmarkers|-blue[2]') + ), + _h.paragraph('Removing all blue status markers:'), + _h.inset( + _h.pre('!token-mod --set statusmarkers|-blue[]') + ), + + _h.paragraph('All of these operations can be combine in a single statusmarkers command.'), + _h.inset( + _h.pre('!token-mod --set statusmarkers|blue:3|-dead|red:3') + ), + helpParts.availableStatusMarkers(context), + _h.paragraph(`Status Markers with a space in the name must be specified using the tag name, which appears in ${_h.code('[')}${_h.code(']')} above.`), + _h.inset( + _h.pre('!token-mod --set statusmarkers|Mountain_Pass::1234568') + ), + _h.paragraph(`You can use a semicolon (${_h.code(';')}) in place of a colon (${_h.code(':')}) to allow setting statuses with numbers from API Buttons.`), + _h.inset( + _h.pre(`${ch('[')}3 Mountain Pass](!token-mod --set statusmarkers|Mountain_Pass;;1234568;3)`) + ) + + ) + ), + + setImage: ( /* context */) => _h.join( + _h.subhead('Image'), + _h.inset( + _h.paragraph(`The Image type lets you manage the image a token uses, as well as the available images for Multi-Sided tokens. Images must be in a user library or will be ignored. The full path must be provided.`), + _h.minorhead('Available Image Properties:'), + _h.inset( + _h.grid( + _h.cell('imgsrc') + ) + ), + _h.paragraph(`Setting the token image to a library image using a url (in this case, the orange ring I use for ${_h.italic('TurnMarker1')}):`), + _h.inset( + _h.pre('!token-mod --set imgsrc|https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580') + ), + _h.paragraph(`Setting the token image from another token by specifying it${ch("'")}s token_id:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|${_h.attr.target('token_id')} --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`${_h.bold('WARNING:')} Because of a Roll20 bug with ${_h.attr.target('')} and the API, you must specify the tokens you want to change using ${_h.code('--ids')} when using ${_h.attr.target('')}.`), + + _h.minorhead('Multi-Sided Token Options'), + _h.inset( + _h.subhead('Appending (+)'), + _h.inset( + _h.paragraph(`You can append additional images to the list of sides by prefacing the source of an image with ${_h.code('+')}:`), + _h.inset( + _h.pre('!token-mod --set imgsrc|+https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580'), + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')} --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`If you follow the ${_h.code('+')} with a ${_h.code('=')}, it will update the current side to the freshly added image:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+=${_h.attr.target('token_id')} --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`When getting the image from a token, you can append a ${_h.code(':')} and follow it with an index to copy. Indicies start at 1, if you specify an index that doesn${ch("'")}t exist, nothing will happen:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:3 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`You can specify the ${_h.code('=')} with this syntax:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+=${_h.attr.target('token_id')}:3 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`You can specify multiple indices to copy by using a ${_h.code(',')} separated list:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:3,4,5,9 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`Using ${_h.code('=')} with this syntax will set the current side to the last added image:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+=${_h.attr.target('token_id')}:3,4,5,9 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`Images are copied in the order specified. You can even copy images from a token you${ch("'")}re setting.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:3,2,1 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`You can use an ${_h.code(ch('*'))} after the ${_h.code(':')} to copy all the images from a token. The order will be from 1 to the maximum image.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:${ch('*')} --ids ${_h.attr.selected('token_id')}`) + ), + + _h.paragraph(`When appending a url, you can use a ${_h.code(ch(':@'))} followed by a number to specify where to place the new image. Indicies start at 1.`), + _h.inset( + _h.pre('!token-mod --set imgsrc|+https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580:@1') + ), + + _h.paragraph(`When appending from a token, you can use an ${_h.code(ch('@'))} followed by a number to specify where each copied image is inserted. Indicies start at 1.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:3@1,4@2,5@4,9@5 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`Note that inserts are performed in order, so continuously inserting at a position will insert in reverse order.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|+${_h.attr.target('token_id')}:3@1,4@1,5@1,9@1 --ids ${_h.attr.selected('token_id')}`) + ) + ), + + _h.subhead('Replacing (^)'), + _h.inset( + _h.paragraph(`You can replace images in the list of sides by prefacing the source of an image with ${_h.code('^')} and append ${_h.code(ch(':@'))} followed by a number to specify which images to replace. Indicies start at 1.`), + _h.inset( + _h.pre('!token-mod --set imgsrc|^https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580:@2'), + _h.pre(`!token-mod --set imgsrc|^${_h.attr.target('token_id')}:@2 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`When replacing from a token, you can specify multiple replacements from a source token to the destination token:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|^${_h.attr.target('token_id')}:3@1,4@2,5@4,9@5 --ids ${_h.attr.selected('token_id')}`) + ) + ), + + _h.subhead('Reordering (/)'), + _h.inset( + _h.paragraph(`You can use a ${_h.code(ch('/'))} followed by a pair of numbers separated by ${_h.code('@')} to move an image on the token from one postion to another. Indicies start at 1.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|/3@1 --ids ${_h.attr.selected('token_id')}`) + ), + _h.paragraph(`You can string these together with commas. Note that operationes are performed in order and may displace prior moved images.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|/3@1,4@2,5@3,9@4 --ids ${_h.attr.selected('token_id')}`) + ) + ), + + _h.subhead('Removing (-)'), + _h.inset( + _h.paragraph(`You can remove images from the image list using ${_h.code('-')} followed by the index to remove. If you remove the currently used image, the side will be set to 1.`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|-3`) + ), + _h.paragraph(`If you omit the number, it will remove the current side:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|-`) + ), + + _h.paragraph(`You can follow the ${_h.code('-')} with a ${_h.code(',')} separated list of indicies to remove. If any of the indicies don${ch("'")}t exist, they will be ignored:`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|-3,4,7`) + ), + + _h.paragraph(`You can follow the ${_h.code('-')} with an ${_h.code(ch('*'))} to remove all the images, turning the Multi-Sided token back into a regular token. (This also happens if you remove the last image by index.):`), + _h.inset( + _h.pre(`!token-mod --set imgsrc|-${ch('*')}`) + ) + ), + + _h.paragraph(`${_h.bold('WARNING:')} If you attempt to change the image list for a token with images in the Marketplace Library, it will remove all of them from that token.`) + ) + ) + ), + + setSideNumber: ( /* context */) => _h.join( + _h.subhead('SideNumber'), + _h.inset( + _h.paragraph(`This is the index of the side to show for Multi-Sided tokens. Indicies start at 1. If you have a 6-sided token, it will have indicies 1, 2, 3, 4, 5 and 6. An empty index is considered to be 1. If a token doesn't have the index specified, it isn't changed.`), + _h.paragraph(`${_h.bold('NOTICE:')} This only works for images in the User Image library. If your token has images that are stored in the Marketplace Library, they will not be selectable with this command. You can download those images and upload them to your User Image Library to use them with this.`), + _h.minorhead('Available SideNumber Properties:'), + _h.inset( + _h.grid( + _h.cell('currentside') + ) + ), + _h.paragraph(`Setting a token to index 2:`), + _h.inset( + _h.pre('!token-mod --set currentside|2') + ), + _h.paragraph(`Not specifying an index will set the index to 1, the first image:`), + _h.inset( + _h.pre('!token-mod --set currentside|') + ), + + _h.paragraph(`You can shift the image by some amount by using ${_h.code('+')} or ${_h.code('-')} followed by an optional number.`), + _h.paragraph(`Moving all tokens to the next image:`), + _h.inset( + _h.pre('!token-mod --set currentside|+') + ), + _h.paragraph(`Moving all tokens back 2 images:`), + _h.inset( + _h.pre('!token-mod --set currentside|-2') + ), + _h.paragraph(`By default, if you go off either end of the list of images, you will wrap back around to the opposite side. If this token is showing image 3 out of 4 and this command is run, it will be on image 2:`), + _h.inset( + _h.pre('!token-mod --set currentside|+3') + ), + _h.paragraph(`If you preface the command with a ${_h.code('?')}, the index will be bounded to the number of images and not wrap. In the same scenario, this would leave the above token at image 4:`), + _h.inset( + _h.pre('!token-mod --set currentside|?+3') + ), + _h.paragraph(`In the same scenario, this would leave the above token at image 1:`), + _h.inset( + _h.pre('!token-mod --set currentside|?-30') + ), + + _h.paragraph(`If you want to choose a random image, you can use ${_h.code(ch('*'))}. This will choose one of the valid images at random (all equally weighted):`), + _h.inset( + _h.pre(`!token-mod --set currentside|${ch('*')}`) + ) + ) + ), + + setCharacterID: ( /*context*/ ) => _h.join( + _h.subhead('Character ID'), + _h.inset( + _h.paragraph(`You can use the ${_h.attr.char('character_id')} syntax to specify a character_id directly or use the name of a character (quoted if it contains spaces) or just the shortest part of the name that is unique (${ch("'")}Sir Maximus Strongbow${ch("'")} could just be ${ch("'")}max${ch("'")}.). Not case sensitive: Max = max = MaX = MAX`), + _h.minorhead('Available Character ID Properties:'), + _h.inset( + _h.grid( + _h.cell('represents') + ) + ), + _h.paragraph('Here is setting the represents to the character Bob.'), + _h.inset( + _h.pre(`!token-mod --set represents|${_h.attr.char('character_id','Bob')}`) + ), + _h.paragraph('Note that setting the represents will clear the links for the bars, so you will probably want to set those again.') + ) + ), + + setAttributeName: ( /*context*/ ) => _h.join( + _h.subhead('Attribute Name'), + _h.inset( + _h.paragraph(`These are resolved from the represented character id. If the token doesn${ch("'")}t represent a character, these will be ignored. If the Attribute Name specified doesn${ch("'")}t exist for the represented character, the link is unchanged. You can clear a link by passing a blank Attribute Name.`), + _h.minorhead('Available Attribute Name Properties:'), + _h.inset( + _h.grid( + _h.cell('bar1_link'), + _h.cell('bar2_link'), + _h.cell('bar3_link') + ) + ), + _h.paragraph('Here is setting the represents to the character Bob and setting bar1 to be the npc hit points attribute.'), + _h.inset( + _h.pre(`!token-mod --set represents|${_h.attr.char('character_id','Bob')} bar1_link|npc_HP`) + ), + _h.paragraph('Here is clearing the link for bar3:'), + _h.inset( + _h.pre('!token-mod --set bar3_link|') + ) + ) + ), + + + setPlayer: ( /*context*/ ) => _h.join( + _h.subhead('Player'), + _h.inset( + _h.paragraph('You can specify Players using one of five methods: Player ID, Roll20 ID Number, Player Name Matching, Token ID, Character ID'), + _h.inset( + _h.ul( + 'Player ID is a unique identifier assigned that player in a specific game. You can only find this id from the API, so this is likely the least useful method.', + 'Roll20 ID Number is a unique identifier assigned to a specific player. You can find it in the URL of their profile page as the number preceeding their name. This is really useful if you play with the same people all the time, or are cloning the same game with the same players, etc.', + 'Player Name Matching is a string that will be matched to the current name of the player in game. Just like with Characters above, it can be quoted if it has spaces and is case insensitive. All players that match a given string will be used.', + 'Token ID will be used to collect the controlledby entries for a token or the associated character if the token represetns one.', + 'Character ID will be used to collect the controlledby entries for a character.' + ) + ), + _h.paragraph(`Note that you can use the special string ${_h.italic('all')} to denote the All Players special player.`), + _h.minorhead('Available Player Properties:'), + _h.inset( + _h.grid( + _h.cell('controlledby') + ) + ), + + _h.paragraph(`Controlled by supports multiple values, all separated by ${_h.code('|')} as seen below.`), + _h.inset( + _h.pre('!token-mod --set controlledby|aaron|+stephen|+russ') + ), + + _h.paragraph(`There are 3 operations that can be specified with leading characters: ${_h.code('+')}, ${_h.code('-')}, ${_h.code('=')} (default)`), + _h.inset( + _h.ul( + `${_h.code('+')} will add the player(s) to the controlledby list.`, + `${_h.code('-')} will remove the player(s) from the controlledby list.`, + `${_h.code('=')} will set the controlledby list to only the player(s). (Default)` + ) + ), + + _h.paragraph('Adding control for roll20 player number 123456:'), + _h.inset( + _h.pre('!token-mod --set controlledby|+123456') + ), + + _h.paragraph('Setting control for all players:'), + _h.inset( + _h.pre('!token-mod --set controlledby|all') + ), + + _h.paragraph('Adding all the players with k in their name but removing karen:'), + _h.inset( + _h.pre('!token-mod --set controlledby|+k|-karen') + ), + + _h.paragraph( 'Adding the player with player id -JsABCabc123-12:' ), + _h.inset( + _h.pre( '!token-mod --set controlledby|+-JsABCabc123-12' ) + ), + + _h.paragraph( 'In the case of a leading character on the name that would be interpreted as an operation, you can use quotes:' ), + _h.inset( + _h.pre('!token-mod --set controlledby|"-JsABCabc123-12"') + ), + + _h.paragraph( `When using Token ID or Character ID methods, it${ch("'")}s a good idea to use an explicit operation:` ), + _h.inset( + _h.pre( `!token-mod --set controlledby|=${_h.attr.target('token_id')}`) + ), + + _h.paragraph( 'Quotes will also help with names that have spaces, or with nested other quotes:' ), + _h.inset( + _h.pre( `!token-mod --set controlledby|+${ch("'")}Bob "tiny" Slayer${ch("'")}`) + ), + + _h.paragraph( 'You can remove all controlling players by using a blank list or explicitly setting equal to nothing:'), + _h.inset( + _h.pre('!token-mod --set controlledby|'), + _h.pre('!token-mod --set controlledby|=') + ), + + _h.paragraph( `A specified action that doesn${ch("'")}t match any player(s) will be ignored. If there are no players named Tim, this won${ch("'")}t change the list:`), + _h.inset( + _h.pre('!token-mod --set controlledby|tim') + ), + + _h.paragraph( 'If you wanted to force an empty list and set tim if tim is available, you can chain this with blanking the list:'), + _h.inset( + _h.pre('!token-mod --set controlledby||tim') + ), + + _h.minorhead( 'Using controlledby with represents'), + _h.paragraph( 'When a token represents a character, the controlledby property that is adjusted is the one on the character. This works as you would want it to, so if you are changing the represents as part of the same command, it will adjust the location that will be correct after all commands are run.'), + + _h.paragraph( 'Set the token to represent the character with rook in the name and assign control to players matching bob:'), + _h.inset( + _h.pre('!token-mod --set represents|rook controlledby|bob') + ), + + _h.paragraph( 'Remove the represent setting for the token and then give bob control of that token (useful for one-offs from npcs or monsters):'), + _h.inset( + _h.pre('!token-mod --set represents| controlledby|bob') + ) + ) + ), + + setDefaultToken: ( /*context*/ ) => _h.join( + _h.subhead('DefaultToken'), + _h.inset( + _h.paragraph(`You can set the default token by specifying defaulttoken in your set list.`), + _h.minorhead('Available DefaultToken Properties:'), + _h.inset( + _h.grid( + _h.cell('defaulttoken') + ) + ), + _h.paragraph('There is no argument for defaulttoken, and this relies on the token representing a character.'), + _h.inset( + _h.pre('!token-mod --set defaulttoken') + ), + _h.paragraph('Setting defaulttoken along with represents works as expected:'), + _h.inset( + _h.pre(`!token-mod --set represents|${_h.attr.char('character_id','Bob')} defaulttoken`) + ), + _h.paragraph(`Be sure that defaulttoken is after all changes to the token you want to store are made. For example, if you set the defaulttoken, then set the bar links, the bars won${ch("'")}t be linked when you pull out the token.`) + ) + ), + + sets: ( context ) => _h.join( + // SECTION: --set, etc + _h.section('Set Arguments', + _h.paragraph(`${_h.italic('--set')} takes key-value pairs, separated by ${_h.code('|')} characters (or ${_h.code('#')} characters).`), + _h.inset( + _h.pre('!token-mod --set key|value key|value key|value') + ), + _h.paragraph(`You can use inline rolls wherever you like, including rollable tables:`), + _h.inset( + _h.pre(`!token-mod --set bar${ch('[')}${ch('[')}1d3${ch(']')}${ch(']')}_value|X statusmarkers|blue:${ch('[')}${ch('[')}1d9${ch(']')}${ch(']')}|green:${ch('[')}${ch('[')}1d9${ch(']')}${ch(']')} name:${ch('"')}${ch('[')}${ch('[')}1t${ch('[')}randomName${ch(']')}${ch(']')}${ch(']')}"`) + ), + + _h.paragraph(`You can use ${_h.code('+')} or ${_h.code('-')} before any number to make an adjustment to the current value:`), + _h.inset( + _h.pre('!token-mod --set bar1_value|-3 statusmarkers|blue:+1|green:-1') + ), + + _h.paragraph(`You can preface a ${_h.code('+')} or ${_h.code('-')} with an ${_h.code('=')} to explicitly set the number to a negative or positive value:`), + _h.inset( + _h.pre('!token-mod --set bar1_value|=+3 light_radius|=-10') + ), + + _h.paragraph('There are several types of keys with special value formats:'), + _h.inset( + helpParts.setNumbers(context), + helpParts.setPercentage(context), + helpParts.setNumbersOrBlank(context), + helpParts.setDegrees(context), + helpParts.setCircleSegment(context), + helpParts.setColors(context), + helpParts.setText(context), + helpParts.setNightVisionEffect(context), + helpParts.setBarLocation(context), + helpParts.setCompactBar(context), + helpParts.setLayer(context), + helpParts.setStatus(context), + helpParts.setImage(context), + helpParts.setSideNumber(context), + helpParts.setCharacterID(context), + helpParts.setAttributeName(context), + helpParts.setDefaultToken(context), + helpParts.setPlayer(context) + ) + ) + ), + + reports: (/* context */) => _h.join( + // SECTION: --report + _h.section('Report', + _h.paragraph(`${_h.experimental()} ${_h.italic('--report')} provides feedback about the changes that were made to each token that a command affects. Arguments to the ${_h.italic('--report')} command are ${_h.code('|')} separated pairs of Who to tell, and what to tell them, with the following format:`), + _h.inset( + _h.pre(`!token-mod --report Who[:Who ...]|Message`) + ), + _h.paragraph(`You can specify multiple different Who arguments by separating them with a ${_h.code(':')}. Be sure you have no spaces.`), + _h.minorhead('Available options for Who'), + _h.inset( + _h.ul( + `${_h.code('player')} will whisper the report to the player who issued the command.`, + `${_h.code('gm')} will whisper the report to the gm.`, + `${_h.code('all')} will send the report publicly to chat for everyone to see.`, + `${_h.code('token')} will whisper to whomever controls the token.`, + `${_h.code('character')} will whisper to whomever controls the character the token represents.`, + `${_h.code('control')} will whisper to whomever can control the token from either the token or character controlledby list. This is equivalent to specifying ${_h.code('token:character')}.` + ) + ), + _h.paragraph(`The Message must be enclosed in quotes if it has spaces in it. The Message can contain any of the properties of the of the token, enclosed in ${_h.code('{ }')}, and they will be replaced with the final value of that property. Additionally, each property may have a modifier to select slightly different information:`), + _h.minorhead('Available options for Property Modifiers'), + _h.inset( + _h.ul( + `${_h.code('before')} -- Show the value of the property before a change was applied.`, + `${_h.code('change')} -- Show the change that was applied to the property. (Only works on numeric fields, will result in 0 on things like name or imagsrc.)`, + `${_h.code('abschange')} -- Show the absolute value of the change that was applied to the property. (Only works on numeric fields, will result in 0 on things like name or imagsrc.)` + ) + ), + _h.paragraph(`Showing the amount of damage done to a token.`), + _h.inset( + _h.preformatted( + '!token-mod {{', + ' --set', + ` bar1_value|-${ch('[')}${ch('[')}2d6+8${ch(']')}${ch(']')}`, + ' --report', + ' all|"{name} takes {bar1_value:abschange} points of damage."', + '}}' + ) + ), + _h.paragraph(`Showing everyone the results of the hit, but only the gm and the controlling players the actual damage and original hit point value.`), + _h.inset( + _h.preformatted( + '!token-mod {{', + ' --set', + ` bar1_value|-${ch('[')}${ch('[')}2d6+8${ch(']')}${ch(']')}`, + ' --report', + ' all|"{name} takes a vicious wound leaving them at {bar1_value}hp out of {bar1_max}hp."', + ' gm:control|"{name} damage: {bar1_value:change}hp, was at {bar1_value:before}hp"', + '}}' + ) + ) + ) + ), + config: (context) => _h.join( + // SECTION: --config, etc + _h.section('Configuration', + _h.paragraph(`${_h.italic('--config')} takes option value pairs, separated by | characters.`), + _h.inset( + _h.pre( '!token-mod --config option|value option|value') + ), + _h.minorhead('Available Configuration Properties:'), + _h.ul( + `${_h.bold('players-can-ids')} -- Determines if players can use --ids. Specifying a value which is true allows players to use --ids. Omitting a value flips the current setting. ` + ), + ( playerIsGM(context.playerid) + ? _h.paragraph(getConfigOption_PlayersCanIDs()) + : '' + ) + ) + ), + + apiInterface: (/* context */) => _h.join( + // SECTION: .ObserveTokenChange(), etc + _h.section('API Notifications', + _h.paragraph( 'API Scripts can register for the following notifications:'), + _h.inset( + _h.paragraph( `${_h.bold('Token Changes')} -- Register your function by passing it to ${_h.code('TokenMod.ObserveTokenChange(yourFuncObject);')}. When TokenMod changes a token, it will call your function with the Token as the first argument and the previous properties as the second argument, identical to an ${_h.code("on('change:graphic',yourFuncObject);")} call.`), + _h.paragraph( `Example script that notifies when a token${ch("'")}s status markers are changed by TokenMod:`), + _h.inset( + _h.preformatted( + `on('ready',function(){`, + ` if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){`, + ` TokenMod.ObserveTokenChange(function(obj,prev){`, + ` if(obj.get('statusmarkers') !== prev.statusmarkers){`, + ` sendChat('Observer Token Change','Token: '+obj.get('name')+' has changed status markers!');`, + ` }`, + ` });`, + ` }`, + `});` + ) + ) + ) + ) + ), + + helpBody: (context) => _h.join( + _h.header( + _h.paragraph( 'TokenMod provides an interface to setting almost all settable properties of a token.') + ), + helpParts.commands(context), + helpParts.booleans(context), + helpParts.sets(context), + helpParts.move(context), + helpParts.reports(context), + helpParts.config(context), + helpParts.apiInterface(context) + ), + + helpDoc: (context) => _h.join( + _h.title('TokenMod',version), + helpParts.helpBody(context) + ), + + helpChat: (context) => _h.outer( + _h.title('TokenMod',version), + helpParts.helpBody(context) + ), + + helpStatusMarkers: (context) => _h.outer( + _h.title('TokenMod',version), + helpParts.availableStatusMarkers(context) + ), + + rebuiltHelp: (/*context*/) => _h.outer( + _h.title('TokenMod',version), + _h.header( + _h.paragraph( `${_h.helpHandoutLink()} regenerated.`) + ) + ) + }; + + + const showHelp = function(playerid) { + let who=(getObj('player',playerid)||{get:()=>'API'}).get('_displayname'); + let context = { + who, + playerid + }; + sendChat('', '/w "'+who+'" '+ helpParts.helpChat(context)); + }; + + + const getRelativeChange = function(current,update) { + let cnum,unum,op='='; + if(_.isString(update)){ + if( _.has(update,0) && ('=' === update[0]) ){ + return parseFloat(_.rest(update).join('')); + } + if( _.has(update,0) && ('!' === update[0]) ){ + if(''===current || 0===parseInt(current) ){ + return parseFloat(_.rest(update).join('')); + } else { + return ''; + } + } + + if(update.match(/^[+\-/*]/)){ // */ + op=update[0]; + update=_.rest(update).join(''); + } + } + + cnum = parseFloat(current); + unum = parseFloat(update); + + if(!_.isNaN(unum) && !_.isUndefined(unum) ) { + if(!_.isNaN(cnum) && !_.isUndefined(cnum) ) { + switch(op) { + case '+': + return cnum+unum; + case '-': + return cnum-unum; + case '*': + return cnum*unum; + case '/': + return cnum/(unum||1); + + default: + return unum; + } + } else { + return unum; + } + } + return update; + }; + + const parseArguments = function(a) { + let args = a.replace(/(\|#|##)/g,'|%%HASHMARK%%').split(/[|#]/).map((v)=>v.replace('%%HASHMARK%%','#')); + let cmd = unalias(args.shift().toLowerCase()); + let retr={}; + let t; + let t2; + + if(_.has(fields,cmd)) { + retr[cmd]=[]; + switch(fields[cmd].type) { + case 'boolean': { + let v = args.shift().toLowerCase(); + if(filters.isTruthyArgument(v)){ + retr[cmd].push(true); + } else if (probBool.hasOwnProperty(v)){ + retr[cmd].push(probBool[v]()); + } else { + retr[cmd].push(false); + } + } + break; + + case 'text': + retr[cmd].push(args.shift().replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')); + break; + + case 'option': { + let o = option_fields[cmd]; + let ks = Object.keys(o); + let arg = args.shift().toLowerCase(); + if(0 === arg.length || !ks.includes(arg)) { + arg='__default__'; + } + retr[cmd].push(o[arg](args.shift())); + } + break; + + + case 'numberBlank': + retr[cmd].push(numberOp.parse(cmd,args.shift())); + break; + + case 'number': + retr[cmd].push(numberOp.parse(cmd,args.shift(),false)); + break; + + case 'percentage': + retr[cmd].push(numberOp.parse(cmd,transforms.percentage(args.shift()),false)); + break; + + case 'degrees': + if( '=' === args[0][0] ) { + t='='; + args[0]=args[0].slice(1); + } else { + t=''; + } + retr[cmd].push(t+(['-','+'].includes(args[0][0]) ? args[0][0] : '') + Math.abs(transforms.degrees(args.shift()))); + break; + + case 'circleSegment': + if( '=' === args[0][0] ) { + t='='; + args[0]=_.rest(args[0]); + } else { + t=''; + } + retr[cmd].push(t+(_.contains(['-','+'],args[0][0]) ? args[0][0] : '') + transforms.circleSegment(args.shift())); + break; + + case 'layer': + retr[cmd].push((args.shift().match(regex.layers)||[]).shift()); + if(0 === (retr[cmd][0]||'').length) { + retr = undefined; + } + break; + + case 'defaulttoken': // blank + retr[cmd].push(''); + break; + + case 'sideNumber': { + let c = sideNumberOp.parseSideNumber(args.shift()); + if(c){ + retr[cmd].push(c); + } else { + retr = undefined; + } + } + break; + + case 'image': { + let c = imageOp.parseImage(args.shift()); + if(c){ + retr[cmd].push(c); + } else { + retr = undefined; + } + } + break; + + case 'color': { + let c = ColorOp.parseColor(args.shift()); + if(c){ + retr[cmd].push(c); + } else { + retr = undefined; + } + } + break; + + case 'character_id': + if('' === args[0]){ + retr[cmd].push(''); + } else { + t=getObj('character', args[0]); + if(t) { + retr[cmd].push(args[0]); + } else { + // try to find a character with this name + t2=findObjs({type: 'character',archived: false}); + t=_.chain([ args[0].replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1') ]) + .map(function(n){ + let l=_.filter(t2,function(c){ + return c.get('name').toLowerCase() === n.toLowerCase(); + }); + return ( 1 === l.length ? l : _.filter(t2,function(c){ + return -1 !== c.get('name').toLowerCase().indexOf(n.toLowerCase()); + })); + }) + .flatten() + .value(); + if(1 === t.length) { + retr[cmd].push(t[0].id); + } else { + retr=undefined; + } + } + } + break; + + case 'attribute': + retr[cmd].push(args.shift().replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')); + break; + + case 'player': + _.each(args, function(p){ + let parts = p.match(/^([+-=]?)(.*)$/), + pids = (parts ? getPlayerIDs(parts[2].replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')):[]); + if(pids.length){ + _.each(pids,(pid)=>{ + retr[cmd].push({ + pid: pid, + operation: parts[1] || '=' + }); + parts[1]='+'; + }); + } else if(_.contains(['','='],p)){ + retr[cmd].push({ + pid:'', + operation:'=' + }); + } + }); + break; + + case 'status': + _.each(args, function(a) { + retr[cmd].push(statusOp.parse(a)); + }); + break; + + default: + retr=undefined; + break; + } + } + + return retr; + }; + + const expandMetaArguments = function(memo,a) { + let args=a.split(/[|#]/), + cmd=args.shift(); + switch(cmd) { + case 'bar1': + case 'bar2': + case 'bar3': + args=args.join('|'); + memo.push(cmd+'_value|'+args); + memo.push(cmd+'_max|'+args); + break; + case 'scale': + args.join('|'); + memo.push(`width|${args}`); + memo.push(`height|${args}`); + break; + default: + memo.push(a); + break; + } + return memo; + }; + + const parseOrderArguments = function(list,base) { + return _.chain(list) + .map(transforms.orderType) + .reject(_.isUndefined) + .union(base) + .value(); + }; + + const parseSetArguments = function(list,base) { + return _.chain(list) + .filter(filters.hasArgument) + .reduce(expandMetaArguments,[]) + .map(parseArguments) + .reject(_.isUndefined) + .reduce(function(memo,i){ + _.each(i,function(v,k){ + switch(k){ + case 'statusmarkers': + if(_.has(memo,k)) { + memo[k]=_.union(v,memo[k]); + } else { + memo[k]=v; + } + break; + default: + memo[k]=v; + break; + } + }); + return memo; + },base) + .value(); + }; + + const parseMoveArguments = (list,base) => + list + .reduce((m,a)=>{ + let args=a.replace(/(\|#|##)/g,'|%%HASHMARK%%').split(/[|#]/).map((v)=>v.replace('%%HASHMARK%%','#')); + m.push(moveOp.parse(args)); + return m; + },base) + ; + + const parseReportArguments = (list,base) => + list + .filter(filters.hasArgument) + .reduce((m,a)=>{ + let args=a.replace(/(\|#|##)/g,'|%%HASHMARK%%').split(/[|#]/).map((v)=>v.replace('%%HASHMARK%%','#')); + let whose=args.shift().toLowerCase().split(/[:;]/); + let msg = args.shift(); + if(/^(".*")|('.*')$/.test(msg)){ + msg=msg.slice(1,-1); + } + whose = whose.filter((w)=>reportTypes.includes(w)); + if(whose.length){ + m.push({who:whose,msg}); + } + return m; + },base) + ; + + const doSetWithWorkerOnLinkedBars = (token, mods) => { + [1,2,3].forEach(n=>{ + if(mods.hasOwnProperty(`bar${n}_value`) || mods.hasOwnProperty(`bar${n}_max`)){ + let a = getObj('attribute',token.get(`bar${n}_link`)); + if(a) { + let ops = {}; + if(mods.hasOwnProperty(`bar${n}_value`)){ + ops[`current`]=mods[`bar${n}_value`]; + delete mods[`bar${n}_value`]; + } + if(mods.hasOwnProperty(`bar${n}_max`)){ + ops[`max`]=mods[`bar${n}_max`]; + delete mods[`bar${n}_max`]; + } + if(Object.keys(ops).length){ + a.setWithWorker(ops); + } + } + } + }); + + return mods; + }; + + const applyModListToToken = function(modlist, token) { + let ctx={ + token: token, + prev: JSON.parse(JSON.stringify(token)) + }, + mods={ + statusmarkers: token.get('statusmarkers') + }, + delta, + cid, + repChar, + controlList = (modlist.set && (modlist.set.controlledby || modlist.set.defaulttoken)) ? (function(){ + let list; + repChar = getObj('character', modlist.set.represents || token.get('represents')); + + list = (repChar ? repChar.get('controlledby') : token.get('controlledby')); + return (list ? list.split(/,/) : []); + }()) : []; + + _.each(modlist.order,function(f){ + switch(f){ + case 'tofront': + toFront(token); + break; + + case 'toback': + toBack(token); + break; + } + }); + _.each(modlist.on,function(f){ + mods[f]=true; + }); + _.each(modlist.off,function(f){ + mods[f]=false; + }); + _.each(modlist.flip,function(f){ + mods[f]=!token.get(f); + }); + _.each(modlist.set,function(f,k){ + switch(k) { + case 'controlledby': + _.each(f, function(cb){ + switch(cb.operation){ + case '=': controlList=[cb.pid]; break; + case '+': controlList=_.union(controlList,[cb.pid]); break; + case '-': controlList=_.without(controlList,cb.pid); break; + } + }); + if(repChar){ + repChar.set('controlledby',controlList.join(',')); + } else { + mods[k]=controlList.join(','); + } + forceLightUpdateOnPage(token.get('pageid')); + break; + + case 'defaulttoken': + if(repChar){ + token.set(mods); + setDefaultTokenForCharacter(repChar,token); + } + break; + + case 'statusmarkers': + _.each(f, function (sm){ + mods.statusmarkers = sm.getMods(mods.statusmarkers).statusmarkers; + }); + break; + + case 'represents': + mods[k]=f[0]; + mods.bar1_link=''; + mods.bar2_link=''; + mods.bar3_link=''; + break; + + case 'bar1_link': + case 'bar2_link': + case 'bar3_link': + if( '' === f[0] ) { + mods[k]=''; + } else { + cid=mods.represents || token.get('represents') || ''; + if('' !== cid) { + delta=findObjs({type: 'attribute', characterid: cid, name: f[0]}, {caseInsensitive: true})[0]; + if(delta) { + mods[k]=delta.id; + mods[k.split(/_/)[0]+'_value']=delta.get('current'); + mods[k.split(/_/)[0]+'_max']=delta.get('max'); + } else { + let c = getObj('character',cid); + if(c) { + if(IsComputedAttr.IsComputed(c,f[0])){ + if(IsComputedAttr.Assignable(f[0])){ + mods[k]=f[0]; + } + } else { + mods[k]=`sheetattr_${f[0]}`; + } + } + } + } + } + break; + + + case 'dim_light_opacity': + mods = Object.assign( mods, f[0].getMods(token,mods)); + break; + + case 'left': + case 'top': + case 'width': + case 'height': + mods = Object.assign( mods, f[0].getMods(token,mods)); + break; + + case 'rotation': + case 'limit_field_of_vision_center': + case 'limit_field_of_night_vision_center': + case 'directional_bright_light_center': + case 'directional_dim_light_center': + delta=getRelativeChange(token.get(k),f[0]); + if(_.isNumber(delta)) { + mods[k]=(((delta%360)+360)%360); + } + break; + + case 'light_angle': + case 'light_losangle': + case 'limit_field_of_vision_total': + case 'limit_field_of_night_vision_total': + case 'directional_bright_light_total': + case 'directional_dim_light_total': + delta=getRelativeChange(token.get(k),f[0]); + if(_.isNumber(delta)) { + mods[k] = Math.min(360,Math.max(0,delta)); + } + break; + + case 'light_radius': + case 'light_dimradius': + case 'light_multiplier': + case 'light_sensitivity_multiplier': + case 'aura2_radius': + case 'aura1_radius': + case 'adv_fow_view_distance': + case 'night_vision_distance': + case 'bright_light_distance': + case 'low_light_distance': + case 'night_distance': + case 'bright_distance': + case 'low_distance': + mods = Object.assign( mods, f[0].getMods(token,mods)); + break; + + + case 'bar1_reset': + case 'bar2_reset': + case 'bar3_reset': { + let field = k.replace(/_reset$/,'_max'); + delta = mods[field] || token.get(field); + if(!_.isUndefined(delta)) { + mods[k.replace(/_reset$/,'_value')]=delta; + } + } + break; + + + case 'bar1_value': + case 'bar2_value': + case 'bar3_value': + if(regex.numberString.test(f[0])){ + delta=getRelativeChange(token.get(k),f[0]); + if(_.isNumber(delta) || _.isString(delta)) { + if(/!$/.test(f[0])) { + delta = Math.max(0,Math.min(delta,token.get(k.replace(/_value$/,'_max')))); + } + let link = token.get(k.replace(/_value$/,'_link')); + if(IsComputedAttr.Check(link)) { + if(!IsComputedAttr.Readonly(link)){ + setComputed({characterId:token.get('represents'),property:link,args:[delta]}); + mods[k]=delta; + } + } else { + mods[k]=delta; + } + } + } else { + mods[k]=f[0]; + } + break; + + case 'bar1_max': + case 'bar2_max': + case 'bar3_max': + if(regex.numberString.test(f[0])){ + delta=getRelativeChange(token.get(k),f[0]); + if(_.isNumber(delta) || _.isString(delta)) { + let link = `${token.get(k.replace(/_max$/,'_link'))}_max`; + if(IsComputedAttr.Check(link)) { + if(!IsComputedAttr.Readonly(link)){ + setComputed({characterId:token.get('represents'),property:link,args:[delta]}); + mods[k]=delta; + } + } else { + mods[k]=delta; + } + } + } else { + mods[k]=f[0]; + } + break; + case 'name': + if(regex.numberString.test(f[0])){ + delta=getRelativeChange(token.get(k),f[0]); + if(_.isNumber(delta) || _.isString(delta)) { + mods[k]=delta; + } + } else { + mods[k]=f[0]; + } + break; + + case 'currentSide': + case 'currentside': + mods = Object.assign( mods, f[0].getMods(token,mods)); + break; + case 'imgsrc': + mods = Object.assign( mods, f[0].getMods(token,mods)); + break; + + case 'aura1_color': + case 'aura2_color': + case 'tint_color': + case 'night_vision_tint': + case 'lightColor': + mods[k]=f[0].applyTo(token.get(k)).toHTML(); + break; + + case 'night_vision_effect': + mods[k]=f[0](token,mods); + break; + +/* + case 'light_sensitivity_multiplier': + // {type: 'number'}, + break; + + // 'None', 'Dimming', 'Nocturnal' + break; + case 'bar_location': + // null, 'overlap_top', 'overlap_bottom', 'below' + break; + + case 'compact_bar': + // null, 'compact' + break; +*/ + + default: + mods[k]=f[0]; + break; + } + }); + + // move ops + _.each(modlist.move,function(f){ + mods = Object.assign(mods, f.getMods(token,mods)); + }); + + mods = doSetWithWorkerOnLinkedBars(token,mods); + + token.set(mods); + notifyObservers('tokenChange',token,ctx.prev); + return ctx; + }; + + const getWho = (()=> { + let cache={}; + return (ids) => { + let names = []; + ids.forEach(id=>{ + if(cache.hasOwnProperty(id)){ + names.push(cache[id]); + } else { + if('all'===id){ + cache.all = 'all'; + names.push('all'); + } else { + let p = findObjs({ type: 'player', id})[0]; + if(p){ + cache[id]=p.get('displayname'); + names.push(cache[id]); + } + } + } + }); + if(names.includes('all')){ + return ['all']; + } + if(0===names.length){ + return ['gm']; + } + return names; + }; + })(); + + const doReports = (ctx,reports,callerWho) => { + const transforms = { + identity: a=>a, + addOne: a=>a+1 + }; + + const getTransform = (p) => { + switch(p){ + case 'currentSide': return transforms.addOne; + default: return transforms.identity; + } + }; + + + const getChange = (()=> { + const charName = (cid) => (getObj('character',cid)||{get:()=>'[Missing]'}).get('name'); + const attrName = (aid) => (/^sheetattr_/.test(aid) ? aid.replace(/^sheetattr_/,'') : (getObj('attribute',aid)||{get:()=>'[Missing]'}).get('name')); + const playerName = (pid) => (getObj('player',pid)||{get:()=>pid}).get('_displayname'); + const nameList = (pl) => pl.split(/\s*,\s*/).filter(s=>s.length).map(playerName).join(', '); + const boolName = (b) => (b ? 'true' : 'false'); + + const diffNum = (was,is) => is-was; + const showDiff = (was,is) => `${was} -> ${is}`; + const funcs = { + boolean: (was,is) => showDiff(boolName(was),boolName(is)), + number: diffNum, + degrees: diffNum, + circleSegment: diffNum, + numberBlank: diffNum, + sideNumber: diffNum, + text: (was,is) => showDiff(was,is), + status: (was,is) => showDiff(was,is), + layer: (was,is) => showDiff(was,is), + character_id: (was,is) => showDiff(charName(was),charName(is)), + attribute: (was,is) => showDiff(attrName(was),attrName(is)), + player: (was,is) => showDiff(nameList(was),nameList(is)), + defaulttoken: (was,is) => showDiff(HE(was),HE(is)) + }; + + return (type,was,is) => (funcs.hasOwnProperty(type) ? funcs[type] : ()=>'[not supported]')(was,is); + + })(); + + reports.forEach( r =>{ + let pmsg = r.msg.replace(/\{(.+?)\}/g, (m,n)=>{ + let parts=n.toLowerCase().split(/[:;]/); + let prop=unalias(parts[0]); + let t = getTransform(prop); + + let mod=parts[1]; + + switch(mod){ + case 'before': + return t(ctx.prev[prop]); + + case 'abschange': + return t(Math.abs((parseFloat(ctx.token.get(prop))||0) - (parseFloat(ctx.prev[prop]||0)))); + + case 'change': + return t(getChange((fields[prop]||{type:'unknown'}).type,ctx.prev[prop],ctx.token.get(prop))); + + default: + return t(ctx.token.get(prop)); + } + }); + + let whoList = r.who.reduce((m, w)=>{ + switch(w){ + case 'gm': + return [...new Set([...m,'gm'])]; + + case 'player': + return [...new Set([...m,callerWho])]; + + case 'all': + return [...new Set([...m,'all'])]; + + case 'token': + return [...new Set([...m, ...getWho(ctx.token.get('controlledby').split(/,/))])]; + + case 'character': { + let c = getObj('character',ctx.token.get('represents')) || {get:()=>''}; + return [...new Set([...m, ...getWho(c.get('controlledby').split(/,/))])]; + } + + case 'control': { + let c = getObj('character',ctx.token.get('represents')) || {get:()=>''}; + return [...new Set([ + ...m, + ...getWho(ctx.token.get('controlledby').split(/,/)), + ...getWho(c.get('controlledby').split(/,/)) + ])]; + } + } + }, []); + + if(whoList.includes('all')){ + sendChat('',`${pmsg}`); + } else { + whoList.forEach(w=>sendChat('',`/w "${w}" ${pmsg}`)); + } + }); + }; + + const handleConfig = function(config, id) { + let args, cmd, who=(getObj('player',id)||{get:()=>'API'}).get('_displayname'); + + if(config.length) { + while(config.length) { + args=config.shift().split(/[|#]/); + cmd=args.shift(); + switch(cmd) { + case 'players-can-ids': + if(args.length) { + state.TokenMod.playersCanUse_ids = filters.isTruthyArgument(args.shift()); + } else { + state.TokenMod.playersCanUse_ids = !state.TokenMod.playersCanUse_ids; + } + sendChat('','/w "'+who+'" '+ + '
    '+ + getConfigOption_PlayersCanIDs()+ + '
    ' + ); + break; + default: + sendChat('', '/w "'+who+'" '+ + '
    '+ + 'Error: '+ + 'No configuration setting for ['+cmd+']'+ + '
    ' + ); + break; + } + } + } else { + sendChat('','/w "'+who+'" '+ + '
    '+ + '
    '+ + 'TokenMod v'+version+ + '
    '+ + getConfigOption_PlayersCanIDs()+ + '
    ' + ); + } + }; + + + + +const OutputDebugInfo = (msg,ids /*, modlist, badCmds */) => { + let selMap = (msg.selected||[]).map(o=>o._id); + let who=(getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); + let fMsg = HE(msg.content.replace(//g,'')).replace(/ /g,' ').replace(/\$/g,'$'); + let fIds = ids.map((o)=>{ + if(undefined !== o.token){ + return `${_h.bold('Token:')} ${o.token.get('name')} [${_h.code(o.token.id)}]${selMap.includes(o.token.id)?` ${_h.bold('Selected')}`:''}`; + } else if(undefined !== o.character){ + return `${_h.bold('Character:')} ${o.character.get('name')} [${_h.code(o.character.id)}]`; + } + return `${_h.bold('Unknown:')} [${_h.code(o.id)}]`; + }); + + sendChat('TokenMod: Debug',`/w "${who}" &{template:default}{{Command=${_h.pre(fMsg)}}}{{Targets=${_h.ul(...fIds)}}}`); + + //$d({msg:msg.content,fMsg,modlist,badCmds}); + }; + + + const processInlinerolls = (msg) => { + if(msg.hasOwnProperty('inlinerolls')){ + return msg.inlinerolls + .reduce((m,v,k) => { + let ti=v.results.rolls.reduce((m2,v2) => { + if(v2.hasOwnProperty('table')){ + m2.push(v2.results.reduce((m3,v3) => [...m3,(v3.tableItem||{}).name],[]).join(", ")); + } + return m2; + },[]).join(', '); + return [...m,{k:`$[[${k}]]`, v:(ti.length && ti) || v.results.total || 0}]; + },[]) + .reduce((m,o) => m.replaceAll(o.k,o.v), msg.content); + } else { + return msg.content; + } + }; + +// */ + const handleInput = function(msg_orig) { + try { + if (msg_orig.type !== "api" || !/^!token-mod(\b\s|$)/.test(msg_orig.content)) { + return; + } + + let msg = _.clone(msg_orig); + let who=(getObj('player',msg_orig.playerid)||{get:()=>'API'}).get('_displayname'); + let playerid = msg.playerid; + let args; + let cmds; + let ids=[]; + let ignoreSelected = false; + let pageRestriction=[]; + let modlist={ + flip: [], + on: [], + off: [], + set: {}, + move: [], + order: [] + }; + let reports=[]; + + msg.content = processInlinerolls(msg); + + args = msg.content + .replace(/\n/g, ' ') + .replace(/(\{\{(.*?)\}\})/g," $2 ") + .split(/\s+--/); + + let IsDebugRequest = false; + let Debug_UnrecognizedCommands = []; + + + while(args.length) { + cmds=args.shift().match(/([^\s]+[|#]'[^']+'|[^\s]+[|#]"[^"]+"|[^\s]+)/g); + let cmd = cmds.shift(); + switch(cmd) { + case 'help-statusmarkers': { + let context = { + who, + playerid:msg.playerid + }; + sendChat('', '/w "'+who+'" '+ helpParts.helpStatusMarkers(context)); + } + return; + + case 'rebuild-help': { + assureHelpHandout(true); + let context = { + who, + playerid:msg.playerid + }; + + sendChat('', `/w "${who}" ${helpParts.rebuiltHelp(context)}`); + + } + return; + + case 'help': + +// !tokenmod --help [all] +// just the top part and ToC + +// !tokenmod --help +// just the top part and ToC + +// !tokenmod --help[-only] [set|on|off|flip|config] +// top part, plus the command parts +// -only leaves off top part + +// !tokenmod --help[-only] [ +// explains the parts command + + + showHelp(playerid); + return; + + case 'api-as': + if('API' === playerid){ + let player = getObj('player',cmds[0]); + if(player){ + playerid = player.id; + who = player.get('_displayname'); + } + } + break; + + case 'debug': { + IsDebugRequest = true; + } + break; + + case 'config': + if(playerIsGM(playerid)) { + handleConfig(cmds,playerid); + } + return; + + + case 'flip': + modlist.flip=_.union(_.filter(cmds.map(unalias),filters.isBoolean),modlist.flip); + break; + + case 'on': + modlist.on=_.union(_.filter(cmds.map(unalias),filters.isBoolean),modlist.on); + break; + + case 'off': + modlist.off=_.union(_.filter(cmds.map(unalias),filters.isBoolean),modlist.off); + break; + + case 'set': + modlist.set=parseSetArguments(cmds,modlist.set); + break; + + case 'order': + modlist.order=parseOrderArguments(cmds,modlist.order); + break; + + case 'report': + reports= parseReportArguments(cmds,reports); + break; + + case 'move': + modlist.move = parseMoveArguments(cmds,modlist.move); + break; + + case 'ignore-selected': + ignoreSelected=true; + break; + + case 'active-pages': + pageRestriction=getActivePages(); + break; + + case 'current-page': + pageRestriction=[getPageForPlayer(playerid)]; + break; + + case 'ids': + ids=_.union(cmds,ids); + break; + + default: + Debug_UnrecognizedCommands.push({cmd,args:cmds}); + break; + } + } + modlist.off=_.difference(modlist.off,modlist.on); + modlist.flip=_.difference(modlist.flip,modlist.on,modlist.off); + if( !playerIsGM(playerid) && !state.TokenMod.playersCanUse_ids ) { + ids=[]; + } + + if(!ignoreSelected) { + ids=_.union(ids,_.pluck(msg.selected,'_id')); + } + + let pageFilter = pageRestriction.length + ? (o) => pageRestriction.includes(o.get('pageid')) + : () => true; + + ids = [...new Set([...ids])] + .map(function(t){ + return { + id: t, + token: getObj('graphic',t), + character: getObj('character',t) + }; + }); + + if(IsDebugRequest){ + OutputDebugInfo(msg_orig,ids,modlist,Debug_UnrecognizedCommands); + } + + if(ids.length){ + [...new Set(ids.reduce(function(m,o){ + if(o.token){ + m.push(o.token); + } else if(o.character){ + m=_.union(m,findObjs({type:'graphic',represents:o.character.id})); + } + return m; + },[]))] + .filter(o=>undefined !== o) + .filter(pageFilter) + .forEach((t) => { + let ctx = applyModListToToken(modlist,t); + doReports(ctx,reports,who); + }); + } + } catch (e) { + let who=(getObj('player',msg_orig.playerid)||{get:()=>'API'}).get('_displayname'); + sendChat('TokenMod',`/w "${who}" `+ + `
    `+ + `
    There was an error while trying to run your command:
    `+ + `
    ${msg_orig.content}
    `+ + `
    Please send me this information so I can make sure this doesn't happen again (triple click for easy select in most browsers.):
    `+ + `
    `+ + JSON.stringify({msg: msg_orig, version:version, stack: e.stack, API_Meta})+ + `
    `+ + `
    ` + ); + } + + }; + + const registerEventHandlers = function() { + on('chat:message', handleInput); + on('change:campaign:_token_markers',()=>StatusMarkers.init()); + }; + + on("ready",() => { + checkInstall(); + registerEventHandlers(); + }); + + return { + ObserveTokenChange: observeTokenChange + }; +})(); + +{try{throw new Error('');}catch(e){API_Meta.TokenMod.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.TokenMod.offset);}} diff --git a/TokenMod/TokenMod.js b/TokenMod/TokenMod.js index daa0ef5cda..24bf6cd94f 100644 --- a/TokenMod/TokenMod.js +++ b/TokenMod/TokenMod.js @@ -8,9 +8,9 @@ API_Meta.TokenMod={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; const TokenMod = (() => { // eslint-disable-line no-unused-vars const scriptName = "TokenMod"; - const version = '0.8.80'; + const version = '0.8.81'; API_Meta.TokenMod.version = version; - const lastUpdate = 1735875678; + const lastUpdate = 1736302467; const schemaVersion = 0.4; const fields = { @@ -3532,7 +3532,7 @@ const TokenMod = (() => { // eslint-disable-line no-unused-vars let c = getObj('character',cid); if(c) { if(IsComputedAttr.IsComputed(c,f[0])){ - if(IsComputedAttr.IsAssignable(f[0])){ + if(IsComputedAttr.Assignable(f[0])){ mods[k]=f[0]; } } else { diff --git a/TokenMod/script.json b/TokenMod/script.json index 9e127b8995..8a09e0404c 100644 --- a/TokenMod/script.json +++ b/TokenMod/script.json @@ -1,7 +1,7 @@ { "name": "TokenMod", "script": "TokenMod.js", - "version": "0.8.80", + "version": "0.8.81", "description": "TokenMod provides an interface to setting almost all settable properties of a token.\r\rFor instructions see the *Help: TokenMod* Handout in game, or run `!token-mod --help` in game, or visit [TokenMod Forum Thread](https://app.roll20.net/forum/post/4225825/script-update-tokenmod-an-interface-to-adjusting-properties-of-a-token-from-a-macro-or-the-chat-area).", "authors": "The Aaron", "roll20userid": "104025", @@ -73,6 +73,7 @@ "0.8.76", "0.8.77", "0.8.78", - "0.8.79" + "0.8.79", + "0.8.80" ] } \ No newline at end of file From 7246cfcf7d865887aeecacbe1b68f44491eed0ef Mon Sep 17 00:00:00 2001 From: boli32 Date: Thu, 9 Jan 2025 10:03:48 +0000 Subject: [PATCH 29/42] QuestTracker And CalanderData --- CalenderData/1.0/CalenderData.js | 15822 +++++++++++++++++++++++++++++ CalenderData/script.json | 15 + QuestTracker/1.0/QuestTracker.js | 4896 +++++++++ QuestTracker/README.md | 519 + QuestTracker/script.json | 21 + 5 files changed, 21273 insertions(+) create mode 100644 CalenderData/1.0/CalenderData.js create mode 100644 CalenderData/script.json create mode 100644 QuestTracker/1.0/QuestTracker.js create mode 100644 QuestTracker/README.md create mode 100644 QuestTracker/script.json diff --git a/CalenderData/1.0/CalenderData.js b/CalenderData/1.0/CalenderData.js new file mode 100644 index 0000000000..29c8a404fc --- /dev/null +++ b/CalenderData/1.0/CalenderData.js @@ -0,0 +1,15822 @@ +// Github: https://github.com/boli32/QuestTracker/blob/main/QuestTracker.js +// By: Boli (Steven Wrighton): Professional Software Developer, Enthusiatic D&D Player since 1993. +// Contact: https://app.roll20.net/users/3714078/boli +// Readme https://github.com/boli32/QuestTracker/blob/main/README.md + + +on('ready', () => { + 'use strict'; + if (!state.CalenderData) { + state.CalenderData = {}; + } + state.CalenderData.CALENDARS = { + "gregorian": { + "name": "Gregorian", + "months": [ + { "id": 1, "name": "January", "days": 31 }, + { + "id": 2, + "name": "February", + "days": 28, + "leap": { + "days": 29, + "logic": { + "operation": "and", + "conditions": [ + { "operation": "mod", "operand": 4, "equals": 0 }, + { + "operation": "or", + "conditions": [ + { "operation": "mod", "operand": 100, "equals": 0, "negate": true }, + { "operation": "mod", "operand": 400, "equals": 0 } + ] + } + ] + } + } + }, + { "id": 3, "name": "March", "days": 31 }, + { "id": 4, "name": "April", "days": 30 }, + { "id": 5, "name": "May", "days": 31 }, + { "id": 6, "name": "June", "days": 30 }, + { "id": 7, "name": "July", "days": 31 }, + { "id": 8, "name": "August", "days": 31 }, + { "id": 9, "name": "September", "days": 30 }, + { "id": 10, "name": "October", "days": 31 }, + { "id": 11, "name": "November", "days": 30 }, + { "id": 12, "name": "December", "days": 31 } + ], + "daysOfWeek": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + "defaultDate": "1970-01-01", + "startingWeekday": "Thursday", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "lunarCycle": { + "baselineNewMoon": "1970-01-07", + "cycleLength": 29.53059, + "phases": [ + { "name": "New Moon", "start": 0, "end": 1 }, + { "name": "Waxing Crescent", "start": 1, "end": 7.4 }, + { "name": "First Quarter", "start": 7.4, "end": 14.8 }, + { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, + { "name": "Full Moon", "start": 22.1, "end": 29.5 }, + { "name": "Waning Crescent", "start": 29.5, "end": 29.53059 } + ] + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "New Year's Day", + "2-29": "Leap Day", + "3-20": "Spring Equinox", + "5-1": "May Day", + "6-21": "Summer Solstice", + "9-22": "Autumn Equinox", + "10-31": "Halloween", + "12-21": "Winter Solstice", + "12-25": "Christmas Day", + "11-1": "All Saints' Day", + "2-14": "Valentine's Day" + } + }, + "harptos": { + "name": "Harptos", + "months": [ + { "id": 1, "name": "Hammer", "days": 30 }, + { "id": 2, "name": "Midwinter", "days": 1 }, + { "id": 3, "name": "Alturiak", "days": 30 }, + { "id": 4, "name": "Ches", "days": 30 }, + { "id": 5, "name": "Tarsakh", "days": 30 }, + { "id": 6, "name": "Greengrass", "days": 1 }, + { "id": 7, "name": "Mirtul", "days": 30 }, + { "id": 8, "name": "Kythorn", "days": 30 }, + { "id": 9, "name": "Flamerule", "days": 30 }, + { "id": 10, "name": "Midsummer", "days": 1 }, + { + "id": 11, + "name": "Shieldmeet", + "days": 0, + "leap": { + "days": 1, + "logic": { + "operation": "and", + "conditions": [ + { "operation": "mod", "operand": 4, "equals": 0 } + ] + } + } + }, + { "id": 12, "name": "Eleasis", "days": 30 }, + { "id": 13, "name": "Eleint", "days": 30 }, + { "id": 14, "name": "Highharvestide", "days": 1 }, + { "id": 15, "name": "Marpenoth", "days": 30 }, + { "id": 16, "name": "Uktar", "days": 30 }, + { "id": 17, "name": "Feast of the Moon", "days": 1 }, + { "id": 18, "name": "Nightal", "days": 30 } + ], + "daysOfWeek": ["First Day", "Second Day", "Third Day", "Fourth Day", "Fifth Day", "Sixth Day", "Seventh Day", "Eighth Day", "Ninth Day", "Tenth Day"], + "defaultDate": "1372-01-01", + "startingWeekday": "First Day", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "lunarCycle": { + "baselineNewMoon": "1372-01-01", + "cycleLength": 30.4375, + "phases": [ + { "name": "New Moon", "start": 0, "end": 3.8 }, + { "name": "Young", "start": 3.8, "end": 7.6 }, + { "name": "Waxing Crescent", "start": 7.6, "end": 11.4 }, + { "name": "Waxing Quarter", "start": 11.4, "end": 15.2 }, + { "name": "Waxing Gibbous", "start": 15.2, "end": 19.0 }, + { "name": "Full Moon", "start": 19.0, "end": 22.8 }, + { "name": "Waning Gibbous", "start": 22.8, "end": 26.6 }, + { "name": "Waning Quarter", "start": 26.6, "end": 29.0 }, + { "name": "Waning Crescent", "start": 29.0, "end": 30.4375 } + ] + }, + "climates": { + "Icewind Dale": { + "seasons": ["Long Winter", "Brief Thaw"], + "modifiers": { + "temperature": { "Long Winter": -20, "Brief Thaw": -10 }, + "precipitation": { "Long Winter": -5, "Brief Thaw": 5 }, + "wind": { "Long Winter": 20, "Brief Thaw": 12 }, + "humid": { "Long Winter": 12, "Brief Thaw": 25 }, + "visibility": { "Long Winter": 8, "Brief Thaw": 18 }, + "cloudy": { "Long Winter": 30, "Brief Thaw": 22 } + }, + "seasonStart": { "Long Winter": 1, "Brief Thaw": 7 } + }, + "Moonshae Isles": { + "seasons": ["Wet Winter", "Stormy Spring", "Mild Summer", "Rainy Autumn"], + "modifiers": { + "temperature": { "Wet Winter": -5, "Stormy Spring": -5, "Mild Summer": 5, "Rainy Autumn": 5 }, + "precipitation": { "Wet Winter": 15, "Stormy Spring": 10, "Mild Summer": 5, "Rainy Autumn": 17.5 }, + "wind": { "Wet Winter": 18, "Stormy Spring": 28, "Mild Summer": 12, "Rainy Autumn": 22 }, + "humid": { "Wet Winter": 18, "Stormy Spring": 30, "Mild Summer": 28, "Rainy Autumn": 35 }, + "visibility": { "Wet Winter": 8, "Stormy Spring": 18, "Mild Summer": 25, "Rainy Autumn": 12 }, + "cloudy": { "Wet Winter": 35, "Stormy Spring": 30, "Mild Summer": 15, "Rainy Autumn": 32 } + }, + "seasonStart": { + "Wet Winter": 1, + "Stormy Spring": 5, + "Mild Summer": 9, + "Rainy Autumn": 13 + } + }, + "Waterdeep": { + "seasons": ["Mild Winter", "Breezy Spring", "Warm Summer", "Rainy Autumn"], + "modifiers": { + "temperature": { "Mild Winter": -5, "Breezy Spring": 0, "Warm Summer": 10, "Rainy Autumn": 5 }, + "precipitation": { "Mild Winter": 10, "Breezy Spring": 5, "Warm Summer": -5, "Rainy Autumn": 12.5 }, + "wind": { "Mild Winter": 12, "Breezy Spring": 22, "Warm Summer": 8, "Rainy Autumn": 18 }, + "humid": { "Mild Winter": 18, "Breezy Spring": 28, "Warm Summer": 30, "Rainy Autumn": 35 }, + "visibility": { "Mild Winter": 12, "Breezy Spring": 25, "Warm Summer": 28, "Rainy Autumn": 18 }, + "cloudy": { "Mild Winter": 30, "Breezy Spring": 25, "Warm Summer": 15, "Rainy Autumn": 28 } + }, + "seasonStart": { + "Mild Winter": 1, + "Breezy Spring": 5, + "Warm Summer": 9, + "Rainy Autumn": 13 + } + }, + "Baldur's Gate": { + "seasons": ["Cool Winter", "Rainy Spring", "Humid Summer", "Stormy Autumn"], + "modifiers": { + "temperature": { "Cool Winter": -5, "Rainy Spring": 0, "Humid Summer": 10, "Stormy Autumn": 5 }, + "precipitation": { "Cool Winter": 5, "Rainy Spring": 15, "Humid Summer": 5, "Stormy Autumn": 12.5 }, + "wind": { "Cool Winter": 18, "Rainy Spring": 15, "Humid Summer": 12, "Stormy Autumn": 22 }, + "humid": { "Cool Winter": 18, "Rainy Spring": 28, "Humid Summer": 35, "Stormy Autumn": 30 }, + "visibility": { "Cool Winter": 12, "Rainy Spring": 18, "Humid Summer": 28, "Stormy Autumn": 14 }, + "cloudy": { "Cool Winter": 30, "Rainy Spring": 35, "Humid Summer": 18, "Stormy Autumn": 32 } + }, + "seasonStart": { + "Cool Winter": 1, + "Rainy Spring": 5, + "Humid Summer": 9, + "Stormy Autumn": 13 + } + }, + "Neverwinter": { + "seasons": ["Cold Winter", "Wet Spring", "Warm Summer", "Breezy Autumn"], + "modifiers": { + "temperature": { "Cold Winter": -5, "Wet Spring": 0, "Warm Summer": 7.5, "Breezy Autumn": 5 }, + "precipitation": { "Cold Winter": 5, "Wet Spring": 15, "Warm Summer": 5, "Breezy Autumn": 0 }, + "wind": { "Cold Winter": 18, "Wet Spring": 15, "Warm Summer": 8, "Breezy Autumn": 20 }, + "humid": { "Cold Winter": 15, "Wet Spring": 30, "Warm Summer": 25, "Breezy Autumn": 20 }, + "visibility": { "Cold Winter": 10, "Wet Spring": 22, "Warm Summer": 28, "Breezy Autumn": 18 }, + "cloudy": { "Cold Winter": 30, "Wet Spring": 35, "Warm Summer": 15, "Breezy Autumn": 20 } + }, + "seasonStart": { + "Cold Winter": 1, + "Wet Spring": 5, + "Warm Summer": 9, + "Breezy Autumn": 13 + } + }, + "Haranshire": { + "seasons": ["Harsh Winter", "Blooming Spring", "Hot Summer", "Crisp Autumn"], + "modifiers": { + "temperature": { "Harsh Winter": -10, "Blooming Spring": 5, "Hot Summer": 15, "Crisp Autumn": 0 }, + "precipitation": { "Harsh Winter": 10, "Blooming Spring": 15, "Hot Summer": -10, "Crisp Autumn": 7.5 }, + "wind": { "Harsh Winter": 20, "Blooming Spring": 10, "Hot Summer": 8, "Crisp Autumn": 12 }, + "humid": { "Harsh Winter": 15, "Blooming Spring": 30, "Hot Summer": 25, "Crisp Autumn": 20 }, + "visibility": { "Harsh Winter": 10, "Blooming Spring": 18, "Hot Summer": 28, "Crisp Autumn": 15 }, + "cloudy": { "Harsh Winter": 30, "Blooming Spring": 25, "Hot Summer": 10, "Crisp Autumn": 20 } + }, + "seasonStart": { + "Harsh Winter": 1, + "Blooming Spring": 5, + "Hot Summer": 9, + "Crisp Autumn": 13 + } + }, + "Spine of the World": { + "seasons": ["Perpetual Winter", "Short Thaw"], + "modifiers": { + "temperature": { "Perpetual Winter": -20, "Short Thaw": -5 }, + "precipitation": { "Perpetual Winter": -10, "Short Thaw": 10 }, + "wind": { "Perpetual Winter": 20, "Short Thaw": 15 }, + "humid": { "Perpetual Winter": 15, "Short Thaw": 30 }, + "visibility": { "Perpetual Winter": 8, "Short Thaw": 18 }, + "cloudy": { "Perpetual Winter": 35, "Short Thaw": 20 } + }, + "seasonStart": { + "Perpetual Winter": 1, + "Short Thaw": 9 + } + }, + "Dales Region": { + "seasons": ["Harsh Winter", "Blooming Spring", "Hot Summer", "Rainy Autumn"], + "modifiers": { + "temperature": { "Harsh Winter": -15, "Blooming Spring": 2, "Hot Summer": 10, "Rainy Autumn": 5 }, + "precipitation": { "Harsh Winter": 10, "Blooming Spring": 15, "Hot Summer": -10, "Rainy Autumn": 20 }, + "wind": { "Harsh Winter": 10, "Blooming Spring": 20, "Hot Summer": 8, "Rainy Autumn": 12 }, + "humid": { "Harsh Winter": 25, "Blooming Spring": 30, "Hot Summer": 15, "Rainy Autumn": 35 }, + "visibility": { "Harsh Winter": 8, "Blooming Spring": 18, "Hot Summer": 25, "Rainy Autumn": 12 }, + "cloudy": { "Harsh Winter": 30, "Blooming Spring": 25, "Hot Summer": 10, "Rainy Autumn": 35 } + }, + "seasonStart": { + "Harsh Winter": 1, + "Blooming Spring": 5, + "Hot Summer": 9, + "Rainy Autumn": 13 + } + }, + "Candlekeep": { + "seasons": ["Mild Winter", "Windy Spring", "Warm Summer", "Foggy Autumn"], + "modifiers": { + "temperature": { "Mild Winter": -10, "Windy Spring": 5, "Warm Summer": 12, "Foggy Autumn": 3 }, + "precipitation": { "Mild Winter": 15, "Windy Spring": 10, "Warm Summer": -10, "Foggy Autumn": 0 }, + "wind": { "Mild Winter": 10, "Windy Spring": 25, "Warm Summer": 8, "Foggy Autumn": 20 }, + "humid": { "Mild Winter": 30, "Windy Spring": 25, "Warm Summer": 15, "Foggy Autumn": 35 }, + "visibility": { "Mild Winter": 12, "Windy Spring": 20, "Warm Summer": 25, "Foggy Autumn": 8 }, + "cloudy": { "Mild Winter": 35, "Windy Spring": 20, "Warm Summer": 15, "Foggy Autumn": 40 } + }, + "seasonStart": { + "Mild Winter": 1, + "Windy Spring": 5, + "Warm Summer": 9, + "Foggy Autumn": 13 + } + }, + "Phandalin": { + "seasons": ["Chilly Winter", "Blooming Spring", "Warm Summer", "Cool Autumn"], + "modifiers": { + "temperature": { "Chilly Winter": -10, "Blooming Spring": 5, "Warm Summer": 10, "Cool Autumn": 3 }, + "precipitation": { "Chilly Winter": 10, "Blooming Spring": 15, "Warm Summer": -10, "Cool Autumn": -5 }, + "wind": { "Chilly Winter": 15, "Blooming Spring": 10, "Warm Summer": 5, "Cool Autumn": 10 }, + "humid": { "Chilly Winter": 25, "Blooming Spring": 35, "Warm Summer": 30, "Cool Autumn": 20 }, + "visibility": { "Chilly Winter": 10, "Blooming Spring": 20, "Warm Summer": 25, "Cool Autumn": 15 }, + "cloudy": { "Chilly Winter": 35, "Blooming Spring": 30, "Warm Summer": 15, "Cool Autumn": 25 } + }, + "seasonStart": { + "Chilly Winter": 1, + "Blooming Spring": 5, + "Warm Summer": 9, + "Cool Autumn": 13 + } + }, + "Chult": { + "seasons": ["Wet Season", "Dry Season"], + "modifiers": { + "temperature": { "Wet Season": 5, "Dry Season": 10 }, + "precipitation": { "Wet Season": 25, "Dry Season": -15 }, + "wind": { "Wet Season": 10, "Dry Season": 5 }, + "humid": { "Wet Season": 75, "Dry Season": 50 }, + "visibility": { "Wet Season": 10, "Dry Season": 20 }, + "cloudy": { "Wet Season": 50, "Dry Season": 15 } + }, + "seasonStart": { + "Wet Season": 5, + "Dry Season": 13 + } + }, + "Thay": { + "seasons": ["Cold Winter", "Blooming Spring", "Hot Summer", "Dry Autumn"], + "modifiers": { + "temperature": { "Cold Winter": -10, "Blooming Spring": 5, "Hot Summer": 15, "Dry Autumn": 0 }, + "precipitation": { "Cold Winter": 5, "Blooming Spring": 10, "Hot Summer": -5, "Dry Autumn": -5 }, + "wind": { "Cold Winter": 10, "Blooming Spring": 5, "Hot Summer": -5, "Dry Autumn": 10 }, + "humid": { "Cold Winter": -5, "Blooming Spring": 10, "Hot Summer": 15, "Dry Autumn": 5 }, + "visibility": { "Cold Winter": -10, "Blooming Spring": 5, "Hot Summer": 15, "Dry Autumn": 10 }, + "cloudy": { "Cold Winter": 15, "Blooming Spring": 10, "Hot Summer": -5, "Dry Autumn": 5 } + }, + "seasonStart": { + "Cold Winter": 1, + "Blooming Spring": 5, + "Hot Summer": 9, + "Dry Autumn": 13 + } + }, + "Stormwreck Isle": { + "seasons": ["Blustery Winter", "Tempestuous Spring", "Humid Summer", "Stormy Autumn"], + "modifiers": { + "temperature": { "Blustery Winter": -10, "Tempestuous Spring": 5, "Humid Summer": 10, "Stormy Autumn": -5 }, + "precipitation": { "Blustery Winter": 12.5, "Tempestuous Spring": 15, "Humid Summer": 5, "Stormy Autumn": 17.5 }, + "wind": { "Blustery Winter": -8, "Tempestuous Spring": 12, "Humid Summer": 5, "Stormy Autumn": 8 }, + "humid": { "Blustery Winter": -10, "Tempestuous Spring": 8, "Humid Summer": 15, "Stormy Autumn": 5 }, + "visibility": { "Blustery Winter": -15, "Tempestuous Spring": 8, "Humid Summer": 10, "Stormy Autumn": 5 }, + "cloudy": { "Blustery Winter": -8, "Tempestuous Spring": 15, "Humid Summer": 5, "Stormy Autumn": 8 } + }, + "seasonStart": { + "Blustery Winter": 1, + "Tempestuous Spring": 4, + "Humid Summer": 7, + "Stormy Autumn": 10 + } + } + }, + "significantDays": { + "2-1": "Midwinter", + "6-1": "Greengrass", + "10-1": "Midsummer", + "11-1": "Shieldmeet", + "14-1": "Highharvestide", + "17-1": "The Feast of the Moon", + "4-19": "Spring Equinox", + "13-22": "Autumn Equinox", + "8-20": "Summer Solstice", + "18-20": "Winter Solstice" + } + }, + "barovian": { + "name": "Barovian", + "months": [ + { "id": 1, "name": "Dekavr", "days": 28 }, + { "id": 2, "name": "Yinyavr", "days": 28 }, + { "id": 3, "name": "Fenravr", "days": 28 }, + { "id": 4, "name": "Martavr", "days": 28 }, + { "id": 5, "name": "Prylla", "days": 28 }, + { "id": 6, "name": "Mada", "days": 28 }, + { "id": 7, "name": "Eyun", "days": 28 }, + { "id": 8, "name": "Eyul", "days": 28 }, + { "id": 9, "name": "Ugavr", "days": 28 }, + { "id": 10, "name": "Sintavr", "days": 28 }, + { "id": 11, "name": "Ottyavr", "days": 28 }, + { "id": 12, "name": "Neyavr", "days": 28 } + ], + "daysOfWeek": ["Vasárnap", "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat"], + "defaultDate": "735-01-01", + "startingWeekday": "Vasárnap", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "lunarCycle": { + "baselineNewMoon": "735-01-15", + "cycleLength": 28, + "phases": [ + { "name": "Full Moon", "start": 0, "end": 1 }, + { "name": "Waning Gibbous", "start": 2, "end": 7 }, + { "name": "Left Half", "start": 8, "end": 8 }, + { "name": "Waning Crescent", "start": 9, "end": 14 }, + { "name": "New Moon", "start": 15, "end": 15 }, + { "name": "Waxing Crescent", "start": 16, "end": 21 }, + { "name": "Right Half", "start": 22, "end": 22 }, + { "name": "Waxing Gibbous", "start": 23, "end": 28 } + ] + }, + "climates": { + "barovian standard": { + "seasons": ["Cold", "Dreary"], + "modifiers": { + "temperature": { "Cold": -10, "Dreary": -5 }, + "precipitation": { "Cold": 10, "Dreary": 5 }, + "wind": { "Cold": 7.5, "Dreary": 12.5 }, + "humid": { "Cold": 12.5, "Dreary": 15 }, + "visibility": { "Cold": -15, "Dreary": -20 }, + "cloudy": { "Cold": 20, "Dreary": 20 } + }, + "seasonStart": { "Cold": 1, "Dreary": 7 } + } + }, + "significantDays": { + "1-1": "Festival of the First Moon", + "6-15": "Midsummer's Vigil", + "11-15": "Night of the Misty Veil", + "12-28": "Day of Mourning", + "3-1": "Vampire's Descent", + "9-7": "Harvest Moon Festival" + } + }, + "golarion": { + "name": "Golarion", + "months": [ + { "id": 1, "name": "Abadius", "days": 31 }, + { "id": 2, "name": "Calistril", "days": "(year) => (year % 8 === 0 ? 29 : 28)" }, + { "id": 3, "name": "Pharast", "days": 31 }, + { "id": 4, "name": "Gozran", "days": 30 }, + { "id": 5, "name": "Desnus", "days": 31 }, + { "id": 6, "name": "Sarenith", "days": 30 }, + { "id": 7, "name": "Erastus", "days": 31 }, + { "id": 8, "name": "Arodus", "days": 31 }, + { "id": 9, "name": "Rova", "days": 30 }, + { "id": 10, "name": "Lamashan", "days": 31 }, + { "id": 11, "name": "Neth", "days": 30 }, + { "id": 12, "name": "Kuthona", "days": 31 } + ], + "daysOfWeek": ["Moonday", "Toilday", "Wealday", "Oathday", "Fireday", "Starday", "Sunday"], + "defaultDate": "4712-01-01", + "startingWeekday": "Moonday", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "lunarCycle": { + "baselineNewMoon": "4712-01-12", + "cycleLength": 29.5, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7.375 }, + { "name": "First Quarter", "start": 7.375, "end": 14.75 }, + { "name": "Full Moon", "start": 14.75, "end": 22.125 }, + { "name": "Last Quarter", "start": 22.125, "end": 29.5 } + ] + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "First Day of the Year", + "6-1": "Day of the Sun", + "9-1": "Harvest Celebration", + "12-21": "Winter Solstice", + "3-20": "Spring Equinox", + "6-21": "Summer Solstice", + "9-22": "Autumn Equinox" + } + }, + "greyhawk": { + "name": "Greyhawk", + "months": [ + { "id": 1, "name": "Needfest", "days": 7 }, + { "id": 2, "name": "Fireseek", "days": 28 }, + { "id": 3, "name": "Readying", "days": 28 }, + { "id": 4, "name": "Coldeven", "days": 28 }, + { "id": 5, "name": "Growfest", "days": 7 }, + { "id": 6, "name": "Planting", "days": 28 }, + { "id": 7, "name": "Flocktime", "days": 28 }, + { "id": 8, "name": "Wealsun", "days": 28 }, + { "id": 9, "name": "Richfest", "days": 7 }, + { "id": 10, "name": "Reaping", "days": 28 }, + { "id": 11, "name": "Goodmonth", "days": 28 }, + { "id": 12, "name": "Harvester", "days": 28 }, + { "id": 13, "name": "Brewfest", "days": 7 }, + { "id": 14, "name": "Patchwall", "days": 28 }, + { "id": 15, "name": "Ready'reat", "days": 28 }, + { "id": 16, "name": "Sunsebb", "days": 28 } + ], + "daysOfWeek": ["Starday", "Sunday", "Moonday", "Godsday", "Waterday", "Earthday", "Freeday"], + "defaultDate": "591-01-01", + "startingWeekday": "Starday", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "lunarCycle": { + "baselineNewMoon": "591-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "First Quarter", "start": 7, "end": 14 }, + { "name": "Full Moon", "start": 14, "end": 21 }, + { "name": "Last Quarter", "start": 21, "end": 28 } + ] + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "Needfest (Start of the Year)", + "2-11": "Midwinter Feast", + "3-4": "Festival of Ready'reat", + "5-1": "Growfest (Planting Begins)", + "7-10": "Festival of Flocktime", + "9-1": "Richfest (High Summer)", + "13-1": "Brewfest (Harvest Celebrations)", + "16-20": "Feast of Sunsebb" + } + }, + "exandria": { + "name": "Exandria", + "months": [ + { "id": 1, "name": "Horisal", "days": 29 }, + { "id": 2, "name": "Misuthar", "days": 30 }, + { "id": 3, "name": "Dualahei", "days": 30 }, + { "id": 4, "name": "Thunsheer", "days": 31 }, + { "id": 5, "name": "Unndilar", "days": 30 }, + { "id": 6, "name": "Brussendar", "days": 31 }, + { "id": 7, "name": "Sydenstar", "days": 30 }, + { "id": 8, "name": "Fessuran", "days": 30 }, + { "id": 9, "name": "Quen'pillar", "days": 31 }, + { "id": 10, "name": "Cuersaar", "days": 29 }, + { "id": 11, "name": "Duscar", "days": 30 } + ], + "daysOfWeek": ["Miresen", "Grissen", "Whelsen", "Conthsen", "Folsen", "Yulisen"], + "defaultDate": "835-01-01", + "lunarCycle": { + "baselineNewMoon": "835-01-01", + "cycleLength": 29.5, + "phases": [ + { "name": "New Moon", "start": 0, "end": 3.6 }, + { "name": "Waxing Crescent", "start": 3.6, "end": 7.4 }, + { "name": "First Quarter", "start": 7.4, "end": 14.8 }, + { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, + { "name": "Full Moon", "start": 22.1, "end": 29.5 } + ] + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "Renewal Dawn", + "2-13": "Reckoning Eve", + "4-15": "Feast of the Dawnfather", + "7-21": "Wildmother's Grace", + "9-5": "Day of the Harvest", + "11-1": "Duscar's End" + } + } + }; + state.CalenderData.WEATHER = { + "weather": { + "Arctic Chill": { + "conditions": { + "temperature": { "lte": 20 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 40 }, + "humidity": { "lte": 30 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "Icy winds sweep across the snow-covered fields, shimmering under the pale sun.", + "2": "The farm is silent under a thick blanket of frost, with the occasional crack of ice breaking the calm.", + "3": "Heavy snow falls relentlessly, obscuring visibility and blanketing the landscape in white.", + "4": "The air is crisp and biting, with sunlight glinting off frozen crops like shards of glass.", + "5": "A light sleet falls, coating every surface with a thin, slippery layer of ice.", + "6": "Bitter winds howl through the fields, stirring loose snow into swirling clouds.", + "7": "The sky is a flat, leaden gray, and the cold seems to seep into every corner of the farm.", + "8": "Soft snow covers the fields, muffling sound and creating a peaceful, frozen stillness.", + "9": "Every tree and fence post is crusted with sparkling frost under a pale, arctic dawn.", + "10": "The wind drives snow in furious waves, making even the simplest tasks a struggle.", + "11": "The sun barely peeks over the horizon, casting a dim, ethereal light on the icy farm.", + "12": "The ground is frozen solid, with cracks in the ice revealing just how deep the cold runs." + }, + "village": { + "1": "Snow blankets the rooftops, smoke curling lazily from chimneys into the frosty air.", + "2": "The streets are eerily quiet, with a biting wind carrying the faint crunch of footsteps.", + "3": "Snow flurries swirl through the narrow lanes, cloaking the village in a soft white haze.", + "4": "Icicles hang like daggers from every eave, glinting faintly under a pale gray sky.", + "5": "A cold drizzle slicks the cobblestones, leaving the village shrouded in icy mist.", + "6": "Villagers huddle near their fires as the wind howls through the frozen streets.", + "7": "The morning dawns bright and clear, the snow sparkling like diamonds across the village.", + "8": "A heavy blizzard reduces visibility, with only the muffled sound of voices carrying through the storm.", + "9": "Frost covers every surface, turning the village into a glimmering, icy wonderland.", + "10": "The sun barely rises, casting long shadows over the snow-drifted streets.", + "11": "A chilling fog rolls in, blurring the edges of buildings and muting all sound.", + "12": "Snow piles high against doors and walls, leaving the village wrapped in an icy embrace." + }, + "city": { + "1": "Snow dusts the towering buildings, the streets bustling with bundled-up figures braving the chill.", + "2": "A cold wind howls between the tall structures, carrying flurries of snow through the crowded avenues.", + "3": "Icicles dangle from lampposts and window ledges, shimmering faintly under the weak winter sun.", + "4": "The city is veiled in a frosty fog, with only the muffled sounds of activity breaking the stillness.", + "5": "Chimneys puff plumes of smoke into the icy sky as the cobblestone streets glisten with frost.", + "6": "Snow piles up along sidewalks, where carts and carriages leave trails through the fresh powder.", + "7": "Bright sunlight reflects off the frozen river winding through the heart of the city.", + "8": "A blizzard sweeps through, forcing citizens to seek shelter from the swirling snow and fierce wind.", + "9": "The city is quiet under a thick blanket of snow, interrupted only by the crunch of boots on icy paths.", + "10": "Torches cast a soft glow on the frosted streets, the chill of the night sinking into every corner.", + "11": "The skyline is barely visible through the drifting snow, the storm muffling the city's usual bustle.", + "12": "Frozen fountains stand as icy monuments, the city wrapped in a serene and bitterly cold silence." + }, + "plains": { + "1": "The vast plains are covered in a glistening sheet of snow, stretching endlessly under a pale sky.", + "2": "Bitter winds sweep across the open expanse, stirring the snow into swirling white drifts.", + "3": "The horizon is obscured by a blizzard, reducing the plains to a blur of white and gray.", + "4": "Frost clings to the sparse grasses poking through the snow, glittering faintly in the weak sunlight.", + "5": "The plains lie silent and still, the snow unbroken save for the occasional trail of footprints.", + "6": "The wind carries a bone-chilling cold, howling across the barren landscape with relentless force.", + "7": "Sunlight reflects blindingly off the snow-covered plains, the glare making the cold feel sharper.", + "8": "A dense fog creeps over the plains, blending sky and ground into a seamless, icy gray expanse.", + "9": "The snow is pocked with icy patches, the frozen ground below making every step precarious.", + "10": "An arctic haze softens the edges of the plains, the bitter chill hanging heavy in the air.", + "11": "Snowdrifts pile high against the few rocks and shrubs dotting the otherwise featureless plains.", + "12": "The open plains are serene under a full moon, the snow glowing faintly in the icy stillness." + }, + "forest": { + "1": "The forest is blanketed in snow, the trees standing silent and frost-laden under a gray sky.", + "2": "A biting wind rustles through the frozen branches, scattering a fine mist of snow to the ground.", + "3": "The forest is eerily quiet, the only sound the soft crunch of snow underfoot.", + "4": "Icicles hang from the branches like nature's chandeliers, glistening faintly in the pale light.", + "5": "A heavy snowfall obscures the forest paths, leaving the woods shrouded in a soft, white silence.", + "6": "Frost coats the bark of the trees, turning the forest into a shimmering, icy wonderland.", + "7": "The forest is alive with the sound of creaking branches as the weight of snow bends them low.", + "8": "A dense fog wraps around the trees, blurring the edges of the forest into a ghostly gray haze.", + "9": "Sunlight filters through the bare branches, casting long, cold shadows across the snowy ground.", + "10": "The air is thick with the smell of pine and frost, the stillness of the forest broken only by the occasional flurry of snow.", + "11": "The forest glows faintly under the moonlight, the snow sparkling like a sea of tiny crystals.", + "12": "A sudden gust shakes the treetops, sending a cascade of snowflakes drifting to the forest floor." + }, + "swamp": { + "1": "The swamp is a frozen mire, with icy patches forming on the surface of stagnant water.", + "2": "Frost clings to the gnarled roots and reeds, the air thick with an icy mist.", + "3": "Snow blankets the swamp, disguising the treacherous ground beneath a deceptive white layer.", + "4": "The bitter cold has frozen parts of the swamp, with icy cracks radiating across shallow pools.", + "5": "The swamp is eerily silent, the occasional groan of shifting ice breaking the stillness.", + "6": "A low, icy fog creeps over the swamp, blending water and land into a ghostly expanse.", + "7": "Bare trees reach up from the frozen mire, their branches coated in frost and ice.", + "8": "Snow gathers in clumps around twisted roots, creating a stark contrast against the dark water below.", + "9": "Icicles dangle from the swamp’s moss-covered branches, glinting faintly in the dim light.", + "10": "A biting wind sweeps over the swamp, rippling the icy surface of the partially frozen pools.", + "11": "The swamp glows faintly in the moonlight, the frost-covered reeds sparkling like tiny stars.", + "12": "The icy swamp crackles underfoot, each step a careful negotiation with the frozen ground." + }, + "jungle": { + "1": "The jungle is eerily quiet, its lush foliage coated in frost and glistening under a pale sky.", + "2": "Icy tendrils cling to the vines, the once-vibrant greenery muted by a layer of frost.", + "3": "A freezing mist hangs heavy in the air, shrouding the jungle in a ghostly white haze.", + "4": "The jungle floor is a treacherous mix of frozen mud and patches of frost-covered vegetation.", + "5": "Icicles dangle from the broad leaves, shimmering faintly in the weak sunlight.", + "6": "The dense jungle canopy sags under the weight of accumulated snow, creating an unusual arctic labyrinth.", + "7": "Frost-covered vines twist around the trunks, the jungle transformed into a surreal icy wilderness.", + "8": "A biting wind whistles through the jungle, scattering frost from the high branches onto the frozen ground.", + "9": "The air is heavy with a mix of cold and damp, each breath forming visible puffs in the icy jungle.", + "10": "The jungle is shrouded in an icy fog, blending the dense undergrowth into a blurry, frozen expanse.", + "11": "Snow gathers in the crevices of the jungle, its pristine white a stark contrast to the dark, frozen flora.", + "12": "The jungle glows faintly in the moonlight, its icy canopy shimmering with an otherworldly beauty." + }, + "hills": { + "1": "The rolling hills are blanketed in snow, their peaks gleaming under a pale winter sun.", + "2": "Icy winds howl across the hills, leaving the grass frozen and brittle beneath the frost.", + "3": "Snow drifts gather in the valleys, creating smooth, unbroken expanses between the frozen slopes.", + "4": "The hills are shrouded in a thin, icy mist, their contours barely visible through the haze.", + "5": "Frost clings to the rocky outcroppings, turning the hills into a frozen, glittering landscape.", + "6": "The frozen ground crunches underfoot, each step revealing patches of frost-covered earth.", + "7": "The hills are stark and silent, the occasional crack of ice breaking the eerie stillness.", + "8": "Snowflakes swirl gently in the air, settling lightly on the already frozen hillsides.", + "9": "The wind cuts sharply through the hills, carrying with it a stinging chill that bites at exposed skin.", + "10": "Frost-covered bushes dot the landscape, their branches weighed down by the accumulating ice.", + "11": "The hills sparkle in the moonlight, the snow and frost creating a silvery, otherworldly glow.", + "12": "A biting gale sweeps across the hills, stirring the snow into fleeting, ghostly whorls." + }, + "mountains": { + "1": "The mountain peaks are cloaked in thick snow, their jagged edges lost in swirling frost.", + "2": "Icy winds roar through the mountain passes, carrying flurries of snow that obscure the view.", + "3": "Sheer cliffs glisten with frost, their surfaces treacherous and coated in a thin layer of ice.", + "4": "The mountains stand silent and foreboding, their slopes blanketed in deep snowdrifts.", + "5": "Frozen waterfalls hang motionless, their icy cascades glinting faintly in the muted light.", + "6": "A heavy fog clings to the mountain ridges, blending the peaks into an endless, frozen expanse.", + "7": "The air is sharp and biting, each breath forming visible clouds in the freezing mountain cold.", + "8": "Icicles jut from rocky overhangs, their crystalline forms catching the weak sunlight.", + "9": "Snow swirls violently around the higher altitudes, creating an almost impenetrable white wall.", + "10": "The mountain trails are buried under snow, their paths rendered invisible by the relentless winter.", + "11": "The peaks loom ominously, their outlines blurred by the constant flurry of snow and ice.", + "12": "The valleys between the mountains are eerily quiet, muffled by the thick layers of snow." + }, + "desert": { + "1": "The frozen dunes shimmer under a weak sun, their icy surfaces glinting faintly.", + "2": "Frigid winds whip across the desert, carving sharp ridges into the frozen sand.", + "3": "The landscape is barren and stark, a sea of frost-covered dunes stretching endlessly.", + "4": "Thin sheets of ice coat the desert floor, cracking underfoot with each step.", + "5": "Snow dusts the frozen sands, blurring the boundary between earth and sky.", + "6": "The desert is silent and still, the icy air biting with an unforgiving chill.", + "7": "The frozen horizon glows faintly as the sun sets, casting long shadows over the icy dunes.", + "8": "Icy gusts sweep across the landscape, driving small drifts of snow along the frozen sand.", + "9": "The frigid expanse is broken only by patches of frost-rimed rock jutting from the sand.", + "10": "The desert feels lifeless, its icy stillness interrupted only by the distant howl of wind.", + "11": "Thin frost patterns lace the surface of the dunes, reflecting faint light in delicate shapes.", + "12": "The air is painfully cold, carrying a biting wind that stings any exposed skin." + }, + "coastal": { + "1": "Icy waves crash against the frozen shore, sending sprays of salt and frost into the air.", + "2": "The coastline is cloaked in snow, with jagged ice formations jutting from the water's edge.", + "3": "The sea is a dark, frozen expanse, its surface shimmering with thin sheets of drifting ice.", + "4": "Bitter winds howl along the coast, carrying a sharp chill and flecks of snow.", + "5": "Frost clings to the rocky shoreline, the ground slippery with a mix of ice and snow.", + "6": "The horizon is shrouded in mist, where the icy ocean meets the overcast sky.", + "7": "Frozen foam caps the waves as they crash against ice-coated boulders near the shore.", + "8": "The air smells faintly of salt and ice, a bitter combination that stings the lungs.", + "9": "Patches of sea ice drift lazily near the shoreline, their edges glittering faintly in the dim light.", + "10": "The coastal wind bites sharply, cutting through even the thickest layers of warmth.", + "11": "Icicles hang from overhanging cliffs, their points dripping slowly in the weak sunlight.", + "12": "The coastline is eerily quiet, muffled by the thick blanket of snow and ice underfoot." + }, + "volcano": { + "1": "Frozen ash dusts the volcanic slopes, mingling with patches of icy snow.", + "2": "Steam rises from fissures in the ice-covered ground, hissing softly in the frigid air.", + "3": "The icy landscape is broken by dark rock formations, stark against the snow.", + "4": "Chill winds carry the faint scent of sulfur across the frozen volcanic terrain.", + "5": "Frost clings to the edges of ancient lava flows, now hardened and buried under snow.", + "6": "Thin ice sheets form over pools of geothermal water, their surfaces steaming faintly.", + "7": "The summit is obscured by swirling snow, a stark contrast to the dark rock below.", + "8": "Frozen ridges of rock and ash line the slopes, etched by the harsh Arctic winds.", + "9": "A thin mist of steam rises from cracks in the earth, quickly dissipating in the cold.", + "10": "The volcanic terrain is jagged and treacherous, blanketed in frost and ice.", + "11": "The icy ground crunches underfoot, revealing black volcanic rock beneath.", + "12": "A faint, eerie glow seeps through the ice in places, hinting at geothermal heat below." + }, + "arctic": { + "1": "Blinding snow stretches endlessly under a pale, muted sky.", + "2": "Frozen tundra crackles underfoot, the air sharp and biting.", + "3": "Icy winds whip across the barren expanse, carrying stinging flecks of snow.", + "4": "The horizon is a blend of white and grey, broken only by jagged ice formations.", + "5": "A faint aurora shimmers overhead, casting an eerie glow on the frozen landscape.", + "6": "Thick snow blankets the ground, muffling all sound in a chilling silence.", + "7": "Frost clings to every surface, glistening like a crystalline sheet in the dim light.", + "8": "The air is deathly still, broken only by the occasional distant crack of shifting ice.", + "9": "Snowdrifts pile high, creating rolling dunes of icy powder.", + "10": "The ground is a mix of permafrost and hardened snow, slick and treacherous.", + "11": "Thin, icy fog hangs low over the terrain, obscuring distant shapes in a pale haze.", + "12": "The cold is oppressive, the kind that seeps into bones and numbs all sensation." + }, + "cursed": { + "1": "Icy winds carry whispers that seem to echo from nowhere, chilling both body and soul.", + "2": "The ground is frozen solid, marked with eerie patterns resembling claw marks in the ice.", + "3": "Snow falls thick and heavy, muffling sound and creating an oppressive stillness.", + "4": "Dark clouds swirl unnaturally overhead, casting flickering shadows across the frozen expanse.", + "5": "A faint, spectral glow pulses from beneath the ice, hinting at something buried deep below.", + "6": "Frozen figures dot the landscape, twisted and contorted as if frozen mid-scream.", + "7": "The air is filled with an unnatural chill, even more biting than the Arctic cold.", + "8": "Faint, ghostly forms appear in the distance, dissolving into mist upon approach.", + "9": "The ground emits a faint cracking sound, as though warning of something lurking beneath.", + "10": "The snow is stained with streaks of dark, icy crimson, stark against the white expanse.", + "11": "A relentless wind howls through the landscape, carrying faint, haunting cries.", + "12": "An eerie silence hangs heavy, broken only by sudden, unexplainable noises in the distance." + } + } + }, + "Blizzard": { + "conditions": { + "temperature": { "lte": 20 }, + "precipitation": { "gte": 60 }, + "wind": { "gte": 60 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 20 } + }, + "descriptions": { + "farm": { + "1": "Snow blankets the fields, whipping violently in the howling winds, obscuring all vision.", + "2": "The farmhouse shutters rattle as icy gusts tear through the farmstead, leaving drifts piled high.", + "3": "Frozen livestock pens creak under the weight of accumulating snow, barely visible through the whiteout.", + "4": "The blizzard reduces the once-vibrant farm to a desolate landscape of ice and swirling frost.", + "5": "Snow swirls chaotically, burying crops and fencing, the farm almost unrecognizable.", + "6": "Winds roar across the frozen farmland, snow flurries turning the landscape into an impenetrable haze.", + "7": "The barn doors struggle against the storm, snow forcing its way through every crack.", + "8": "Icy tendrils creep across the windows of the farmhouse, visibility outside completely gone.", + "9": "Chickens huddle in their coop, their shelter barely holding against the relentless snowstorm.", + "10": "The blizzard howls across the farm, thick snowdrifts threatening to bury equipment and pathways.", + "11": "The bitter cold penetrates the thickest clothing as farmers struggle to secure their animals.", + "12": "Snow piles against the farmhouse walls, the storm's unyielding fury isolating the farm from the outside world." + }, + "village": { + "1": "Snow-laden winds howl through the narrow streets, making it nearly impossible to see the village square.", + "2": "Thatched roofs sag under the weight of heavy snow, and villagers scramble to reinforce them.", + "3": "The relentless storm buries carts and barrels, leaving the village eerily quiet except for the howling wind.", + "4": "Faint lantern light flickers through swirling snow, barely illuminating the storm-shrouded paths.", + "5": "Villagers huddle in their homes as the blizzard batters shutters and doors, icy drafts seeping inside.", + "6": "The church bell tolls faintly through the storm, its sound swallowed by the roaring wind.", + "7": "Footsteps in the snow are quickly erased as the storm's fury consumes the village roads.", + "8": "Icicles form rapidly on eaves and fences, adding a crystalline menace to the frozen landscape.", + "9": "Smoke from chimneys is torn away by the gale, leaving the air filled with icy stillness.", + "10": "A cart overturned in the snow drifts marks a futile attempt to reach the village outskirts.", + "11": "Frost coats every surface as the blizzard's bitter chill seeps through cracks in wooden walls.", + "12": "The village well is buried beneath a mound of snow, its location marked only by a faint outline." + }, + "city": { + "1": "Snow drifts pile high against the city gates, leaving guards struggling to clear the paths.", + "2": "The blizzard blankets the market square, stalls hidden under mounds of snow and ice.", + "3": "Chimney smoke barely rises above the rooftops, swirling away in the icy gusts.", + "4": "Street lamps flicker dimly, their light barely piercing the white chaos enveloping the city.", + "5": "The cacophony of city life is muted, replaced by the relentless howl of the storm.", + "6": "Bridges and cobblestones become treacherous as ice coats every surface underfoot.", + "7": "The city's bell tower stands shrouded in snow, its toll barely audible through the storm's roar.", + "8": "Frozen water spills from the city fountain, turning it into an icy sculpture.", + "9": "Merchants hastily close their shops, securing doors and shutters against the blizzard's fury.", + "10": "City guards wrapped in thick furs struggle to patrol the nearly impassable streets.", + "11": "Abandoned wagons line the streets, their wheels frozen solid in the deep snow.", + "12": "The sound of wind tearing through narrow alleys echoes eerily, adding to the city's desolate feel.", + "13": "Snow piles against city walls, leaving gates partially buried and creaking under the weight.", + "14": "Market stalls collapse under the relentless weight of snow, their goods long abandoned.", + "15": "Icy winds howl through narrow streets, turning alleys into impassable corridors of frost.", + "16": "Lanterns flicker dimly as the blizzard smothers their flames, casting shadows over silent streets.", + "17": "Shutters rattle violently against the storm, many breaking loose to scatter in the wind.", + "18": "Frozen fountains glisten like eerie statues as snow swirls in the storm's unrelenting grip.", + "19": "The grand cathedral’s spire is lost in a swirl of white, its bells muffled by the howling winds.", + "20": "Streets become rivers of snow, carriages abandoned in icy drifts that block passage.", + "21": "City guards huddle under makeshift shelters, their fur-lined cloaks barely holding back the chill.", + "22": "Merchants' signs creak and snap as the storm rages, scattering wood and fabric across the square.", + "23": "The cacophony of the storm drowns out even the loudest cries for aid, isolating the city's districts.", + "24": "Ice forms along the rooftops, sending chunks crashing to the streets below as the blizzard worsens." + }, + "plains": { + "1": "The blizzard sweeps across the endless plains, creating a white expanse with no horizon in sight.", + "2": "Snow drifts pile against sparse hills, forming icy barriers that stretch for miles.", + "3": "The wind howls unhindered, cutting like blades through the open, desolate landscape.", + "4": "Grasslands vanish beneath layers of snow, the tall stalks now brittle and frozen.", + "5": "Wandering livestock are barely visible, their forms shrouded in swirling flurries of snow.", + "6": "Travelers find themselves lost as landmarks vanish in the blinding storm.", + "7": "Thin layers of ice cover streams and ponds, their surfaces hidden beneath the storm’s fury.", + "8": "Tracks in the snow are erased almost instantly as the blizzard presses on relentlessly.", + "9": "The open sky is a pale, swirling gray, indistinguishable from the snow-covered ground below.", + "10": "Sparse trees groan under the weight of ice, their branches cracking loudly in the storm.", + "11": "The plains seem lifeless, all sound swallowed by the overwhelming roar of the blizzard.", + "12": "Snow dunes rise and fall across the plains, shifting with the relentless gusts of wind." + }, + "forest": { + "1": "The dense forest is blanketed in thick snow, branches bowing under the storm's weight.", + "2": "Howling winds rush through the trees, creating a cacophony of creaks and cracks.", + "3": "Snow falls in relentless sheets, obscuring the forest floor and masking trails.", + "4": "Frozen branches snap under the storm’s force, sending shards of ice tumbling to the ground.", + "5": "Visibility is reduced to mere feet as the swirling snow engulfs the forest in a white haze.", + "6": "The storm muffles all sound save for the wind, creating an eerie silence among the trees.", + "7": "Wildlife hides in burrows and nests, the forest seemingly abandoned to the storm's fury.", + "8": "Tree trunks become coated in ice, their surfaces shimmering faintly in fleeting light.", + "9": "Snow drifts build unevenly, burying fallen logs and obscuring landmarks.", + "10": "The forest floor crunches loudly underfoot, layers of ice forming beneath the snow.", + "11": "Icicles dangle from branches, growing longer as freezing winds whip through the canopy.", + "12": "Navigating the forest becomes treacherous as familiar paths are concealed by snow and ice." + }, + "swamp": { + "1": "The swamp is a chaotic mix of snow and slush, with frozen patches forming over the murky waters.", + "2": "Snow piles unevenly atop the gnarled roots and boggy ground, turning the swamp into a treacherous maze.", + "3": "Freezing winds howl through the skeletal trees, carrying flurries of snow and ice shards.", + "4": "Visibility is near zero as snowstorm flurries blend with the swamp’s misty vapors.", + "5": "Icy sludge forms on the surface of stagnant pools, cracking ominously under any weight.", + "6": "The storm freezes dangling moss into icy ropes, swaying in the relentless wind.", + "7": "Frozen reeds crack and snap, unable to withstand the heavy snow and blistering winds.", + "8": "Drifts of snow accumulate in odd patterns, masking treacherous bogs and concealed pools.", + "9": "The ground beneath the snow is slick and unstable, making every step a risk in the frozen swamp.", + "10": "Icy water drips from overhead branches as the storm's weight presses down on the swamp canopy.", + "11": "Wild creatures retreat into hidden dens, leaving the swamp eerily silent except for the howling blizzard.", + "12": "Snow-laden fog hangs heavy over the swamp, turning it into a ghostly, frozen expanse." + }, + "jungle": { + "1": "Snow blankets the dense jungle, weighing down tropical foliage and creating an unnatural, frozen canopy.", + "2": "Icy winds roar through the jungle, snapping branches and freezing the vibrant undergrowth.", + "3": "Thick vines and leaves are coated in frost, shimmering faintly through the relentless blizzard.", + "4": "Snow drifts collect in dense patches, concealing roots and undergrowth in the frozen jungle floor.", + "5": "The blizzard turns the jungle into a surreal winter maze, where icy fog masks the towering trees.", + "6": "Tropical streams and waterfalls freeze mid-flow, leaving jagged ice sculptures in their wake.", + "7": "The once-humid jungle air is replaced with a biting cold, frosting even the hardiest plants.", + "8": "Frost creeps over colorful flowers, transforming them into crystalline, fragile versions of themselves.", + "9": "The canopy overhead offers little protection as snow and ice tumble through the thick branches.", + "10": "Animals retreat deep into their burrows, leaving the frozen jungle eerily silent apart from the storm's howl.", + "11": "Icicles dangle precariously from thick vines and branches, glistening faintly in the dim blizzard light.", + "12": "Snow and ice crush smaller plants under their weight, creating a stark, barren expanse in the jungle." + }, + "hills": { + "1": "Snow swirls violently across the rolling hills, creating deep drifts in the valleys.", + "2": "Howling winds whip through the open landscape, making visibility almost nonexistent.", + "3": "The blizzard buries pathways and landmarks under a thick layer of snow.", + "4": "Frozen streams cut through the hills, their surfaces glazed with ice.", + "5": "Wild animals huddle together for warmth, their tracks quickly erased by the storm.", + "6": "Snow piles high against rocky outcrops, turning the hills into a frozen wasteland.", + "7": "Chilling gusts funnel through the valleys, carrying shards of ice that sting exposed skin.", + "8": "Even the hardy grasses of the hills bow under the relentless weight of snow.", + "9": "Visibility fades as the blizzard's icy embrace blankets the undulating terrain.", + "10": "Small shelters and hilltop ruins are barely visible under layers of frost and snow.", + "11": "Snow drifts crest the hilltops, creating deceptive ridges that collapse underfoot.", + "12": "Frozen ground cracks underfoot as the blizzard rages, consuming the hills in white." + }, + "mountains": { + "1": "Snow cascades down rocky cliffs, creating treacherous avalanches during the storm.", + "2": "Icy winds howl through the mountain peaks, carrying loose snow like a spectral veil.", + "3": "Frost clings to jagged rock faces, turning the mountains into a glacial fortress.", + "4": "Paths and trails vanish under thick layers of freshly fallen snow.", + "5": "Ice builds along the edges of mountain ledges, making every step perilous.", + "6": "The blizzard obscures the peaks entirely, leaving only a roaring white void.", + "7": "Glacial crevasses fill with drifting snow, concealing deadly drops.", + "8": "Even hardy mountain goats retreat to crevices, sheltering from the biting cold.", + "9": "Icicles form rapidly on overhangs, falling like daggers in the raging winds.", + "10": "The storm scours the mountain passes, turning them into lifeless, frozen trails.", + "11": "Snow piles against crags and outcroppings, creating precarious cornices.", + "12": "The air is so cold it burns, frost clinging to any exposed surface." + }, + "desert": { + "1": "The blizzard turns dunes into frozen waves, the sand glazed with frost.", + "2": "Icy winds sweep across the desert, creating a surreal frozen wasteland.", + "3": "Snow gathers in the lee of dunes, creating patches of cold white in the arid expanse.", + "4": "The biting cold hardens the sand, making it crunch underfoot like brittle ice.", + "5": "Frozen mirages shimmer in the distance as snow coats the desert horizon.", + "6": "The blizzard buries cacti and shrubs under layers of snow, their spines frosted over.", + "7": "Snowflakes melt upon contact with the warm desert stones but quickly freeze again.", + "8": "The desert wind carries ice crystals, turning the air into a stinging, freezing torrent.", + "9": "Sandstorms are replaced with snow flurries, disorienting travelers in the frozen expanse.", + "10": "Even the sun’s heat fails to penetrate the icy grip of the blizzard.", + "11": "Snow caps form atop towering rock formations, lending them an alien, wintery aura.", + "12": "The cold desert night intensifies the blizzard, creating deadly subzero conditions." + }, + "coastal": { + "1": "Snow drives inland from the sea, freezing waves as they crash against the shore.", + "2": "Ice forms along the coastline, locking boats in frozen harbors.", + "3": "Gulls struggle to fly as icy winds whip across the frothing surf.", + "4": "The blizzard obscures the horizon, blending the sea and sky into a single white expanse.", + "5": "Frozen spray from the sea coats the shoreline, creating jagged ice sculptures.", + "6": "Drifts of snow collect along dunes and cliffs, burying the coastal landscape.", + "7": "Seaweed and driftwood are locked in icy prisons along the snow-covered beach.", + "8": "Lighthouses struggle to shine through the swirling snow and ice-laden winds.", + "9": "Icicles dangle from fishing nets and rigging, clinking faintly in the storm’s howl.", + "10": "The tides push ice floes onto the shore, where they grind against the rocks.", + "11": "Snow buries coastal paths, turning familiar routes into treacherous mazes.", + "12": "The roar of the sea is muffled under the relentless battering of the blizzard." + }, + "volcano": { + "1": "Snow swirls against the volcanic slopes, melting instantly near the steaming vents.", + "2": "The blizzard blankets the ash-covered ground, creating an eerie mix of black and white.", + "3": "Icy winds clash with geothermal heat, creating a chaotic storm of steam and snow.", + "4": "Lava flows steam as snowflakes land, creating hissing clouds of vapor.", + "5": "The blizzard covers craters with frost, obscuring their dangerous edges.", + "6": "Heat from the volcano melts snow near its vents, leaving slushy, treacherous ground.", + "7": "Snow piles on cooled lava flows, softening the jagged terrain under a deceptive white cover.", + "8": "Steam vents send plumes of mist into the freezing air, creating ghostly shapes in the storm.", + "9": "The blizzard buries volcanic trails, leaving only the faint warmth of the ground as a guide.", + "10": "The stark contrast of snow against blackened rock creates a surreal, otherworldly landscape.", + "11": "Icy winds howl over the volcano, scattering ash and snow in equal measure.", + "12": "The warmth of the volcano provides little comfort against the storm's relentless cold." + }, + "arctic": { + "1": "The blizzard reduces the icy tundra to a featureless expanse of white.", + "2": "Snow piles atop glaciers, the wind carving frozen dunes across their surface.", + "3": "Icy winds cut like blades, driving snow into every crack and crevice.", + "4": "Polar bears and seals retreat to hidden shelters as the storm intensifies.", + "5": "The air is so cold it freezes breath, adding to the frost on exposed faces.", + "6": "Visibility drops to nothing as the storm cloaks the horizon in white.", + "7": "Snow drifts consume ice floes, hiding dangerous crevasses beneath their weight.", + "8": "Frost clings to everything, from towering icebergs to the smallest snow-covered rocks.", + "9": "Auroras flicker faintly through the storm clouds, offering a glimmer of light in the chaos.", + "10": "The biting cold hardens snow into ice, making travel perilous and slow.", + "11": "The blizzard drives icy shards into frozen waterways, thickening their layers of ice.", + "12": "Even the hardy arctic wildlife seems subdued as the relentless blizzard rages on." + }, + "cursed": { + "1": "The blizzard howls with ghostly whispers, the snow carrying unnatural shadows.", + "2": "Frost spreads in jagged, eerie patterns, as though guided by an unseen force.", + "3": "Snowflakes fall slowly, glowing faintly as they land with an unsettling hiss.", + "4": "The storm's winds wail like mourning spirits, chilling to the bone.", + "5": "Icy tendrils creep across the ground, twisting around anything in their path.", + "6": "Drifts of snow shift unnaturally, as though moved by unseen hands.", + "7": "The blizzard's cold feels alive, sapping warmth with an unnatural malice.", + "8": "Figures appear and vanish in the swirling snow, their presence fleeting yet unnerving.", + "9": "Frosted trees crackle ominously, their branches snapping as though under unseen strain.", + "10": "Snow falls in rhythmic pulses, almost as if matching the beat of an ethereal heart.", + "11": "Shadows linger longer in the storm, casting doubt on what is real and what is illusion.", + "12": "The air hums faintly, the storm carrying a sinister, otherworldly resonance." + } + } + }, + "Choking Heat": { + "conditions": { + "temperature": { "gte": 80 }, + "precipitation": { "lte": 20 }, + "wind": { "lte": 20 }, + "humidity": { "gte": 70 }, + "cloudCover": { "lte": 40 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Crops wilt under the relentless heat, and livestock shelter in the sparse shade.", + "2": "The air shimmers above the fields, with the sun baking the ground into cracked patterns.", + "3": "Farmhands move sluggishly, wiping sweat from their brows as the heat saps their energy.", + "4": "Dried-up water troughs force animals to gather near the remaining dwindling well.", + "5": "The once-green pastures are now golden and brittle, crunching underfoot.", + "6": "A thick haze of dust rises from the dry earth with every step, making the air harder to breathe.", + "7": "Farmers pray for rain, their faces drawn and weary from the oppressive heat.", + "8": "The sun’s harsh rays reflect off metal tools, making them too hot to touch.", + "9": "Small animals retreat to burrows, leaving the fields eerily quiet except for cicadas.", + "10": "The intense heat causes distant mirages, making the landscape appear wavy and distorted.", + "11": "Buckets of water carried to the crops evaporate almost as fast as they’re poured.", + "12": "The stifling heat presses down on everything, making even the simplest tasks exhausting." + }, + "village": { + "1": "Villagers cluster under awnings and trees, seeking respite from the merciless sun.", + "2": "The well’s water level drops precariously, with buckets raised covered in sweat and worry.", + "3": "Children play listlessly in the shade, their usual energy drained by the choking heat.", + "4": "Merchants cover their goods with cloth to prevent them from spoiling under the blazing sun.", + "5": "The village square is deserted, as most hide indoors or under the shade of their porches.", + "6": "Chickens pant in their coops, their wings held away from their bodies to cool down.", + "7": "The smell of baking bread mingles with the overwhelming scent of hot, dry earth.", + "8": "Villagers fan themselves with whatever they can find, their movements slow and labored.", + "9": "The relentless sun turns cobbled streets into baking stones underfoot.", + "10": "Beads of sweat drip down the faces of blacksmiths as their forges amplify the heat.", + "11": "Distant fields shimmer in the heat, their shapes warped and dancing like phantoms.", + "12": "The bell tower’s shadow provides the only cool refuge in the center of the village." + }, + "city": { + "1": "Streets are choked with the smell of heated stone and the sweat of its citizens.", + "2": "Shops hang damp cloth over doorways, hoping to cool the air as customers pass through.", + "3": "City fountains draw crowds of overheated citizens, children splashing wildly in the cool water.", + "4": "Horses pulling carts slow to a trudge, their coats lathered with sweat under the sun.", + "5": "The markets seem quieter, as many stall owners retreat to shaded alleys or close early.", + "6": "The air in crowded taverns is thick and oppressive, offering little relief from the heat outside.", + "7": "Guards atop the city walls squint against the bright sun, their armor burning to the touch.", + "8": "Buckets of water are dashed onto streets in an effort to keep the dust and heat at bay.", + "9": "Citizens move with sluggish precision, their energy sapped by the sweltering weather.", + "10": "The smell of overheated waste and stagnant water makes some streets unbearable to walk.", + "11": "Clerics tend to fainting citizens in shaded corners of the main plaza.", + "12": "The unrelenting heat makes time seem to slow, as the city groans under its weight." + }, + "plains": { + "1": "The sun blazes down on the open plains, the grass drying into a brittle, golden carpet.", + "2": "Winds carry shimmering waves of heat across the endless expanse of flat land.", + "3": "Animals huddle beneath lone trees or rocks, their tongues lolling in exhaustion.", + "4": "The horizon wavers with mirages, making distant objects appear to float above the ground.", + "5": "A haze of dust rises with every step, clinging to sweat-drenched skin.", + "6": "The once-vivid green of the plains has turned into dull yellows and browns.", + "7": "Streams and ponds have dried to cracked earth, leaving animals to search desperately for water.", + "8": "Every breath feels labored, as the heat seems to sap even the air’s vitality.", + "9": "Birds circle high above, avoiding the stifling heat closer to the ground.", + "10": "The heat distorts the view, making it hard to tell where the land ends and the sky begins.", + "11": "Insects buzz relentlessly, their hum the only sound in the oppressive stillness.", + "12": "The plains seem abandoned, save for a lone traveler trudging under the glaring sun." + }, + "forest": { + "1": "The canopy traps the heat, making the forest air thick and suffocating.", + "2": "Sweat drips as the humidity mixes with the overpowering warmth of the choking heat.", + "3": "Leaves droop on their branches, the forest seeming to wilt under the relentless sun.", + "4": "Streams slow to a trickle, their beds cracked and parched in the oppressive weather.", + "5": "Animals stay hidden, their usual calls replaced by the heavy silence of the heat.", + "6": "Patches of sunlight filter through the trees, their brightness almost blinding.", + "7": "The undergrowth crackles with dryness, each step stirring up motes of hot dust.", + "8": "Even in the shade, the temperature offers little relief, and the air feels oppressive.", + "9": "Birds perch in silence, their wings tucked close to avoid the energy drain of flying.", + "10": "The dense vegetation holds the heat, turning the forest into a humid furnace.", + "11": "A faint breeze stirs, but it only carries the warmth deeper into the woods.", + "12": "The forest floor is dry and cracked, the usual dampness nowhere to be found." + }, + "swamp": { + "1": "The swamp steams under the choking heat, the air thick with moisture and warmth.", + "2": "Mud bubbles lazily in the oppressive stillness, the heat clinging to every surface.", + "3": "Insects swarm relentlessly, thriving in the swamp’s suffocating warmth.", + "4": "The water stagnates under the sun, the smell of decay rising with the heat.", + "5": "Alligators lie motionless on the banks, their jaws agape in an effort to cool down.", + "6": "Shadows of trees provide little comfort, the heat and humidity overwhelming the senses.", + "7": "Every step through the muck releases a wave of warm, fetid air.", + "8": "The swamp feels alive, its oppressive heat amplifying every sound and scent.", + "9": "Frogs croak faintly, their voices dulled by the smothering heat.", + "10": "The sun glares off the water’s surface, turning it into a mirror of burning light.", + "11": "Vines sag under the weight of the damp heat, the air thick with their musky scent.", + "12": "Even the swamp’s usual breeze is gone, leaving a stifling, unmoving haze." + }, + "jungle": { + "1": "The jungle simmers under the choking heat, the air humid and heavy with moisture.", + "2": "Vibrant foliage wilts slightly, the sun’s intensity even taxing the dense greenery.", + "3": "Birds call out weakly, their usual vitality sapped by the oppressive warmth.", + "4": "Every surface feels sticky with humidity, as if the jungle itself is sweating.", + "5": "The dense canopy traps the heat, turning the jungle floor into a suffocating oven.", + "6": "Streams struggle to flow, their edges drying into cracked mud under the relentless sun.", + "7": "Insects thrive, their constant buzzing filling the sweltering air.", + "8": "The smell of damp earth and decaying plants intensifies in the choking heat.", + "9": "Animals move sluggishly, their movements deliberate and energy-saving.", + "10": "Even the shadows feel warm, the heat seeping into every crevice of the jungle.", + "11": "Leaves gleam with moisture, their surfaces sweating under the blazing sun.", + "12": "The jungle hums with the sound of life struggling against the overwhelming warmth." + }, + "hills": { + "1": "The hills radiate heat, their grassy slopes turned golden and brittle.", + "2": "Travelers stop to rest in the rare shade of rocky outcroppings.", + "3": "A haze of heat shimmers over the rolling landscape, warping distant views.", + "4": "Shepherds retreat with their flocks to shaded valleys to escape the oppressive sun.", + "5": "Even the wind feels warm, offering no relief as it sweeps across the hills.", + "6": "Streams run low, their trickling waters barely sustaining the parched land.", + "7": "The usually vibrant hills feel eerily quiet, save for the buzzing of insects.", + "8": "Exhausted animals huddle under scattered trees, panting heavily in the stifling air.", + "9": "The ground cracks underfoot, dry and dusty from days of unrelenting heat.", + "10": "Faint mirages ripple across the hills, distorting the edges of the horizon.", + "11": "Wildflowers wither, their once-bright colors fading under the brutal sun.", + "12": "The air is thick with the smell of sun-baked earth and drying grass." + }, + "mountains": { + "1": "The thin mountain air feels heavy with heat, making every step exhausting.", + "2": "Snowcaps melt quickly, feeding streams that rush down parched valleys below.", + "3": "Rocks radiate stored heat, their surfaces scorching to the touch.", + "4": "Mountain trails are deserted, the choking heat making travel dangerous.", + "5": "The sun beats down fiercely, leaving no respite even in the highest peaks.", + "6": "Wildlife retreats to caves and shaded cliffs, avoiding the oppressive warmth.", + "7": "The usual crisp mountain breezes are replaced by dry, sweltering gusts.", + "8": "Thin streams of water evaporate before reaching the valleys below.", + "9": "The horizon shimmers with heat, obscuring the distant peaks in a hazy blur.", + "10": "Plants cling stubbornly to the rocky slopes, their leaves curling from the heat.", + "11": "Birds of prey circle high above, their shadows gliding silently over the parched stone.", + "12": "The scent of hot pine sap lingers in the air, carried by occasional dry winds." + }, + "desert": { + "1": "The desert sand burns underfoot, glowing like molten gold under the blazing sun.", + "2": "Cacti and hardy shrubs seem to wilt in the overwhelming heat.", + "3": "Mirages ripple across the horizon, creating illusions of water and shelter.", + "4": "Travelers wrap their faces in cloth to shield against the searing air and sun.", + "5": "The relentless sun turns the desert into a furnace, its heat choking and stifling.", + "6": "Scorpions and snakes retreat deep into their burrows, avoiding the surface heat.", + "7": "The air shimmers with heat, and the dunes seem to dance in the distance.", + "8": "The occasional gust of wind carries a fine, hot grit that stings the skin.", + "9": "Water skins are emptied faster than planned, the heat draining energy rapidly.", + "10": "The sun dominates the sky, leaving no room for shade or respite.", + "11": "The barren landscape seems to hum with the intensity of the sun’s glare.", + "12": "Even the hardy desert plants appear scorched, their edges brittle and dry." + }, + "coastal": { + "1": "The usually cool sea breeze blows warm, carrying the heat of the day inland.", + "2": "The sand on the shore is too hot to walk on, shimmering under the glaring sun.", + "3": "Seabirds pant in the heat, their usual calls muffled by the choking warmth.", + "4": "The water feels warm to the touch, offering little relief from the oppressive heat.", + "5": "The salt air is heavy and stifling, amplifying the heat’s oppressive weight.", + "6": "Fishing boats stay docked, the heat too intense for crews to work safely.", + "7": "Distant waves sparkle blindingly, the sun reflecting off the water's surface.", + "8": "Villagers gather in shaded spots, fanning themselves against the relentless warmth.", + "9": "Seaweed on the shore bakes in the sun, filling the air with a sharp, pungent odor.", + "10": "Palm trees sag slightly, their fronds wilting under the harsh sun.", + "11": "The horizon wavers with heat, blending sea and sky into a hazy mirage.", + "12": "Shadows are short and sharp, offering little escape from the sun’s fierce rays." + }, + "volcano": { + "1": "The air is stifling, the choking heat magnified by the volcano’s constant warmth.", + "2": "Rocks radiate heat, their surfaces hot enough to sear exposed skin.", + "3": "Steam vents hiss from cracks in the earth, adding to the oppressive atmosphere.", + "4": "Ash particles hang in the air, making each breath feel thick and labored.", + "5": "The ground feels warm underfoot, as if the volcano's pulse beats just below the surface.", + "6": "Lava flows faintly glow in the distance, their heat adding to the stifling environment.", + "7": "The sun beats down mercilessly, adding its fire to the volcano’s smoldering heat.", + "8": "The sulfurous scent of brimstone fills the air, clinging to clothes and skin.", + "9": "Shimmering heat waves rise from the blackened rocks, distorting the surrounding view.", + "10": "The faint rumble of the volcano adds an ominous tension to the oppressive warmth.", + "11": "Even hardy plants near the volcano appear scorched and brittle.", + "12": "The heat feels alive, a heavy, suffocating force pressing down on everything." + }, + "arctic": { + "1": "The snow melts into slushy puddles under the unusual and oppressive heat.", + "2": "Ice cracks and groans, releasing chillingly warm air into the frozen expanse.", + "3": "The sun beats down relentlessly, reflecting blindingly off the remaining ice fields.", + "4": "Polar bears and seals retreat to what shade they can find, panting heavily.", + "5": "Glaciers shed chunks of ice, their surfaces melting faster than usual.", + "6": "The usually biting arctic winds carry an unfamiliar warmth, melting frost as they pass.", + "7": "The horizon wavers in heat shimmers, a strange sight in the frozen north.", + "8": "Snowfields darken as the top layer turns to slush under the scorching sun.", + "9": "The oppressive heat leaves even the hardy arctic fauna lethargic and still.", + "10": "Snowstorms turn into warm, damp mists as the snowflakes melt midair.", + "11": "The icy tundra seems alien, its usual biting cold replaced by choking warmth.", + "12": "Frozen rivers crack open, their icy surfaces giving way to flowing water." + }, + "cursed": { + "1": "The choking heat feels unnatural, as if the air itself is cursed to suffocate.", + "2": "Shadows twist and warp in the oppressive warmth, casting eerie shapes on the ground.", + "3": "The ground beneath burns unnaturally, the heat seemingly radiating from nowhere.", + "4": "A heavy, sulfurous scent lingers in the air, choking breath and burning eyes.", + "5": "The sunlight seems darker, its heat somehow more sinister and oppressive.", + "6": "Plants blacken and wither, their decay spreading unnaturally fast in the heat.", + "7": "Sweat evaporates instantly, leaving skin dry and cracked under the relentless warmth.", + "8": "The heat seems alive, pressing down with a malevolent force that defies reason.", + "9": "Streams run red with heat-borne silt, their surfaces steaming under the cursed sun.", + "10": "The air hums faintly, a sinister vibration that only adds to the choking heat.", + "11": "Even magical wards falter under the oppressive warmth, their energy sapped unnaturally.", + "12": "The horizon blurs with waves of heat, hiding the cursed landscape’s true form." + } + } + }, + "Clear Skies with Heatwaves": { + "conditions": { + "temperature": { "gte": 70 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 30 }, + "humidity": { "lte": 40 }, + "cloudCover": { "lte": 30 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "The crops wilt under the relentless sun, their leaves curling in the heat.", + "2": "Farmers pause work to wipe sweat from their brows, seeking shelter under sparse trees.", + "3": "The ground is cracked and dry, with dust swirling in the lightest breeze.", + "4": "Livestock crowd around water troughs, panting in the scorching heat.", + "5": "The air shimmers with heat, making distant fields appear as if underwater.", + "6": "The once-lush greenery looks pale and brittle, thirsting for rain.", + "7": "Even the wind offers no relief, blowing warm and dry across the farmland.", + "8": "Irrigation ditches struggle to keep the soil moist under the blazing sun.", + "9": "The barn cats lie sprawled in the shade, too lethargic to chase rodents.", + "10": "Children carry buckets of water to parched garden beds, their efforts barely enough.", + "11": "The air smells of dust and sun-warmed hay, thick and unyielding.", + "12": "Farm tools grow hot to the touch, abandoned in favor of cooler indoor tasks." + }, + "village": { + "1": "Villagers huddle in the shade of thatched rooftops, fanning themselves against the heat.", + "2": "The cobblestone streets radiate warmth, making barefoot travel unbearable.", + "3": "The village well is crowded, with buckets quickly emptied by thirsty townsfolk.", + "4": "Windows and doors are left open, inviting in any hint of a breeze.", + "5": "The local smithy is abandoned for the day, the forge adding unbearable heat.", + "6": "Children splash in a shallow creek, seeking relief from the relentless sun.", + "7": "The village square is eerily quiet, save for the occasional rustle of dry leaves.", + "8": "Vegetable gardens droop, their leaves yellowing in the punishing weather.", + "9": "Merchants set up stalls under large canopies, shading their wares from the sun.", + "10": "Carts laden with barrels of water creak through the village, an unusual sight.", + "11": "The bell tower seems to sway in the heat, its metal fixtures too hot to touch.", + "12": "A faint mirage hovers over the road leading out of the village, distorting the horizon." + }, + "city": { + "1": "The city streets are deserted at midday, the stone walls amplifying the heat.", + "2": "Market vendors splash water on their stalls, trying to keep their goods from wilting.", + "3": "The scent of hot tar and stone fills the air, mingling with the faint smell of sweat.", + "4": "Even the bustling city square falls silent, its usual clamor stifled by the heat.", + "5": "Guards abandon their posts to seek shade, their armor too hot to wear comfortably.", + "6": "Fountains become gathering points as citizens dunk their hands in the cool water.", + "7": "Children play in the spray of overflowing water barrels, their laughter piercing the quiet.", + "8": "The city’s rooftops shimmer, reflecting the relentless sunlight back into the streets.", + "9": "The bellows of blacksmiths fall silent, forges extinguished in the oppressive weather.", + "10": "Citizens fan themselves with scraps of parchment, the air too heavy to move.", + "11": "Long lines form at the city wells, tempers flaring in the sweltering heat.", + "12": "The air above the city gates shimmers, blending the sky and horizon into one." + }, + "plains": { + "1": "The grasses on the plains sway gently, their edges dry and brittle under the heat.", + "2": "Wildflowers wilt and fade, their vibrant colors dulled by the relentless sun.", + "3": "The air is filled with the sound of crickets, their chirping slow in the heat.", + "4": "Herds of deer cluster near dwindling water sources, wary yet desperate for relief.", + "5": "The vast openness of the plains radiates warmth, offering no shade to travelers.", + "6": "Dust devils spiral across the landscape, kicking up dry earth into the scorching air.", + "7": "Birds circle high above, their shadows faint against the sunlit ground.", + "8": "The horizon seems to shimmer endlessly, obscuring distant landmarks in a mirage.", + "9": "Streams dry to a trickle, their rocky beds exposed and glinting in the sunlight.", + "10": "Cattle move sluggishly, their grazing interrupted by frequent trips to waterholes.", + "11": "The sky is a blazing dome of blue, not a single cloud in sight.", + "12": "Even the breeze feels hot, brushing across the plains like a giant exhale." + }, + "forest": { + "1": "The forest feels stifling, the heat trapped beneath the thick canopy of leaves.", + "2": "Birdsong is subdued, the forest animals too lethargic to make much noise.", + "3": "Leaves droop under the sun’s intense rays, their edges curling slightly.", + "4": "The forest floor feels dry and brittle, crackling underfoot as travelers pass.", + "5": "Streams run lower than usual, their cool waters barely reaching the thirsty roots.", + "6": "The scent of sun-warmed pine needles and earth fills the still, heavy air.", + "7": "Animals retreat to shaded burrows, their usual rustling absent from the forest.", + "8": "The trees cast short, flickering shadows that provide little respite from the heat.", + "9": "Wildflowers in clearings wilt under the sun, their petals curling inward.", + "10": "Insects swarm in the humid air, their buzzing an oppressive backdrop to the heat.", + "11": "A faint haze of heat clings to the forest, blurring distant trees.", + "12": "Even the streams seem warm, their once-refreshing waters tepid and uninviting." + }, + "swamp": { + "1": "The swamp air is thick and oppressive, the heat amplifying the stench of decay.", + "2": "Mosquitoes swarm in the humid air, their buzzing relentless against the heat.", + "3": "The water in the swamp is warm and stagnant, releasing a foul, steamy odor.", + "4": "The thick vegetation droops, its vibrant greens dulled by the relentless sun.", + "5": "Frogs croak lazily, their usual vigor diminished by the sweltering heat.", + "6": "Travelers wade through warm, knee-deep waters, the mud sticky and cloying.", + "7": "Even the swamp’s normally abundant wildlife seems subdued in the choking heat.", + "8": "Steam rises from the swamp's surface, creating a humid haze over the murky waters.", + "9": "The air tastes of salt and decay, sticking to the skin and making it hard to breathe.", + "10": "Dragonflies flit lazily across the swamp, their movement slow and deliberate.", + "11": "Shadows from the cypress trees do little to cool the suffocating warmth below.", + "12": "The swamp’s shallow pools shrink, their edges cracked and lined with drying mud." + }, + "jungle": { + "1": "The jungle air is thick and steamy, the heat wrapping around like a heavy blanket.", + "2": "Even the vibrant leaves of the jungle seem dulled, their surfaces coated in sweat-like dew.", + "3": "Bird calls echo faintly, their energy sapped by the oppressive heat.", + "4": "The jungle floor is a tangle of humid vines and wilting undergrowth.", + "5": "Monkeys sit idly in the trees, fanning themselves with large leaves.", + "6": "The air smells of wet earth and decaying vegetation, intensified by the heat.", + "7": "Streams of sweat run down every exposed surface, evaporating almost instantly.", + "8": "Pools of water shimmer in the humid heat, teeming with tiny, darting insects.", + "9": "Travelers hack sluggishly through thick vines, the air too hot for swift movement.", + "10": "The sound of dripping water is constant, a reminder of the jungle’s relentless moisture.", + "11": "The jungle canopy offers shade, but no relief from the heavy, stifling heat.", + "12": "Even the fiercest predators move slowly, conserving energy in the choking warmth." + }, + "hills": { + "1": "The rolling hills shimmer under a relentless sun, their grassy slopes turning yellow and brittle.", + "2": "Shepherds guide flocks to the few shaded groves, seeking relief from the heat.", + "3": "Streams dry up, leaving cracked earth where water once flowed freely.", + "4": "The air smells of sun-warmed grass and dry earth, heavy with heat.", + "5": "Birdsong is faint and sporadic, the heat sapping energy from all creatures.", + "6": "Rabbits dart into burrows, avoiding the blazing sunlight above.", + "7": "The wind offers no respite, blowing hot and dry across the hills.", + "8": "Shadows under trees seem smaller, the harsh sun casting sharp outlines.", + "9": "Herd animals move sluggishly, their movements slowed by the oppressive heat.", + "10": "Dust kicks up with every step, clinging to skin and clothing in the dry air.", + "11": "The heatwaves create mirages, distorting the horizon and nearby fields.", + "12": "The hills feel desolate, as if abandoned by life, under the crushing heat." + }, + "mountains": { + "1": "The mountain air is still, the sun's glare bouncing off rocky slopes.", + "2": "Snow patches on high peaks melt rapidly, feeding streams that soon dry out below.", + "3": "Climbers pause frequently, the heat making each step heavier and more taxing.", + "4": "The wind whistles through narrow passes, carrying the smell of hot stone.", + "5": "Vegetation clings desperately to life, its leaves curling under the sun’s intensity.", + "6": "The rocky paths radiate heat, making travel uncomfortable and dangerous.", + "7": "Birds of prey soar high, their calls muffled by the oppressive atmosphere.", + "8": "Shadows seem sharper, but they bring little relief from the blazing sun.", + "9": "Goats huddle near cool, shaded rocks, their tongues lolling from the heat.", + "10": "Sweat beads on foreheads before quickly evaporating in the dry mountain air.", + "11": "Streams run low, their sources struggling against the unyielding heat.", + "12": "The horizon blurs, the heatwaves rising from the craggy terrain distorting the view." + }, + "desert": { + "1": "The desert sands radiate unbearable heat, shimmering under the blazing sun.", + "2": "Cacti and scrub plants stand as solitary guardians of this parched landscape.", + "3": "The sky is a brilliant, cloudless blue, offering no promise of rain or relief.", + "4": "Sand dunes shift lazily in the dry wind, their surfaces almost too hot to touch.", + "5": "Lizards dart quickly from shadow to shadow, their movements swift and deliberate.", + "6": "The air is dry and stings the skin, carrying the faint scent of baked earth.", + "7": "Travelers wrap their faces, shielding themselves from the relentless sun and heat.", + "8": "The horizon quivers with heat mirages, creating phantom pools of water in the distance.", + "9": "Boots sink into the soft, hot sand, each step an effort against the hostile terrain.", + "10": "The wind blows like a furnace, scattering grains of sand into eyes and clothing.", + "11": "Bones of past creatures bleach under the sun, stark reminders of the desert's dangers.", + "12": "Every breath feels dry and labored, the heat pressing down like a tangible weight." + }, + "coastal": { + "1": "The sun blazes over the coastline, turning the sand into a hot, shimmering surface.", + "2": "Waves crash lazily, the water warm to the touch and offering little reprieve.", + "3": "Fishermen rest in the shade of their boats, their faces flushed from the heat.", + "4": "The salty sea breeze carries warmth rather than coolness, adding to the discomfort.", + "5": "Seagulls wheel overhead, their calls mingling with the crashing surf.", + "6": "The tide pools steam slightly, their shallow waters heated by the midday sun.", + "7": "The horizon is a hazy blur where sea and sky meet under the shimmering heat.", + "8": "Shells and pebbles on the beach feel hot to the touch, forcing barefoot walkers to retreat.", + "9": "Children wade into the surf, splashing with more caution than usual as the heat saps their energy.", + "10": "Fish markets reek of warm, briny air as seafood spoils quickly under the sun.", + "11": "The scent of salt and sun-warmed kelp fills the air, clinging to everything it touches.", + "12": "Ships moored in the harbor creak in the heat, their wood expanding under the relentless sun." + }, + "volcano": { + "1": "The volcanic landscape feels alive, the heat from the sun and earth merging into one.", + "2": "Cracks in the ground shimmer with residual heat, adding to the oppressive atmosphere.", + "3": "Sulfur scents linger heavily in the hot, still air, making breathing difficult.", + "4": "The black rock absorbs and radiates the sun’s heat, making the terrain almost untouchable.", + "5": "Steam vents hiss sporadically, their warm exhalations blending with the air’s heat.", + "6": "Lava flows glimmer faintly in the distance, their glow adding to the oppressive environment.", + "7": "The sparse vegetation on the slopes wilts, their hardy leaves struggling against the dual heat.", + "8": "Birds avoid the volcano's slopes, their usual songs replaced by an eerie silence.", + "9": "The heat waves rising from the ground distort the surrounding landscape into surreal shapes.", + "10": "Boots crunch against scorched gravel, each step feeling heavier in the stifling atmosphere.", + "11": "Faint tremors shake the ground, adding to the uneasy weight of the oppressive heat.", + "12": "The horizon above the crater blurs, a mix of sunlight, heatwaves, and sulfurous haze." + }, + "arctic": { + "1": "Even in the Arctic, the sun feels unyielding, bouncing off the snow in blinding glares.", + "2": "Glaciers melt slightly under the sun’s unrelenting heat, sending rivulets of water into the sea.", + "3": "The snow turns slushy and uneven, making travel exhausting under the oppressive sun.", + "4": "Animals pant in the shade of ice formations, their fur damp from the unusual warmth.", + "5": "The air is unusually still, the faintest breeze carrying a deceptive warmth.", + "6": "Icebergs glimmer under the sun, their sharp edges softening as the heat takes its toll.", + "7": "The horizon blends into the sky as heatwaves distort the otherwise barren landscape.", + "8": "Expeditions halt as supplies dwindle, the unexpected warmth straining resources.", + "9": "Polar bears rest near the water’s edge, reluctant to move in the midday heat.", + "10": "Cracks form in the ice, releasing faint pops and groans in the unusual warmth.", + "11": "Snowdrifts shrink visibly, their pristine whiteness marred by exposed earth.", + "12": "Even the sea ice seems to sweat, its glossy surface shimmering under the sun." + }, + "cursed": { + "1": "The oppressive heat carries an unnatural weight, as if cursed by unseen forces.", + "2": "Shadows warp under the intense sun, twisting into eerie, unsettling shapes.", + "3": "The air feels alive, heavy and whispering faintly as if warning travelers away.", + "4": "Plants wilt not from heat alone but from a palpable sense of decay.", + "5": "The ground cracks open, emitting faint wisps of black smoke with each fissure.", + "6": "Animals flee the area, their cries echoing unnaturally loud in the stifling silence.", + "7": "Sweat drips from every pore, yet the heat feels internal, as if burning the soul.", + "8": "The sun's rays seem darker, casting long, haunting shadows across the cursed land.", + "9": "Faint whispers ride the wind, carrying words of dread and despair in the oppressive heat.", + "10": "Water sources bubble unnaturally, their surfaces reflecting a distorted, fiery sky.", + "11": "Even the insects avoid the area, the usual hum of life replaced by a haunting silence.", + "12": "Travelers report a faint burning sensation on their skin, as if the heat carried malice." + } + } + }, + "Dust or Sand Storm": { + "conditions": { + "temperature": { "gte": 30, "lte": 70 }, + "precipitation": { "lte": 10 }, + "wind": { "gte": 60 }, + "humidity": { "lte": 20 }, + "cloudCover": { "lte": 40 }, + "visibility": { "lte": 20 } + }, + "descriptions": { + "farm": { + "1": "A thick haze of dust blankets the fields, obscuring crops and tools alike.", + "2": "The wind carries choking clouds of dust, making it hard to breathe.", + "3": "Animals in the barn are restless, sensing the approaching storm.", + "4": "The air tastes gritty as dust seeps into every corner of the farmhouse.", + "5": "Loose hay swirls in chaotic patterns, carried by the fierce wind.", + "6": "The farm’s windmill creaks under the strain of the powerful, sand-laden gusts.", + "7": "Dust devils dance across the fields, uprooting small plants in their wake.", + "8": "Visibility drops to near nothing as the storm envelops the land.", + "9": "Doors and windows are hastily barred, but fine dust still seeps inside.", + "10": "The storm muffles all sound, leaving only the roar of the wind.", + "11": "Cows and chickens huddle together, their cries drowned out by the gale.", + "12": "The soil of the farmstead is stripped bare, leaving scars across the fields." + }, + "village": { + "1": "The storm turns the village into a ghostly outline, hidden behind swirling dust.", + "2": "Villagers pull scarves tightly over their faces, braving the wind to secure homes.", + "3": "Shutters and doors rattle violently as the sandstorm batters the buildings.", + "4": "Dust piles up in corners, sneaking under doors and through cracks in the walls.", + "5": "Children are kept indoors, their laughter replaced by the howling wind outside.", + "6": "The village well is temporarily abandoned as the storm coats it with sand.", + "7": "Roofs creak under the strain of sand-laden gusts threatening to tear them apart.", + "8": "The main road becomes impassable, buried under layers of shifting sand.", + "9": "The air smells dry and acrid, the taste of dust lingering on the tongue.", + "10": "Flickering lanterns struggle to light the storm-shrouded streets.", + "11": "Merchants hurriedly pack their wares, sand grinding into fabrics and wood alike.", + "12": "A villager loses their hat to the wind, watching helplessly as it vanishes in the storm." + }, + "city": { + "1": "The towering city walls do little to keep out the relentless tide of dust.", + "2": "Market stalls collapse as vendors scramble to protect their goods from the storm.", + "3": "The streets are eerily deserted, with only the wind howling through narrow alleys.", + "4": "Guards atop the city walls shield their faces, unable to see far through the storm.", + "5": "Dust clings to fine silks and tapestries, staining them beyond recognition.", + "6": "The air inside taverns grows heavy with grit, despite closed windows and doors.", + "7": "Cathedral bells chime faintly, muffled by the deafening roar of the storm.", + "8": "The city fountain runs brown, filled with sand whipped up by the gale.", + "9": "Shopkeepers curse as sand slips under their doors, ruining finely crafted wares.", + "10": "The once vibrant plazas are now silent, hidden beneath a layer of fine dust.", + "11": "Wealthy citizens retreat to their mansions, leaving the streets to the storm.", + "12": "Even the air itself seems to turn against the city, choking its inhabitants." + }, + "plains": { + "1": "The endless expanse of plains disappears into the swirling, dust-choked horizon.", + "2": "Waves of sand roll like a restless sea, carried by the merciless wind.", + "3": "Herds of wild animals scatter, seeking shelter from the storm’s wrath.", + "4": "The ground trembles faintly as the wind tears through the open plains.", + "5": "Grass blades whip violently, stripped of their color by the abrasive dust.", + "6": "Small rocks and debris pelt travelers, carried far by the storm's ferocity.", + "7": "The sky above the plains darkens with the storm, day fading unnaturally early.", + "8": "Shallow streams dry up, clogged by the constant influx of dust and sand.", + "9": "Travelers crouch behind what little cover they can find, bracing against the gale.", + "10": "The howling wind echoes across the plains, a constant and deafening presence.", + "11": "Dust piles form against sparse rocks, mimicking dunes in the flat expanse.", + "12": "The storm leaves the plains scarred and barren, a wasteland of shifting sand." + }, + "forest": { + "1": "Dust filters through the canopy, turning the once-green forest into a dull haze.", + "2": "Leaves fall prematurely, torn from branches by the abrasive winds.", + "3": "Animals burrow into the undergrowth, seeking refuge from the choking air.", + "4": "Tree trunks bear fresh scars, sand-blasted by the relentless storm.", + "5": "The once-cool forest air turns hot and gritty, stinging the lungs.", + "6": "Paths through the forest disappear under a layer of fine dust and debris.", + "7": "Branches creak and groan as the wind forces them to bow and twist unnaturally.", + "8": "Birds abandon their nests, their cries lost in the roar of the storm.", + "9": "The underbrush rustles ominously, though no creatures are visible in the storm’s chaos.", + "10": "Fine sand clings to the bark, turning even ancient trees pale and weathered.", + "11": "Streams within the forest grow sluggish, clogged with dirt and silt.", + "12": "The storm leaves the forest floor barren, a stark contrast to its former vibrancy." + }, + "swamp": { + "1": "The swamp is cloaked in a thick layer of airborne silt, choking out its usual stench.", + "2": "Mud bubbles as the storm stirs the stagnant pools into frothy chaos.", + "3": "Reeds and cattails sway wildly, bending almost flat under the storm’s force.", + "4": "The once-clear waterways are now murky, thick with sand and debris.", + "5": "Croaking frogs fall silent, the swamp’s usual cacophony muffled by the gale.", + "6": "The ground becomes treacherous, with shifting sands masking deeper, muddy pits.", + "7": "Even the swamp’s insects struggle to navigate through the choking storm.", + "8": "Waterlogged trees groan under the weight of sand settling in their branches.", + "9": "The storm creates phantom shapes in the haze, unsettling those who pass through.", + "10": "The swamp’s fetid smell mixes with the acrid tang of disturbed earth and dust.", + "11": "The sun struggles to pierce through the storm, casting an eerie, orange glow.", + "12": "Creatures of the swamp retreat to burrows, leaving the storm to rage unchecked." + }, + "jungle": { + "1": "The dense jungle turns into a suffocating maze of dust-filled air and muted greenery.", + "2": "Leaves curl and brown under the abrasive assault of sand carried by the wind.", + "3": "Monkeys chatter nervously, retreating to the highest, densest parts of the canopy.", + "4": "Fine dust clogs the jungle’s vibrant streams, turning them into sluggish trickles.", + "5": "The thick vegetation offers little respite, with the air inside the jungle just as choked as outside.", + "6": "The storm muffles the jungle’s vibrant noises, leaving an eerie, unnatural silence.", + "7": "The wind forces vines to swing wildly, creating dangerous tangles for travelers.", + "8": "The ground becomes unstable, a mix of sand, mud, and trampled plants.", + "9": "Jungle animals flee in panic, their footfalls swallowed by the ever-present wind.", + "10": "Pollen and sand mix, creating a choking cloud that stings the throat and eyes.", + "11": "The jungle canopy sags under the weight of dust settling atop its leaves.", + "12": "The storm transforms the vibrant jungle into a monochrome expanse of dull browns and yellows." + }, + "hills": { + "1": "Dust rolls down the slopes, blanketing the hills in a choking haze.", + "2": "Gusts of wind sweep loose soil from the hilltops, creating swirling clouds.", + "3": "Travelers on the hills struggle for footing as the wind howls around them.", + "4": "Visibility drops to a mere few feet as the storm engulfs the landscape.", + "5": "Sheep and livestock huddle together, their coats turned brown with dust.", + "6": "Small rocks and pebbles pelt travelers, carried by the fierce winds.", + "7": "The once-green hills are reduced to barren mounds of sand and debris.", + "8": "Grit stings the eyes and clings to every surface, even beneath clothing.", + "9": "The sound of the storm echoes eerily as it rolls across the hills.", + "10": "Dry grass and shrubs whip violently in the wind, stripped bare of leaves.", + "11": "Paths between the hills vanish, obscured by shifting layers of dust.", + "12": "The storm leaves behind uneven dunes where once there were gentle slopes." + }, + "mountains": { + "1": "Dust spirals between the peaks, turning the rugged mountains into a hazy maze.", + "2": "Strong winds funnel through mountain passes, carrying grit and debris.", + "3": "Mountain goats struggle to find stable footing as loose rocks tumble down.", + "4": "The air grows thick with choking dust, making breathing difficult.", + "5": "The sun disappears behind the storm, casting the mountains in shadow.", + "6": "Sheer cliffs bear fresh scars where sand-laden winds have scoured the rock.", + "7": "Echoes of the storm reverberate through narrow valleys and craggy outcroppings.", + "8": "Travelers shield their faces as sand pelts them from every direction.", + "9": "The storm reduces towering peaks to vague, ghostly outlines in the distance.", + "10": "Rivers running down the mountains turn brown with silt and debris.", + "11": "Loose boulders shift dangerously, dislodged by the relentless winds.", + "12": "The storm's aftermath leaves the slopes littered with debris and fine dust." + }, + "desert": { + "1": "The desert becomes a swirling sea of sand, with dunes shifting in moments.", + "2": "The horizon vanishes entirely as the storm envelops the desert.", + "3": "Travelers wrap themselves in cloth, but the sand still finds its way inside.", + "4": "Palm trees bend under the assault, their fronds whipped away by the wind.", + "5": "The storm’s roar is deafening, drowning out all other sounds in the desert.", + "6": "Sand pelts exposed skin, leaving it raw and stinging with pain.", + "7": "Footsteps are quickly erased, the desert’s surface constantly reshaped by the storm.", + "8": "Mirages shimmer faintly before disappearing into the haze of the storm.", + "9": "Camels kneel down, turning their backs to the gale to endure the storm.", + "10": "The air tastes dry and metallic, with sand grinding between clenched teeth.", + "11": "The sun is obscured, leaving the desert in an eerie, diffuse glow.", + "12": "Once the storm subsides, the landscape is unrecognizable, dunes shifted miles away." + }, + "coastal": { + "1": "The sandstorm sweeps inland, turning the beach into a chaotic blur.", + "2": "Seagulls scatter as the winds carry dust and sand over the shore.", + "3": "Ships anchored in the harbor vanish behind the storm's dusty veil.", + "4": "Salt and grit mix in the air, leaving a sharp, stinging taste on the tongue.", + "5": "Dunes along the coast shift rapidly, reshaped by the fierce winds.", + "6": "Palm trees along the shore creak and groan, some uprooted entirely.", + "7": "The ocean turns a murky brown as sand spills into the waves.", + "8": "Fishermen abandon their nets, retreating to shelter from the storm’s fury.", + "9": "Sand invades every crevice, even inside tightly sealed cabins near the beach.", + "10": "The once-clear coastline is lost to a swirling haze of golden dust.", + "11": "The storm churns the waves into frothy chaos, mirroring the turmoil on land.", + "12": "When the storm clears, boats are buried in dunes far from their moorings." + }, + "volcano": { + "1": "The sandstorm mixes with volcanic ash, creating a choking, acrid haze.", + "2": "Lava flows glow faintly through the thick clouds of dust and ash.", + "3": "The ground trembles as the storm’s wind howls through jagged volcanic peaks.", + "4": "Ash clings to every surface, mixed with fine grains of sand from the storm.", + "5": "The heat of the volcano intensifies the storm, making the air nearly unbearable.", + "6": "Steam vents hiss violently, sand swirling into the scalding plumes.", + "7": "Molten rocks shift and crack under the relentless pressure of the wind.", + "8": "Visibility drops to near nothing as dust and ash envelop the volcano’s slopes.", + "9": "Travelers struggle to navigate the treacherous terrain, blind to looming dangers.", + "10": "The storm muffles the rumble of the volcano, creating an eerie stillness.", + "11": "Sand and ash settle into pools of lava, creating sudden bursts of steam.", + "12": "The aftermath reveals a desolate landscape, scarred by both fire and storm." + }, + "artic": { + "1": "The icy tundra turns gray as the sandstorm sweeps across the frozen ground.", + "2": "Snow mixes with dust, creating a gritty, blinding slurry in the air.", + "3": "The cold cuts through thick furs as the storm’s winds whip relentlessly.", + "4": "Ice crystals and sand pelt exposed skin, leaving painful welts.", + "5": "The storm buries tracks in the snow, erasing all signs of passage.", + "6": "Visibility is reduced to a blur of white and brown, with no landmarks in sight.", + "7": "The roar of the storm echoes strangely over the frozen expanse.", + "8": "Snowdrifts collapse as sand settles atop them, creating unstable ground.", + "9": "Frosted trees groan under the weight of sand and ice clinging to their branches.", + "10": "Animals of the tundra retreat to their dens, avoiding the brutal storm.", + "11": "The once-pristine snowfields are left gray and gritty, scarred by the storm.", + "12": "The storm leaves the arctic barren and desolate, the landscape transformed." + }, + "cursed": { + "1": "The storm carries whispers in its winds, unnerving those who hear them.", + "2": "Dust swirls in unnatural patterns, forming fleeting shapes of skeletal hands.", + "3": "The air tastes bitter, as if the storm itself were laced with poison.", + "4": "Shadows seem to dance in the haze, though there is no source of light.", + "5": "The storm rattles bones in forgotten graves, unsettling spirits within.", + "6": "Fine dust settles on skin, leaving a faint, glowing residue behind.", + "7": "The howling wind carries faint screams, whether real or imagined is unclear.", + "8": "The storm warps the landscape, creating illusions of movement in the distance.", + "9": "Dust forms strange symbols on the ground, quickly erased by the shifting winds.", + "10": "Travelers feel an inexplicable dread as the storm envelopes the cursed land.", + "11": "The storm is silent save for a low, pulsing hum that sets nerves on edge.", + "12": "When the storm clears, the air feels heavy, as though something lingers unseen." + } + } + }, + "Gale-Force Winds": { + "conditions": { + "temperature": { "gte": 30, "lte": 60 }, + "precipitation": { "lte": 40 }, + "wind": { "gte": 60, "lte": 80 }, + "humidity": { "gte": 20, "lte": 60 }, + "cloudCover": { "gte": 40, "lte": 70 }, + "visibility": { "gte": 50 } + }, + "descriptions": { + "farm": { + "1": "Wind tears through the fields, flattening crops and scattering hay bales.", + "2": "The barn doors creak ominously, straining against the relentless gusts.", + "3": "Animals huddle in their pens as the wind howls through the farmyard.", + "4": "Shutters bang against the farmhouse as the storm intensifies.", + "5": "Loose straw swirls in the air, creating a golden haze around the farm.", + "6": "The wind carries the scent of tilled earth mixed with distant rain.", + "7": "Fences lean precariously, threatening to collapse under the strain.", + "8": "A loud crack echoes as a tree on the edge of the field is felled.", + "9": "Tools and buckets scatter across the yard, clattering noisily.", + "10": "Chickens squawk in alarm as their coop rattles violently in the wind.", + "11": "The wind whistles through gaps in the barn, chilling the air inside.", + "12": "Dust rises in clouds, obscuring the once-clear view of the fields." + }, + "village": { + "1": "Thatch roofs strain against the gale, with bits of straw ripped away.", + "2": "Villagers secure their doors, shouting to be heard over the roaring wind.", + "3": "Loose laundry flutters wildly, some pieces torn free and carried away.", + "4": "Lanterns swing dangerously on their posts, their light flickering in the gusts.", + "5": "Dust and debris swirl through the narrow village streets.", + "6": "The wind howls between cottages, creating an eerie, mournful sound.", + "7": "A market stall collapses as the wind catches its awning.", + "8": "Smoke from chimneys is blown sideways, disappearing into the stormy sky.", + "9": "Children peek nervously from windows, watching the chaos outside.", + "10": "A tree in the village square groans loudly before losing a large branch.", + "11": "Loose shingles clatter to the ground as the wind batters rooftops.", + "12": "The wind carries the scent of damp earth and distant storms." + }, + "city": { + "1": "Flags atop the city walls snap sharply in the gale, their poles swaying.", + "2": "Merchants scramble to save their wares as market stalls collapse.", + "3": "The wind howls between narrow alleys, scattering loose papers and trash.", + "4": "Chimneys rattle ominously, and a few spill soot into the streets below.", + "5": "Heavy doors creak and bang shut, echoing through the city's stone corridors.", + "6": "Streetlights flicker as the wind threatens to extinguish their flames.", + "7": "The sound of creaking wood and metal fills the air as signs sway dangerously.", + "8": "Windows rattle in their frames, some cracking under the strain.", + "9": "Dust and dirt coat the faces of passersby as the storm sweeps through.", + "10": "Awnings flap violently, some tearing loose and flying into the street.", + "11": "Cloaks and hats are whipped from shoulders and heads by sudden gusts.", + "12": "The distant sound of a cart overturning is followed by shouted curses." + }, + "plains": { + "1": "The grass ripples like waves under the relentless assault of the wind.", + "2": "A solitary tree bends nearly double, its branches creaking ominously.", + "3": "Dust clouds rise in swirling columns, obscuring the horizon.", + "4": "Travelers lean into the wind, their cloaks flapping violently.", + "5": "Birds struggle to fly, some grounded by the powerful gusts.", + "6": "The wind roars across the open expanse, carrying the scent of wildflowers.", + "7": "Loose stones and debris pelt those caught in the storm.", + "8": "The plains become a sea of motion, with every blade of grass trembling.", + "9": "Sounds are carried far, the wind amplifying distant calls and crashes.", + "10": "Tents and shelters strain at their stakes, some ripped free entirely.", + "11": "A lone horse rears in panic as the storm swirls around it.", + "12": "The gale leaves the plains battered, with grass flattened and debris scattered." + }, + "forest": { + "1": "Tree branches thrash wildly, some snapping and crashing to the ground.", + "2": "Leaves are torn from the canopy, creating a green flurry in the air.", + "3": "The wind whistles through the trees, a haunting, high-pitched sound.", + "4": "Loose bark and twigs shower the forest floor as the storm rages on.", + "5": "Animals retreat into their dens, their calls silenced by the gale.", + "6": "A massive tree topples with a deafening crash, leaving a gap in the forest.", + "7": "The underbrush sways violently, exposing glimpses of creatures scurrying away.", + "8": "The forest floor is littered with fallen branches and leaves after the storm.", + "9": "Dust and pollen are whipped into the air, creating a choking haze.", + "10": "The sound of creaking wood fills the forest, accompanied by the roar of wind.", + "11": "Pine needles sting exposed skin as they are carried by the gusts.", + "12": "Paths through the forest vanish beneath layers of debris and fallen foliage." + }, + "swamp": { + "1": "The wind ripples the swamp's surface, sending waves across stagnant pools.", + "2": "Rotten logs creak and shift as the gale sweeps through the boggy terrain.", + "3": "Clouds of insects are scattered, leaving the air momentarily clear.", + "4": "The swamp trees groan, their roots straining to hold firm in the muddy ground.", + "5": "Loose vegetation flies through the air, landing haphazardly in the water.", + "6": "Reeds and cattails bend nearly horizontal under the force of the wind.", + "7": "Mud and water spray upward as gusts churn the swampy ground.", + "8": "The swamp's eerie silence is broken by the relentless roar of the gale.", + "9": "Cloying humidity is momentarily dispersed by the storm's violent energy.", + "10": "Animal calls echo faintly as creatures flee deeper into the swamp.", + "11": "The wind carries the stench of decay from disturbed bogs and marshes.", + "12": "After the storm, the swamp is littered with fallen branches and uprooted plants." + }, + "jungle": { + "1": "Palm fronds whip wildly in the wind, creating a cacophony of rustling.", + "2": "Vines swing dangerously, some snapping free and flying through the air.", + "3": "The wind scatters colorful flowers and fruit across the jungle floor.", + "4": "Monkeys screech as they cling to swaying branches high above.", + "5": "The dense canopy groans under the strain, with branches cracking loudly.", + "6": "Loose leaves and twigs rain down, creating a soft but constant patter.", + "7": "The jungle air fills with the sharp scent of crushed vegetation.", + "8": "Dense undergrowth sways violently, revealing flashes of darting wildlife.", + "9": "Waterfalls foam as the wind drives their spray far beyond their usual bounds.", + "10": "Tree trunks groan, and some ancient giants fall with ground-shaking crashes.", + "11": "The storm amplifies jungle sounds, turning whispers into roars.", + "12": "After the gale, the jungle floor is a chaotic tangle of debris and fallen trees." + }, + "hills": { + "1": "The wind rushes over the hills, bending grasses and shrubs nearly to the ground.", + "2": "Loose rocks tumble down slopes as gusts shake the hillsides.", + "3": "Shepherds struggle to herd their flocks as the gale scatters them across the hills.", + "4": "The wind carries an eerie, hollow sound as it whips through the valleys.", + "5": "Small trees on the hilltops sway violently, some uprooted entirely.", + "6": "The air is filled with flying leaves and twigs, stinging any exposed skin.", + "7": "Travelers struggle to crest the ridges as the wind threatens to topple them.", + "8": "Dust devils twist and dance across the hilltops, scattering debris in their wake.", + "9": "Distant howls of the wind echo across the hills, making it hard to tell direction.", + "10": "Shepherd's crooks and hats are snatched away by the relentless gusts.", + "11": "The grass whistles sharply as the wind tears through the open landscape.", + "12": "The storm leaves the hills littered with broken branches and uprooted shrubs." + }, + "mountains": { + "1": "The wind howls through the mountain passes, echoing like a mournful wail.", + "2": "Loose stones clatter down rocky cliffs, disturbed by the forceful gusts.", + "3": "The high peaks are obscured by clouds of swirling dust and snow.", + "4": "Travelers cling to rocky outcroppings to avoid being swept off narrow ledges.", + "5": "The wind tears at cloaks and packs, making every step a struggle.", + "6": "Thin mountain air amplifies the howling winds, creating an eerie atmosphere.", + "7": "Snow and ice are carried by the gale, stinging exposed skin like needles.", + "8": "Pine trees on the lower slopes creak and groan under the strain of the storm.", + "9": "The peaks seem alive as the storm roars through, sending avalanches crashing down.", + "10": "Shelter is hard to find, with every cave entrance filled with whirling debris.", + "11": "The wind carries the distant crash of falling boulders and tumbling snow.", + "12": "The aftermath reveals a changed landscape, with paths blocked by fallen rocks." + }, + "desert": { + "1": "Sand swirls in violent whirlwinds, reducing visibility to mere feet.", + "2": "Dunes shift and reshape under the relentless force of the wind.", + "3": "The air is thick with sand, making it difficult to breathe or see.", + "4": "Campsites are obliterated as tents and supplies are carried away by the storm.", + "5": "The wind creates a constant, high-pitched whistle as it blows through the open desert.", + "6": "Palm trees in distant oases bend nearly double under the gale.", + "7": "The desert floor becomes a chaotic mess of swirling sand and uprooted vegetation.", + "8": "Caravans struggle to stay together, with camels braying in panic.", + "9": "The sun is blotted out by the dense clouds of sand carried by the wind.", + "10": "Tracks are erased in moments, leaving no sign of where travelers have passed.", + "11": "The storm ends as suddenly as it began, leaving an eerily quiet desert behind.", + "12": "In the aftermath, buried artifacts and bones are revealed by the shifting sands." + }, + "coastal": { + "1": "Waves crash against the shore, whipped into frothy chaos by the gale.", + "2": "Fishing boats struggle to return to harbor, their sails shredded by the wind.", + "3": "The salty spray is carried far inland, coating everything with a thin crust of salt.", + "4": "Palm trees along the beach sway violently, some snapping under the strain.", + "5": "Shoreline sand is whipped into the air, stinging eyes and skin.", + "6": "The roar of the wind is drowned out only by the thunderous waves.", + "7": "Cliffside homes tremble as the gale batters their walls and roofs.", + "8": "Sea birds are grounded, huddling in small groups for shelter.", + "9": "The harbor is in chaos, with ropes snapping and ships colliding in the storm.", + "10": "Beach debris is scattered across the coastal roads and fields.", + "11": "The wind carries the strong scent of seaweed and salt far inland.", + "12": "After the storm, the beach is littered with driftwood and wreckage." + }, + "volcano": { + "1": "Ash and debris are whipped into the air, creating an almost impenetrable cloud.", + "2": "Lava flows ripple under the fierce wind, carrying glowing embers far and wide.", + "3": "The wind howls through craggy volcanic fissures, creating deep, resonant sounds.", + "4": "Heat waves shimmer violently in the gale, distorting the air.", + "5": "Loose rocks and ash tumble down the volcanic slopes, carried by the gusts.", + "6": "The smell of sulfur is thick in the air, carried far by the relentless winds.", + "7": "The wind ignites small fires where embers meet dry vegetation.", + "8": "Hot volcanic sands sting skin, carried with force by the violent gusts.", + "9": "Lava fountains spray higher as the wind intensifies, spreading molten rock.", + "10": "Shelters built against the volcano shake violently, some collapsing entirely.", + "11": "The wind amplifies the roar of the volcano, making the ground tremble.", + "12": "The aftermath reveals a scorched and reshaped landscape, eerily quiet." + }, + "artic": { + "1": "Snow and ice are blasted into the air, creating a whiteout that blinds all who venture out.", + "2": "The wind howls across the tundra, carrying a bone-chilling cold.", + "3": "Ice shards whip through the air, cutting into anything unprotected.", + "4": "Snowdrifts are reshaped by the relentless gale, blocking paths and entrances.", + "5": "Frost forms instantly on exposed metal and skin as the wind saps all warmth.", + "6": "Polar bears and other wildlife seek shelter, huddling against the icy winds.", + "7": "The wind tears at sleds and packs, scattering supplies across the frozen expanse.", + "8": "The storm drowns out all sound except the deafening roar of the wind.", + "9": "The wind creates strange, almost musical tones as it passes over icy peaks.", + "10": "The frozen ground cracks and groans under the pressure of the storm.", + "11": "The air is filled with the sharp scent of ice and distant salt from frozen seas.", + "12": "When the storm clears, the landscape is left sparkling and alien, reshaped by the wind." + }, + "cursed": { + "1": "The gale carries a haunting, whispering voice that seems to come from nowhere.", + "2": "Dark clouds churn overhead, forming unnatural shapes in the sky.", + "3": "The wind brings an eerie chill, despite the oppressive heat of the cursed land.", + "4": "Shadows seem to move in the storm, darting just out of sight.", + "5": "The gale rattles bones and debris scattered across the cursed ground.", + "6": "The wind carries the scent of decay and burnt flesh, making it hard to breathe.", + "7": "Ghostly figures appear in the blowing dust, only to vanish when approached.", + "8": "The cursed ground trembles as the wind seems to strike with unnatural force.", + "9": "The air feels alive, crackling with an otherworldly energy as the storm rages.", + "10": "Structures collapse as the cursed wind corrodes wood and stone.", + "11": "The wind amplifies the wails and screams of spirits trapped in the cursed land.", + "12": "After the gale, the cursed land is left eerily quiet, the air heavy with dread." + } + } + }, + "Hailstorm": { + "conditions": { + "temperature": { "gte": 20, "lte": 40 }, + "precipitation": { "gte": 70 }, + "wind": { "gte": 40, "lte": 80 }, + "humidity": { "gte": 50, "lte": 80 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Hailstones pummel the fields, flattening crops and denting metal tools.", + "2": "Livestock scatter, seeking shelter from the relentless hailstorm.", + "3": "Farmers rush to protect their barns and homes as the hail batters their roofs.", + "4": "The fields are left littered with chunks of ice, damaging fragile plants.", + "5": "Hail smashes through the thatched roofs of storage sheds, ruining stored grain.", + "6": "Fruit trees lose their blossoms, the hail tearing them away violently.", + "7": "The sound of hailstones hitting wooden fences and rooftops echoes across the farm.", + "8": "Carts and wagons left in the open are dented and splintered by the icy barrage.", + "9": "Chickens huddle under coops, frightened by the loud clatter of falling hail.", + "10": "Rainwater pools in the ruts made by hail, turning paths into muddy traps.", + "11": "The storm leaves the fields in disarray, crops trampled and plants shredded.", + "12": "Farmers emerge to assess the damage, finding tools and fences in ruin." + }, + "village": { + "1": "Hailstones ricochet off cobblestone streets, creating a deafening cacophony.", + "2": "Villagers huddle indoors, listening to the ice hammer against their shutters.", + "3": "Roofs groan under the weight of accumulated hail, threatening collapse.", + "4": "The town square is abandoned, littered with ice chunks and broken tiles.", + "5": "Windows crack under the impact of large hailstones, letting the cold in.", + "6": "Children peek out from doorways, watching the storm with wide-eyed fear.", + "7": "Market stalls are destroyed, their coverings torn and goods scattered.", + "8": "The hailstorm drives all villagers into their homes, leaving the streets eerily empty.", + "9": "Ice clogs gutters, causing water to spill over and flood the streets.", + "10": "Animals bleat and whine as they huddle in their pens, seeking protection.", + "11": "The village well is surrounded by a pile of hailstones, its water chilling rapidly.", + "12": "When the storm clears, villagers begin repairs, gathering fallen roof tiles and wood." + }, + "city": { + "1": "Hail pelts the stone walls and towers, echoing loudly across the city.", + "2": "Merchants scramble to protect their wares as hailstones smash into market stalls.", + "3": "Stone streets become slick with ice, making travel dangerous.", + "4": "Guards on the city walls take cover as hail clatters against their armor.", + "5": "Smoke from chimneys mixes with the icy air, creating a surreal haze.", + "6": "Citizens duck under overhangs and arches, shielding themselves from the icy barrage.", + "7": "Lamps and lanterns are snuffed out by the force of falling hailstones.", + "8": "Hail bounces off the city gates, creating a near-constant racket.", + "9": "Narrow alleys fill with icy runoff as hailstones melt in the rain-soaked gutters.", + "10": "Piles of hail block doorways, forcing citizens to clear the ice with brooms.", + "11": "Decorative statues and fountains are pitted and chipped by the relentless storm.", + "12": "When the hailstorm passes, the city square is a chaotic mess of ice and debris." + }, + "plains": { + "1": "Hailstones pound the open plains, creating a blanket of white ice.", + "2": "The wind carries the sting of hail across the flat expanse, battering everything in its path.", + "3": "Wild animals flee the open plains, seeking refuge from the unrelenting hail.", + "4": "Grass is flattened by the icy bombardment, leaving the landscape slick and sodden.", + "5": "Horses shy and bolt, their coats pelted by the sharp, cold stones.", + "6": "Small streams form in the hollows of the plains, swollen with melted hail.", + "7": "Shepherds scramble to protect their flocks, using their cloaks as makeshift shields.", + "8": "The horizon becomes a shimmering sheet of ice, reflecting the dim light.", + "9": "Campsites are abandoned as tents collapse under the weight of hail.", + "10": "The storm leaves craters in the soft earth, filled with melting ice.", + "11": "The distant rumble of thunder adds to the chaos of the pounding hail.", + "12": "The plains glisten under the sun as the storm passes, a field of icy shards." + }, + "forest": { + "1": "Hail batters the treetops, sending leaves and broken branches crashing to the ground.", + "2": "The forest floor becomes a slippery mess as hailstones pile up among the roots.", + "3": "Animals take refuge in hollow logs and burrows, sheltering from the storm.", + "4": "Hail ricochets off tree trunks, creating a constant, sharp drumming sound.", + "5": "Birds scatter from the canopy, their calls drowned out by the storm's roar.", + "6": "Saplings bend under the weight of hail, some snapping under the strain.", + "7": "Hailstones pierce through gaps in the foliage, striking the ground with force.", + "8": "Streams in the forest swell with icy runoff, carrying broken twigs and leaves.", + "9": "The forest is eerily dim as clouds block the light, leaving only the sound of hail.", + "10": "The underbrush is torn apart by the relentless pounding of hailstones.", + "11": "Insects swarm briefly after the storm, drawn to the moisture of the melting hail.", + "12": "When the hailstorm ends, the forest is littered with debris and glistening ice." + }, + "swamp": { + "1": "Hailstones plunge into the swamp, leaving ripples across the water's surface.", + "2": "The murky water churns as hail smashes into it, scaring off waterfowl.", + "3": "Tree branches in the swamp creak and snap under the onslaught of ice.", + "4": "The storm stirs up the swamp's muddy bottom, filling the air with a foul smell.", + "5": "Hail bounces off the gnarled roots of mangroves, splashing into the water below.", + "6": "Swamp creatures burrow into the muck, hiding from the icy barrage.", + "7": "The swamp's reeds bend and break, leaving only stumps in the hail's wake.", + "8": "The eerie quiet of the swamp is shattered by the constant hammering of hailstones.", + "9": "Pools of water become frothy and turbulent as hail continues to fall.", + "10": "The storm leaves a thick layer of ice coating the swamp's surface.", + "11": "Frogs and insects fall silent, their calls replaced by the crack of breaking ice.", + "12": "When the hail stops, the swamp is a surreal landscape of ice and shattered vegetation." + }, + "jungle": { + "1": "Hail smashes through the dense canopy, shredding leaves and branches.", + "2": "The jungle floor is littered with ice, leaves, and broken vines after the storm.", + "3": "Monkeys and birds flee the treetops, seeking shelter from the icy bombardment.", + "4": "Hail ricochets off thick tree trunks, the sound echoing through the jungle.", + "5": "The storm transforms streams into icy torrents, overflowing their banks.", + "6": "Flowers and delicate plants are destroyed, their petals scattered like confetti.", + "7": "The hailstorm turns the humid jungle into a chaotic mess of falling debris.", + "8": "Tangled vines snap under the weight of accumulating ice, falling like heavy ropes.", + "9": "The air is filled with the scent of crushed vegetation and melting ice.", + "10": "The storm leaves the jungle steaming as the ice quickly melts in the heat.", + "11": "Predators use the storm's cover to move unseen, stalking their prey.", + "12": "When the storm passes, the jungle is quieter than usual, recovering from the chaos." + }, + "hills": { + "1": "Hail cascades down the slopes, leaving the grass coated in icy shards.", + "2": "The hailstorm rattles across the rolling hills, echoing through the valleys.", + "3": "Shepherds hurry to gather their flocks, the hail battering man and beast alike.", + "4": "Hailstones pelt the rocky outcrops, bouncing off and shattering noisily.", + "5": "Streams of water and ice flow down the hills, carving new paths in the soil.", + "6": "Tents and shelters struggle under the weight of accumulating hail.", + "7": "The storm drives wild animals from the hills, seeking shelter from the icy onslaught.", + "8": "Thunder rumbles as hail strikes the earth, creating a symphony of sound.", + "9": "The paths through the hills are slick with melting ice, treacherous to travelers.", + "10": "Bushes and small trees are stripped of their leaves, torn by relentless hail.", + "11": "The storm passes, leaving the hills sparkling with ice and scattered debris.", + "12": "Villagers on the hills emerge cautiously, surveying the damage left behind." + }, + "mountains": { + "1": "Hailstones batter the cliffs and peaks, cascading into the valleys below.", + "2": "The storm reduces visibility, ice and fog swirling together in the mountain air.", + "3": "Campsites in the high passes are buried under piles of icy hail.", + "4": "Mountain goats cling to the cliffs, seeking refuge from the icy bombardment.", + "5": "Echoes of the hailstorm reverberate through the crags and gorges.", + "6": "Hail piles up on narrow trails, making them slippery and dangerous.", + "7": "Icicles form almost instantly as hailstones melt and refreeze on exposed rock.", + "8": "Travelers in the mountains hunker down, braving the relentless hailstorm.", + "9": "Streams turn to torrents as hail melts and flows down the rugged slopes.", + "10": "The storm clears, leaving the peaks gleaming under a thin coat of ice.", + "11": "Avalanches of hail and snow thunder down the slopes, crushing everything in their path.", + "12": "The once-pristine mountain paths are littered with debris and frozen hailstones." + }, + "desert": { + "1": "Hail falls like stones from the sky, pocking the sand with countless craters.", + "2": "The storm creates an eerie contrast, ice piling up on the hot desert dunes.", + "3": "Cacti and scrub plants are battered, their spines no match for the relentless hail.", + "4": "The desert becomes a frozen wasteland, hailstones scattered across the sands.", + "5": "Hail ricochets off rocky outcrops, leaving tiny dents and chips behind.", + "6": "The storm is short but fierce, leaving a layer of ice shimmering in the sun.", + "7": "Nomads scramble to secure their tents as hailstones hammer down upon them.", + "8": "Dry riverbeds fill with runoff as the hail melts rapidly in the desert heat.", + "9": "Animals burrow deeper into the sand, escaping the storm’s icy wrath.", + "10": "The hailstorm moves on, leaving behind a desert dotted with chunks of ice.", + "11": "Tracks in the sand are erased, replaced by the pockmarks of falling hailstones.", + "12": "The sun re-emerges, quickly melting the hail and turning the desert back to its arid state." + }, + "coastal": { + "1": "Hail hammers the shoreline, bouncing off rocks and splashing into the waves.", + "2": "Fishing boats struggle to reach the harbor as hailstones pound the sea.", + "3": "The storm creates a layer of ice along the docks, slippery and dangerous.", + "4": "Seabirds circle overhead, their cries drowned out by the relentless storm.", + "5": "Hailstones batter the thatched roofs of coastal homes, leaving holes in their wake.", + "6": "Foam and ice mix where the hail meets the surf, creating a surreal sight.", + "7": "The storm drives fishermen and sailors indoors, abandoning their nets and gear.", + "8": "The sandy beaches are left strewn with icy shards and debris after the storm.", + "9": "Waves crash against the cliffs, mixing the sound of the sea with the pelting hail.", + "10": "The harbor bells ring out faintly through the storm, warning of treacherous conditions.", + "11": "The sky clears, but the coast remains icy and slick from the hailstorm.", + "12": "Sea spray freezes on contact, leaving a glistening layer of ice on rocks and vegetation." + }, + "volcano": { + "1": "Hailstones hiss and melt as they strike the warm volcanic slopes.", + "2": "The unusual storm creates steam clouds as hail falls into lava flows.", + "3": "Ash and ice mix in the air, creating an eerie, otherworldly haze.", + "4": "Travelers near the volcano take cover as hailstones ricochet off jagged rocks.", + "5": "The storm turns streams of lava into sizzling cauldrons as ice collides with molten rock.", + "6": "Hail piles up in cooler areas, creating icy patches on otherwise warm terrain.", + "7": "The volcano’s usual heat seems diminished as the hailstorm blankets its lower slopes.", + "8": "Steam vents hiss louder, the storm’s ice adding to the geothermal activity.", + "9": "Animals adapted to the volcanic heat retreat, confused by the icy onslaught.", + "10": "The hailstorm passes quickly, leaving patches of ice steaming under the volcanic heat.", + "11": "Rocks around the volcano are pitted and marked by the impact of the hail.", + "12": "When the skies clear, the volcano’s heat rapidly melts the remaining hail." + }, + "artic": { + "1": "The hailstorm adds another layer of ice to the already frozen tundra.", + "2": "Hailstones blend into the snow, creating a dangerous, uneven landscape.", + "3": "Polar animals huddle together, shielding themselves from the storm's icy barrage.", + "4": "Icebergs crack and groan under the impact of heavy hailstones.", + "5": "The wind drives the hail sideways, creating sharp stinging bursts of icy rain.", + "6": "The storm turns the landscape into a blur of white and gray, obscuring all visibility.", + "7": "Snow drifts are compacted by the falling hail, becoming hard and slick.", + "8": "The frozen ground is covered with a new layer of pockmarked ice from the hail.", + "9": "Travelers in the Arctic struggle to move as hail clogs their paths and weighs them down.", + "10": "The storm creates small ice dunes, sculpted by wind and hail combined.", + "11": "Hail smashes against icy cliffs, echoing across the frozen wilderness.", + "12": "The skies clear, leaving behind a harsh, glittering Arctic expanse." + }, + "cursed": { + "1": "The hailstorm is accompanied by an eerie green glow, lighting the cursed land.", + "2": "Hailstones fall unnaturally heavy, leaving deep, steaming craters in the ground.", + "3": "Whispers echo through the storm, as if the hail carries voices from beyond.", + "4": "The ice melts into a strange, foul-smelling liquid as it strikes the cursed soil.", + "5": "Hailstones seem to target travelers, pelting them relentlessly as they try to escape.", + "6": "The storm leaves behind hailstones etched with arcane symbols, cold to the touch.", + "7": "The hail burns instead of freezes, leaving strange marks on the cursed ground.", + "8": "No animals are seen during the storm, the land eerily silent but for the hail.", + "9": "The storm’s ice seems to resist melting, lingering unnaturally long in the cursed landscape.", + "10": "Travelers report seeing ghostly shapes in the hail-filled sky, fleeting and terrifying.", + "11": "When the hailstorm ends, the ground is left scarred with dark, ominous cracks.", + "12": "The cursed hail hums faintly, as if alive, before crumbling into ash-like fragments." + } + } + }, + "Heatwave": { + "conditions": { + "temperature": { "gte": 80 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 30 }, + "humidity": { "lte": 40 }, + "cloudCover": { "lte": 40 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Crops wilt under the relentless sun, the soil cracking in the heat.", + "2": "Farm animals seek shade beneath sparse trees, panting heavily.", + "3": "The air shimmers with heat, making distant objects appear wavy.", + "4": "Farmers struggle to keep their fields watered, wells running low.", + "5": "Hay stored in the barn feels dangerously warm to the touch.", + "6": "Dust clouds rise with every step, the ground dry and powdery.", + "7": "Even the breeze feels hot, carrying no relief.", + "8": "Irrigation ditches run dry, leaving crops gasping for water.", + "9": "The smell of parched earth fills the air, heavy and dry.", + "10": "Workers pause often to wipe sweat from their brows, exhaustion setting in.", + "11": "Fruit ripens quickly on the vine, threatening to spoil in the heat.", + "12": "Even the nights offer little relief, the ground radiating stored heat." + }, + "village": { + "1": "Villagers fan themselves with anything at hand, seeking relief from the heat.", + "2": "The village well sees constant use, the water level dropping steadily.", + "3": "Children play listlessly in the shade, too tired to run in the heat.", + "4": "Dogs lie flat on the ground, their tongues lolling out in the oppressive warmth.", + "5": "The air is thick and heavy, the heat making it hard to breathe.", + "6": "Sweat-soaked villagers gather under the largest tree, escaping the sun.", + "7": "The smithy is abandoned, the forge too hot to work near.", + "8": "Buckets of water are splashed on stone walls to cool them, evaporating quickly.", + "9": "Bread bakes faster in ovens, the heat from the hearths adding to the discomfort.", + "10": "Roofing materials grow soft and sag under the intense sun.", + "11": "Animals wander into the village, seeking water and respite.", + "12": "Fields of grass surrounding the village turn yellow and brittle." + }, + "city": { + "1": "Streets shimmer with heat, the cobblestones too hot to touch.", + "2": "Merchants sell fans and water at inflated prices, seeing the desperation of the people.", + "3": "City fountains are crowded with people dipping their hands and faces in the cool water.", + "4": "Even the wealthy cannot escape the heat, their mansions feeling stifling inside.", + "5": "Horses collapse in the streets, their handlers struggling to revive them.", + "6": "The smell of sweat and refuse hangs heavy in the thick, unmoving air.", + "7": "Market stalls close early, their wares wilting and spoiling in the relentless sun.", + "8": "Water barrels are rolled into the squares, drawing large, thirsty crowds.", + "9": "Shadows are crowded with people seeking relief, the sun unbearable on open streets.", + "10": "Guards along the walls faint from heat exhaustion, forcing rotations to shorten.", + "11": "The heat brings tempers to a boil, arguments flaring in the oppressive weather.", + "12": "Street performers give up their acts, unable to compete with the harsh conditions." + }, + "plains": { + "1": "The endless horizon ripples with heat, making it hard to discern distance.", + "2": "Grasslands are brittle underfoot, snapping with every step.", + "3": "Animals gather around the few waterholes, competing for the precious resource.", + "4": "The sun beats down unrelentingly, turning the plains into a blazing oven.", + "5": "A lone breeze stirs the grass, but it brings no relief, only hot air.", + "6": "The ground radiates heat, making the air above it shimmer like a mirage.", + "7": "Birds circle high above, their cries carried faintly on the still air.", + "8": "Travelers struggle to cover their heads, the sun threatening heatstroke.", + "9": "Streams run shallow, their beds cracked and parched in the heat.", + "10": "Herds of animals migrate, seeking cooler pastures far away.", + "11": "Wildfires ignite easily, spreading quickly across the dry plains.", + "12": "The sky remains cloudless, offering no hope of shade or rain." + }, + "forest": { + "1": "The heat turns the forest into a suffocating sauna, the canopy trapping the warmth.", + "2": "Leaves wilt on their branches, the heat leeching moisture from everything.", + "3": "Streams and small pools dry up, their beds littered with fallen leaves.", + "4": "The forest floor feels unusually dry, crunching underfoot with each step.", + "5": "Birdsong is muted, the heat keeping most animals quiet and still.", + "6": "The scent of resin and dry pine fills the air, mingling with the oppressive heat.", + "7": "Deer and other animals gather in shaded clearings, panting in the heat.", + "8": "Tree trunks feel warm to the touch, the bark heated by the relentless sun.", + "9": "Insects buzz lazily in the still air, their movements slowed by the heat.", + "10": "The dense foliage offers some shade, but the humidity makes it stifling.", + "11": "Small fires break out where dead leaves and dry wood catch the sun's rays.", + "12": "The forest feels eerily silent, the heat driving all creatures into hiding." + }, + "swamp": { + "1": "The swamp’s usual humidity becomes unbearable, the air thick and stifling.", + "2": "Even the water seems to boil under the relentless sun, evaporating in places.", + "3": "The heat amplifies the swamp’s natural odors, making the air almost unbreathable.", + "4": "Insects swarm in droves, the heat making them more aggressive and numerous.", + "5": "Amphibians retreat to the coolest parts of the water, disappearing from sight.", + "6": "The water level drops noticeably, revealing mudbanks and rotting vegetation.", + "7": "Thick clouds of steam rise from the swamp, turning it into a scalding cauldron.", + "8": "Travelers find their boots sticking in the drying mud, each step a struggle.", + "9": "Trees lose their luster, their bark cracking under the oppressive heat.", + "10": "The heat bakes the swamp, creating an almost desert-like crust in exposed areas.", + "11": "The swamp’s creatures grow lethargic, their movements slowed by the blazing sun.", + "12": "Pools of stagnant water grow smaller, leaving behind fish and other stranded creatures." + }, + "jungle": { + "1": "The jungle is a furnace, the air thick and oppressive under the canopy.", + "2": "Leaves glisten with evaporating moisture, their edges curling in the intense heat.", + "3": "Streams dry into cracked beds, their usual gurgling absent in the heatwave.", + "4": "Wild animals grow restless, prowling for dwindling water sources.", + "5": "The jungle floor steams, its damp earth drying rapidly under the heat.", + "6": "Even the dense foliage provides little relief, the heat penetrating every layer.", + "7": "Birds call out sporadically, their songs cut short by the oppressive air.", + "8": "Insects swarm densely, driven into a frenzy by the unrelenting heat.", + "9": "The jungle smells of decay, the heat hastening the rot of fallen plants.", + "10": "Vines sag heavily, their leaves limp from the strain of the heat.", + "11": "Predators grow bold, hunting near water sources teeming with desperate prey.", + "12": "The heatwave lingers, turning the vibrant jungle into a suffocating green prison." + }, + "hills": { + "1": "Grassy slopes turn yellow and brittle under the relentless heat.", + "2": "Shepherds guide flocks to sparse shade, their animals panting heavily.", + "3": "Streams run dry, leaving cracked beds scattered with stones.", + "4": "The sun bakes the rocky ground, making walking treacherously hot.", + "5": "Heatwaves ripple over the hills, distorting the horizon.", + "6": "The air is thick and still, carrying the faint scent of scorched grass.", + "7": "Hikers struggle to find water, sweat dripping with every step uphill.", + "8": "Wildlife retreats to hidden burrows, avoiding the intense sun.", + "9": "Distant storms seem to tease the parched hills, but no rain comes.", + "10": "Even the breeze carries heat, providing no relief from the sun.", + "11": "Rocks absorb the heat, radiating warmth long after the sun sets.", + "12": "Wildfires ignite easily, their smoke blotting out the azure sky." + }, + "mountains": { + "1": "The sun reflects harshly off rocky peaks, intensifying the heat.", + "2": "Mountain springs shrink to a trickle, their waters warmed unnaturally.", + "3": "Climbers face dehydration as heat saps their strength.", + "4": "Thin air provides no solace, the heat pressing even at higher altitudes.", + "5": "Snowcaps melt faster than expected, causing sudden streams to form.", + "6": "Wild goats and other creatures descend to shaded valleys, seeking refuge.", + "7": "The intense sun bakes exposed cliffs, causing rocks to crack and fall.", + "8": "Shadows offer some relief, but the air remains stiflingly warm.", + "9": "Mountainsides echo with the sound of melting ice, water dripping down sheer faces.", + "10": "Travelers shelter in caves, avoiding the punishing sunlight.", + "11": "Even high-altitude flora withers, its usual resilience no match for the heat.", + "12": "The heatwave stretches on, leaving the peaks eerily silent and lifeless." + }, + "desert": { + "1": "Sand shimmers under the blazing sun, the dunes nearly unbearable to touch.", + "2": "Mirages play tricks on the eyes, distant pools of water tempting the desperate.", + "3": "Oases dry up, their once-lush greenery turning brittle and brown.", + "4": "Travelers shield their faces from the relentless sun, cloths soaked in sweat.", + "5": "The heat is suffocating, making even breathing an exhausting effort.", + "6": "Scorpions and snakes retreat deep into the sand, avoiding the surface heat.", + "7": "Winds carry searing air, turning dust devils into blinding storms of grit.", + "8": "Rocks crack under the heat, their sharp edges worn smooth by the elements.", + "9": "Nightfall offers little reprieve, the ground radiating stored heat.", + "10": "The sun's glare is blinding, forcing travelers to cover their eyes.", + "11": "Camels collapse under the strain, their water stores running dangerously low.", + "12": "The desert's usual harshness is amplified, transforming it into a lethal furnace." + }, + "coastal": { + "1": "The sun beats down on the shore, turning the sand scorching hot.", + "2": "Sea breezes offer little relief, carrying the heat inland.", + "3": "Fish markets smell stronger than usual, the heat accelerating spoilage.", + "4": "Water evaporates quickly, leaving tide pools warmer than usual.", + "5": "Boats creak in the heat, their wood drying and cracking.", + "6": "The horizon ripples with heat haze, distorting distant ships.", + "7": "Fishermen sweat under wide hats, their nets heavy with effort.", + "8": "Seagulls circle lazily overhead, too drained to squabble for scraps.", + "9": "The sea itself feels warm, its cool touch a distant memory.", + "10": "Docks are empty as workers retreat to the shade of nearby taverns.", + "11": "Salt encrusts the edges of evaporated pools, glittering in the sunlight.", + "12": "Even the waves seem slower, the heat sapping their usual energy." + }, + "volcano": { + "1": "The air is suffused with heat, magnified by the volcanic terrain.", + "2": "Steam vents hiss louder than usual, the heatwave amplifying their activity.", + "3": "Lava flows glow brighter, the molten rock moving sluggishly in the heat.", + "4": "Rocks feel searingly hot underfoot, forcing careful steps.", + "5": "The smell of sulfur hangs heavy in the air, intensified by the oppressive warmth.", + "6": "Crevices in the ground radiate heat, making the area dangerously unstable.", + "7": "Wildlife is absent, fleeing to cooler areas far from the volcanic slopes.", + "8": "Pools of water near hot springs boil faster, their steam rising ominously.", + "9": "Ash from previous eruptions bakes into a solid crust in the relentless sun.", + "10": "The heatwave turns the area into a cauldron, with no escape from the sweltering air.", + "11": "Mountainsides glow faintly at night, heat stored in the rocks radiating outward.", + "12": "Fumes from the volcano grow denser, turning the already hostile environment deadly." + }, + "arctic": { + "1": "Snow and ice melt in patches, revealing the dark earth below.", + "2": "Glaciers creak and groan, shedding chunks of ice under the heat.", + "3": "Wildlife struggles to adapt, with polar bears wandering farther for food.", + "4": "Permafrost softens, turning once-solid ground into treacherous mud.", + "5": "Streams of meltwater carve new paths across the icy landscape.", + "6": "The sun reflects brightly off the melting ice, blinding travelers.", + "7": "Icebergs shrink rapidly, their edges collapsing into the sea.", + "8": "Seals and other marine creatures seek cooler waters, abandoning usual hunting grounds.", + "9": "Heat shimmers even in the Arctic, a surreal sight against the white backdrop.", + "10": "The air feels unnaturally warm, a stark contrast to the usual chill.", + "11": "Frozen lakes develop thin cracks, threatening those who dare cross them.", + "12": "Even at night, the ice fails to refreeze, the heatwave unrelenting." + }, + "cursed": { + "1": "The ground cracks underfoot, an unnatural heat rising from below.", + "2": "Shadows deepen and lengthen as the sun blazes unnaturally overhead.", + "3": "Foul odors rise from the earth, as though the land itself is rotting.", + "4": "Water sources dry up instantly, as if cursed by an unseen force.", + "5": "The heat amplifies eerie whispers, carried on the still, oppressive air.", + "6": "Plants wither and blacken, their decay spreading rapidly in the heat.", + "7": "Creatures avoid the area entirely, sensing the unnatural danger.", + "8": "The air hums faintly, as if charged with dark magic feeding on the heat.", + "9": "Structures groan and crack, the heat warping even stone foundations.", + "10": "The sun seems larger and closer, its rays oppressive and unrelenting.", + "11": "Faint screams echo through the heatwaves, their source impossible to locate.", + "12": "Time feels distorted, the oppressive heat dragging every moment into eternity." + } + } + }, + "Heavy Snowfall": { + "conditions": { + "temperature": { "lte": 40 }, + "precipitation": { "gte": 60 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Fields vanish under a blanket of deep, fresh snow.", + "2": "Barn roofs sag under the weight of the accumulating snow.", + "3": "Livestock huddle together for warmth as snow piles against the enclosures.", + "4": "Paths between farmhouses become unrecognizable, buried beneath white drifts.", + "5": "The sound of creaking timber echoes as fences bow under the heavy snow.", + "6": "Farmhands shovel tirelessly, trying to clear snow from essential areas.", + "7": "The world feels muffled, the heavy snow dampening all sound.", + "8": "Icicles form on the edges of rooftops as snow continues to fall.", + "9": "Trudging through the snow takes twice the effort, each step sinking deeply.", + "10": "Crops in storage are heavily relied upon as outdoor fields remain inaccessible.", + "11": "Smoke from chimneys rises into the snowy sky, a sign of life amidst the cold.", + "12": "Children play briefly in the snow before the chill sends them indoors." + }, + "village": { + "1": "Snowdrifts climb high against the sides of the cottages.", + "2": "Villagers clear paths between homes, their breath visible in the cold air.", + "3": "Shutters are tightly drawn to keep out the persistent snowfall.", + "4": "The village well is nearly hidden, requiring constant digging to access.", + "5": "Candles flicker inside, the snow casting a blue glow through windows.", + "6": "Children peer out, longing to play, but the storm is too fierce.", + "7": "Heavy snow muffles the usual hustle and bustle of the village square.", + "8": "The inn’s hearth becomes a gathering point for warmth and camaraderie.", + "9": "Horses are stabled early, their manes frosted with snowflakes.", + "10": "The village market closes, its stalls buried under the falling snow.", + "11": "Smoke rises steadily from chimneys, a sign of warmth and preparation.", + "12": "Lanterns light the snow-covered streets, their glow reflecting off the white." + }, + "city": { + "1": "Snow piles against the city gates, making entry difficult for travelers.", + "2": "The main streets become choked with snow, slowing movement to a crawl.", + "3": "City guards work to clear paths for wagons, their cloaks dusted with snow.", + "4": "Merchants complain as stalls and wares are hidden under thick snow.", + "5": "The city square turns into a winter wonderland, blanketed in white.", + "6": "Snowflakes swirl endlessly between towering stone buildings.", + "7": "Icicles dangle from eaves, some large enough to be dangerous.", + "8": "Cathedral bells ring faintly through the muffling snowfall.", + "9": "Children laugh as they build snowmen in open courtyards.", + "10": "Bakers keep their ovens roaring, enticing citizens with warmth and smells.", + "11": "The city’s fountains freeze solid, transforming into natural ice sculptures.", + "12": "Nightfall brings a magical glow as torches light up the snowy streets." + }, + "plains": { + "1": "Snow sweeps across the open plains, leaving no landmark uncovered.", + "2": "Winds drive the snow into drifts, creating waves of white across the landscape.", + "3": "The flat expanse becomes featureless, blending horizon and ground into one.", + "4": "Travelers struggle to navigate, their tracks vanishing almost instantly.", + "5": "Sparse trees stand like lonely sentinels, their branches heavy with snow.", + "6": "The plains are eerily quiet, save for the whisper of falling snow.", + "7": "Wild animals dig frantically for food beneath the snow-covered ground.", + "8": "Frozen grasses crackle underfoot, buried beneath an icy crust.", + "9": "Snowfall obscures the sun, turning day into a muted twilight.", + "10": "Shepherds huddle with their flocks, seeking what little shelter can be found.", + "11": "Snow buries natural trails, forcing travelers to rely on instinct alone.", + "12": "The horizon becomes a blur as snowfall thickens, swallowing the distance." + }, + "forest": { + "1": "Tree branches sag under the weight of heavy snow, some snapping with loud cracks.", + "2": "The forest floor is hidden beneath layers of untouched snow.", + "3": "Animal tracks dot the snowy ground, hinting at unseen movement.", + "4": "Icicles dangle from every branch, glittering like crystals in the pale light.", + "5": "Snowfall muffles every sound, creating a deep and profound silence.", + "6": "The forest feels otherworldly, transformed into a white and gray expanse.", + "7": "Paths disappear under the snow, leaving travelers disoriented and wary.", + "8": "Birds huddle silently in the trees, their feathers puffed against the cold.", + "9": "Small streams freeze, their surfaces glistening beneath the falling snow.", + "10": "Tree trunks stand stark against the white backdrop, their bark frosted.", + "11": "The snowfall is relentless, burying everything in its path.", + "12": "A rare patch of sunlight breaks through the clouds, reflecting off the snow." + }, + "swamp": { + "1": "Snow settles awkwardly on the swampy ground, mixing with slush and ice.", + "2": "Frozen reeds crackle as the snow weighs them down.", + "3": "Pools of water freeze over, their surfaces coated with thin layers of snow.", + "4": "The air smells damp and cold, carrying the swamp's unique earthy scent.", + "5": "Snow obscures the treacherous terrain, making travel perilous.", + "6": "Amphibians retreat to burrows, the swamp eerily devoid of sound.", + "7": "Moss hangs frozen from trees, shimmering with a frosty sheen.", + "8": "Dead trees stand like ghosts, their bark frosted and stark in the storm.", + "9": "The swamp's usual mists freeze, hanging low over the snowy ground.", + "10": "Travelers step carefully, their boots crunching through snow and frozen mud.", + "11": "The snowstorm transforms the swamp into a hauntingly beautiful landscape.", + "12": "Small streams run slower, their surfaces partially frozen under the snowfall." + }, + "jungle": { + "1": "Snow falls unevenly, sticking to the dense canopy above.", + "2": "The jungle's vibrant green fades beneath patches of white frost.", + "3": "Humidity battles the cold, creating an unusual mix of ice and mist.", + "4": "Leaves droop heavily under the unexpected weight of snow.", + "5": "Paths become treacherous as snow hides roots and slippery mud.", + "6": "Jungle creatures fall silent, confused by the sudden chill.", + "7": "Snow melts quickly in some areas, creating streams of icy runoff.", + "8": "Bird calls echo faintly, their usual energy muted by the cold.", + "9": "The dense undergrowth becomes impassable, tangled with ice and snow.", + "10": "Frost clings to vines and leaves, glinting in the dim jungle light.", + "11": "Steam rises where the snow meets warm ground, creating eerie clouds.", + "12": "Snowfall transforms the jungle into an alien landscape, still and hushed." + }, + "hills": { + "1": "Rolling hills vanish under a thick, white blanket of snow.", + "2": "Snow accumulates on rocky outcrops, creating treacherous footing.", + "3": "The wind whips snow across the hills, reducing visibility.", + "4": "Shepherds struggle to guide their flocks through the deep snow.", + "5": "Paths between hills are buried, making navigation difficult.", + "6": "Snowdrifts pile high against hillsides, forming natural barriers.", + "7": "The hills are eerily silent, the snow dampening all sound.", + "8": "Sparse trees on the slopes bow under the weight of snow.", + "9": "Animal trails are obscured as fresh snow covers the land.", + "10": "Icicles form along cliff edges, shimmering in the pale light.", + "11": "Travelers huddle against the cold as snow blankets the area.", + "12": "Snow swirls endlessly in the valleys, driven by icy winds." + }, + "mountains": { + "1": "Snow cascades down steep cliffs, creating small avalanches.", + "2": "Mountain peaks are lost in a veil of heavy snowfall.", + "3": "Paths become impassable as snow piles up on narrow trails.", + "4": "The howling wind carries snow, cutting through the thin air.", + "5": "Ice forms quickly on exposed rocks, making climbing hazardous.", + "6": "Shelters are buried under snow, their roofs barely visible.", + "7": "The sound of cracking ice echoes ominously through the peaks.", + "8": "Mountain goats navigate the snow with surprising ease.", + "9": "Deep snowdrifts fill mountain passes, blocking all travel.", + "10": "Snow obscures cairns and markers, leaving climbers disoriented.", + "11": "The air grows colder with every flake, sapping strength.", + "12": "Snow crystals sparkle faintly in the weak mountain sunlight." + }, + "desert": { + "1": "Snow covers the sand dunes, a rare and surreal sight.", + "2": "The desert’s golden sands turn white under heavy snowfall.", + "3": "Cacti wear frosty crowns as snow settles on their spines.", + "4": "Snowflakes swirl in the wind, clashing with the arid landscape.", + "5": "Tracks in the sand disappear under a fresh blanket of snow.", + "6": "The desert air turns frigid, an unusual chill seeping through.", + "7": "Snow clings to rocky outcroppings, transforming their appearance.", + "8": "The horizon blurs as snow obscures distant dunes.", + "9": "Nomads huddle by their fires, astonished by the snowfall.", + "10": "Frozen sand crunches beneath boots, an alien sensation.", + "11": "Snow and sand mix, creating a strange, patchy landscape.", + "12": "The desert is silent, the snowfall dampening all sound." + }, + "coastal": { + "1": "Snow falls into the ocean, melting as it touches the waves.", + "2": "Fishing boats sit idle, their decks buried under snow.", + "3": "Icicles form on piers, glinting in the gray light.", + "4": "The beach is unrecognizable, blanketed in white.", + "5": "Seagulls circle above, their cries muffled by the storm.", + "6": "Waves crash against snow-laden rocks, creating icy spray.", + "7": "The coastal air is sharp and cold, filled with salty snow.", + "8": "Snowdrifts form along the dunes, blending sea and land.", + "9": "Fishing nets freeze stiff as snow settles on them.", + "10": "The lighthouse beam cuts through the swirling snowstorm.", + "11": "Snow piles up against seaside cottages, blocking doorways.", + "12": "Frozen seafoam coats the shore, mingling with the snow." + }, + "volcano": { + "1": "Snow settles on the volcanic slopes, an odd contrast to the dark rock.", + "2": "Steam rises as snow meets hot vents, creating dense fog.", + "3": "Lava flows cool under a fresh blanket of snow, hissing softly.", + "4": "The crater rim is white with snow, but warmth emanates from below.", + "5": "The barren landscape is transformed, softened by heavy snowfall.", + "6": "Ash and snow mix, forming a strange gray slush.", + "7": "Fumaroles hiss, their heat melting nearby snow.", + "8": "Snow muffles the usual sounds of the volcanic terrain.", + "9": "The air is filled with a mix of sulfur and crisp snow-laden winds.", + "10": "Footing becomes treacherous as snow hides sharp volcanic rocks.", + "11": "Snow piles in crevices, contrasting sharply with blackened stone.", + "12": "The volcano appears dormant, shrouded in a thick blanket of snow." + }, + "artic": { + "1": "Snow piles relentlessly, burying all but the tallest icebergs.", + "2": "The arctic expanse becomes a uniform white under heavy snowfall.", + "3": "Snow and ice blend seamlessly, erasing the horizon.", + "4": "Polar bears and seals vanish into the snow-covered landscape.", + "5": "Snowdrifts form natural barriers, impeding movement across the tundra.", + "6": "The air is thick with snow, visibility reduced to mere feet.", + "7": "Ice floes crack under the weight of the accumulating snow.", + "8": "Auroras shimmer faintly through the snowy skies.", + "9": "Tracks disappear instantly, swallowed by the falling snow.", + "10": "The ice glows faintly blue beneath layers of fresh snow.", + "11": "Howling winds carry snow across the barren landscape.", + "12": "Even the ocean appears frozen as snow sweeps across its surface." + }, + "cursed": { + "1": "Snow falls blackened and ashen, an omen of ill tidings.", + "2": "The snow carries whispers, voices that fade upon approach.", + "3": "Footprints in the snow vanish moments after they’re made.", + "4": "Snowflakes sting like needles, carrying an unnatural cold.", + "5": "The snow seems to pulse, faintly glowing with a malevolent light.", + "6": "Heavy snow muffles all sound, yet distant screams can be heard.", + "7": "Icicles form rapidly, dripping a dark, tar-like substance.", + "8": "Snow piles up unnaturally, defying logic and balance.", + "9": "The air tastes metallic, the snow tinged with a faint crimson hue.", + "10": "Shadows dart beneath the snow, their shapes indistinct and fleeting.", + "11": "The snow twists and spirals, as if alive and sentient.", + "12": "Strange patterns appear in the snow, only to fade moments later." + } + } + }, + "Ice Storm": { + "conditions": { + "temperature": { "gte": 20, "lte": 40 }, + "precipitation": { "gte": 70 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Fields are coated in a thick layer of ice, rendering them lifeless.", + "2": "The barn roof creaks under the weight of frozen sleet.", + "3": "Ice encases scarecrows, turning them into eerie statues.", + "4": "Farm tools are frozen solid, stuck where they were left.", + "5": "Icicles hang from fences, glinting in the faint light.", + "6": "Livestock huddle together, their breaths forming icy clouds.", + "7": "Frozen ground cracks loudly as the ice storm intensifies.", + "8": "Every tree in the orchard glimmers with a dangerous coat of ice.", + "9": "Paths to the barn are treacherous, slick with black ice.", + "10": "The ice storm batters the fields, breaking brittle crops.", + "11": "A shimmering glaze of ice covers the farmhouse windows.", + "12": "Frosted windmills stand motionless, frozen in place." + }, + "village": { + "1": "Cobblestone streets are slick with a deadly sheen of ice.", + "2": "Villagers struggle to walk as icy sleet lashes their faces.", + "3": "Shutters rattle and freeze shut under the storm’s force.", + "4": "Roof tiles crack and fall under the relentless ice.", + "5": "Frozen laundry hangs stiffly, coated in glistening frost.", + "6": "Lanterns flicker weakly, their light distorted by frozen glass.", + "7": "The village well is sealed over with a thick sheet of ice.", + "8": "Children watch the storm in awe, their windows glazed with frost.", + "9": "The church bell is silent, its rope frozen stiff.", + "10": "Smoke struggles to rise from chimneys, weighed down by icy winds.", + "11": "Icicles form rapidly, dangling menacingly from roofs.", + "12": "The air rings with the sound of cracking ice as trees snap." + }, + "city": { + "1": "The marketplace is abandoned, its stalls glazed in ice.", + "2": "The city's grand fountain is frozen solid, transformed into an icy monument.", + "3": "Cobblestones are treacherous as the ice storm worsens.", + "4": "Windows crack under the pressure of frozen sleet.", + "5": "Watchmen struggle to keep their footing on icy walls.", + "6": "The cathedral's spire gleams under a coat of crystalline frost.", + "7": "Frozen gutters send icicles cascading down onto empty streets.", + "8": "Merchants’ carts are immovable, encased in thick layers of ice.", + "9": "Even the busiest streets fall silent under the icy onslaught.", + "10": "Bridges are dangerously slick, coated in layers of ice.", + "11": "The sound of cracking ice echoes through the alleys.", + "12": "Statues in the plaza appear otherworldly, cloaked in shimmering frost." + }, + "plains": { + "1": "The plains are a vast, glittering expanse of ice.", + "2": "Grass blades stand frozen, encased in delicate layers of frost.", + "3": "The storm howls across the open land, biting and relentless.", + "4": "Animal tracks vanish as sleet freezes over the earth.", + "5": "Ice sheets stretch endlessly, reflecting pale sunlight.", + "6": "Small streams freeze solid, their surfaces polished and smooth.", + "7": "The sound of cracking ice punctuates the storm's roar.", + "8": "Lonely trees bow under the weight of encroaching ice.", + "9": "Snowy winds sweep across the plains, biting into exposed skin.", + "10": "Herds of animals scatter, seeking refuge from the biting storm.", + "11": "Frozen earth makes travel slow and treacherous.", + "12": "Every rock and shrub is coated in a glistening layer of ice." + }, + "forest": { + "1": "Trees groan and crack as ice weighs heavily on their branches.", + "2": "Icicles form in dense clusters, hanging from every branch.", + "3": "The forest floor is a dangerous sheet of frozen debris.", + "4": "Frozen leaves snap underfoot as the ice storm intensifies.", + "5": "Animals retreat to burrows, avoiding the biting cold.", + "6": "The storm’s wind whistles eerily through the frozen canopy.", + "7": "The forest sparkles under a heavy coat of ice, almost magical.", + "8": "Every twig is glazed in frost, creating a crystal-like forest.", + "9": "Branches snap and fall, sending shards of ice flying.", + "10": "Streams and ponds freeze over, their surfaces eerily still.", + "11": "The forest is silent except for the constant sound of ice breaking.", + "12": "Ice-laden branches form an impassable wall in places." + }, + "swamp": { + "1": "The swamp freezes over, its murky waters sealed beneath ice.", + "2": "Reeds and cattails bend under the weight of accumulating ice.", + "3": "Frozen moss glistens on ancient trees, transformed by the storm.", + "4": "The sound of cracking ice fills the air as the swamp hardens.", + "5": "Frost coats the swamp, turning it into a treacherous maze.", + "6": "Icy fog lingers low, obscuring the frozen waters beneath.", + "7": "The swamp’s usual noises are silenced by the biting cold.", + "8": "Every step threatens to break through thin sheets of ice.", + "9": "Boggy pools turn into reflective patches of frozen water.", + "10": "Vines and roots are covered in thick layers of frost.", + "11": "The air smells of decay, frozen in time by the storm.", + "12": "Frozen bubbles are trapped beneath sheets of ice, oddly still." + }, + "jungle": { + "1": "The jungle transforms as ice encases its vibrant flora.", + "2": "Frozen vines hang heavy, snapping under their own weight.", + "3": "Exotic birds fall silent, huddling for warmth amid the storm.", + "4": "Glistening icicles dangle from the dense jungle canopy.", + "5": "The jungle floor becomes slick and treacherous with ice.", + "6": "Frozen waterfalls create glittering sculptures amidst the foliage.", + "7": "Humidity gives way to biting cold, an eerie transformation.", + "8": "Leaves droop under the weight of accumulating frost.", + "9": "Even the jungle’s thick undergrowth cannot escape the icy glaze.", + "10": "Creeping vines are frozen mid-reach, stiff and brittle.", + "11": "The sound of shattering ice echoes through the dense jungle.", + "12": "Frost-tipped ferns glimmer under a pale, icy light." + }, + "hills": { + "1": "Rolling hills are transformed into treacherous ice-covered slopes.", + "2": "Small streams freeze mid-flow, glistening in the pale light.", + "3": "Grass blades stiffen, coated in a thick layer of frost.", + "4": "Icicles dangle precariously from rocky outcrops.", + "5": "The wind drives icy sleet into every crevice of the hills.", + "6": "Shepherds struggle to reach their flocks as paths ice over.", + "7": "Low stone walls are slick and treacherous under the icy barrage.", + "8": "Hilltop trees bow under the weight of accumulating ice.", + "9": "Fog mixes with freezing rain, cloaking the hills in an eerie veil.", + "10": "Animal tracks disappear, sealed under layers of frozen sleet.", + "11": "Hollows in the hills turn into pools of frozen water.", + "12": "The sound of cracking ice echoes as stones shift under its weight." + }, + "mountains": { + "1": "Peaks glisten dangerously as the storm coats them in ice.", + "2": "Sheer cliffs become death traps, slick with frozen rain.", + "3": "Mountain passes are sealed off by cascading ice flows.", + "4": "Icicles form rapidly on jagged rocks, pointing like icy daggers.", + "5": "Freezing winds howl through the crags, carrying shards of ice.", + "6": "The storm transforms waterfalls into frozen cascades.", + "7": "Thin ledges are coated in treacherous layers of frost.", + "8": "Snow is compacted under the ice storm, forming a lethal glaze.", + "9": "Mountain goats struggle to maintain footing on icy ridges.", + "10": "Shelters in the mountains are sealed by layers of frozen sleet.", + "11": "Frost creeps along every surface, freezing ropes and tools.", + "12": "The peaks are silent, muffled by the thick layer of ice." + }, + "desert": { + "1": "The desert’s sands harden into a slick sheet of ice.", + "2": "Cacti shimmer, coated in an unexpected layer of frost.", + "3": "Wind drives freezing rain across the dunes, forming icy ridges.", + "4": "The rare desert flora bends under the weight of accumulating ice.", + "5": "Frozen sleet crusts over the barren ground, reflecting the pale light.", + "6": "Dry riverbeds glisten as freezing rain pools and solidifies.", + "7": "The air feels heavy with icy moisture, an alien sensation in the desert.", + "8": "The storm transforms the dunes into jagged frozen waves.", + "9": "Lizards and snakes retreat into burrows, avoiding the biting ice.", + "10": "The desert feels otherworldly, shimmering under a frozen glaze.", + "11": "Icy winds sweep over the sands, carrying shards of frozen sleet.", + "12": "Even the driest shrubs are encased in delicate ice crystals." + }, + "coastal": { + "1": "Waves crash against icy shores, freezing mid-splash.", + "2": "Fishing boats are encased in layers of frost, their sails stiff.", + "3": "Seagulls circle above, their cries muted by the icy storm.", + "4": "Jetties are slick with frozen rain, rendering them impassable.", + "5": "Salt spray mixes with sleet, forming jagged ice along the coast.", + "6": "Tide pools freeze over, trapping sea creatures in suspended animation.", + "7": "Fishermen’s nets stiffen and freeze, unusable in the storm.", + "8": "Ice forms along the cliff edges, making them perilously fragile.", + "9": "The sea roars, waves dark and angry beneath a veil of freezing rain.", + "10": "Frozen kelp washes ashore, encased in translucent ice.", + "11": "The lighthouse beacon struggles to shine through layers of frost.", + "12": "Harbors fall silent, the water thick with freezing sleet." + }, + "volcano": { + "1": "Ice clashes with heat as volcanic vents steam under freezing rain.", + "2": "Lava flows hiss and crackle, encased in thin layers of ice.", + "3": "Volcanic rocks are slick and treacherous under the icy onslaught.", + "4": "Steam clouds rise as freezing rain hits the warm ground.", + "5": "The storm coats volcanic ash, transforming it into brittle ice.", + "6": "The crater’s rim glistens, encased in jagged frost.", + "7": "Magma pools bubble beneath a fragile glaze of ice.", + "8": "Frozen rain coats fumaroles, their steam rising eerily.", + "9": "Lava tubes are blocked as ice seals the openings.", + "10": "Icicles form rapidly on jagged volcanic outcrops.", + "11": "The storm silences the usual rumble of volcanic activity.", + "12": "Paths to the summit are impassable, sheathed in layers of frozen sleet." + }, + "artic": { + "1": "Endless ice fields are battered by the relentless storm.", + "2": "Glaciers creak and groan, coated in layers of fresh ice.", + "3": "Frozen tundra becomes even more unforgiving under the storm’s wrath.", + "4": "Icebergs shimmer as freezing rain adds to their icy bulk.", + "5": "Blinding sleet drives across the snow, obscuring visibility.", + "6": "The ground is a dangerous sheet of ice, cracking underfoot.", + "7": "Polar bears retreat into dens, avoiding the biting storm.", + "8": "The storm layers frost over frost, deepening the cold.", + "9": "Every breath freezes in the air, falling as ice crystals.", + "10": "Snow dunes harden into icy ridges, unforgiving and sharp.", + "11": "Frozen lakes shimmer under the storm, their surfaces brittle and cracked.", + "12": "The artic becomes eerily quiet, muffled by layers of frost." + }, + "cursed": { + "1": "Icy winds howl, carrying whispers of the cursed dead.", + "2": "Black ice spreads unnaturally, glinting with an eerie light.", + "3": "The storm feels alive, its icy breath clawing at the land.", + "4": "Frozen rain solidifies into jagged shards, sharp as daggers.", + "5": "Cursed ground turns brittle, shattering under the storm’s weight.", + "6": "The ice glows faintly, as though imbued with a malevolent force.", + "7": "Structures groan and crack, consumed by the cursed ice.", + "8": "Even the air feels hostile, freezing breath into choking frost.", + "9": "Ice forms grotesque patterns, resembling faces frozen mid-scream.", + "10": "Dark figures seem to move within the frozen mist.", + "11": "The storm brings silence, broken only by the echo of cracking ice.", + "12": "Every surface is coated in frost, etched with cursed runes." + } + } + }, + "Lightning Storm": { + "conditions": { + "temperature": { "gte": 30, "lte": 70 }, + "precipitation": { "gte": 40, "lte": 80 }, + "wind": { "gte": 40, "lte": 70 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 80 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Blinding lightning illuminates the fields, thunder shaking the ground.", + "2": "Barn roofs rattle as the storm's fury sweeps across the farmland.", + "3": "Animals scatter, frightened by the crackling bolts overhead.", + "4": "Lightning strikes a lone tree, setting it ablaze in the distance.", + "5": "Fences splinter under the intense winds accompanying the storm.", + "6": "Rain lashes down as bolts streak through the darkened skies.", + "7": "The farmhouse windows glow briefly with the flash of nearby lightning.", + "8": "Cornstalks bend and sway violently under the storm’s relentless force.", + "9": "Hail mingles with rain as the storm intensifies across the fields.", + "10": "Distant thunder rolls, echoing through the open countryside.", + "11": "Pools of water reflect the jagged streaks of lightning overhead.", + "12": "The storm lingers, its fury refusing to abate over the farm." + }, + "village": { + "1": "Lightning illuminates the narrow cobblestone streets, casting eerie shadows.", + "2": "Thatched roofs tremble as thunder rumbles above the village.", + "3": "Villagers huddle indoors, the storm raging fiercely outside.", + "4": "A bolt strikes the village square, charring the cobbles.", + "5": "Lightning dances across the rooftops, illuminating the bell tower.", + "6": "Rainwater floods the narrow lanes as the storm intensifies.", + "7": "Thunder cracks directly overhead, shaking the small homes to their frames.", + "8": "The village well overflows with rain, reflecting the storm’s fury.", + "9": "Horses whinny and rear in fear as lightning streaks the sky.", + "10": "The wind howls, driving rain through any cracks in the wooden shutters.", + "11": "The scent of ozone fills the air after each nearby lightning strike.", + "12": "The storm moves slowly, its relentless fury battering the village." + }, + "city": { + "1": "Lightning illuminates the towering walls, casting sharp shadows over the city.", + "2": "Rain pours in sheets, flooding the streets and alleyways.", + "3": "Thunder booms, rattling the heavy gates and echoing through the city square.", + "4": "Bolts streak across the sky, illuminating the cathedral spire.", + "5": "Merchants scramble to cover their wares as the storm drenches the marketplace.", + "6": "Citizens rush for cover, slipping on the wet cobblestones.", + "7": "A lightning strike briefly illuminates the watchtower, sending sparks flying.", + "8": "Gutters overflow, sending torrents of water cascading into the lower streets.", + "9": "The air hums with electricity, the storm’s power felt in every corner of the city.", + "10": "Torches struggle to stay lit as wind and rain lash the city walls.", + "11": "Windows shake violently with each crack of thunder.", + "12": "The city falls eerily quiet between flashes, the storm’s energy palpable." + }, + "plains": { + "1": "Lightning streaks across the flat horizon, a dazzling spectacle.", + "2": "Thunder rolls endlessly, echoing over the vast open plains.", + "3": "The wind whips through the tall grasses, bending them low.", + "4": "A lightning strike sets a small patch of grass ablaze in the distance.", + "5": "Rain lashes down relentlessly, turning the dirt into slippery mud.", + "6": "Animals scatter, fleeing from the storm’s fury as lightning dances above.", + "7": "The horizon is lit in rapid succession, the storm moving steadily across the plains.", + "8": "Pools of water reflect the jagged lightning bolts.", + "9": "The sky is a swirling mass of black clouds, broken by sudden flashes.", + "10": "Every rumble of thunder feels closer, the storm bearing down.", + "11": "The plains seem endless under the constant barrage of lightning.", + "12": "The storm lingers, its chaotic energy roiling over the wide-open land." + }, + "forest": { + "1": "Lightning illuminates the dense canopy, casting brief shadows.", + "2": "Trees sway dangerously, branches snapping under the storm’s force.", + "3": "Rainwater pours off leaves, forming rivulets along the forest floor.", + "4": "A bolt strikes a tree, splitting it with a deafening crack.", + "5": "The forest air is thick with the scent of ozone and damp earth.", + "6": "Thunder rolls through the woods, reverberating among the trunks.", + "7": "Animals scurry for cover, startled by the unrelenting storm.", + "8": "Lightning briefly reveals the shapes of creatures hidden among the trees.", + "9": "The forest floor becomes a quagmire as rainwater collects in low spots.", + "10": "Every sound is magnified, the storm's fury echoing through the forest.", + "11": "The dense canopy offers little protection from the wind and rain.", + "12": "The storm moves slowly, its relentless power shaking the forest." + }, + "swamp": { + "1": "Lightning dances over the murky waters, illuminating the mist.", + "2": "Thunder rolls deeply, rattling the stagnant air of the swamp.", + "3": "Rain churns the muddy ground, turning paths into treacherous mire.", + "4": "A bolt strikes a lone cypress, its branches splintering loudly.", + "5": "The swamp glows faintly under the frequent flashes of lightning.", + "6": "Water pools ripple violently with every gust of wind.", + "7": "Frogs and insects fall silent, the storm dominating the swamp.", + "8": "Lightning reflects off the surface of dark, still water.", + "9": "The storm turns the air heavy, each breath feeling damp and oppressive.", + "10": "Trees groan under the wind, their roots straining in the soggy ground.", + "11": "The scent of wet earth and decaying plants fills the air.", + "12": "The swamp seems alive, every corner stirred by the storm’s wrath." + }, + "jungle": { + "1": "Lightning cuts through the dense canopy, momentarily revealing the jungle below.", + "2": "Thunder shakes the ground, echoing through the thick foliage.", + "3": "Rain pours relentlessly, forming streams along the jungle floor.", + "4": "A bolt strikes a massive tree, sending splinters flying.", + "5": "The jungle buzz is silenced, overwhelmed by the storm’s power.", + "6": "Leaves glisten with rain as the storm intensifies above.", + "7": "Lightning reflects in pools of water, lighting the undergrowth.", + "8": "The storm bends the tallest trees, their tops swaying violently.", + "9": "The humid air is thick, every flash of lightning adding to its weight.", + "10": "Vines whip in the wind, snapping like cords under the strain.", + "11": "The jungle feels chaotic, every sound magnified by the storm.", + "12": "Animals screech and roar, their cries lost in the storm’s roar." + }, + "hills": { + "1": "Lightning forks across the rolling hills, each strike briefly illuminating the landscape.", + "2": "Thunder echoes endlessly, rolling over the crests and valleys.", + "3": "Rainwater cascades down slopes, carving rivulets through the soft earth.", + "4": "A lone tree atop a hill is struck by lightning, splitting it in half.", + "5": "The wind howls, bending the grasses and whipping the rain sideways.", + "6": "Lightning streaks from cloud to cloud, turning the night into a flickering stormscape.", + "7": "Shepherds scramble to shelter their flocks as the storm intensifies.", + "8": "The hills seem alive, each rumble of thunder vibrating through the ground.", + "9": "Lightning briefly illuminates a distant tower, its silhouette stark against the storm.", + "10": "Raindrops pelt the exposed terrain, soaking the hillsides quickly.", + "11": "The scent of wet earth and ozone fills the air as the storm rages on.", + "12": "Thunder cracks overhead, startling creatures hiding in the grassy knolls." + }, + "mountains": { + "1": "Lightning strikes the mountain peaks, sending showers of sparks and stone tumbling.", + "2": "Thunder reverberates through the crags, shaking loose rocks down the slopes.", + "3": "Clouds shroud the peaks, flashes of lightning glowing within their depths.", + "4": "Rain lashes the mountainside, turning trails into treacherous torrents.", + "5": "A bolt strikes a cliff face, leaving a scorched mark on the rock.", + "6": "Echoes of the storm bounce endlessly between the towering peaks.", + "7": "Mountain goats cling to ledges, braving the storm’s relentless fury.", + "8": "The air feels charged, every gust carrying a hint of electricity.", + "9": "Lightning illuminates distant ridges, revealing their jagged silhouettes.", + "10": "Rainwater cascades down in sudden waterfalls, fed by the relentless storm.", + "11": "The storm’s power is magnified, its intensity heightened at the mountain’s heights.", + "12": "Cracks of thunder shake loose boulders, sending them crashing into the valleys below." + }, + "desert": { + "1": "Lightning flashes illuminate the endless dunes, casting long shadows.", + "2": "Thunder crashes, its deep rumble rolling across the barren sands.", + "3": "The dry air hums with electricity as the storm gathers above.", + "4": "Rare raindrops spatter the hot sand, hissing as they evaporate.", + "5": "A bolt strikes a dune, scattering sand into the air in a brilliant plume.", + "6": "The wind howls, whipping sand into swirling, stinging clouds.", + "7": "Lightning briefly reveals a caravan, their silhouettes moving quickly for shelter.", + "8": "The storm seems otherworldly, its fury amplified in the vast emptiness.", + "9": "The sky churns with dark clouds, broken only by dazzling flashes.", + "10": "Thunder echoes like a drumbeat, the sound carrying for miles.", + "11": "Ozone fills the air, sharp and biting in the heat of the desert.", + "12": "The storm’s violence is a stark contrast to the desert’s usual silence." + }, + "coastal": { + "1": "Lightning strikes the waves, sending up great plumes of steam and spray.", + "2": "Thunder rolls across the shore, mingling with the crash of waves.", + "3": "Ships in the harbor rock violently, their masts illuminated by the storm.", + "4": "Winds whip the surf into a frenzy, sending salt spray far inland.", + "5": "A bolt strikes the lighthouse, briefly casting the storm in eerie relief.", + "6": "Rain lashes the docks, soaking fishermen scrambling for shelter.", + "7": "The sea glows faintly with each flash, its surface churning violently.", + "8": "Seabirds scream as they fight to stay aloft in the howling winds.", + "9": "Foam-topped waves crash against the cliffs, the storm unrelenting.", + "10": "The horizon flickers with lightning, the storm stretching endlessly over the ocean.", + "11": "Salt and ozone fill the air, thick and heavy with the storm’s fury.", + "12": "The storm’s power feels unstoppable, its energy pulsing with the tides." + }, + "volcano": { + "1": "Lightning streaks across the smoky sky, illuminating the volcanic ash clouds.", + "2": "Thunder mingles with the low rumble of the restless volcano.", + "3": "The ground trembles underfoot as the storm and volcano seem to compete.", + "4": "Bolts strike the rocky slopes, their light casting eerie shadows on the lava flows.", + "5": "Ash swirls in the gusting wind, adding to the storm’s chaos.", + "6": "Rain turns the volcanic ash to sludge, making footing treacherous.", + "7": "The sky glows intermittently, flashes of lightning blending with volcanic eruptions.", + "8": "The air is thick with sulfur and ozone, oppressive and suffocating.", + "9": "Lightning dances along the crater’s edge, its light reflected in molten pools.", + "10": "Rocks dislodged by the storm crash down the volcano’s slopes.", + "11": "Thunder echoes through the volcanic valley, loud and menacing.", + "12": "The storm’s fury seems drawn to the volcano, their powers merging ominously." + }, + "artic": { + "1": "Lightning streaks across the icy expanse, briefly revealing jagged glaciers.", + "2": "Thunder rumbles, muted by the snow but still shaking the frozen ground.", + "3": "Blinding flashes of light reflect off the ice, dazzling and disorienting.", + "4": "The storm drives snow and ice before it, turning the world into a white blur.", + "5": "Bolts strike frozen cliffs, sending shards of ice flying into the air.", + "6": "The freezing wind howls, carrying the storm’s wrath across the tundra.", + "7": "Auroras dance faintly behind the storm clouds, their colors briefly visible between flashes.", + "8": "The snow glows eerily with each lightning strike, the landscape alien and foreboding.", + "9": "Icicles shatter as the storm’s fury shakes even the most frozen places.", + "10": "Thunder cracks loudly, echoing endlessly across the barren expanse.", + "11": "The storm’s energy adds to the already harsh, biting cold.", + "12": "The storm passes slowly, leaving the icy world battered and silent once more." + }, + "cursed": { + "1": "Lightning strikes with unnatural precision, as if guided by a malevolent force.", + "2": "Thunder echoes with an eerie, otherworldly tone, shaking the cursed ground.", + "3": "The storm’s lightning illuminates twisted, haunting shapes in the cursed landscape.", + "4": "Rain falls black and thick, pooling like ink in the uneven terrain.", + "5": "Bolts of unnatural green light split the sky, casting an ominous glow.", + "6": "The wind carries whispers, chilling and incomprehensible, as the storm rages on.", + "7": "The ground trembles with each thunderclap, as if the earth itself recoils.", + "8": "Dark shapes move in the storm’s shadow, visible only in fleeting lightning flashes.", + "9": "The air feels heavy, laden with both electricity and unspoken dread.", + "10": "The storm’s lightning casts fleeting, horrifying shadows on cursed structures.", + "11": "Each thunderclap feels like a pulse, syncing with the cursed land’s own rhythm.", + "12": "The storm lingers, its presence oppressive and filled with an unnatural menace." + } + } + }, + "Polar Vortex": { + "conditions": { + "temperature": { "lte": 10 }, + "precipitation": { "lte": 20 }, + "wind": { "gte": 50, "lte": 80 }, + "humidity": { "lte": 40 }, + "cloudCover": { "gte": 40, "lte": 70 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "The fields are frozen solid, with a biting wind scouring the landscape.", + "2": "Livestock huddle together, their breath forming frost in the frigid air.", + "3": "Icicles hang from the eaves of barns, growing longer by the hour.", + "4": "Farmers struggle to break through the thick ice covering the water troughs.", + "5": "Snow drifts pile high against fences, making movement near impossible.", + "6": "The polar vortex turns the farm into an icy wasteland, devoid of color.", + "7": "Tools left outside are encased in frost, their metal too cold to touch.", + "8": "The bitter wind howls through the empty fields, carrying an icy sting.", + "9": "Chickens refuse to leave their coops, their feathers fluffed against the cold.", + "10": "The air is so cold that even breathing feels like shards of ice in the lungs.", + "11": "Frost creeps across windows, etching delicate patterns on the glass.", + "12": "The sky is a dull gray, the sunlight weak and offering no warmth." + }, + "village": { + "1": "Villagers wrap themselves in layers as the icy wind cuts through the streets.", + "2": "Fires burn low in every hearth, the cold seeping into even the sturdiest homes.", + "3": "Snow blankets the village square, burying market stalls in icy drifts.", + "4": "Children's laughter is absent as the extreme cold forces everyone indoors.", + "5": "Icicles hang precariously from rooftops, glinting ominously in the weak sunlight.", + "6": "The wind whips through narrow alleys, carrying flurries of snow with it.", + "7": "Wooden shutters rattle against the relentless gale, threatening to break free.", + "8": "Villagers gather in the inn, huddling close to the fire for warmth.", + "9": "The polar vortex silences the usual hustle and bustle of daily life.", + "10": "Frozen wells make fetching water a grueling task, with ice forming faster than it melts.", + "11": "The bell in the chapel tower is encased in frost, its sound muffled.", + "12": "The village is cloaked in an eerie stillness, broken only by the howling wind." + }, + "city": { + "1": "The grand city streets are nearly deserted, as the cold drives people indoors.", + "2": "Fountains are frozen solid, their once-flowing water now motionless sculptures.", + "3": "Merchants struggle to keep their stalls open as frost covers their goods.", + "4": "Guards shiver in their posts, their armor frosted and hands numb.", + "5": "Chimney smoke rises sluggishly into the bitter air, struggling against the wind.", + "6": "Ice coats the cobblestones, making every step treacherous.", + "7": "The polar vortex silences the usual clamor of the city, leaving an eerie quiet.", + "8": "Frost climbs the city walls, its crystalline structure glittering faintly.", + "9": "Beggars huddle under blankets, their breath visible in the frigid air.", + "10": "The frigid wind howls through the city gates, carrying snow into every crevice.", + "11": "Even the markets are subdued, with only the bravest merchants braving the cold.", + "12": "The city's grand fountains and waterways are locked in icy stillness." + }, + "plains": { + "1": "The endless plains are white with snow, the horizon barely visible through the storm.", + "2": "The polar vortex turns the flat landscape into a frozen desert.", + "3": "The icy wind races unhindered across the open plains, cutting like a blade.", + "4": "Snow drifts form and reform, sculpted by the relentless winds.", + "5": "Wild animals are nowhere to be seen, hidden from the bitter chill.", + "6": "The sun is pale and weak, offering no relief from the freezing cold.", + "7": "Grass blades are encased in ice, cracking underfoot like glass.", + "8": "The howl of the wind is the only sound, eerie and unrelenting.", + "9": "Travelers on the plains huddle together, their breath visible in the icy air.", + "10": "The horizon is obscured by a wall of blowing snow, direction lost in the storm.", + "11": "The polar vortex freezes the plains into a desolate, lifeless expanse.", + "12": "Even the hardy plants of the plains are bowed under a layer of frost." + }, + "forest": { + "1": "The forest is silent, its trees creaking under the weight of snow and ice.", + "2": "Icicles hang like daggers from every branch, glinting in the weak light.", + "3": "The polar vortex freezes the undergrowth, turning it into a brittle maze.", + "4": "Animals retreat to their dens, their tracks disappearing under fresh snow.", + "5": "The wind howls through the trees, scattering snow in swirling clouds.", + "6": "Frost climbs the bark of ancient trees, encasing them in icy armor.", + "7": "Snow blankets the forest floor, muffling every step in a thick silence.", + "8": "The canopy is heavy with snow, branches threatening to snap under the weight.", + "9": "The air is sharp and cold, each breath forming a misty cloud.", + "10": "The polar vortex leaves the forest frozen and still, as if time itself has stopped.", + "11": "Shadows stretch long and eerie in the faint light, the forest seemingly endless.", + "12": "Even the streams are frozen solid, their usual babble replaced by silence." + }, + "swamp": { + "1": "The swamp is eerily silent, its waters frozen and reeds encased in ice.", + "2": "The polar vortex freezes the mire, turning mud to solid ground.", + "3": "Ice spreads across the stagnant pools, cracking ominously underfoot.", + "4": "Frost clings to the twisted trees, turning the swamp into a surreal landscape.", + "5": "The usual stench of the swamp is muted, replaced by the sharp smell of cold.", + "6": "Snow gathers on the sparse vegetation, giving the swamp an otherworldly appearance.", + "7": "The wind whistles through the swamp, carrying flurries of snow.", + "8": "The swamp creatures are nowhere to be seen, their burrows sealed by ice.", + "9": "Even the sluggish streams are frozen, their surfaces glinting in the faint light.", + "10": "Fog clings low to the frozen ground, adding to the swamp's eerie stillness.", + "11": "The polar vortex transforms the swamp into a frozen, alien expanse.", + "12": "Icicles hang from the mangroves, shimmering faintly in the weak sunlight." + }, + "jungle": { + "1": "The polar vortex turns the vibrant jungle into a frosty wonderland.", + "2": "Snow blankets the dense foliage, bending branches under its weight.", + "3": "The usual cacophony of jungle life is silenced by the extreme cold.", + "4": "The vines are coated in ice, glittering like frozen ropes in the faint light.", + "5": "Mist rises from the frozen ground, the jungle's heat meeting the bitter cold.", + "6": "Icicles hang from leaves, their weight pulling the plants earthward.", + "7": "The jungle floor is slick with frost, each step crunching in the silence.", + "8": "The polar vortex freezes even the thickest of vines, making them brittle.", + "9": "The air feels heavy and unnatural, the jungle's humidity turned icy.", + "10": "Frost creeps across the jungle canopy, silencing the rustle of leaves.", + "11": "The storm’s cold penetrates the dense foliage, leaving no refuge from the chill.", + "12": "The jungle's rivers are frozen over, their surfaces shimmering in the pale light." + }, + "hills": { + "1": "The rolling hills are shrouded in white, snow whipping across their frozen crests.", + "2": "Wind screams over the hills, carving snow drifts into strange shapes.", + "3": "Frost clings to the rocky outcrops, shimmering in the weak light.", + "4": "The polar vortex turns the grassy hills into a frozen, lifeless expanse.", + "5": "Each rise and fall of the terrain is coated in an icy glaze, treacherous to traverse.", + "6": "Snow gathers in the valleys, creating a stark contrast to the exposed, windswept peaks.", + "7": "The bitter cold turns the hills into a realm of icy silence.", + "8": "Sheep and goats huddle together, their breaths forming clouds in the frigid air.", + "9": "Icicles hang from rocky ledges, glinting faintly in the pale sunlight.", + "10": "The polar vortex leaves the hills desolate, with every sound muffled by the snow.", + "11": "The wind carries snow in swirling patterns, obscuring the horizon.", + "12": "The landscape is a frozen tapestry of white, stretching as far as the eye can see." + }, + "mountains": { + "1": "The towering peaks are hidden behind a veil of snow and ice, the storm relentless.", + "2": "Avalanches thunder down the mountainsides as the polar vortex rages.", + "3": "Snow clings to the jagged cliffs, turning them into glacial monoliths.", + "4": "The wind howls through mountain passes, carrying ice and snow in its wake.", + "5": "The mountain trails are buried under layers of snow, making passage impossible.", + "6": "Icicles as long as spears hang from the cliffs, glistening in the faint light.", + "7": "The cold air bites at exposed skin, a reminder of the mountain's harshness.", + "8": "The peaks are silent save for the groan of ice shifting under the polar vortex.", + "9": "Snow cascades from the cliffs, forming deadly traps for the unwary.", + "10": "The polar vortex turns the mountains into an inhospitable frozen fortress.", + "11": "Frost coats the rocky terrain, making every step perilous.", + "12": "The storm's icy breath freezes even the hardy mountain pines." + }, + "desert": { + "1": "The desert sands are locked in ice, shimmering under the weak sunlight.", + "2": "Frozen dunes create an alien landscape, sculpted by the polar vortex.", + "3": "The air is bitterly cold, the usual heat of the desert utterly absent.", + "4": "Snow coats the cacti and sparse vegetation, a surreal sight in the barren wasteland.", + "5": "The desert wind carries icy particles, stinging like needles on bare skin.", + "6": "The polar vortex transforms the desert into a frostbitten expanse of silence.", + "7": "Frozen sand crunches underfoot, each step leaving a trail of ice chips.", + "8": "The oasis is locked in ice, its once-warm waters now lifeless and still.", + "9": "The sun is a pale orb, offering no warmth to the desolate landscape.", + "10": "Frost lines the edges of dunes, creating a strange interplay of white and gold.", + "11": "The storm silences the desert, even the wind's usual howls muted by snow.", + "12": "Tracks of desert creatures vanish quickly as snow fills them in moments." + }, + "coastal": { + "1": "The waves freeze mid-crash, jagged ice forming along the shoreline.", + "2": "The polar vortex lashes the coast with freezing winds, turning sea spray into frost.", + "3": "Ice coats the boats moored in the harbor, their ropes stiff and brittle.", + "4": "The usually roaring ocean is subdued, its surface glazed with ice.", + "5": "Sea birds struggle to fly, their wings weighed down by frost.", + "6": "The polar vortex freezes the tide pools, trapping marine life beneath ice.", + "7": "Snow gathers on the sandy shores, blending sea and land into a white expanse.", + "8": "The salt air carries a sharp bite, freezing moisture as it touches skin.", + "9": "Fishermen stay indoors, their nets frozen solid in icy heaps.", + "10": "The horizon blurs as snow and sea blend into a featureless white void.", + "11": "The lighthouse stands encased in ice, its beacon barely visible through the storm.", + "12": "The coastal cliffs glisten with frost, their edges sharp and dangerous." + }, + "volcano": { + "1": "The polar vortex clashes with volcanic heat, creating strange mists and icy lava flows.", + "2": "The volcanic rock is coated in frost, an eerie contrast to its usual fiery glow.", + "3": "Steam rises from fissures, freezing into delicate crystals in the icy air.", + "4": "Snow gathers on the crater's edge, defying the volcano's usual heat.", + "5": "The lava fields are still warm beneath their icy surface, cracks steaming in the cold.", + "6": "The polar vortex creates icy winds that howl through the volcanic ridges.", + "7": "The storm freezes the air, muting the volcano's rumbling to distant echoes.", + "8": "Icicles form on cooled lava flows, creating surreal frozen sculptures.", + "9": "The once-fiery slopes are eerily silent, their heat overwhelmed by the icy storm.", + "10": "The polar vortex leaves frost creeping down the volcano's flanks.", + "11": "The sulfurous vents hiss under layers of ice, their heat dissipating into the cold.", + "12": "The crater's glow is dimmed by the storm, its heat barely penetrating the icy air." + }, + "artic": { + "1": "The polar vortex deepens the cold, turning the tundra into an unyielding ice field.", + "2": "Snow swirls endlessly, erasing all sense of direction in the frozen expanse.", + "3": "The icy wind cuts like a blade, leaving frostbite in moments of exposure.", + "4": "Glaciers creak and groan under the polar vortex's icy grip.", + "5": "The air is so cold that it feels solid, each breath painful and sharp.", + "6": "The polar vortex buries landmarks under layers of snow, leaving a blank canvas of white.", + "7": "The northern lights flicker dimly above, barely visible through the storm.", + "8": "Even the hardy Arctic creatures struggle to survive in the storm's ferocity.", + "9": "The ice fields seem endless, the horizon blurred by blowing snow.", + "10": "The polar vortex silences the Arctic, leaving only the whistle of the wind.", + "11": "Frost clings to everything, freezing clothing and gear solid in moments.", + "12": "The sun hangs low on the horizon, its light cold and unyielding." + }, + "cursed": { + "1": "The polar vortex twists unnaturally, bringing biting cold and a sense of dread.", + "2": "Dark shapes move within the storm, their outlines blurred by the blowing snow.", + "3": "The wind carries ghostly whispers, the cold seeping into the soul.", + "4": "Snow falls black and ash-like, coating the cursed ground in a dark frost.", + "5": "The polar vortex seems alive, its winds howling with an eerie, malevolent force.", + "6": "Icicles form into unnatural shapes, jagged and menacing in the dim light.", + "7": "The air feels heavy and oppressive, the cold amplified by an unseen presence.", + "8": "Shadows flicker and twist, the storm's wind forming unnatural patterns in the snow.", + "9": "The storm’s chill is laced with despair, freezing both body and spirit.", + "10": "Tracks in the snow vanish almost instantly, as if the storm seeks to hide them.", + "11": "The cursed ground cracks and freezes, the storm’s touch spreading corruption.", + "12": "Even the storm's silence is unnatural, a void filled with unspoken dread." + } + } + }, + "Snowstorm": { + "conditions": { + "temperature": { "lte": 30 }, + "precipitation": { "gte": 60 }, + "wind": { "gte": 30, "lte": 60 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Snow blankets the fields, burying crops and fences under a thick white layer.", + "2": "The barn creaks under the weight of accumulating snow as wind howls outside.", + "3": "Chickens and livestock huddle together, their shelters barely holding against the storm.", + "4": "The farmhouse windows are frosted over, the view outside obscured by swirling snow.", + "5": "Paths to the well and barn disappear beneath the relentless snowfall.", + "6": "Icicles hang from the farmhouse roof, clinking softly in the frigid wind.", + "7": "Snowdrifts pile high against the walls of the barn, making doors hard to open.", + "8": "The sky remains a pale, icy gray as the storm rages on.", + "9": "Farmhands struggle to clear paths, their efforts erased by the next gust of snow.", + "10": "The distant tree line vanishes in a wall of white, visibility near zero.", + "11": "Even the farm's scarecrow looks forlorn, buried to its shoulders in snow.", + "12": "Cold seeps into every corner, making even the indoors feel like ice." + }, + "village": { + "1": "Snow fills the cobbled streets, forcing villagers to stay indoors.", + "2": "The church bell is muffled under layers of snow and frost.", + "3": "Children peek out from frosty windows, their games halted by the storm.", + "4": "Snow piles high against thatched roofs, threatening to collapse the weaker ones.", + "5": "Villagers wrap themselves in heavy cloaks, struggling against biting winds.", + "6": "Lanterns barely illuminate the swirling snow, their light lost in the storm.", + "7": "Shutters rattle in the icy gale, the wind finding every crack in the walls.", + "8": "The village square is deserted, the fountain frozen solid and covered in snow.", + "9": "Tracks in the snow quickly vanish, leaving the streets eerily pristine.", + "10": "The storm transforms the village into a ghostly white expanse.", + "11": "Snow collects in doorways, creating a constant battle for villagers to keep paths clear.", + "12": "Even the local tavern feels colder than usual, its fire struggling against the chill." + }, + "city": { + "1": "The city streets become treacherous, with snow and ice coating every surface.", + "2": "Merchants close their stalls as the snowstorm intensifies, leaving markets empty.", + "3": "Guards struggle to patrol, their boots crunching loudly on frozen cobblestones.", + "4": "The towering spires of the city are obscured by the heavy snowfall.", + "5": "Snow gathers in alleys and corners, forming icy barricades for travelers.", + "6": "Chimneys work overtime, releasing plumes of smoke into the snowy sky.", + "7": "The city gates are nearly frozen shut, making travel impossible.", + "8": "Muffled cries echo as citizens fight the cold to clear paths to their homes.", + "9": "Snow drifts pile high against city walls, adding a layer of quiet insulation.", + "10": "Street lamps cast dim glows, their light swallowed by the snow-filled air.", + "11": "The storm makes even the busiest streets eerily quiet, the city almost still.", + "12": "Icicles as long as swords hang from gutters, a hazard for the unwary." + }, + "plains": { + "1": "The open plains are a sea of white, with snow drifting endlessly in the wind.", + "2": "Sparse trees bow under the weight of snow, their branches cracking in the cold.", + "3": "Wildlife huddles in the few available shelters, braving the unrelenting storm.", + "4": "Snowfall erases the horizon, making navigation impossible across the plains.", + "5": "The bitter wind sweeps across the plains, carrying stinging ice crystals.", + "6": "Tracks of travelers vanish in minutes, swallowed by the swirling snow.", + "7": "The storm turns the plains into a frozen wasteland, desolate and silent.", + "8": "Grass is buried under a thick layer of snow, the plains appearing lifeless.", + "9": "The sound of the wind is deafening, drowning out all other noises.", + "10": "Snow piles into rippling drifts, mimicking the waves of an icy sea.", + "11": "Even hardy nomads struggle to find their way as the storm worsens.", + "12": "The plains feel endless and cold, the snowstorm stretching on without end." + }, + "forest": { + "1": "Snow blankets the canopy, bending trees under its relentless weight.", + "2": "The forest floor disappears under layers of snow, hiding roots and hazards alike.", + "3": "Branches crack and fall, unable to bear the storm's icy burden.", + "4": "The usual sounds of the forest are muted by the falling snow.", + "5": "Animal tracks vanish quickly, their makers hidden away in burrows and dens.", + "6": "Icicles hang from every branch, gleaming faintly in the dim light.", + "7": "Snowdrifts form against tree trunks, creating natural windbreaks.", + "8": "The forest feels claustrophobic, the storm closing in from all sides.", + "9": "Visibility is poor, with snow turning the dense forest into a white labyrinth.", + "10": "Even the tallest pines groan under the weight of the relentless snowstorm.", + "11": "The storm muffles footsteps, making every sound seem distant and eerie.", + "12": "A chilling stillness pervades the forest, broken only by the storm's howl." + }, + "swamp": { + "1": "Snow covers the swamp's usual muck, turning it into a frozen quagmire.", + "2": "Icicles form on moss-covered trees, a rare sight in the swamp's wet landscape.", + "3": "Pools of water freeze solid, trapping reeds and plants in icy prisons.", + "4": "The swamp's usual sounds are silenced, replaced by the whistle of the storm.", + "5": "Snow piles on rotting logs, creating precarious paths for travelers.", + "6": "The storm turns the swamp into a treacherous mix of ice and snow.", + "7": "Fog mingles with the snowstorm, creating an otherworldly atmosphere.", + "8": "The icy wind carries the swamp's pungent odors, freezing them in the air.", + "9": "Visibility drops to nothing as the snowstorm swirls through the swamp.", + "10": "Frozen plants stand rigid in the cold, their usual droop replaced by icy stillness.", + "11": "The swamp's surface is a patchwork of snow-covered ice and hidden dangers.", + "12": "Even the swamp's hardy creatures retreat, leaving the frozen expanse desolate." + }, + "jungle": { + "1": "Snow piles on the dense canopy, an unnatural sight in the usually warm jungle.", + "2": "The jungle floor is covered in snow, vines and roots hidden beneath the frost.", + "3": "Icicles hang from thick leaves, gleaming like jewels in the storm.", + "4": "The storm silences the jungle's usual cacophony, leaving an eerie stillness.", + "5": "Snow weighs down the dense foliage, breaking weaker branches with loud cracks.", + "6": "Jungle creatures hide in confusion, unaccustomed to the icy onslaught.", + "7": "The polar vortex turns the vibrant jungle into a monochrome wasteland.", + "8": "Snow collects in the underbrush, making the terrain slippery and dangerous.", + "9": "The storm transforms the jungle paths into a maze of icy obstacles.", + "10": "Even the air feels frozen, the humidity replaced by a biting chill.", + "11": "Tracks of predators vanish in minutes, concealed by the relentless snowfall.", + "12": "The jungle's vibrant colors are muted under a blanket of white and gray." + }, + "hills": { + "1": "Snow sweeps over the rolling hills, creating drifting mounds along the slopes.", + "2": "The wind howls fiercely, driving snow into every crevice of the rugged landscape.", + "3": "Hillsides become slippery with snow, treacherous for any attempting to traverse them.", + "4": "The snowstorm blurs the gentle contours of the hills, making paths unrecognizable.", + "5": "Sparse trees dotting the hills sway under the weight of heavy snow.", + "6": "The storm transforms the hills into a featureless white expanse, disorienting travelers.", + "7": "Snow piles at the base of the hills, creating deep drifts in low-lying areas.", + "8": "The sound of the storm echoes strangely through the valleys, amplifying its ferocity.", + "9": "Icicles form on exposed rocks and ledges, gleaming faintly in the storm's dim light.", + "10": "The hills seem to shift under the relentless storm, visibility near zero.", + "11": "Wildlife seeks refuge in burrows, their tracks quickly buried by the snow.", + "12": "The storm makes the hills feel vast and unyielding, the journey an endless struggle." + }, + "mountains": { + "1": "Snow cascades down the mountain slopes, forming small avalanches in the storm's wake.", + "2": "Treacherous icy winds whip around the peaks, making even shelter dangerous.", + "3": "Snow and ice cling to rocky outcroppings, turning them into jagged white sculptures.", + "4": "Mountain paths disappear entirely, buried under layers of drifting snow.", + "5": "The storm howls through narrow passes, its roar amplified by the rocky walls.", + "6": "Snow piles precariously on ledges, threatening to give way at any moment.", + "7": "Visibility drops to nothing, the towering peaks vanishing in a sea of white.", + "8": "The storm's force makes climbing impossible, even seasoned mountaineers forced to retreat.", + "9": "Sheltered caves become lifelines, their entrances rimed with frost.", + "10": "The mountains seem alive with the storm, snow swirling like angry spirits.", + "11": "Icicles as long as spears hang from crags, ready to fall at any vibration.", + "12": "The cold bites deeper with altitude, the snowstorm relentless at the peaks." + }, + "desert": { + "1": "Snow falls over the dunes, the storm transforming the desert into an icy wasteland.", + "2": "The golden sands are hidden beneath a thick layer of snow, an eerie sight.", + "3": "Snow swirls in the desert wind, creating icy drifts where dunes once stood.", + "4": "Cacti and scrub are coated in frost, their spines glistening in the storm.", + "5": "The cold is bone-deep, the storm turning the desert into a frozen nightmare.", + "6": "Tracks across the sand vanish in minutes, buried by the relentless snowfall.", + "7": "The desert feels alien under the storm, its usual heat replaced by biting cold.", + "8": "Sand and snow mix in strange patterns, the storm blending the two elements.", + "9": "Visibility is near zero, the flat desert swallowed by the swirling snow.", + "10": "The storm howls, its icy breath cutting through the open expanse of the desert.", + "11": "Snow accumulates around boulders, turning them into frozen islands in a white sea.", + "12": "Even the hardy desert creatures vanish, retreating to burrows in the face of the storm." + }, + "coastal": { + "1": "Snow blankets the beach, waves crashing against the icy shore.", + "2": "The sea spray freezes midair, coating nearby rocks and sand in glistening ice.", + "3": "Fishing boats are grounded, their decks buried under layers of snow.", + "4": "The snowstorm mixes with the salty air, creating a biting, frozen mist.", + "5": "Winds whip snow into swirling patterns, the storm masking the sound of the waves.", + "6": "The coastline vanishes into the storm, the horizon indistinguishable from the sky.", + "7": "Ice forms on docked ships, their masts creaking under the weight.", + "8": "Seagulls struggle to fly against the storm, their cries drowned by the wind.", + "9": "Snow piles against sea cliffs, creating treacherous overhangs ready to collapse.", + "10": "The storm transforms the coastal village into a frozen, wind-battered haven.", + "11": "The ocean itself seems to freeze, with icy waves crashing sluggishly to shore.", + "12": "Saltwater pools freeze over, their surfaces jagged with thin layers of ice." + }, + "volcano": { + "1": "Snow clashes with volcanic heat, creating plumes of steam that obscure the landscape.", + "2": "The storm turns the volcano's slopes into a hazardous mix of ice and ash.", + "3": "Snow clings to jagged lava rocks, an unsettling contrast against the black terrain.", + "4": "Steam vents hiss loudly, their heat battling against the encroaching cold.", + "5": "Snow collects in dormant craters, their usual glow muted under the storm.", + "6": "The storm howls around the peak, the sound mixing with the volcano's eerie silence.", + "7": "Tracks in the ash are quickly obscured by falling snow, disorienting travelers.", + "8": "The volcano's heat keeps the snow from settling in places, leaving an uneven landscape.", + "9": "Frost forms on lava flows, turning the molten rock into a bizarre frozen tableau.", + "10": "Visibility drops to near nothing, the volcano shrouded in the storm's fury.", + "11": "Snow and ash swirl together, the storm creating a surreal, otherworldly scene.", + "12": "Even the volcano seems subdued, its heat barely holding against the storm." + }, + "artic": { + "1": "The snowstorm intensifies, turning the Arctic tundra into an endless white void.", + "2": "Snowdrifts tower high, creating barriers against the relentless wind.", + "3": "The sky remains a dull gray, the storm obscuring all signs of the sun.", + "4": "Frozen lakes are buried under layers of snow, their surfaces treacherous to cross.", + "5": "The biting cold freezes even the air, every breath visible as a plume of mist.", + "6": "The storm creates strange shapes in the snow, the Arctic landscape alien and shifting.", + "7": "The sound of the wind is constant, a low howl that never fades.", + "8": "Even the hardy Arctic creatures seek shelter, their tracks disappearing in minutes.", + "9": "The storm blurs the line between land and sky, making navigation impossible.", + "10": "Snow piles against icebergs, turning them into towering white monoliths.", + "11": "The Arctic feels endless and unforgiving, the storm a reminder of its power.", + "12": "Icicles hang from every surface, the storm adding to the frozen landscape's beauty." + }, + "cursed": { + "1": "Snow falls blackened and twisted, the storm carrying an unnatural chill.", + "2": "The cursed storm howls with voices, whispers carried on the icy wind.", + "3": "Snow drifts form eerie shapes, like figures frozen in time.", + "4": "The storm feels malevolent, as if it hunts those who dare to cross its path.", + "5": "Snow melts into strange patterns, forming runes that vanish moments later.", + "6": "The air feels thick and wrong, the storm carrying more than just cold.", + "7": "Even the snow glows faintly, an unholy light emanating from within.", + "8": "Icicles form into jagged spikes, almost as if they are weapons waiting to fall.", + "9": "The cursed landscape changes under the storm, as if the snow reshapes reality.", + "10": "Shadows dance in the storm's fury, their source unseen and unknowable.", + "11": "The cold burns rather than numbs, the storm's touch unnatural and cruel.", + "12": "The storm leaves no tracks behind, as if erasing all who pass through it." + } + } + }, + "Torrential Rain": { + "conditions": { + "temperature": { "gte": 50, "lte": 80 }, + "precipitation": { "gte": 80 }, + "wind": { "gte": 30, "lte": 70 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 80 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Rain pounds the fields, turning soil into a thick, impassable mire.", + "2": "Streams overflow, flooding the farmland and drowning young crops.", + "3": "The barn roof creaks under the weight of constant downpour.", + "4": "Visibility is reduced to a blur as rain sheets across the fields.", + "5": "Farm animals huddle under makeshift shelters, drenched and uneasy.", + "6": "Every step squelches in the waterlogged ground, boots quickly soaked.", + "7": "Rain barrels overflow, spilling their contents across muddy courtyards.", + "8": "The pounding rain creates small rivers winding through the farmland.", + "9": "Wind-driven rain lashes against windows, leaking through cracks in the walls.", + "10": "Harvests are abandoned as farmers rush to secure their tools and livestock.", + "11": "The constant drumming of rain drowns out all other sounds.", + "12": "Puddles grow into ponds, the farmland almost unrecognizable under the deluge." + }, + "village": { + "1": "Cobblestone streets turn into streams as water flows freely between buildings.", + "2": "Villagers race to secure roofs and shutters against the relentless rain.", + "3": "Flooded paths make movement through the village slow and hazardous.", + "4": "Chimneys struggle to release smoke, rain dousing hearth fires.", + "5": "Children peer nervously from doorways, watching the torrents outside.", + "6": "Wells overflow, spilling water into the muddy streets.", + "7": "The rain drowns out the sound of the village bell, its chime barely audible.", + "8": "Hay piles become sodden heaps, no longer usable for feeding livestock.", + "9": "Water rushes through drainage ditches, carrying debris from the storm.", + "10": "Carts are abandoned in the streets, wheels stuck in deep mud.", + "11": "The village square becomes a shallow lake, market stalls submerged.", + "12": "Rain seeps into every building, leaving villagers cold and miserable." + }, + "city": { + "1": "Stone streets are slick with rainwater, creating treacherous footing.", + "2": "Gutters overflow, spilling water into the lower districts of the city.", + "3": "City guards struggle to maintain order as flooding displaces citizens.", + "4": "Rain pounds against the rooftops, creating a deafening roar in the markets.", + "5": "The city gates are surrounded by pools of water, making entry difficult.", + "6": "Merchants scramble to protect their goods from the relentless downpour.", + "7": "Rooftops leak, forcing families to place buckets under dripping beams.", + "8": "Rain collects in the courtyards of noble estates, turning them into shallow ponds.", + "9": "The relentless rain creates waterfalls from the city's aqueducts.", + "10": "Chimneys puff weakly, their smoke overwhelmed by the rain.", + "11": "The sewers struggle to keep up, threatening to spill into the streets.", + "12": "City criers fall silent as the storm drowns out their voices." + }, + "plains": { + "1": "Rain sweeps across the open plains, reducing visibility to a few feet.", + "2": "Grasses bow under the weight of heavy raindrops, forming streams between tufts.", + "3": "The plains become a quagmire, travel slowed to a crawl.", + "4": "Rivers swell, spilling onto the plains and flooding low-lying areas.", + "5": "The horizon vanishes behind a wall of falling rain.", + "6": "Small burrows and nests are overwhelmed, driving creatures to higher ground.", + "7": "The sound of rain hitting the grass is a constant, deafening hiss.", + "8": "Carts and wagons sink into the mud, their wheels stuck fast.", + "9": "Lightning briefly illuminates the rain-soaked expanse, revealing nothing but water.", + "10": "Pools of water spread rapidly, creating new streams and ponds.", + "11": "Travelers on the plains struggle to find shelter against the unrelenting rain.", + "12": "Even the hardiest of plants begin to wilt under the relentless downpour." + }, + "forest": { + "1": "Rain streams from the canopy above, creating a constant drumming sound.", + "2": "Forest trails turn into rivulets, paths obscured by flowing water.", + "3": "Tree branches sag under the weight of accumulating water.", + "4": "Animals retreat to their burrows, the forest eerily quiet despite the storm.", + "5": "Puddles form in every hollow, soaking boots and slowing progress.", + "6": "The smell of wet earth and rotting leaves permeates the air.", + "7": "Streams and creeks within the forest swell, washing away debris.", + "8": "Rainwater pools around tree roots, creating small, murky ponds.", + "9": "Leaves glisten under the relentless rain, reflecting faint light from above.", + "10": "The underbrush becomes almost impassable, tangled and sodden.", + "11": "The storm's roar is muffled slightly by the thick forest canopy.", + "12": "Mosses and fungi thrive in the downpour, spreading rapidly across wet bark." + }, + "swamp": { + "1": "Rain hammers the swamp, the water level rising with alarming speed.", + "2": "Pools of stagnant water are churned into murky whirlpools by the storm.", + "3": "The swamp's usual sounds are drowned out by the drumming rain.", + "4": "Mud becomes an impassable trap, every step sinking deep into the muck.", + "5": "Vegetation is battered by the rain, bending and snapping under the force.", + "6": "Frogs and insects fall silent as the rain dominates the swamp's soundscape.", + "7": "Rainwater mixes with the swamp's murk, creating an even more foul stench.", + "8": "Paths through the swamp vanish entirely, the ground submerged in water.", + "9": "The rain turns the swamp's eerie mist into an impenetrable fog.", + "10": "Trees sway and groan, their roots struggling to hold firm in the wet ground.", + "11": "The swamp's predators vanish, retreating to hidden lairs to wait out the storm.", + "12": "Lightning flashes briefly illuminate the rain-soaked mire, revealing a desolate expanse." + }, + "jungle": { + "1": "The jungle canopy drips constantly, the rain falling in heavy sheets.", + "2": "Paths through the jungle are flooded, turning into rushing streams.", + "3": "Leaves glisten with water, their surfaces reflecting brief flashes of lightning.", + "4": "The humid air grows oppressive as the rain intensifies.", + "5": "Every step squelches in the jungle's soggy undergrowth.", + "6": "Animals chatter nervously, their calls barely audible over the storm.", + "7": "Rain pools in hollows of tree trunks, creating reservoirs for the stormwater.", + "8": "The storm creates small waterfalls where the jungle terrain slopes downward.", + "9": "The thick air smells of wet vegetation and decay, amplified by the rain.", + "10": "Branches hang heavy with water, occasionally breaking under the weight.", + "11": "Mosquitoes swarm in the rain, their numbers seemingly unaffected by the storm.", + "12": "Vines and roots become slippery hazards in the jungle's dense, rain-soaked paths." + }, + "hills": { + "1": "Rain rushes down the slopes, carving rivulets into the hillside.", + "2": "Grasslands become sodden, the ground slippery and treacherous.", + "3": "Streams swell into rushing torrents, overflowing their banks.", + "4": "Low-lying areas between hills fill with water, creating temporary ponds.", + "5": "Shepherds struggle to guide their flocks through the muddy terrain.", + "6": "Paths become impassable as mudslides threaten to sweep everything away.", + "7": "The rain creates a drumming symphony as it strikes rocks and shrubs.", + "8": "Visibility is poor, with mist and rain blending into a gray curtain.", + "9": "The sound of thunder reverberates off the hills, adding to the storm's intensity.", + "10": "Small wildlife scurries to higher ground, seeking refuge from the rising waters.", + "11": "Footpaths are washed away, leaving only deep, water-filled ruts.", + "12": "Hillsides glisten under the relentless rain, their colors muted by the storm." + }, + "mountains": { + "1": "Waterfalls roar as rain swells mountain streams into torrents.", + "2": "Mountain trails turn to mud, dangerous and nearly impassable.", + "3": "Cliffs weep with rainwater, forming temporary cascades.", + "4": "Fog rises from the rain-soaked ground, obscuring the mountain peaks.", + "5": "Loose rocks are dislodged by the rain, tumbling noisily down the slopes.", + "6": "Ravines flood rapidly, the sound of rushing water echoing through the valleys.", + "7": "Rain and wind lash against rocky outcrops, the air thick with moisture.", + "8": "Mountain goats and other wildlife huddle beneath overhangs for shelter.", + "9": "Lightning streaks across the peaks, illuminating the storm for brief moments.", + "10": "Clouds hang low, draping the mountains in a thick, gray shroud.", + "11": "Cold rain mixes with sleet at higher elevations, creating icy hazards.", + "12": "The storm's roar is amplified by the mountain's natural acoustics." + }, + "desert": { + "1": "Rain strikes the parched ground, quickly pooling on the hardened surface.", + "2": "Dry riverbeds roar to life, carrying torrents of water through the desert.", + "3": "Cacti and scrub plants glisten with rain, their colors vibrant against the storm.", + "4": "Dust turns to mud, creating slippery patches in the normally arid landscape.", + "5": "Rain evaporates as quickly as it falls, creating a humid, oppressive atmosphere.", + "6": "Wind drives the rain sideways, pelting the desert with stinging droplets.", + "7": "Lightning illuminates the barren terrain, casting long, eerie shadows.", + "8": "Small creatures emerge to drink from temporary puddles before they vanish.", + "9": "The scent of wet sand and earth fills the air, rare and invigorating.", + "10": "Rocks gleam under the rain, their surfaces polished by the downpour.", + "11": "Flash floods carve new paths through the desert, altering the landscape.", + "12": "The storm leaves behind a brief, stunning rainbow against the dark sky." + }, + "coastal": { + "1": "Waves crash violently against the shore, driven by the torrential rain.", + "2": "Seawater mixes with rainwater, flooding low-lying coastal areas.", + "3": "Fishing boats are lashed to docks, their crews sheltering from the storm.", + "4": "The ocean appears angry, its surface churned to froth by the wind and rain.", + "5": "Cliffs drip with water, the storm creating cascading waterfalls into the sea.", + "6": "Seagulls circle overhead, their cries barely audible over the storm.", + "7": "Coastal roads are submerged, their stones slick with rain and sea spray.", + "8": "Salt and rain mix in the air, the scent overwhelming.", + "9": "Beaches vanish under the rising tide, their sands turned to mud.", + "10": "Fishermen secure their nets, rain pounding their backs as they work.", + "11": "Villagers near the coast retreat indoors, listening to the storm's fury.", + "12": "The horizon is obscured by a wall of rain and mist, blending sea and sky." + }, + "volcano": { + "1": "Rain sizzles against the still-hot rocks, creating steam that shrouds the area.", + "2": "Ashy soil turns to slippery mud, making movement treacherous.", + "3": "Rainwater carves paths through volcanic debris, carrying ash and soot downhill.", + "4": "Steam vents hiss louder as rain cools the surrounding rocks.", + "5": "The storm creates a heavy, sulfurous mist that clings to the ground.", + "6": "Cracks in the volcanic surface fill with water, creating boiling pools.", + "7": "Lightning reflects off the jagged volcanic peaks, illuminating the eerie terrain.", + "8": "Ash mixes with rain, forming a sticky, dark sludge that clogs boots.", + "9": "Rocks gleam under the relentless downpour, their sharp edges softened by water.", + "10": "Streams of rainwater flow through lava tubes, echoing faintly.", + "11": "The air is heavy with the mingled scents of rain and brimstone.", + "12": "Small geysers erupt as rainwater meets subterranean heat, adding to the chaos." + }, + "artic": { + "1": "Rain turns to ice as it hits the frozen ground, creating a slick surface.", + "2": "The snowpack absorbs the rain, forming heavy, slushy drifts.", + "3": "Ice sheets crack under the weight of pooling rainwater.", + "4": "Fog rises from the cold ground, blending with the rain to obscure visibility.", + "5": "Rain pelts against glaciers, creating rivulets that carve into the ice.", + "6": "Wind drives the rain sideways, making the frigid storm even more punishing.", + "7": "Frozen puddles form quickly as rainwater freezes in the arctic chill.", + "8": "The storm darkens the sky, the sun barely visible behind thick clouds.", + "9": "Icicles drip with rainwater, their surfaces slick and reflective.", + "10": "Small avalanches are triggered as rain destabilizes snow on steep slopes.", + "11": "Wildlife retreats to hidden dens, seeking shelter from the freezing rain.", + "12": "The air feels heavy and wet, the rain sapping warmth from everything it touches." + }, + "cursed": { + "1": "Rain falls as dark, viscous drops, leaving an oily sheen on everything.", + "2": "Shadows twist unnaturally in the storm, the rain seeming to hum as it falls.", + "3": "The ground absorbs the rain like a sponge, dark puddles forming everywhere.", + "4": "The rain smells faintly of decay, filling the cursed land with dread.", + "5": "Each raindrop seems to sting, leaving behind faintly glowing marks.", + "6": "Pools of water ripple without cause, as if something unseen moves beneath them.", + "7": "Lightning illuminates grotesque shapes in the storm clouds, gone in a flash.", + "8": "The rain whispers faintly, voices just out of comprehension riding the storm.", + "9": "Mud bubbles with strange gases, the air heavy with a metallic tang.", + "10": "The rain corrodes exposed metal, leaving tools and weapons pitted and weak.", + "11": "Shadows lengthen unnaturally, the rain seeming to dim even nearby lanterns.", + "12": "The storm leaves behind a sense of unease, its effects lingering long after." + } + } + }, + "Tropical Cyclone": { + "conditions": { + "temperature": { "gte": 70, "lte": 90 }, + "precipitation": { "gte": 80 }, + "wind": { "gte": 80 }, + "humidity": { "gte": 80 }, + "cloudCover": { "gte": 90 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Fierce winds tear through crops, scattering debris across the fields.", + "2": "Livestock huddle together, bracing against the roaring storm.", + "3": "Floodwaters rise quickly, swamping barns and washing away topsoil.", + "4": "The air is thick with driving rain, visibility reduced to mere feet.", + "5": "Farmers struggle to secure their homes as the cyclone rages on.", + "6": "The howl of the wind drowns out all other sounds, shaking wooden structures.", + "7": "Trees are uprooted and flung across pastures by the relentless gale.", + "8": "Rivers near the farm overflow, sweeping fences and tools away.", + "9": "Rain batters thatched roofs, water streaming into every crevice.", + "10": "The storm darkens the sky, creating an eerie twilight at midday.", + "11": "Barrels and wagons are tossed around like toys in the storm's fury.", + "12": "Mud and debris cover the land, the farm almost unrecognizable in the storm's aftermath." + }, + "village": { + "1": "Thatched roofs are torn from cottages, leaving homes exposed to the elements.", + "2": "Streets turn into rivers as rainwater overwhelms the village's drainage.", + "3": "Villagers seek refuge in the church, its stone walls holding against the cyclone.", + "4": "Winds howl through the narrow lanes, scattering debris in all directions.", + "5": "Wooden buildings creak and groan, some collapsing under the storm's force.", + "6": "Rain pours in torrents, turning the village square into a muddy pool.", + "7": "Livestock pens are swept away, animals running frantically through the storm.", + "8": "Trees fall across roads and paths, cutting off escape routes.", + "9": "Villagers huddle indoors, praying for the storm to pass quickly.", + "10": "The relentless cyclone rips through the market stalls, scattering goods.", + "11": "Flickering lanterns struggle to stay lit as the wind tears through the village.", + "12": "Rain lashes against stone walls, water seeping through cracks and flooding homes." + }, + "city": { + "1": "Towering walls are battered by the storm, cracks forming in weaker sections.", + "2": "Market stalls are destroyed, their wares swept away in the driving rain.", + "3": "Floodwaters rise in the lower districts, forcing residents to higher ground.", + "4": "The wind howls through the streets, tearing banners and breaking windows.", + "5": "Rain pours from rooftops, creating cascading waterfalls in the alleys.", + "6": "Boats in the harbor are capsized, their wreckage littering the docks.", + "7": "Stone buildings shake under the storm's assault, though they hold firm.", + "8": "Gutters overflow, water spilling into already flooded streets.", + "9": "Citizens secure shutters and doors, struggling to keep the storm at bay.", + "10": "Lightning flashes illuminate the storm-soaked city in brief, eerie bursts.", + "11": "Bridges sway dangerously under the combined force of wind and rain.", + "12": "The city's main square is submerged, statues and landmarks barely visible." + }, + "plains": { + "1": "The flatlands offer no shelter from the cyclone's unrelenting fury.", + "2": "Grass bends and breaks under the assault of the howling wind.", + "3": "Rain sweeps across the plains in sheets, flooding every hollow and dip.", + "4": "Scattered trees are uprooted and thrown across the open landscape.", + "5": "Wild animals flee in desperation, seeking shelter from the storm.", + "6": "Paths and trails vanish under the rising floodwaters.", + "7": "The horizon is obscured by a gray wall of rain and wind.", + "8": "Tents and temporary shelters are ripped apart, their remains scattered.", + "9": "Travelers caught in the storm struggle to stay upright against the gale.", + "10": "The cyclone's roar drowns out every other sound on the open plains.", + "11": "Rivers overflow, cutting new channels through the sodden ground.", + "12": "The storm leaves behind a desolate, waterlogged expanse under heavy skies." + }, + "forest": { + "1": "Trees creak and groan, many snapping like twigs under the gale-force winds.", + "2": "The forest floor is a quagmire, rain pooling faster than it can drain.", + "3": "Fallen branches and debris litter the ground, creating hazardous conditions.", + "4": "Animals dart through the underbrush, fleeing the relentless cyclone.", + "5": "The canopy offers little protection as wind and rain tear through the forest.", + "6": "Streams overflow, cutting paths of destruction through the woodland.", + "7": "The storm's fury leaves a trail of uprooted trees and shattered vegetation.", + "8": "Visibility is reduced to near zero as rain and mist shroud the forest.", + "9": "The sound of cracking wood echoes through the storm-tossed trees.", + "10": "Foliage is stripped from branches, leaving the forest eerily bare.", + "11": "Lightning illuminates the rain-soaked woods, creating fleeting, haunting shadows.", + "12": "Paths and trails are lost under fallen trees and rushing water." + }, + "swamp": { + "1": "The swamp becomes a churning mass of water and mud as the cyclone hits.", + "2": "Rain fills every hollow and depression, turning the area into a vast lake.", + "3": "Mangroves sway dangerously, some snapping under the relentless wind.", + "4": "Creatures of the swamp retreat to hidden burrows, seeking refuge from the storm.", + "5": "The wind drives the rain sideways, pelting every exposed surface.", + "6": "Floodwaters mix with the swamp's murky pools, creating hazardous currents.", + "7": "Creeping vines and moss are torn from their perches, scattered across the swamp.", + "8": "The air is thick with the scent of wet decay and churned mud.", + "9": "Paths through the swamp vanish, leaving only a treacherous expanse of water.", + "10": "Lightning illuminates the swamp, casting stark shadows through the rain.", + "11": "Crocodiles and other creatures lie still, submerged beneath the rising waters.", + "12": "The storm's chaos leaves the swamp unrecognizable, a waterlogged wasteland." + }, + "jungle": { + "1": "Dense vegetation offers little shelter as rain and wind tear through the jungle.", + "2": "Canopy trees sway violently, their branches breaking and crashing to the ground.", + "3": "Rivers swell rapidly, flooding the jungle floor and cutting off escape routes.", + "4": "Animals cry out in panic, their calls drowned by the storm's roar.", + "5": "The thick underbrush becomes a mire, trapping anyone who dares venture through it.", + "6": "Lightning flashes illuminate the tangled chaos of the storm-tossed jungle.", + "7": "Vines and moss are stripped from the trees, adding to the debris below.", + "8": "Rain falls in torrents, creating streams and waterfalls where none existed before.", + "9": "The air is heavy with moisture, every breath labored in the storm's heat.", + "10": "Monkeys and birds retreat to hollows, their shelters barely holding against the storm.", + "11": "The storm leaves a trail of destruction, trees toppled and paths obliterated.", + "12": "Water pools across the jungle floor, teeming with displaced wildlife." + }, + "hills": { + "1": "The cyclone tears across the rolling hills, carving gullies into the sodden earth.", + "2": "Grass and shrubs are flattened under the relentless wind and driving rain.", + "3": "Floodwaters cascade down the slopes, turning valleys into raging torrents.", + "4": "Scattered trees on hilltops are wrenched from the ground by the storm's force.", + "5": "Shepherds struggle to lead their flocks to shelter as the gale intensifies.", + "6": "Visibility drops as sheets of rain obscure the peaks and valleys.", + "7": "Hollowed rocks and crevices offer refuge to wildlife fleeing the storm.", + "8": "Paths and trails are washed away, leaving treacherous mud and debris behind.", + "9": "The storm's howls echo off the hills, creating a cacophony of noise.", + "10": "Lightning illuminates the drenched terrain, revealing the storm's destruction.", + "11": "Small streams overflow, joining to create destructive currents in the lowlands.", + "12": "The hills are left scarred, with deep rivulets and uprooted vegetation." + }, + "mountains": { + "1": "The cyclone batters the mountain peaks, sending loose rocks tumbling down.", + "2": "Climbers and travelers are forced to seek refuge in caves as winds rage.", + "3": "Streams turn into waterfalls, cascading down cliffs in thunderous torrents.", + "4": "Snow and ice are stripped from high elevations, leaving barren stone exposed.", + "5": "The narrow mountain trails vanish under mudslides and flowing water.", + "6": "Roaring winds tear through alpine forests, toppling trees like twigs.", + "7": "Rainwater gathers in crevices, bursting into sudden floods in the valleys below.", + "8": "Lightning illuminates the jagged peaks, casting fleeting shadows on the slopes.", + "9": "Avalanches of debris crash down, reshaping the mountain's surface.", + "10": "Clouds cling to the peaks, creating an eerie shroud of mist and rain.", + "11": "Stone shelters hold firm, though battered by the storm's unrelenting force.", + "12": "The storm leaves the mountain range battered and scarred, with landslides marking its path." + }, + "desert": { + "1": "The cyclone sweeps across the sands, forming towering dust spirals under dark skies.", + "2": "Campsites and caravans are scattered as the storm tears through the desert.", + "3": "Rain falls in sudden torrents, creating rare, short-lived streams across the dunes.", + "4": "Winds howl, shifting vast amounts of sand and obscuring visibility entirely.", + "5": "The air turns oppressive, with choking dust mixing with the pounding rain.", + "6": "Lightning flashes illuminate the barren landscape, casting eerie shadows.", + "7": "Rare desert plants are torn from the ground, carried away by the gale.", + "8": "Oases are flooded, their usually calm waters churned by the storm.", + "9": "Tracks and trails are erased as the sand shifts under the storm's power.", + "10": "Tents and shelters are flattened, offering no protection from the cyclone.", + "11": "The sky churns with dark clouds, the horizon lost to the swirling sand.", + "12": "Once the storm passes, the desert is left unrecognizable, its dunes reshaped." + }, + "coastal": { + "1": "Waves crash violently against the shore, flooding the coastline with seawater.", + "2": "Fishing boats are tossed like toys, many breaking apart against the docks.", + "3": "The wind tears through coastal villages, leaving roofs and walls in ruins.", + "4": "Rain lashes the shore, creating streams that flow directly into the ocean.", + "5": "Sea spray mixes with the torrential rain, reducing visibility to almost nothing.", + "6": "Cliffs crumble under the relentless assault of waves and wind.", + "7": "Coastal forests are battered, with salt-laden winds killing fragile plants.", + "8": "The storm surges inland, flooding low-lying areas and sweeping debris away.", + "9": "Lightning strikes illuminate the roiling sea, casting shadows on the soaked coast.", + "10": "Tidal pools overflow, washing away small marine creatures caught in the chaos.", + "11": "The air is thick with salt and rain, the ocean's fury overwhelming all else.", + "12": "When the storm passes, the coastline is scarred, littered with wreckage and driftwood." + }, + "volcano": { + "1": "The cyclone tears through volcanic slopes, dislodging ash and loose rock.", + "2": "Steam rises where rain meets the volcano's still-warm surface, shrouding the area.", + "3": "Rivers of mud and debris flow down the slopes, creating temporary valleys.", + "4": "The storm's winds scatter ash and soot, darkening the already ominous skies.", + "5": "Lava tubes flood as rainwater pours into the volcano's many crevices.", + "6": "The winds howl through volcanic craters, amplifying their eerie echoes.", + "7": "Lightning strikes the volcanic peak, illuminating its rugged silhouette.", + "8": "Rockslides thunder down, reshaping the volcanic terrain with every crash.", + "9": "Rain turns ash into a slick, hazardous sludge, coating the slopes.", + "10": "Sulfurous gases mix with the storm, making the air nearly unbreathable.", + "11": "Shelters carved into the rock struggle to hold against the relentless cyclone.", + "12": "The storm leaves the volcano's slopes ravaged, a chaotic blend of ash, mud, and water." + }, + "artic": { + "1": "The cyclone's winds whip across the ice, creating blinding whiteouts.", + "2": "Snow and ice are ripped from the ground, pelting everything in the storm's path.", + "3": "Glaciers groan and crack under the pressure of the relentless storm.", + "4": "Visibility drops to nothing as snow and wind obscure the frozen landscape.", + "5": "Icicles are torn from ledges, crashing to the ground with dangerous force.", + "6": "Winds howl through icy ravines, echoing like the cries of lost spirits.", + "7": "Icebergs shift and collide in the turbulent waters stirred by the cyclone.", + "8": "The storm's fury leaves snowdrifts piled high, blocking paths and shelter entrances.", + "9": "Polar bears and other wildlife retreat to hidden dens, avoiding the worst of the storm.", + "10": "The sky churns with dark clouds, the occasional flash of lightning splitting the gloom.", + "11": "Frostbite sets in quickly for anyone exposed to the icy, gale-driven winds.", + "12": "When the storm passes, the artic is left eerily quiet, its icy surface newly sculpted." + }, + "cursed": { + "1": "The cyclone howls with an unnatural wail, as if filled with tormented souls.", + "2": "Dark, otherworldly clouds swirl above, crackling with eerie, green lightning.", + "3": "The cursed ground trembles as the storm's winds uproot even the sturdiest trees.", + "4": "Rain falls as black ichor, staining the land and filling the air with a foul stench.", + "5": "The wind carries whispers, incomprehensible yet chilling to all who hear them.", + "6": "Visibility is nearly zero as a thick, cursed mist rises in the storm's wake.", + "7": "Structures collapse under the combined force of gale winds and supernatural pressure.", + "8": "Pools of water left by the storm shimmer with an unnatural, sickly glow.", + "9": "Lightning strikes with unnatural precision, igniting cursed flames that resist rain.", + "10": "The storm's passage leaves the land twisted, with bizarre growths and eerie silence.", + "11": "Wildlife flees in terror, their cries distorted and haunting in the storm's winds.", + "12": "When the cyclone subsides, the cursed land feels heavier, as if bearing the weight of unseen eyes." + } + } + }, + "Windstorm": { + "conditions": { + "temperature": { "gte": 30, "lte": 70 }, + "precipitation": { "lte": 20 }, + "wind": { "gte": 70, "lte": 90 }, + "humidity": { "lte": 50 }, + "cloudCover": { "lte": 50 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Crops are flattened as fierce winds howl through the open fields.", + "2": "Barn doors slam uncontrollably, threatening to break their hinges.", + "3": "Livestock scatter, seeking shelter from the relentless gusts.", + "4": "Hay bales are lifted and tossed across the fields like toys.", + "5": "Loose thatch from rooftops is carried off into the swirling storm.", + "6": "Farm tools clatter and fly, leaving the yard in disarray.", + "7": "The air is thick with dust and debris, stinging the eyes.", + "8": "Windmills creak and groan, spinning dangerously fast.", + "9": "Chickens huddle in their coops, feathers ruffled by the storm.", + "10": "Fences buckle and break as the winds tear through the farmland.", + "11": "The relentless wind uproots smaller plants and scatters seeds.", + "12": "The farmstead is left battered, with signs of chaos everywhere." + }, + "village": { + "1": "Wooden homes groan under the strain of gale-force winds.", + "2": "Villagers struggle to secure shutters and doors against the storm.", + "3": "The village square is a whirlwind of debris and broken stalls.", + "4": "Smoke from chimneys is ripped away, leaving hearths to sputter.", + "5": "Children cling to their parents as the wind howls like a beast.", + "6": "The church bell clangs wildly, its sound lost in the storm.", + "7": "Roofs lose shingles, scattering them across the cobblestone streets.", + "8": "Villagers yell over the storm, their voices barely audible.", + "9": "Small trees in the village are bent nearly to the ground.", + "10": "Laundry is ripped from lines, disappearing into the stormy skies.", + "11": "Livestock pens are broken apart, animals fleeing into the chaos.", + "12": "When the wind finally dies, the village is eerily quiet and battered." + }, + "city": { + "1": "Market stalls collapse as the winds barrel through narrow alleys.", + "2": "Stone towers sway ever so slightly, unnerving their inhabitants.", + "3": "Signs and banners are torn from their mounts, fluttering away.", + "4": "The sound of shutters banging and tiles falling fills the air.", + "5": "Citizens huddle indoors, peering nervously through rattling windows.", + "6": "Streetlamps flicker and extinguish as the storm dominates the city.", + "7": "Bridges over canals groan under the storm's relentless force.", + "8": "Stray cats and dogs dart for cover as debris scatters everywhere.", + "9": "The city gates creak ominously, straining against the wind's power.", + "10": "Flags atop battlements are shredded by the gale in mere moments.", + "11": "Merchants abandon their carts, goods scattered across the streets.", + "12": "The storm passes, leaving streets littered and citizens shaken." + }, + "plains": { + "1": "The wide-open plains offer no refuge from the howling wind.", + "2": "Grass ripples like waves, bending under the relentless storm.", + "3": "Dust clouds are kicked up, making it hard to see or breathe.", + "4": "Lonely trees are stripped of branches, their trunks bending dangerously.", + "5": "Wild animals flee, their shapes barely visible through the dust.", + "6": "Tents and makeshift shelters are ripped apart by the gale.", + "7": "Travelers stumble forward, leaning into the storm to keep moving.", + "8": "Fences collapse, the poles snapping under the storm's power.", + "9": "The winds howl uninterrupted, echoing across the flat landscape.", + "10": "Streams are disturbed, their surfaces rippling violently in the wind.", + "11": "Grass seeds are carried far and wide by the roaring gusts.", + "12": "The storm leaves the plains windswept and eerily quiet in its wake." + }, + "forest": { + "1": "Tree branches snap and crash to the forest floor under the strain.", + "2": "Leaves are torn from their branches, swirling in chaotic eddies.", + "3": "The wind roars through the trees, drowning out all other sounds.", + "4": "Wildlife retreats into burrows and dens, seeking refuge from the storm.", + "5": "Loose bark and pinecones are sent flying, littering the forest floor.", + "6": "Tall trees sway ominously, their trunks creaking under the pressure.", + "7": "Paths become treacherous as fallen branches block the way.", + "8": "The canopy offers little protection as the wind tears through it.", + "9": "Campfires are extinguished, their embers scattered by the gusts.", + "10": "Fallen leaves create whirlpools of color amidst the storm.", + "11": "Birds take flight, struggling against the ferocious wind currents.", + "12": "The forest is left littered with broken branches and uprooted trees." + }, + "swamp": { + "1": "The wind churns the swamp's waters, creating ripples and small waves.", + "2": "Reeds and cattails bend nearly flat under the gale's force.", + "3": "Mud and water spray into the air, covering everything in filth.", + "4": "Trees with shallow roots topple into the murky waters below.", + "5": "The wind carries the swamp's pungent smell far and wide.", + "6": "Croaking and chirping are drowned out by the storm's ferocity.", + "7": "Swamp creatures retreat into the water, fleeing the relentless gusts.", + "8": "Planks of swamp bridges are ripped up and carried into the storm.", + "9": "Fog is torn apart by the howling winds, exposing the murky terrain.", + "10": "Lanterns flicker and go out, leaving travelers in eerie darkness.", + "11": "The swamp's trees sway and creak, many losing limbs to the storm.", + "12": "The winds die down, leaving the swamp eerily quiet and debris-laden." + }, + "jungle": { + "1": "Vines and leaves are torn from the canopy, scattering across the jungle.", + "2": "The dense jungle amplifies the sound of the wind, creating a deafening roar.", + "3": "Monkeys and birds scream as they flee deeper into the jungle's heart.", + "4": "Fallen trees block paths and create new obstacles amidst the storm.", + "5": "The thick vegetation does little to shield against the relentless gale.", + "6": "Torrents of rain join the wind, soaking the jungle floor in moments.", + "7": "Small streams overflow, turning trails into muddy, impassable messes.", + "8": "The wind snaps bamboo stalks, sending shards flying like projectiles.", + "9": "Flimsy shelters are flattened, leaving travelers exposed to the storm.", + "10": "The air is thick with flying debris, making movement hazardous.", + "11": "Thunder rolls above, barely audible over the wind's howling fury.", + "12": "The jungle is left battered and chaotic, its paths obscured by debris." + }, + "hills": { + "1": "Grassy slopes ripple violently as the winds howl over the hills.", + "2": "Shepherds struggle to keep their flocks together as gusts scatter them.", + "3": "The wind carries loose soil and pebbles, stinging the skin.", + "4": "Trees atop the hills creak and groan, some losing branches.", + "5": "Rocky outcrops provide scant shelter from the roaring storm.", + "6": "Hilltop paths become treacherous as debris is hurled across them.", + "7": "The howling wind drowns out the calls of distant animals.", + "8": "Small huts on the hills are battered, their roofs threatening to fly away.", + "9": "Loose stones tumble down the slopes, dislodged by the relentless gale.", + "10": "The air is thick with dust, making it hard to see or breathe.", + "11": "Climbing the hills becomes a struggle as the wind pushes travelers back.", + "12": "When the storm passes, the hills are left scarred and eerily quiet." + }, + "mountains": { + "1": "The wind screeches through mountain passes, echoing like a banshee's wail.", + "2": "Climbers cling to rock faces as the gale threatens to rip them away.", + "3": "Loose rocks and gravel are sent tumbling down steep slopes.", + "4": "Mountain goats take refuge in crevices, huddling against the storm.", + "5": "Snow and ice are whipped into the air, blinding and freezing all exposed.", + "6": "Peaks are shrouded in swirling mist as the wind rages on.", + "7": "Tents are torn from campsites, leaving mountaineers exposed to the elements.", + "8": "The wind howls through valleys, amplifying its already deafening roar.", + "9": "Tree lines below the peaks sway and snap under the storm’s force.", + "10": "Rope bridges sway dangerously, some snapping under the pressure.", + "11": "The mountain's trails vanish beneath layers of wind-blown debris.", + "12": "When the winds finally settle, the mountains feel ominously still." + }, + "desert": { + "1": "Sand is whipped into the air, creating a blinding, stinging storm.", + "2": "The sun is obscured as the wind churns up endless dunes.", + "3": "Travelers shield their faces, struggling to see through the sandy haze.", + "4": "Small shrubs and sparse vegetation are uprooted by the ferocious gusts.", + "5": "The wind carries a mournful whistle as it tears across the barren land.", + "6": "Footsteps in the sand are erased almost instantly by the swirling storm.", + "7": "Camels groan and huddle together, bracing against the relentless winds.", + "8": "Tents are flattened, their stakes ripped from the shifting sands.", + "9": "Sand piles up against rocks and dunes, reshaping the desert's landscape.", + "10": "Oases are hidden as sand and debris engulf the surrounding area.", + "11": "The windstorm leaves travelers disoriented, with no landmarks in sight.", + "12": "As the winds fade, the desert is eerily silent, its dunes transformed." + }, + "coastal": { + "1": "Waves crash violently against the shore as the wind roars inland.", + "2": "Ships in the harbor are tossed about, their masts creaking under strain.", + "3": "Sea spray fills the air, mixing with sand and debris from the beach.", + "4": "Fishing nets and gear are blown away, scattered across the coastline.", + "5": "Seagulls struggle to fly, their cries lost in the howling wind.", + "6": "Small coastal homes are battered, shingles and boards flying loose.", + "7": "Palm trees bend precariously, some snapping under the pressure.", + "8": "The tide surges, flooding low-lying areas with churning, frothy water.", + "9": "Boats are pulled from their moorings, sent adrift into the stormy sea.", + "10": "Driftwood and seaweed are flung far onto the shore by the gale.", + "11": "The sound of the storm is a deafening mix of wind and crashing waves.", + "12": "After the storm, the shoreline is littered with wreckage and debris." + }, + "volcano": { + "1": "The wind whips up ash and soot, creating a choking, blinding cloud.", + "2": "Lava flows hiss as the gale stirs up dust and debris nearby.", + "3": "The air is thick with the acrid smell of sulfur, carried by the storm.", + "4": "Rocks and pebbles are dislodged, tumbling dangerously down volcanic slopes.", + "5": "Small vents on the volcano's surface hiss and sputter in the gale.", + "6": "Ash covers everything, coating the ground and stinging exposed skin.", + "7": "The howling wind amplifies the eerie rumble of the volcanic terrain.", + "8": "Flames flicker wildly, struggling against the ferocious gusts.", + "9": "Shelters built on the volcano's slopes are torn apart by the storm.", + "10": "Ash clouds obscure the sky, plunging the area into a dim twilight.", + "11": "The windstorm stirs up hot embers, creating a hazardous environment.", + "12": "When the storm passes, the volcano looks even more desolate and foreboding." + }, + "artic": { + "1": "Snow and ice are torn from the ground, creating blinding white flurries.", + "2": "The wind cuts like a knife, freezing exposed skin in seconds.", + "3": "Ice sheets crack and groan under the storm's relentless force.", + "4": "Polar bears retreat into dens, their fur whipped by the icy gale.", + "5": "Frost clings to everything, forming thick rime on every surface.", + "6": "Visibility drops to near zero as the wind drives snow into the air.", + "7": "Igloos and shelters tremble, their walls threatening to collapse.", + "8": "The sound of the wind is an eerie, hollow howl across the tundra.", + "9": "Frozen rivers are covered in drifting snow, hiding cracks and danger.", + "10": "The gale leaves drifts of snow piled high, burying paths and landmarks.", + "11": "Travelers hunker down, their breath freezing in the frigid air.", + "12": "The storm's aftermath reveals a landscape transformed into a frozen wasteland." + }, + "cursed": { + "1": "The wind carries an eerie wail, like distant screams of the damned.", + "2": "Debris swirls unnaturally, as if guided by unseen hands.", + "3": "Shadows flicker and dart, cast by no visible light source.", + "4": "The air smells of decay and sulfur, choking all who breathe it in.", + "5": "The wind whispers indistinct words, sending chills down the spine.", + "6": "Faint, ghostly apparitions appear in the swirling storm, vanishing just as quickly.", + "7": "Cursed ruins creak and groan as the gale tears through them.", + "8": "Unnatural lightning crackles, illuminating the storm in brief, sickly flashes.", + "9": "The howling wind carries strange, unearthly moans and growls.", + "10": "Crops wither in the storm's wake, as if drained of life itself.", + "11": "Animals flee in terror, their eyes wide with primal fear.", + "12": "The storm leaves a lingering sense of dread, as though something watches." + } + } + }, + "Breezy": { + "conditions": { + "temperature": { "gte": 40, "lte": 80 }, + "precipitation": { "lte": 30 }, + "wind": { "gte": 20, "lte": 40 }, + "humidity": { "gte": 30, "lte": 70 }, + "cloudCover": { "lte": 60 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "A gentle breeze rustles the crops, carrying the scent of fresh earth.", + "2": "The wind sways the tall grass, a soothing rhythm across the fields.", + "3": "Scarecrows creak as the breeze nudges them back and forth.", + "4": "Windmill blades turn lazily, catching the light breeze.", + "5": "Leaves dance in the air, caught in the gentle current.", + "6": "The breeze carries the distant sound of livestock calling.", + "7": "Soft winds shift hay in the barn, making it hard to stack neatly.", + "8": "Children laugh as the wind tugs at their kites.", + "9": "The scent of blooming flowers drifts through the air.", + "10": "Farmers pause, enjoying the cool breeze as they work.", + "11": "The breeze flutters through open windows, cooling the house.", + "12": "Birds glide effortlessly, carried by the gentle wind." + }, + "village": { + "1": "Banners on the village square flap softly in the breeze.", + "2": "The breeze carries the smell of baking bread from the smithy.", + "3": "Children chase fallen leaves dancing across the cobblestones.", + "4": "The wind rattles wooden shutters, but nothing too alarming.", + "5": "Villagers greet each other, enjoying the refreshing gusts.", + "6": "Chickens cluck nervously as feathers are tossed about by the breeze.", + "7": "The gentle wind carries the chatter of villagers from afar.", + "8": "Laundry strung on lines flutters, drying quickly in the breeze.", + "9": "The smith’s forge flickers slightly as the breeze fans the fire.", + "10": "A dog barks at fallen leaves skipping across the path.", + "11": "The breeze stirs up dust in the village square.", + "12": "Birdsong carries clearly on the light wind, filling the air." + }, + "city": { + "1": "Flags atop the city walls flutter in the steady breeze.", + "2": "Merchants in the marketplace adjust awnings flapping in the wind.", + "3": "Loose parchments drift lazily down busy streets.", + "4": "Smoke from chimneys twists and swirls, carried by the breeze.", + "5": "The scent of roasting meat wafts from market stalls.", + "6": "Street performers delight as the breeze adds drama to their acts.", + "7": "Pigeons coo softly as they nestle against the wind’s chill.", + "8": "Shadows of clouds glide swiftly across cobbled roads.", + "9": "The wind carries faint echoes of distant church bells.", + "10": "The gentle breeze cools the crowded city streets.", + "11": "The city gate creaks softly as the wind nudges it.", + "12": "Children run laughing, kites soaring in the clear sky." + }, + "plains": { + "1": "Waves of grass ripple like a green ocean under the breeze.", + "2": "Wildflowers bob gently, their petals shimmering in the sun.", + "3": "The wind whistles softly across the open plains.", + "4": "Dust devils spin lazily, fading as quickly as they form.", + "5": "Birds soar on the currents, calling out to each other.", + "6": "The breeze carries the faint scent of distant rain.", + "7": "Cattle graze peacefully, tails flicking in the soft wind.", + "8": "The rustling of the grass is the only sound for miles.", + "9": "A lone tree stands firm, its branches swaying slightly.", + "10": "The breeze cools travelers, a welcome relief under the sun.", + "11": "Butterflies flit about, carried gently by the wind.", + "12": "A distant howl of wolves is carried faintly on the breeze." + }, + "forest": { + "1": "Leaves rustle gently, creating a soothing canopy of sound.", + "2": "The wind carries the earthy scent of moss and damp bark.", + "3": "Branches sway rhythmically, casting dancing shadows on the ground.", + "4": "The breeze stirs up fallen leaves, scattering them in spirals.", + "5": "Birdsong is carried sweetly through the forest air.", + "6": "The tops of the trees sway like waves in the soft wind.", + "7": "The breeze cools the dense air under the forest canopy.", + "8": "A deer perks up, ears twitching as the breeze shifts scents.", + "9": "Moss sways slightly on ancient trunks, catching the wind’s touch.", + "10": "The sound of a distant brook mingles with the rustling leaves.", + "11": "The breeze carries the faint buzz of insects deep in the forest.", + "12": "A fallen branch creaks as it moves slightly in the wind." + }, + "swamp": { + "1": "Reeds along the water sway gently as the breeze ripples the surface.", + "2": "The wind carries the earthy, damp scent of the swamp.", + "3": "Mosquitoes are blown away briefly, a small relief to travelers.", + "4": "The murky water reflects the swaying trees above.", + "5": "The breeze creates ripples, disturbing the calm of the swamp pools.", + "6": "Birds perched on branches squawk as the wind shakes their perches.", + "7": "The breeze stirs up the thick air, making it slightly fresher.", + "8": "Small ripples in the water betray the movement of unseen creatures.", + "9": "Dead leaves float lazily across the swamp's surface, pushed by the wind.", + "10": "The breeze whistles through the tall grasses, an eerie sound in the swamp.", + "11": "The wind carries the distant croak of frogs echoing across the mire.", + "12": "A heron glides low over the water, wings steady against the breeze." + }, + "jungle": { + "1": "The breeze rustles through thick foliage, creating a chorus of sound.", + "2": "Vines sway gently, casting shifting patterns of shadow and light.", + "3": "The wind stirs the scent of tropical flowers and damp earth.", + "4": "Leaves as large as shields ripple softly in the breeze.", + "5": "The sound of distant monkeys is carried faintly through the jungle.", + "6": "The breeze cools the stifling heat under the jungle canopy.", + "7": "Birds of bright plumage take flight, their calls echoing in the wind.", + "8": "The wind shifts the hanging moss on ancient trees.", + "9": "The rustling leaves hide the scuttling movements of unseen creatures.", + "10": "The breeze carries the faint roar of a distant waterfall.", + "11": "Insects buzz loudly, their flight patterns disrupted by the wind.", + "12": "The jungle seems alive as the wind moves through it, stirring every leaf." + }, + "hills": { + "1": "Gentle winds roll over the grassy hills, rustling wildflowers.", + "2": "The breeze carries the faint bleat of distant sheep grazing.", + "3": "A steady wind bends tall grass, creating flowing waves of green.", + "4": "Cool air swirls, tugging at cloaks and hats atop the hills.", + "5": "The breeze stirs loose pebbles, causing them to skip down slopes.", + "6": "Crows glide on the wind, cawing against the endless sky.", + "7": "The scent of herbs and wildflowers drifts on the gentle breeze.", + "8": "Small gusts whistle softly through crevices in the rocky outcrops.", + "9": "Cloud shadows race across the hills, propelled by the steady wind.", + "10": "The wind carries a chill, cutting through the warm afternoon sun.", + "11": "Birdsong mixes with the sound of rustling leaves on the breeze.", + "12": "The breeze carries the distant sound of travelers on a winding road." + }, + "mountains": { + "1": "The wind howls faintly, echoing off craggy peaks.", + "2": "Loose stones shift as the breeze blows through narrow mountain paths.", + "3": "The breeze carries a sharp chill, biting exposed skin.", + "4": "Flags at mountaintop shrines flap wildly in the persistent wind.", + "5": "Thin air makes the breeze feel colder than it should.", + "6": "Eagles soar effortlessly on the wind, their cries echoing below.", + "7": "Snow dust is lifted into the air, swirling before settling again.", + "8": "A breeze snakes through mountain passes, creating eerie whistling sounds.", + "9": "The wind shifts clouds, revealing and obscuring the distant valleys below.", + "10": "The scent of pine and cold stone drifts faintly in the breeze.", + "11": "The wind kicks up small avalanches of dust and loose rocks.", + "12": "Climbers brace against sudden gusts, leaning into the mountain’s embrace." + }, + "desert": { + "1": "Hot winds carry the scent of dry earth and sun-scorched stone.", + "2": "The breeze stirs up small whirlwinds of sand, dancing in the heat.", + "3": "The air shimmers as the wind flows over endless dunes.", + "4": "A dry wind tugs at travelers’ cloaks, leaving grit on their faces.", + "5": "Loose grains of sand pepper exposed skin with each gust.", + "6": "The breeze hums through rocky formations, creating haunting sounds.", + "7": "Palm fronds rustle softly, the only movement in the barren landscape.", + "8": "The wind carries the distant cry of a desert hawk.", + "9": "Hot air swirls over the desert, offering no relief from the heat.", + "10": "The wind scatters tracks in the sand, erasing signs of passage.", + "11": "Mirages shift and dance as the breeze stirs the desert floor.", + "12": "A faint wind carries the scent of an oasis, though it’s far away." + }, + "coastal": { + "1": "Sea spray mists the air as the wind whips over the waves.", + "2": "The breeze carries the briny scent of the ocean, refreshing and sharp.", + "3": "Seagulls cry loudly, soaring effortlessly on the coastal winds.", + "4": "Palm trees sway in the breeze, their fronds rattling softly.", + "5": "The breeze pushes waves higher, their foam crashing on the rocks.", + "6": "Fishing nets flap in the wind, their lines taut and swaying.", + "7": "A cool wind carries the distant toll of a lighthouse bell.", + "8": "The air is filled with the salty tang of the ocean and wet wood.", + "9": "The breeze stirs small whirlpools of sand along the shoreline.", + "10": "The wind flutters sails, filling them with life as boats cut through the water.", + "11": "Seaweed sways in the shallow surf, guided by the steady breeze.", + "12": "The breeze carries the distant sound of waves crashing on distant cliffs." + }, + "volcano": { + "1": "Hot winds carry the faint scent of sulfur and ash.", + "2": "The breeze swirls embers from cooling lava, glowing faintly in the air.", + "3": "The wind whistles through jagged rock formations, carrying an ominous hum.", + "4": "Steam vents hiss as the breeze wafts it into swirling clouds.", + "5": "Ash dances in the air, carried by the warm, restless winds.", + "6": "The breeze offers no comfort, stifling in the volcanic heat.", + "7": "Smoke rises in twisting plumes, blown sideways by the steady wind.", + "8": "The wind carries the deep rumble of distant volcanic activity.", + "9": "Sulfurous fumes linger, thickened by the swirling air.", + "10": "The wind stirs loose rocks on the slopes, sending them tumbling.", + "11": "The breeze carries an unnatural warmth, oppressive and heavy.", + "12": "Volcanic glass glints as the wind reveals its sharp edges." + }, + "arctic": { + "1": "Icy winds bite through layers, numbing exposed skin instantly.", + "2": "Snow drifts swirl as the breeze stirs the endless white expanse.", + "3": "The breeze carries a chill that freezes breath in midair.", + "4": "Frost-covered rocks glint in the pale sunlight as the wind blows past.", + "5": "The wind howls mournfully across the frozen tundra.", + "6": "The air is sharp and clear, every gust slicing through the silence.", + "7": "The breeze carries the distant growl of cracking icebergs.", + "8": "Auroras shimmer faintly, their colors swaying in the wind’s rhythm.", + "9": "Snowy owl feathers are tossed gently by the cold breeze.", + "10": "The wind carries the faint scent of the ocean from distant ice floes.", + "11": "The snow glitters under a sunlit breeze, deceptively serene.", + "12": "Icy crystals form in the air, carried softly by the gentle winds." + }, + "cursed": { + "1": "The wind carries faint whispers, sending shivers down your spine.", + "2": "A chilling breeze stirs the mist, revealing fleeting shadows.", + "3": "The air hums with an unnatural energy as the breeze sweeps by.", + "4": "The wind seems to whisper your name, unsettling and cold.", + "5": "The breeze carries the faint scent of decay, sharp and unpleasant.", + "6": "Leaves scatter across the cursed ground, moving as if alive.", + "7": "The wind howls like a distant scream, eerie and unrelenting.", + "8": "The air grows colder with each gust, as if stealing warmth away.", + "9": "The breeze stirs the fog, twisting it into sinister shapes.", + "10": "The sound of rattling chains echoes faintly on the wind.", + "11": "A shadow moves with the wind, though nothing is there to cast it.", + "12": "The air feels heavy despite the breeze, carrying an oppressive weight." + } + } + }, + "Bright and Warm": { + "conditions": { + "temperature": { "gte": 60, "lte": 80 }, + "precipitation": { "lte": 10 }, + "wind": { "lte": 20 }, + "humidity": { "gte": 30, "lte": 60 }, + "cloudCover": { "lte": 60 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "Golden sunlight bathes the fields, the warmth perfect for growing crops.", + "2": "Farmhands work contentedly under a clear blue sky, the warmth invigorating.", + "3": "The sun glistens on freshly watered soil, promising a bountiful harvest.", + "4": "The warm air carries the scent of ripening fruit and blooming flowers.", + "5": "Livestock rest lazily in the sun, enjoying the gentle warmth.", + "6": "A gentle breeze carries the sound of a distant rooster's call.", + "7": "Children laugh and play in the warm sunlight, carefree and happy.", + "8": "The fields shimmer with life as the warmth encourages growth.", + "9": "The warm sun sets a perfect backdrop for a peaceful day of work.", + "10": "Freshly baked bread cools on windowsills, warmed by the sun.", + "11": "The air hums with the sound of bees and the rustle of hay.", + "12": "The farm feels alive, the warmth bringing out vibrant colors and activity." + }, + "village": { + "1": "Villagers bustle about under a warm sun, their spirits lifted by the light.", + "2": "Children chase each other around the market, their laughter echoing.", + "3": "The warmth of the day invites villagers to gather outside for meals.", + "4": "The village square buzzes with activity, bathed in golden sunlight.", + "5": "The scent of freshly baked bread wafts through the warm air.", + "6": "Sunlight glints off the well’s water, creating dancing patterns.", + "7": "The warmth encourages artisans to work outdoors, showcasing their crafts.", + "8": "Shaded porches offer a cool respite for villagers on a sunny day.", + "9": "Birdsong fills the air as villagers greet the day with optimism.", + "10": "Laundry dries quickly on lines, fluttering lightly in the warm breeze.", + "11": "The warmth draws merchants to set up colorful stalls in the square.", + "12": "The day feels tranquil, the village alive with cheerful activity." + }, + "city": { + "1": "Sunlight gleams off the cobblestones, illuminating the bustling streets.", + "2": "Merchants call out their wares, their voices mingling with the hum of the city.", + "3": "The warmth brings life to the city, with crowds thronging the market square.", + "4": "City gardens bloom vividly, their colors enhanced by the warm sun.", + "5": "Street performers draw crowds, their shows lively under the bright sky.", + "6": "The warmth encourages artisans to open their shutters and display their goods.", + "7": "Children play near fountains, the water sparkling in the sunlight.", + "8": "The scent of roasted meat and spices fills the warm city air.", + "9": "The city’s towers seem to glow, their stone warmed by the sun.", + "10": "People linger at outdoor cafes, enjoying the bright and pleasant day.", + "11": "The streets feel alive with activity, the warmth drawing everyone outdoors.", + "12": "The sun’s warmth touches every corner of the city, spreading cheer." + }, + "plains": { + "1": "The open plains stretch endlessly under a bright, cloudless sky.", + "2": "Grass sways gently in the warm breeze, vibrant and green.", + "3": "Wildflowers bloom abundantly, their colors vivid in the sunlight.", + "4": "The warm air carries the scent of fresh grass and wildflowers.", + "5": "Birds soar overhead, their songs clear and joyful in the open air.", + "6": "The plains feel alive, the warmth bringing out the hum of insects.", + "7": "Herds of animals graze peacefully under the warm and bright sky.", + "8": "The horizon shimmers slightly in the gentle heat, adding a golden hue.", + "9": "Travelers move steadily across the plains, energized by the warm weather.", + "10": "The sun warms the earth, creating a perfect day for exploration.", + "11": "The warmth brings a sense of calm, the plains vast and serene.", + "12": "Shadows of passing clouds create fleeting patches of shade on the plains." + }, + "forest": { + "1": "Sunlight filters through the canopy, casting warm dappled patterns on the ground.", + "2": "The forest hums with life, the warmth stirring birds and insects alike.", + "3": "The warm air carries the earthy scent of moss and wildflowers.", + "4": "Sunbeams pierce the foliage, illuminating patches of vibrant green leaves.", + "5": "The forest feels alive, the warmth encouraging the rustle of hidden creatures.", + "6": "Streams glisten under the warm sun, their gentle babble soothing.", + "7": "The warmth stirs the smell of pine and damp wood, rich and inviting.", + "8": "Birdsong fills the warm air, echoing softly through the forest.", + "9": "The trees seem to glow in the sunlight, their bark warm to the touch.", + "10": "Deer graze in the clearings, their movements gentle in the warm air.", + "11": "The forest feels peaceful, its shadows cool and inviting in the warmth.", + "12": "The warmth brings out the forest’s colors, every leaf vivid and alive." + }, + "swamp": { + "1": "Sunlight glints off the water, illuminating the swamp’s tangled greenery.", + "2": "The warm air carries the scent of mud and blooming water lilies.", + "3": "The swamp feels alive, the warmth stirring frogs and dragonflies.", + "4": "Reeds sway gently in the breeze, their tops golden in the sunlight.", + "5": "The sunlight creates shimmering patterns on the swamp’s surface.", + "6": "Warm air clings heavily, mixing with the swamp’s earthy aroma.", + "7": "Birds call out from the trees, their sounds vibrant in the warm air.", + "8": "The warmth stirs ripples in the water, hinting at movement beneath.", + "9": "Sunlight catches on moss-draped branches, casting golden-green shadows.", + "10": "The swamp buzzes with life, the warmth bringing creatures out of hiding.", + "11": "The humid air carries the distant croak of frogs and rustle of leaves.", + "12": "The swamp shimmers in the sunlight, its water sparkling like glass." + }, + "jungle": { + "1": "The jungle teems with life, the warmth intensifying its vibrant colors.", + "2": "Sunlight streams through gaps in the canopy, illuminating lush foliage.", + "3": "The warm air is thick with the scent of exotic flowers and damp earth.", + "4": "The jungle hums with the sounds of birds and insects, alive in the warmth.", + "5": "Bright sunlight highlights the emerald greens of the jungle’s dense leaves.", + "6": "Warmth clings heavily to the jungle, the air thick and alive.", + "7": "The jungle floor glows with patches of sunlight, rich with fallen leaves.", + "8": "Monkeys chatter in the treetops, their calls carrying through the warm air.", + "9": "The warmth brings out the scent of blooming orchids and ripe fruit.", + "10": "The jungle vibrates with life, every leaf and branch seemingly alive in the warmth.", + "11": "Shadows shift and dance as the sun filters through the jungle’s canopy.", + "12": "Streams sparkle in the sunlight, their sound mingling with the jungle’s hum." + }, + "hills": { + "1": "Golden sunlight bathes the rolling hills, making every blade of grass shine.", + "2": "Shepherds guide their flocks under a warm and clear sky.", + "3": "The hills are alive with birdsong, the warm air carrying the sound far.", + "4": "Soft breezes rustle the wildflowers dotting the warm, sunlit slopes.", + "5": "The warmth of the day makes the hills seem endless and serene.", + "6": "Sunlight glistens on a meandering brook, the hills peaceful and calm.", + "7": "The warm air carries the earthy scent of wild grasses and flowers.", + "8": "Shadows from scattered clouds drift lazily over the sunlit hills.", + "9": "The warmth encourages deer and rabbits to graze openly in the fields.", + "10": "The golden hills seem to stretch forever under a brilliant blue sky.", + "11": "The day feels calm and pleasant, perfect for exploring the hills.", + "12": "Sunlight warms the stones of a weathered path winding through the hills." + }, + "mountains": { + "1": "Sunlight glints off rocky peaks, the warmth a welcome reprieve from the chill.", + "2": "The mountains are vibrant, their slopes dotted with blooming wildflowers.", + "3": "Eagles soar overhead, the bright skies perfect for their flight.", + "4": "The warmth brings out the rich scent of pine and fresh mountain air.", + "5": "Streams glisten in the sunlight, cascading down the rugged cliffs.", + "6": "Hikers enjoy the warmth as they traverse sun-drenched mountain trails.", + "7": "The clear air carries the sound of distant waterfalls and rustling leaves.", + "8": "Bright light catches on the snowcaps, making them shimmer like jewels.", + "9": "Goats scale steep cliffs, their movements confident in the warm sun.", + "10": "The warmth brings life to the alpine meadows, filled with colorful blooms.", + "11": "The bright skies offer a breathtaking view of the sprawling valleys below.", + "12": "The mountains feel alive under the warm sunlight, every rock and tree aglow." + }, + "desert": { + "1": "The desert shimmers under a blazing sun, its sands glowing gold.", + "2": "Warm winds stir the dunes, their shifting patterns mesmerizing.", + "3": "The oasis gleams like a mirage, its waters reflecting the bright sky.", + "4": "Sunlight highlights every crack and crevice in the sunbaked earth.", + "5": "Lizards scurry across the sands, their movements quick in the heat.", + "6": "The warmth brings out the sharp, dry scent of the desert plants.", + "7": "The desert feels vast and endless, its beauty stark under the bright sun.", + "8": "The warmth encourages traders to rest under makeshift shades.", + "9": "The desert’s colors come alive, from golden sands to deep red cliffs.", + "10": "Bright sunlight makes the horizon shimmer, a mirage dancing in the heat.", + "11": "The dry air carries the call of a lone hawk circling high above.", + "12": "Even in the heat, the desert feels majestic and timeless." + }, + "coastal": { + "1": "Golden sunlight dances on the waves, making the sea sparkle brilliantly.", + "2": "The warm air carries the salty tang of the ocean breeze.", + "3": "Seagulls cry out as they glide effortlessly under the bright, cloudless sky.", + "4": "The shoreline shimmers, the sand warm and inviting underfoot.", + "5": "The warmth brings locals to gather near the docks, sharing stories and fish.", + "6": "The waves lap gently against the shore, their sound soothing in the sun.", + "7": "Sailboats dot the horizon, their sails bright against the azure sky.", + "8": "The scent of fresh seaweed mingles with the crisp ocean air.", + "9": "The sunlit sea glows with a deep blue, calm and mesmerizing.", + "10": "Warm sunlight makes the coastal cliffs shine, their rugged beauty on full display.", + "11": "The coastal town feels alive, the warmth drawing people outdoors.", + "12": "Fishermen mend their nets in the sun, the warmth comforting and steady." + }, + "volcano": { + "1": "The volcanic slopes shimmer in the sunlight, their colors vibrant and sharp.", + "2": "Warm air rises from the ground, carrying the faint scent of sulfur.", + "3": "Bright sunlight catches on blackened rock, creating a stark, surreal beauty.", + "4": "The warmth intensifies near the lava flows, their glow vivid under the sun.", + "5": "The volcano’s rugged terrain glows with a strange, fiery charm in the heat.", + "6": "The warmth brings out the harsh, otherworldly beauty of the volcanic landscape.", + "7": "Steam vents hiss softly, their vapor shimmering in the sunlight.", + "8": "The ground radiates heat, the sunlight adding to the land’s intense warmth.", + "9": "Bright skies contrast with the dark, jagged rock, creating a striking vista.", + "10": "Birds circle above the crater, their silhouettes stark against the bright sky.", + "11": "The volcanic peak looms majestically, bathed in warm, golden light.", + "12": "The warmth and light make the volcanic region feel both dangerous and awe-inspiring." + }, + "artic": { + "1": "The icy landscape gleams brilliantly under the warm sunlight.", + "2": "Snowfields sparkle as the warmth brings a slight softness to the frost.", + "3": "Warmth stirs faint signs of life, as seals bask on sunlit ice floes.", + "4": "The bright sky casts a serene glow over the endless expanse of ice.", + "5": "The warmth creates delicate patterns of meltwater on the glacial surface.", + "6": "The snow reflects the sunlight, making the arctic almost painfully bright.", + "7": "The icebergs shimmer in hues of blue and white, majestic in the light.", + "8": "The air feels less biting, the warmth a small but welcome change.", + "9": "Snow hares dart across the tundra, their white fur glowing in the sunlight.", + "10": "The glaciers seem to glow from within, their icy depths lit by the sun.", + "11": "Warmth softens the icy edges, creating small streams that glisten in the light.", + "12": "The arctic feels tranquil, the warmth giving it an almost otherworldly beauty." + }, + "cursed": { + "1": "Sunlight struggles to pierce the cursed haze, casting an eerie warm glow.", + "2": "The warmth feels unnatural, the bright light creating long, twisted shadows.", + "3": "The cursed land glows faintly under the sunlight, a disquieting sight.", + "4": "Warmth carries a strange, acrid smell that lingers in the cursed air.", + "5": "The sunlight reflects off dark pools, their surface shimmering ominously.", + "6": "Even under the warmth, the cursed land feels oppressive and silent.", + "7": "Bright light reveals unnatural movement in the shadows of the cursed ground.", + "8": "The warmth fails to soothe, the air thick with a sense of foreboding.", + "9": "The cursed earth cracks and shifts, the warmth exposing strange patterns.", + "10": "Warmth creates a shimmering mirage, distorting the cursed landscape further.", + "11": "The cursed ground steams faintly under the bright light, as if resisting it.", + "12": "Sunlight creates fleeting illusions, the warmth unable to lift the gloom fully." + } + } + }, + "Calm and Still": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "lte": 10 }, + "wind": { "lte": 10 }, + "humidity": { "gte": 30, "lte": 70 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 50 } + }, + "descriptions": { + "farm": { + "1": "The fields are silent, not a whisper of wind disturbs the crops.", + "2": "The stillness makes the chirping of distant crickets seem louder.", + "3": "Farm animals rest quietly under the still, open sky.", + "4": "A calm haze settles over the barn, everything feels paused in time.", + "5": "The warm air hangs heavy, unmoving, as the sun bathes the land.", + "6": "The only sound is the creak of a wooden wheel in the silent yard.", + "7": "The farm feels serene, not a single leaf stirs on the trees.", + "8": "The still air carries the faint scent of fresh hay and tilled earth.", + "9": "The calm makes even the sound of a distant rooster seem startling.", + "10": "The pond reflects the clear sky perfectly in the motionless air.", + "11": "The stillness makes the occasional buzz of a fly seem magnified.", + "12": "The farm lies under a deep calm, as though nature itself rests." + }, + "village": { + "1": "The village square is unusually quiet, the air completely still.", + "2": "Children’s laughter carries farther in the calm, silent atmosphere.", + "3": "Smoke from chimneys rises straight into the sky, undisturbed by wind.", + "4": "The calm air amplifies the clinking of a blacksmith’s hammer.", + "5": "The streets feel tranquil, as if the village is holding its breath.", + "6": "Shops and stalls seem frozen in time, the calm wrapping the village in stillness.", + "7": "The stillness makes even soft conversations sound clear and crisp.", + "8": "A gentle calm settles over the village, broken only by the occasional bark of a dog.", + "9": "The church bell echoes through the still air, its sound lingering longer.", + "10": "The village well reflects the serene sky like a mirror.", + "11": "Even the birds seem subdued, their songs faint in the quiet air.", + "12": "The village feels unusually serene, as if the world outside has paused." + }, + "city": { + "1": "The bustling city feels subdued, its usual clamor softened by the calm.", + "2": "Smoke lingers in the air, rising straight from chimneys in the stillness.", + "3": "The usual chaos of the market feels muted under the tranquil sky.", + "4": "The calm air makes distant street noises seem eerily close.", + "5": "The city feels heavy, as if wrapped in an unseen stillness.", + "6": "The calm amplifies the faint clang of a hammer on steel from the forges.", + "7": "The canals are motionless, reflecting the skyline like a painted scene.", + "8": "The stillness lends the city an unusual sense of peace.", + "9": "The calm makes even the creak of a carriage wheel sound sharp and clear.", + "10": "The usual hum of the city feels distant, muffled by the tranquil air.", + "11": "Banners hang limp, unmoving, as the calm envelops the streets.", + "12": "Even the clatter of hooves on cobblestones feels muted in the stillness." + }, + "plains": { + "1": "The vast plains stretch silently under the still, open sky.", + "2": "The calm air allows every sound to travel far and clear.", + "3": "Grass stands tall and motionless, undisturbed by even the faintest breeze.", + "4": "The plains feel endless and serene, wrapped in the tranquility of the day.", + "5": "The stillness makes the occasional rustle of a small animal startling.", + "6": "The only movement on the plains is the slow drift of clouds overhead.", + "7": "The calm amplifies the distant call of a lone bird.", + "8": "A serene silence blankets the land, broken only by the faint sound of insects.", + "9": "The lack of wind makes the plains feel vast and timeless.", + "10": "The air is heavy with stillness, the scent of earth strong and clear.", + "11": "The plains bask in a tranquil quiet, as if nature herself rests.", + "12": "The still air carries the soft hum of life across the open fields." + }, + "forest": { + "1": "The forest feels enchanted, its trees standing silent and still.", + "2": "The calm amplifies the occasional creak of a shifting branch.", + "3": "Even the rustle of a squirrel sounds loud in the tranquil forest.", + "4": "The stillness makes the forest feel deeper, its shadows more mysterious.", + "5": "Sunlight filters through the leaves, undisturbed by the calm air.", + "6": "The forest floor is quiet, the usual rustle of leaves completely absent.", + "7": "The calm makes the distant chirp of birds feel hauntingly clear.", + "8": "The still air carries the earthy scent of moss and bark.", + "9": "The forest seems to breathe slowly, its tranquility palpable.", + "10": "The lack of wind gives the forest an almost eerie stillness.", + "11": "The forest feels timeless, as if frozen under the weight of the calm.", + "12": "The quiet is broken only by the soft crackle of a falling branch." + }, + "swamp": { + "1": "The swamp is eerily quiet, its still waters reflecting the sky perfectly.", + "2": "The calm air makes the faint croak of frogs sound louder than usual.", + "3": "Even the insects seem subdued, their hum faint in the tranquil swamp.", + "4": "The water lies motionless, its surface broken only by the occasional ripple.", + "5": "The swamp feels heavy and oppressive, the stillness amplifying every sound.", + "6": "The calm air carries the pungent scent of decaying vegetation.", + "7": "The lack of wind makes the swamp’s mist hang thick and unmoving.", + "8": "Every splash of a distant animal echoes in the silent swamp.", + "9": "The still air makes the swamp feel otherworldly, timeless in its quiet.", + "10": "The calm makes the occasional rustle of reeds sound sharp and clear.", + "11": "The swamp feels alive yet eerily subdued, its usual noises hushed.", + "12": "The stillness makes the swamp’s hidden depths seem even more mysterious." + }, + "jungle": { + "1": "The jungle feels unusually quiet, the still air stifling and heavy.", + "2": "Even the leaves hang motionless, their usual rustle absent.", + "3": "The calm amplifies the distant call of a bird echoing through the jungle.", + "4": "The still air makes the dense foliage feel even more suffocating.", + "5": "The jungle seems to pause, its usual cacophony replaced by silence.", + "6": "The calm makes every distant sound, even a dropping fruit, seem louder.", + "7": "The stillness highlights the vibrant colors of the jungle in the filtered light.", + "8": "The humid air clings to the skin, unmoved by even the faintest breeze.", + "9": "The jungle’s dense canopy feels heavy, its stillness adding to its mystery.", + "10": "The quiet makes the occasional chatter of a monkey seem oddly loud.", + "11": "The calm air magnifies the scent of flowers and decaying plants.", + "12": "The jungle feels timeless, its stillness thick and almost tangible." + }, + "hills": { + "1": "The rolling hills lie silent, bathed in the stillness of the day.", + "2": "Not a single blade of grass stirs on the calm, verdant slopes.", + "3": "The hills echo with the sound of distant sheep, carried by the still air.", + "4": "The tranquil scene is broken only by the soft rustle of a lone bird.", + "5": "The hills seem timeless, resting quietly under the unbroken sky.", + "6": "The stillness amplifies the crunch of boots on rocky trails.", + "7": "Even the streams trickle softly, undisturbed by the calm.", + "8": "The air feels heavy and warm, clinging to the slopes in serene silence.", + "9": "The sound of a distant horn carries far in the still atmosphere.", + "10": "The landscape appears frozen, the tranquility wrapping the hills in quiet.", + "11": "The gentle slopes bask in the still sunlight, their colors vivid and rich.", + "12": "The silence makes even the rustle of small animals in the underbrush startling." + }, + "mountains": { + "1": "The towering peaks stand sentinel in the motionless air.", + "2": "Not a breath of wind disturbs the craggy ridges and sharp cliffs.", + "3": "The stillness makes the distant roar of an avalanche seem closer.", + "4": "The mountains feel ancient and eternal under the calm, unbroken sky.", + "5": "Even the sound of falling rocks echoes clearly in the tranquil air.", + "6": "The air is thin and quiet, the stillness amplifying every sound.", + "7": "The snow-covered peaks gleam under the calm, golden sunlight.", + "8": "The silence is heavy, broken only by the occasional cry of an eagle.", + "9": "The valleys below lie hushed, as if the mountains themselves are resting.", + "10": "The stillness enhances the stark beauty of the rugged mountain terrain.", + "11": "The mountain paths are eerily quiet, footsteps echoing in the void.", + "12": "The peaks seem untouched by time, their grandeur magnified by the calm." + }, + "desert": { + "1": "The desert stretches endlessly, silent and still under the blazing sun.", + "2": "Not a single grain of sand shifts in the windless heat.", + "3": "The calm air carries the faint smell of dry earth and distant dunes.", + "4": "The stillness amplifies the crunch of boots on the parched ground.", + "5": "The desert feels timeless, the horizon shimmering in the tranquil heat.", + "6": "The silence is profound, broken only by the faint buzz of an insect.", + "7": "The calm reveals the desert’s stark beauty, every detail sharp and clear.", + "8": "The dry, still air clings to the skin, making every breath feel heavy.", + "9": "The sun blazes down on the silent dunes, casting long, unbroken shadows.", + "10": "The desert feels eternal, its emptiness magnified by the motionless air.", + "11": "The silence makes the distant call of a bird sound hauntingly clear.", + "12": "The desert seems to hold its breath, the calm stretching on endlessly." + }, + "coastal": { + "1": "The ocean lies still, its surface like glass under the calm sky.", + "2": "Not a single wave crashes, the sea and shore locked in silence.", + "3": "The stillness makes the distant cry of gulls sound louder than usual.", + "4": "The air smells of salt and seaweed, unmoving and thick in the calm.", + "5": "The coastal village rests quietly, its sounds softened by the tranquil air.", + "6": "The calm reveals every detail of the shoreline, every rock and shell visible.", + "7": "The horizon is clear, the still water blending seamlessly with the sky.", + "8": "The silence is profound, broken only by the faint lapping of water.", + "9": "Even the boats in the harbor seem frozen, their sails limp in the still air.", + "10": "The sea reflects the golden light of the sun, unbroken by waves.", + "11": "The calm amplifies the sound of footsteps on the wet sand.", + "12": "The coastal landscape feels timeless, its beauty magnified by the stillness." + }, + "volcano": { + "1": "The volcanic slopes are eerily quiet, as if nature is holding its breath.", + "2": "The calm air makes the faint rumble of the earth sound ominous.", + "3": "Not a single ember rises from the vents, the volcano resting silently.", + "4": "The stillness enhances the heat radiating from the volcanic rock.", + "5": "The sulfurous scent hangs heavy in the air, unmoved by wind.", + "6": "The calm makes the distant sound of bubbling magma feel close and menacing.", + "7": "The volcano feels ancient and brooding, its slopes bathed in eerie stillness.", + "8": "The sky above the crater is clear, the calm air amplifying the desolation.", + "9": "The silence makes the occasional crackle of cooling rock sound sharp and loud.", + "10": "The calm air magnifies the heat, making the volcanic slopes feel suffocating.", + "11": "The volcano seems dormant, its menacing presence softened by the stillness.", + "12": "The barren slopes reflect the sun’s light, the calm adding to their stark beauty." + }, + "artic": { + "1": "The icy expanse lies silent, the snow untouched and glistening in the stillness.", + "2": "Not a breath of wind stirs the frost-covered trees and frozen ground.", + "3": "The calm makes the distant crack of shifting ice sound sharp and clear.", + "4": "The artic feels timeless, its vastness magnified by the profound stillness.", + "5": "The cold air clings to the skin, heavy and unmoving in the calm.", + "6": "The stillness reveals the artic’s stark beauty, every detail crystal clear.", + "7": "The silence is broken only by the faint sound of snow crunching underfoot.", + "8": "The calm makes the distant howl of wolves seem haunting and close.", + "9": "The frozen landscape feels endless, the still air amplifying its desolation.", + "10": "The artic sky is clear, the sunlight reflecting brilliantly off the ice.", + "11": "The silence is profound, as if the entire world has paused in the cold.", + "12": "The artic feels both beautiful and unforgiving under the weight of the calm." + }, + "cursed": { + "1": "The cursed land lies deathly still, an unnatural silence hanging in the air.", + "2": "The calm amplifies the faint whispers that seem to emanate from nowhere.", + "3": "The stillness makes the eerie glow of the cursed ground even more unsettling.", + "4": "Not a single leaf stirs, the land frozen under an oppressive calm.", + "5": "The silence is heavy, broken only by the occasional sound of distant wailing.", + "6": "The cursed land feels timeless, its desolation magnified by the still air.", + "7": "The calm makes the faint scent of decay and sulfur seem stronger.", + "8": "The silence amplifies every creak of old wood and crack of brittle stone.", + "9": "The cursed ground seems to hum faintly, its stillness unsettling to the core.", + "10": "The unnatural calm makes even the smallest sound feel deafening.", + "11": "The land feels frozen in time, its cursed presence amplified by the stillness.", + "12": "The calm makes the faint, ghostly lights hovering in the distance all the more haunting." + } + } + }, + "Chilly but Overcast": { + "conditions": { + "temperature": { "gte": 30, "lte": 50 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 30 }, + "humidity": { "gte": 50, "lte": 80 }, + "cloudCover": { "gte": 70 }, + "visibility": { "gte": 20, "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Gray clouds blanket the sky, and a chilly breeze whispers through the fields.", + "2": "The overcast sky casts a dim light over the farmstead, with cold air nipping at exposed skin.", + "3": "A chill hangs in the air, the muted sun struggling to break through thick clouds.", + "4": "The barn creaks softly under the weight of a cold, cloud-covered day.", + "5": "The scent of damp earth rises as a cool breeze sweeps through the fields.", + "6": "The overcast sky gives the farm a somber tone, with a frosty bite in the air.", + "7": "The livestock huddle together for warmth under a sky heavy with gray clouds.", + "8": "Frost lingers in the shaded corners, refusing to melt under the dull daylight.", + "9": "The chilly wind carries the faint smell of hay and distant smoke from the hearth.", + "10": "The farmhouse windows glisten with condensation as the chill settles in.", + "11": "The fields appear lifeless under the gray expanse, with cold air suppressing activity.", + "12": "The sound of distant crows adds to the melancholy of the chilly, overcast day." + }, + "village": { + "1": "The village square is quiet, with villagers wrapped in cloaks against the chilly, gray skies.", + "2": "Smoke rises lazily from chimneys, dissipating into the cold, overcast sky.", + "3": "The cobblestone streets glisten faintly with lingering frost under the muted light.", + "4": "A cold wind blows through the village, rattling loose shutters and creaking signs.", + "5": "The overcast sky casts the village in shades of gray, with an oppressive chill in the air.", + "6": "Villagers move quickly, eager to return to warm fires as the cold clouds loom above.", + "7": "The chilly air carries the smell of burning wood and damp earth.", + "8": "Even the market is subdued, the cold overcast weather keeping most indoors.", + "9": "Children kick at frozen puddles, their breath misting in the frosty air.", + "10": "The sound of footsteps echoes sharply through the still, cold village streets.", + "11": "The distant toll of a bell seems amplified by the heavy, overcast sky.", + "12": "The village well is coated in frost, its surface reflecting the dull gray above." + }, + "city": { + "1": "The bustling city seems subdued under the weight of the overcast, chilly sky.", + "2": "Frost clings to rooftops, and the cold air turns breath into fleeting clouds.", + "3": "The city’s chimneys pour smoke into the gray expanse, adding to the somber atmosphere.", + "4": "Merchants huddle in their stalls, their wares glistening faintly with frost.", + "5": "The overcast sky darkens the city streets, and the chill drives most indoors.", + "6": "The sound of hooves and wheels on cobblestones echoes through the cold, gray air.", + "7": "Cold drafts sneak through narrow alleys, carrying the scent of damp stone and smoke.", + "8": "The city gates creak in the chill, their iron hinges stiff from frost.", + "9": "Street performers huddle for warmth, their songs muted under the heavy clouds.", + "10": "The cold overcast day renders the city’s colors dull and muted.", + "11": "The marketplace is quieter than usual, the chill keeping crowds sparse.", + "12": "Lanterns are lit early, their warm glow contrasting with the gray, overcast sky." + }, + "plains": { + "1": "The wide plains are silent under a sky heavy with gray clouds and a biting chill.", + "2": "Frost tips the grass, the chilly air still and heavy with overcast gloom.", + "3": "The horizon is a blur of gray, the overcast sky blending with the cold plains.", + "4": "A chilly breeze whispers through the tall grass, carrying a sense of stillness.", + "5": "The cold air clings to the plains, dampening the distant cries of birds.", + "6": "The overcast sky casts a shadowless light over the vast, empty fields.", + "7": "The plains feel vast and lonely, the chill making every sound seem distant.", + "8": "Hoarfrost clings to shrubs, sparkling faintly under the dull gray sky.", + "9": "The cold air carries the faint smell of damp earth and distant rain.", + "10": "The plains stretch endlessly under a thick, gray canopy of clouds.", + "11": "The chill numbs the fingers, the overcast sky offering no relief from the cold.", + "12": "The wind whistles faintly, the sound swallowed by the vast, overcast plains." + }, + "forest": { + "1": "The forest is eerily quiet, the chill in the air amplifying the sound of rustling leaves.", + "2": "Gray light filters through the canopy, casting the forest floor in muted tones.", + "3": "Frost clings to the underbrush, the cold air thick with the scent of damp wood.", + "4": "The overcast sky above adds a somber tone to the cold, still forest.", + "5": "The chilly air makes the trees creak softly, their branches heavy with frost.", + "6": "The forest path is slippery with frost, the cold biting through layers of clothing.", + "7": "Even the animals are subdued, the chill and gray sky keeping the forest quiet.", + "8": "The cold wind stirs the leaves, their dry rustling the only sound in the stillness.", + "9": "The forest feels timeless, the chill making every breath visible in the muted light.", + "10": "The overcast sky gives the forest an almost mystical quality, the shadows deep and dark.", + "11": "A faint mist rises from the forest floor, clinging to the chill in the air.", + "12": "The distant sound of a stream echoes faintly through the cold, gray forest." + }, + "swamp": { + "1": "The swamp is damp and cold, the overcast sky casting a gray pall over the water.", + "2": "Frost edges the reeds, the chilly air thick with the smell of decay.", + "3": "The overcast sky makes the swamp feel oppressive, the chill seeping into the bones.", + "4": "The water is still, the chill and gray sky giving the swamp an eerie calm.", + "5": "The cold air carries the faint croak of frogs, their sounds muffled by the overcast sky.", + "6": "Mist rises from the cold water, swirling under the gray, heavy sky.", + "7": "The swamp feels frozen in time, the chill making every step feel sluggish.", + "8": "The overcast day amplifies the swamp’s murkiness, the cold making it feel lifeless.", + "9": "The air smells of wet moss and decay, heavy and unmoving under the gray sky.", + "10": "The swamp’s stillness is broken only by the faint plop of water droplets.", + "11": "The chill clings to the swamp, the muted light making the water appear darker.", + "12": "The overcast sky and chill make the swamp feel otherworldly and foreboding." + }, + "jungle": { + "1": "The jungle is quiet under the overcast sky, the chill muting its usual vibrancy.", + "2": "Gray light filters through the dense canopy, the cold air heavy with dampness.", + "3": "The chill makes the jungle feel subdued, the usual cacophony of sounds strangely quiet.", + "4": "The overcast sky adds a sense of mystery to the cold, shadowed jungle paths.", + "5": "Mist clings to the undergrowth, the chill making the air feel thick and heavy.", + "6": "The cold air carries the scent of wet leaves and damp earth, amplified by the stillness.", + "7": "The jungle feels ancient and untouched, the chill enhancing its timeless aura.", + "8": "The muted light of the overcast sky gives the jungle an ethereal quality.", + "9": "The usual buzzing of insects is muffled, the chill making the jungle eerily quiet.", + "10": "The chill bites at exposed skin, the overcast sky casting the jungle in muted hues.", + "11": "The jungle’s vibrant greens are dulled by the gray light and cold air.", + "12": "The stillness of the jungle is broken only by the occasional rustle of leaves." + }, + "hills": { + "1": "The rolling hills are quiet, shrouded in a chilly mist under gray clouds.", + "2": "Frost clings to the grass, the overcast sky giving the hills a muted tone.", + "3": "A brisk wind cuts across the hills, the cold air making the landscape feel desolate.", + "4": "The gray sky seems to press down on the hills, the chill dampening all sound.", + "5": "The hills are bathed in shadowless light, the cold air carrying the scent of damp soil.", + "6": "A faint frost glistens on the rocky outcrops, the overcast sky dull and lifeless.", + "7": "The chill seeps into the earth, the hills appearing barren and unwelcoming.", + "8": "The low clouds seem to touch the hilltops, the air biting and still.", + "9": "The usual lively sounds of the hills are subdued by the oppressive chill.", + "10": "The wind whistles faintly over the cold, empty hills under the overcast sky.", + "11": "The chill brings a sense of foreboding to the otherwise gentle slopes.", + "12": "The overcast sky dulls the colors of the hills, the cold making every step heavier." + }, + "mountains": { + "1": "The peaks are cloaked in gray clouds, the chill biting at exposed skin.", + "2": "The overcast sky adds an ominous tone to the rugged, cold mountains.", + "3": "Icy wind cuts through the rocky paths, the chill amplified by the clouded sky.", + "4": "The mountains seem lifeless under the gray expanse, the air frigid and sharp.", + "5": "The cold air carries the scent of stone and frost, the overcast sky unyielding.", + "6": "Snow dusts the higher peaks, the chill in the air heavy with the promise of more.", + "7": "The overcast sky casts long shadows in the valleys, the chill oppressive.", + "8": "The usual echo of the mountains is muffled by the thick, cold air.", + "9": "Gray clouds swirl around the peaks, the chill making the ascent treacherous.", + "10": "The rocky terrain feels frozen, the overcast sky offering no reprieve from the cold.", + "11": "Frost covers the mountain trails, the overcast sky dull and featureless.", + "12": "The mountains feel timeless, the chill giving them a harsh, unyielding beauty." + }, + "desert": { + "1": "The desert sands are unusually cold, the overcast sky turning the landscape gray.", + "2": "A biting chill settles over the dunes, the overcast sky casting muted shadows.", + "3": "The wind stirs the sand lightly, the chill in the air a stark contrast to usual heat.", + "4": "The overcast sky makes the desert feel alien, the cold biting at every step.", + "5": "The usual shimmer of the sands is absent, replaced by frost under the gray sky.", + "6": "The desert is eerily silent, the chill making the vast expanse feel lifeless.", + "7": "The overcast sky dulls the golden hues of the desert, the cold air heavy and still.", + "8": "The cold seeps into the sand, the dunes appearing frozen in place.", + "9": "The gray sky makes the desert seem endless, the chill biting at any exposed skin.", + "10": "Frost gathers on the sparse vegetation, the overcast sky lending a surreal atmosphere.", + "11": "The desert winds carry a chill, the overcast sky casting the dunes in shadow.", + "12": "The once-vibrant desert feels barren under the gray expanse, the cold unrelenting." + }, + "coastal": { + "1": "The sea is gray and choppy, the chill in the air biting and damp.", + "2": "The overcast sky blends with the horizon, the coastal air heavy with cold salt spray.", + "3": "The chill bites at the skin, the sound of crashing waves muted under the gray sky.", + "4": "Frost gathers on the rocky shore, the overcast sky casting a pall over the coast.", + "5": "The wind off the sea is frigid, carrying the smell of salt and seaweed.", + "6": "The overcast sky turns the water steel-gray, the cold making the coastline feel stark.", + "7": "The usual cries of seabirds are muted, the chill making the coastal area feel desolate.", + "8": "The cold air carries the sound of distant waves, the overcast sky making the coast seem endless.", + "9": "The chill settles in the sand and rocks, the overcast sky dull and unchanging.", + "10": "The coastal cliffs are shrouded in mist, the cold air heavy and still.", + "11": "The ocean spray feels icy, the overcast sky lending the coast a somber tone.", + "12": "The coastline is quiet, the gray sky and chill giving it an austere beauty." + }, + "volcano": { + "1": "The volcanic slopes are eerily cold, the overcast sky lending an ominous air.", + "2": "The usual heat of the volcano is dampened, the gray sky casting it in shadow.", + "3": "The chill in the air makes the volcanic rock feel sharp and lifeless.", + "4": "The overcast sky gives the volcano an otherworldly appearance, the cold unsettling.", + "5": "Frost gathers in crevices of the cooled lava, the chill biting at exposed skin.", + "6": "The volcanic peak is shrouded in gray clouds, the cold air heavy and still.", + "7": "The chill makes the volcanic terrain feel barren, the overcast sky dull and unchanging.", + "8": "Steam rises faintly from cracks, the cold and gray sky muting its usual intensity.", + "9": "The volcano feels lifeless under the heavy overcast sky, the chill pervasive.", + "10": "The cold air carries a faint smell of sulfur, the overcast sky dark and foreboding.", + "11": "The volcanic crags are frosted, the gray sky lending a surreal beauty to the scene.", + "12": "The chill clings to the volcanic slopes, the overcast sky offering no relief." + }, + "arctic": { + "1": "The arctic expanse is desolate under the gray sky, the chill biting deep.", + "2": "The snow is hard and frozen, the overcast sky casting a dull light over the tundra.", + "3": "The icy wind cuts through the arctic, the gray sky blending with the endless snow.", + "4": "Frost gathers on everything, the overcast sky giving the arctic an eternal twilight feel.", + "5": "The gray clouds hang heavy, the chill seeping into every crevice of the frozen landscape.", + "6": "The arctic feels timeless under the overcast sky, the cold biting and still.", + "7": "Snow drifts softly in the faint breeze, the chill in the air unrelenting.", + "8": "The overcast sky and chill make the arctic seem otherworldly and foreboding.", + "9": "The frozen expanse stretches endlessly under the dull gray sky.", + "10": "The biting cold makes every step in the arctic feel like an effort.", + "11": "The gray sky casts a muted light over the arctic, the chill oppressive.", + "12": "The wind carries ice crystals, the overcast sky turning the arctic into a frozen dream." + }, + "cursed": { + "1": "The cursed land is shrouded in gray clouds, the chill adding to its foreboding aura.", + "2": "The air feels heavy and cold, the overcast sky casting the cursed ground in shadow.", + "3": "Frost gathers unnaturally on dead trees, the chill amplifying the eerie silence.", + "4": "The gray sky seems alive, the chill making the cursed land feel oppressive.", + "5": "Whispers seem to echo in the cold, the overcast sky dulling all natural light.", + "6": "The cursed ground feels frozen in time, the chill making every breath visible.", + "7": "The overcast sky presses down, the cold seeping into the cursed earth.", + "8": "The land is quiet, the chill and gray sky lending a sinister stillness.", + "9": "The air smells faintly of decay, the overcast sky casting long shadows.", + "10": "The chill in the air carries a sense of dread, the gray sky unrelenting.", + "11": "The cursed land feels lifeless under the overcast sky, the cold biting to the bone.", + "12": "The gray sky and chill seem to drain the color from the cursed terrain." + } + } + }, + "Clear and Sunny": { + "conditions": { + "temperature": { "gte": 50, "lte": 80 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 20 }, + "humidity": { "gte": 30, "lte": 60 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 30 } + }, + "descriptions": { + "farm": { + "1": "Golden sunlight bathes the fields, making the crops glisten with morning dew.", + "2": "The air is crisp and fresh, the sun shining brightly over the quiet farmstead.", + "3": "Gentle sunlight warms the barns and fences, creating a serene rural scene.", + "4": "The day is clear, the farm animals basking in the soft, golden light.", + "5": "The bright sun enhances the vibrant green of the fields and pastures.", + "6": "Sunlight streams through the trees lining the farm, casting long shadows.", + "7": "The farm is peaceful under a cloudless sky, the sun invigorating the workers.", + "8": "A light breeze accompanies the sunny weather, rustling the crops gently.", + "9": "The clear day reveals every detail of the distant hills and farmhouses.", + "10": "The farm bustles with activity, the sunny weather energizing everyone.", + "11": "The sun glints off the water in the irrigation ditches, sparkling brightly.", + "12": "The clear skies make the farm's surroundings appear sharper and more vibrant." + }, + "village": { + "1": "The village square is lively under the bright, warm sunlight.", + "2": "Children play outside, their laughter carrying on the gentle breeze of a sunny day.", + "3": "The village rooftops gleam in the golden sunlight, adding to the charm of the scene.", + "4": "Sunshine filters through the narrow streets, casting playful shadows on cobblestones.", + "5": "The village market is bustling, the clear weather drawing out traders and buyers.", + "6": "The clear blue sky provides a perfect backdrop for the church steeple.", + "7": "Villagers hang laundry out to dry, the sun adding warmth to the busy scene.", + "8": "The village well sparkles under the sunlight, its cool water reflecting the bright sky.", + "9": "A light breeze carries the scent of fresh bread through the sunny village streets.", + "10": "The bright day makes the village's stone walls and wooden beams stand out vividly.", + "11": "The sunlight highlights the blooming flowers in the village gardens.", + "12": "Shadows of trees and cottages play across the village green on this sunny day." + }, + "city": { + "1": "The city streets are bustling under the clear, sunny sky.", + "2": "Sunlight reflects off the high towers, casting long shadows on the streets below.", + "3": "The market square is vibrant, the clear skies drawing out merchants and shoppers.", + "4": "The city gates gleam in the sunlight, guards standing watch under the bright sky.", + "5": "The warm sun lights up the stone facades of the city's buildings beautifully.", + "6": "The clear day reveals the intricate details of the statues and fountains in the city square.", + "7": "The city's cobblestones shimmer as the sunlight bounces off the polished stones.", + "8": "The city park is alive with activity, citizens enjoying the sunny weather.", + "9": "The sunlight glints off the helmets of the city watch, adding a regal touch to their patrol.", + "10": "Birds chirp cheerfully as they flit between rooftops under the cloudless sky.", + "11": "The city's waterways sparkle under the sunlight, bustling with activity.", + "12": "Shadows of the city walls and towers stretch across the streets in the bright light." + }, + "plains": { + "1": "The endless plains bask in golden sunlight, the grasses swaying gently in the breeze.", + "2": "The clear blue sky stretches unbroken over the vast, sunlit plains.", + "3": "The warmth of the sun breathes life into the plains, the horizon shimmering faintly.", + "4": "A light breeze stirs the wildflowers scattered across the sunny plains.", + "5": "The plains seem to glow, the bright sunlight enhancing every shade of green and gold.", + "6": "The clear skies make the distant mountains visible, their outlines sharp against the blue.", + "7": "A hawk circles high above the plains, silhouetted against the brilliant sky.", + "8": "The bright sun gives the open plains an inviting, peaceful atmosphere.", + "9": "The plains are alive with the hum of insects, the sunlight energizing every creature.", + "10": "The sunlight dances across the plains, highlighting the rippling waves of grass.", + "11": "The crisp air and sunny weather create perfect visibility across the wide plains.", + "12": "The warmth of the sun creates a comforting contrast to the vast openness of the plains." + }, + "forest": { + "1": "Sunlight streams through the forest canopy, creating a dappled pattern on the ground.", + "2": "The clear skies make the forest feel alive, every leaf glowing in the sunlight.", + "3": "Birdsong fills the forest, the sunny weather bringing out its vibrant inhabitants.", + "4": "The forest paths are bathed in golden light, the air warm and welcoming.", + "5": "The sunlight glints off the dew still clinging to the forest leaves.", + "6": "The forest floor is alive with activity, small creatures bustling in the warm sun.", + "7": "The sun's rays pierce through gaps in the trees, illuminating hidden corners of the forest.", + "8": "The clear weather makes the forest's colors vivid, every shade of green amplified.", + "9": "The scent of pine and wildflowers fills the air, carried on a gentle, sunny breeze.", + "10": "The forest feels timeless under the bright sky, every rustle and chirp magnified by the light.", + "11": "A small brook sparkles as it winds through the forest, the sunlight dancing on its surface.", + "12": "The clear weather highlights the forest's natural beauty, the sunlight invigorating every corner." + }, + "swamp": { + "1": "The swamp is warm and bright, the sunlight reflecting off still, murky waters.", + "2": "Dragonflies dart through the sunlit swamp, their wings shimmering in the light.", + "3": "The sunlight filters through the hanging moss, creating golden patterns on the water.", + "4": "The swamp's muddy paths are illuminated by the bright sun, making navigation easier.", + "5": "The clear skies highlight the vibrant greens of the swamp's dense vegetation.", + "6": "The sunlight warms the swamp's surface, bringing out the earthy smell of wetland soil.", + "7": "A gentle breeze carries the calls of frogs and birds through the sunlit swamp.", + "8": "The swamp's stagnant pools shimmer under the sunlight, reflecting the cloudless sky.", + "9": "The sunlight gives the swamp a surreal beauty, its shadows creating mysterious shapes.", + "10": "The clear skies make the swamp feel less foreboding, its creatures basking in the warmth.", + "11": "The bright sun reveals the intricate patterns of roots and vines in the swamp.", + "12": "The swamp is alive with sounds, the sunny weather amplifying the chorus of life." + }, + "jungle": { + "1": "Sunlight streams through the dense jungle canopy, creating golden pools on the ground.", + "2": "The jungle is alive with vibrant colors, the sunlight enhancing every shade of green.", + "3": "Birds and monkeys call out from the treetops, energized by the bright, sunny day.", + "4": "The jungle's undergrowth is warm and humid, the sunlight dappling every leaf and vine.", + "5": "The air is filled with the hum of insects, the sunlight filtering through the foliage.", + "6": "The jungle paths are illuminated by the bright sun, every detail sharp and vivid.", + "7": "The sunlight reflects off the jungle's pools and streams, creating a sparkling spectacle.", + "8": "The clear skies above the jungle make the vibrant flowers seem even more brilliant.", + "9": "The jungle's towering trees seem to reach for the sun, their leaves shimmering brightly.", + "10": "The air feels alive in the jungle, the sunny weather amplifying its natural energy.", + "11": "Sunlight dances across the jungle canopy, casting intricate shadows below.", + "12": "The bright sun highlights the jungle's lush growth, making it feel like a verdant paradise." + }, + "hills": { + "1": "The rolling hills glow under the bright sunlight, their gentle slopes bathed in golden hues.", + "2": "A warm breeze stirs the grasses covering the hills, the clear sky stretching endlessly.", + "3": "The sunlight reflects off dew-laden wildflowers scattered across the hills.", + "4": "The hills feel alive with the rustling of leaves and chirping of birds under the clear sky.", + "5": "Shadows of scattered trees stretch across the hills, highlighted by the bright sunlight.", + "6": "A lone hawk circles above the sunlit hills, its call echoing in the clear air.", + "7": "The gentle slopes of the hills seem to roll forever under the vivid blue sky.", + "8": "The sun warms the hills, making the scent of wild herbs and grass more pronounced.", + "9": "The landscape is peaceful, with the clear skies revealing every detail of the hills.", + "10": "The bright weather reveals small streams sparkling as they wind through the hills.", + "11": "The hills are tranquil, the sun casting warm, inviting light over the terrain.", + "12": "A light breeze stirs the grassy hills, the sun's rays creating a serene and vibrant scene." + }, + "mountains": { + "1": "The jagged peaks shine under the clear sky, their snowy caps glowing in the sunlight.", + "2": "The mountain air is crisp and invigorating, the clear skies enhancing the grandeur of the peaks.", + "3": "Sunlight streams through the valleys, creating patches of golden warmth amid the rocky slopes.", + "4": "The mountains stand proud against the vibrant blue sky, every ridge sharp and clear.", + "5": "The bright weather reveals distant peaks, their outlines crisp against the clear horizon.", + "6": "The sun casts long shadows across the mountain's rugged terrain, emphasizing its dramatic features.", + "7": "The mountain trails are bathed in sunlight, their paths warm and inviting.", + "8": "A slight wind carries the scent of pine through the sunlit mountain passes.", + "9": "The sunlight glints off scattered boulders, adding a shimmering quality to the mountain slopes.", + "10": "The crisp mountain air feels fresher under the warm sun and cloudless sky.", + "11": "Wildlife is more active, the sunny weather drawing creatures out onto the mountain slopes.", + "12": "The sunlight highlights the vibrant green of alpine meadows nestled among the peaks." + }, + "desert": { + "1": "The desert sands shimmer under the blazing sun, the heat creating faint mirages.", + "2": "The clear sky stretches endlessly over the dunes, their golden crests sharply defined.", + "3": "The sunlight glints off scattered rocks, casting long shadows across the arid landscape.", + "4": "A dry, warm breeze stirs the sand, the bright sunlight intensifying the desert's stark beauty.", + "5": "The horizon wavers under the intense sunlight, the air shimmering with heat.", + "6": "The desert feels timeless and vast under the clear sky, every grain of sand illuminated.", + "7": "The sun's rays highlight the delicate patterns etched into the dunes by the wind.", + "8": "The arid terrain is peaceful, the silence broken only by the occasional call of a distant bird.", + "9": "The clear skies make the desert's rugged cliffs and plateaus appear more dramatic.", + "10": "The desert's scattered cacti and hardy shrubs stand out vividly in the bright light.", + "11": "The sun's warmth creates a shimmering haze across the expanse of the desert sands.", + "12": "The vibrant colors of the desert come alive, the sunlight emphasizing every hue and texture." + }, + "coastal": { + "1": "The sea sparkles under the bright sun, waves crashing gently against the sunlit shore.", + "2": "The clear sky reflects in the calm waters, creating an endless expanse of blue.", + "3": "The coastal cliffs glow in the sunlight, their rugged edges stark against the horizon.", + "4": "Seabirds wheel and dive over the coast, their calls blending with the sound of the surf.", + "5": "The warm sun bathes the beach, the sand warm underfoot and inviting.", + "6": "A gentle breeze carries the salty tang of the sea air, the bright weather energizing the coast.", + "7": "The sunlight catches on the crests of waves, creating sparkling trails across the water.", + "8": "The coastal grasses sway in the breeze, their golden stalks glowing in the sunlight.", + "9": "The bright weather reveals distant ships on the horizon, their sails white against the blue sky.", + "10": "The tide pools glisten in the sunlight, teeming with small, colorful creatures.", + "11": "The coastline is vibrant under the clear skies, the water's edge alive with activity.", + "12": "The sunlight warms the rocky shoreline, making it a haven for basking seabirds." + }, + "volcano": { + "1": "The volcanic slopes are stark and dramatic under the bright sunlight, every crevice visible.", + "2": "The sun warms the rocky terrain, the air heavy with the scent of sulfur.", + "3": "The bright weather highlights the contrast between blackened lava flows and vibrant moss patches.", + "4": "The sunlight reflects off occasional steam vents, creating faint rainbows in the mist.", + "5": "The volcano looms against the clear sky, its peak sharp and imposing.", + "6": "The rocky paths are warm underfoot, the sunlight intensifying the barren beauty of the landscape.", + "7": "The air shimmers with heat near the volcanic vents, the clear skies emphasizing the desolation.", + "8": "A gentle wind carries the scent of ash, the sun illuminating the rugged terrain.", + "9": "The bright light reveals ancient lava flows frozen in time, their textures sharp and vivid.", + "10": "The volcano's slopes are quiet and serene, the clear weather masking its latent power.", + "11": "The sunlight catches on mineral deposits, adding flashes of color to the dark rock.", + "12": "The volcanic crater is visible in the clear weather, its depths illuminated by the bright sun." + }, + "arctic": { + "1": "The arctic expanse glistens under the sunlight, the snow reflecting a blinding brilliance.", + "2": "The clear skies reveal distant icebergs, their shapes stark against the horizon.", + "3": "The sunlight creates rainbows in the frost crystals scattered across the arctic plains.", + "4": "The ice and snow sparkle like diamonds under the clear, bright sky.", + "5": "The air is crisp and biting, the sunlight providing a faint warmth amid the cold.", + "6": "The frozen tundra is peaceful, its silence broken only by the crunch of footsteps on snow.", + "7": "The bright day reveals the arctic's endless expanse, its icy beauty almost surreal.", + "8": "The sun's rays highlight the jagged edges of ice floes, casting long shadows.", + "9": "The arctic animals move cautiously across the sunlit snow, their tracks clearly visible.", + "10": "The sunlight transforms the frozen landscape into a dazzling sea of white and blue.", + "11": "The clear skies enhance the stark beauty of the arctic, every feature sharp and vivid.", + "12": "The sun creates a golden halo on the horizon, illuminating the frozen wilderness." + }, + "cursed": { + "1": "The cursed land is eerily bright, the sunlight doing little to lift its oppressive atmosphere.", + "2": "The sunlight reveals twisted trees and warped terrain, their shadows sharp and haunting.", + "3": "The clear skies are an unsettling contrast to the land's unnatural stillness.", + "4": "The bright weather highlights the faint shimmer of magical corruption in the air.", + "5": "The cursed ground seems to absorb the sunlight, its darkness refusing to be dispelled.", + "6": "The sun casts long, strange shadows, the land's cursed nature warping their shapes.", + "7": "The clear skies do little to soften the ominous feel of the cursed landscape.", + "8": "The sunlight reveals faintly glowing runes etched into the stones scattered across the land.", + "9": "The bright day makes the cursed area's unnatural colors stand out even more vividly.", + "10": "The cursed trees creak in the sunlight, their twisted branches casting eerie shadows.", + "11": "The clear skies allow a better view of the distant cursed ruins, their outlines ominous.", + "12": "The sunlight feels cold and distant, its warmth unable to penetrate the cursed land's aura." + } + } + }, + "Cloudy and Mild": { + "conditions": { + "temperature": { "gte": 50, "lte": 70 }, + "precipitation": { "lte": 50 }, + "wind": { "lte": 30 }, + "humidity": { "gte": 40, "lte": 70 }, + "cloudCover": { "gte": 50, "lte": 80 }, + "visibility": { "gte": 30 } + }, + "descriptions": { + "farm": { + "1": "Gray clouds hang low over the fields, diffusing a soft, even light.", + "2": "The air feels calm and mild, the overcast sky casting a muted tone over the farm.", + "3": "The clouds gather densely, softening the outlines of the distant barns and fences.", + "4": "A light breeze carries the scent of freshly tilled soil under the cloudy sky.", + "5": "The farm feels peaceful, the mild air and cloudy sky making work comfortable.", + "6": "Shadows are faint and stretched as the overcast sky dims the farmyard.", + "7": "The soft gray of the sky mirrors the gentle hum of farm life below.", + "8": "The cloudy weather creates a quiet, contemplative atmosphere on the farm.", + "9": "The overcast sky blends with the rising mist near the fields, softening the view.", + "10": "The muted sunlight gives the farm a subdued, tranquil feel.", + "11": "The air feels damp but not unpleasant as the clouds drift slowly above.", + "12": "The cloudy sky creates a cool, gentle atmosphere across the farmstead." + }, + "village": { + "1": "The village lanes are shaded by a canopy of gray clouds, creating a serene mood.", + "2": "The overcast sky gives the village a quiet, subdued energy.", + "3": "The stone walls of cottages appear softer under the diffused light of the cloudy sky.", + "4": "Villagers move calmly, the mild weather making the day feel unhurried.", + "5": "The cloudy sky hangs heavy, creating a peaceful, almost sleepy atmosphere.", + "6": "The village square is quiet, with muted sounds blending into the overcast day.", + "7": "Smoke from chimneys curls lazily upward, disappearing into the gray sky.", + "8": "The mild air and soft gray clouds make the village feel cozy and timeless.", + "9": "The clouds reflect in puddles from earlier rains, their gray tones blending seamlessly.", + "10": "The streets are cool and calm, the overcast weather casting a serene stillness.", + "11": "The cloudy sky makes the vibrant colors of market goods stand out against the gray.", + "12": "The mild day feels almost dreamlike, the village quiet beneath the overcast sky." + }, + "city": { + "1": "The city streets are calm under the overcast sky, the usual bustle muted.", + "2": "Gray clouds soften the harsh edges of the city’s towering walls and spires.", + "3": "The cloudy sky gives the cobbled streets a gentle, reflective sheen.", + "4": "The city feels quieter than usual, the mild weather inviting a slower pace.", + "5": "Shadows are faint and diffuse as the cloudy sky evenly lights the bustling streets.", + "6": "Market stalls seem brighter under the gray sky, their colors popping against the mild day.", + "7": "The overcast weather makes the city's towers blend into the horizon.", + "8": "The scent of baking bread and city life drifts lightly through the mild air.", + "9": "Street performers play gentle tunes, their notes carrying easily under the cloudy sky.", + "10": "The overcast sky makes the city feel serene, its usual din softer and more harmonious.", + "11": "The even light of the cloudy day makes the city's stonework appear smooth and uniform.", + "12": "The city seems to breathe calmly under the soft gray canopy of clouds." + }, + "plains": { + "1": "The vast plains stretch out under a thick blanket of gray clouds.", + "2": "The air is cool and mild, the cloudy sky softening the distant horizon.", + "3": "A gentle wind ripples through the tall grass, blending seamlessly with the overcast day.", + "4": "The muted light casts no harsh shadows, making the plains feel infinite and calm.", + "5": "The rolling hills are subdued under the heavy gray sky, their colors muted but rich.", + "6": "The cloudy weather adds a layer of mystery to the open plains, the horizon blurred.", + "7": "The scent of earth and grass fills the air as the mild breeze moves across the plains.", + "8": "The overcast sky makes the distant tree lines blend into the gray backdrop.", + "9": "The plains feel serene and still, the cloudy sky lending an air of quiet contemplation.", + "10": "The soft, even light creates a tranquil setting, perfect for long journeys across the plains.", + "11": "The vastness of the plains feels magnified under the low-hanging gray clouds.", + "12": "The mild air and gentle clouds create a soothing, timeless atmosphere across the plains." + }, + "forest": { + "1": "The forest canopy seems thicker under the cloudy sky, the light filtered and soft.", + "2": "The trees sway gently in the mild air, their leaves muted under the overcast sky.", + "3": "The forest floor is damp, the cloudy weather keeping the sunlight at bay.", + "4": "The air feels cool and fresh, the overcast sky blending with the greens of the forest.", + "5": "The soft gray light makes the moss-covered trunks and stones glow faintly.", + "6": "The forest is quiet, the cloudy sky amplifying the rustle of leaves and distant bird calls.", + "7": "The overcast weather makes the shadows between trees feel deeper and more mysterious.", + "8": "The muted colors of the forest create a serene, meditative atmosphere.", + "9": "A light breeze rustles the undergrowth, blending with the gentle sound of the cloudy day.", + "10": "The forest feels insulated, the cloudy sky creating a calm and peaceful environment.", + "11": "The even light brings out the intricate details of bark and leaf alike.", + "12": "The forest feels timeless and tranquil under the soft, gray expanse of the sky." + }, + "swamp": { + "1": "The swamp is quiet under the cloudy sky, the air heavy but cool.", + "2": "The water reflects the gray clouds above, the surface calm and undisturbed.", + "3": "A faint mist rises from the swamp, blending with the overcast sky.", + "4": "The mild weather makes the swamp feel less foreboding, its usual gloom softened.", + "5": "The clouds cast diffuse light over the swamp, highlighting the twisted roots and reeds.", + "6": "The air is damp but refreshing, the cloudy sky lending a calm to the swamp.", + "7": "The overcast weather softens the swamp's usual sharp contrasts, making it feel subdued.", + "8": "The still water mirrors the gray sky, creating a surreal and tranquil atmosphere.", + "9": "A gentle breeze ripples the swamp's surface, blending with the muted sounds of nature.", + "10": "The swamp feels alive yet peaceful, the cloudy sky muffling its usual noises.", + "11": "The diffused light highlights the vibrant greens of the swamp's moss and algae.", + "12": "The swamp is cool and calm, the overcast weather blending with its natural stillness." + }, + "jungle": { + "1": "The jungle hums with life under the overcast sky, the air cool but thick.", + "2": "The cloudy weather softens the jungle's vibrant colors, creating a tranquil atmosphere.", + "3": "The canopy above filters the gray light, casting the jungle floor in soft shadows.", + "4": "The mild air makes the jungle feel welcoming, its usual heat tempered by the clouds.", + "5": "The overcast sky enhances the earthy scents of the jungle, making them richer and deeper.", + "6": "The jungle is alive with sounds, the cloudy weather amplifying its natural rhythm.", + "7": "The soft gray light makes the jungle's dense foliage feel even more impenetrable.", + "8": "A gentle breeze stirs the jungle leaves, the clouds above blending with the greenery.", + "9": "The jungle feels timeless under the overcast sky, its life pulsing steadily in the mild air.", + "10": "The diffused light reveals intricate patterns on the jungle's leaves and bark.", + "11": "The cloudy weather lends the jungle an air of calm mystery, its depths quietly alive.", + "12": "The jungle's vibrant life feels subdued and harmonious under the soft, gray sky." + }, + "hills": { + "1": "Soft gray clouds drift lazily over the rolling hills, casting muted shadows.", + "2": "The hills are calm under a canopy of clouds, their slopes bathed in even light.", + "3": "A gentle breeze stirs the grass, blending with the subdued sky above.", + "4": "The overcast weather lends the hills a peaceful, timeless quality.", + "5": "The muted colors of the hills soften as clouds thicken overhead.", + "6": "The cloudy sky frames the hilltops, creating a serene and tranquil atmosphere.", + "7": "The air is cool and mild, the hills quiet under the gray sky.", + "8": "The hills stretch into the distance, their contours softened by the overcast sky.", + "9": "The clouds seem to settle low, blending with the gentle curves of the hills.", + "10": "Wildflowers stand out against the muted tones of the overcast day.", + "11": "The hills feel alive yet subdued, their quietness enhanced by the cloudy weather.", + "12": "The horizon is blurred as the hills merge seamlessly with the gray sky." + }, + "mountains": { + "1": "The towering peaks are shrouded in soft gray clouds, their outlines faint.", + "2": "The mountains feel distant and mysterious under the mild, cloudy sky.", + "3": "The air is cool and fresh, the overcast weather calming the rugged landscape.", + "4": "The cloudy sky blends with the jagged peaks, creating an ethereal effect.", + "5": "Misty clouds cling to the slopes, adding a serene beauty to the mountains.", + "6": "The muted sunlight casts soft shadows across the rocky terrain.", + "7": "The overcast sky highlights the deep greens and grays of the mountain landscape.", + "8": "A gentle wind stirs the sparse vegetation, blending with the stillness of the peaks.", + "9": "The mountains feel timeless, their grandeur softened by the diffused light.", + "10": "The cloud cover gives the peaks a ghostly, otherworldly appearance.", + "11": "The cool air carries the faint sound of rushing water from distant streams.", + "12": "The gray sky creates a quiet majesty, making the mountains feel eternal." + }, + "desert": { + "1": "The desert stretches out under a hazy sky, the clouds softening the harsh light.", + "2": "The overcast weather dulls the usual glare, creating a cooler desert landscape.", + "3": "The dunes appear muted under the cloudy sky, their golden hues subdued.", + "4": "A gentle breeze stirs the sand, the mild air carrying a faint coolness.", + "5": "The gray sky gives the desert a surreal, dreamlike quality.", + "6": "The overcast weather softens the harsh edges of rocks and dunes alike.", + "7": "The desert feels peaceful, the muted light casting long, gentle shadows.", + "8": "The cool, cloudy day makes the desert seem less forbidding and more inviting.", + "9": "The horizon is blurred, the gray sky blending with the vast expanse of sand.", + "10": "The subdued colors of the desert blend harmoniously with the cloudy weather.", + "11": "The overcast sky seems to stretch endlessly, mirroring the desert below.", + "12": "The desert feels timeless, its vastness enhanced by the soft gray sky." + }, + "coastal": { + "1": "The sea is calm under the gray sky, waves rolling gently to the shore.", + "2": "The overcast weather lends the coastline a peaceful, introspective mood.", + "3": "The muted sunlight reflects softly on the water, creating a shimmering effect.", + "4": "The cool breeze carries the scent of salt, blending with the cloudy sky above.", + "5": "The gray sky merges with the horizon, creating an infinite expanse of calm.", + "6": "Seabirds call faintly, their cries blending with the gentle rhythm of the waves.", + "7": "The rocky coastline appears softened under the even light of the overcast day.", + "8": "The clouds hang low, their reflection merging with the stillness of tidal pools.", + "9": "The cool air and cloudy weather create a tranquil, timeless coastal scene.", + "10": "Fishing boats drift lazily on the water, their colors muted against the gray sky.", + "11": "The shoreline feels serene, the soft light bringing out subtle textures of sand and rock.", + "12": "The mild weather and overcast sky create a calm and contemplative atmosphere." + }, + "volcano": { + "1": "The volcanic slopes appear subdued under the gray sky, their ruggedness softened.", + "2": "A faint mist rises from fissures, blending seamlessly with the overcast weather.", + "3": "The air is cooler than expected, the clouds providing a soothing reprieve.", + "4": "The muted light highlights the dark, rocky terrain in soft, subtle tones.", + "5": "The volcano feels dormant, the cloudy weather masking its latent power.", + "6": "The overcast sky adds a mysterious quality to the volcanic landscape.", + "7": "The air carries a faint sulfuric scent, mingling with the cool, cloudy atmosphere.", + "8": "The rugged terrain is shrouded in soft gray light, lending it an eerie calm.", + "9": "The clouds gather thickly above, their weight matching the gravity of the volcano.", + "10": "Steam rises gently from cracks, its wisps blending with the low-hanging clouds.", + "11": "The cloudy weather makes the landscape feel otherworldly, its contrasts subdued.", + "12": "The volcano's peak disappears into the overcast sky, adding to its mystique." + }, + "artic": { + "1": "The icy expanse stretches endlessly under the gray, overcast sky.", + "2": "The overcast weather diffuses the light, creating a cool, serene arctic scene.", + "3": "The snow sparkles faintly under the muted sunlight filtering through the clouds.", + "4": "The gray sky blends with the icy landscape, creating a seamless horizon.", + "5": "The air is cool and still, the cloudy weather adding to the arctic's tranquility.", + "6": "The ice fields glow faintly, their brilliance softened by the overcast sky.", + "7": "A light wind whispers across the snow, its sound blending with the soft gray light.", + "8": "The arctic feels timeless and endless, its quiet amplified by the cloudy weather.", + "9": "The low-hanging clouds reflect faintly off the ice, creating a surreal effect.", + "10": "The snow crunches softly underfoot, the gray sky lending an air of calm.", + "11": "The arctic's stark beauty is enhanced by the subdued tones of the cloudy day.", + "12": "The frozen landscape feels peaceful and eternal under the soft gray sky." + }, + "cursed": { + "1": "The overcast sky adds an oppressive weight to the already eerie landscape.", + "2": "Gray clouds swirl unnaturally, their patterns disconcertingly alive.", + "3": "The air feels thick and still, the cloudy weather amplifying the cursed atmosphere.", + "4": "The landscape is dim and foreboding, the gray sky casting ominous shadows.", + "5": "The muted light seems to drain color from the surroundings, leaving them lifeless.", + "6": "A chill lingers in the air, the overcast sky enhancing the sense of unease.", + "7": "Whispers seem to rise with the breeze, blending with the clouds above.", + "8": "The cursed ground feels more malevolent under the heavy gray clouds.", + "9": "The cloudy sky feels alive, its movements erratic and unsettling.", + "10": "The muted colors of the cursed landscape seem to shift unnaturally under the clouds.", + "11": "The overcast weather makes the cursed terrain feel suffocating and inescapable.", + "12": "The gray sky presses down on the land, amplifying the oppressive, cursed atmosphere." + } + } + }, + "Cold and Clear": { + "conditions": { + "temperature": { "gte": 20, "lte": 40 }, + "precipitation": { "lte": 10 }, + "wind": { "lte": 20 }, + "humidity": { "gte": 30, "lte": 50 }, + "cloudCover": { "lte": 50 }, + "visibility": { "gte": 20 } + }, + "descriptions": { + "farm": { + "1": "Frost glitters on the fields under the crisp, clear sky.", + "2": "A biting chill fills the air as the sun rises over the farm.", + "3": "The barn stands silent under a cold, cloudless expanse.", + "4": "The ground is hard with frost, shining beneath the clear morning light.", + "5": "Cold winds nip at exposed skin, though the sky remains brilliantly clear.", + "6": "A peaceful stillness settles over the frozen fields under the azure sky.", + "7": "The animals’ breath fogs in the frosty air as sunlight glimmers on the snow.", + "8": "Icy puddles reflect the pale winter sun on the clear, frigid day.", + "9": "The chimney smoke drifts upward into the cloudless sky above the farm.", + "10": "The frost-covered trees shimmer in the sunlight, standing stark against the blue sky.", + "11": "The farm is quiet, the cold air sharpening every sound under the clear sky.", + "12": "The horizon stretches wide, every detail clear and crisp in the freezing air." + }, + "village": { + "1": "The cobbled streets glisten with frost beneath a pale, clear sky.", + "2": "Villagers bundle up against the biting cold as sunlight spills over the rooftops.", + "3": "The chimneys emit thin trails of smoke into the cloudless, frigid sky.", + "4": "The frosty air makes the village seem frozen in time under the bright sky.", + "5": "Icicles hang from the eaves, gleaming in the sharp winter sunlight.", + "6": "The village well is frozen, its surface sparkling under the cold, clear sky.", + "7": "Children’s laughter rings out as they play in the frosty streets.", + "8": "The church bell echoes crisply in the cold, open air.", + "9": "Footsteps crunch in the snow as villagers go about their day beneath the blue sky.", + "10": "Frost-covered windows glow faintly from hearth fires within.", + "11": "The clear, cold day makes the village’s colors seem muted and stark.", + "12": "The village stands serene, bathed in the clarity of the freezing winter sun." + }, + "city": { + "1": "The grand towers rise starkly against the bright, blue winter sky.", + "2": "Market stalls glimmer with frost as merchants bundle up in thick cloaks.", + "3": "The clear sky casts sharp shadows across the cobblestones of the busy streets.", + "4": "The city gates glint with ice as sunlight filters through the cold air.", + "5": "Thin clouds of breath rise from the bustling crowd beneath the azure sky.", + "6": "Frozen fountains catch the sunlight, their icy forms dazzling in the frigid air.", + "7": "The banners on the towers flutter weakly in the crisp, cold air.", + "8": "A sharp chill pervades the streets, though the sky remains bright and clear.", + "9": "The city walls gleam with frost under the golden light of the winter sun.", + "10": "The distant mountains are visible, stark and sharp against the clear sky.", + "11": "The frigid air amplifies the city’s sounds, making every step and voice distinct.", + "12": "The frost on the rooftops glows faintly in the morning light, beneath a sky of perfect blue." + }, + "plains": { + "1": "The open plains shimmer with frost under the brilliant, clear sky.", + "2": "The chill wind sweeps across the frozen grasslands, rippling under the azure sky.", + "3": "A lone tree stands stark against the endless expanse of blue and white.", + "4": "The plains stretch far, the cold air making the horizon appear sharper.", + "5": "Frost-crusted grass crunches underfoot as sunlight bathes the plains.", + "6": "The icy stillness of the plains contrasts with the bright, cloudless sky.", + "7": "The wind carries a sharp chill, its sound uninterrupted across the open expanse.", + "8": "Flocks of birds are silhouetted against the pale blue of the winter sky.", + "9": "A river glitters with ice as it winds its way through the frosty plains.", + "10": "The land is silent, the cold air amplifying the vastness of the open sky.", + "11": "The plains appear endless, their frozen beauty stark against the clear sky.", + "12": "The sun casts long shadows over the frost-covered grasslands under the bright sky." + }, + "forest": { + "1": "The frost-covered trees sparkle in the sunlight beneath the cloudless sky.", + "2": "A cold stillness fills the forest, each twig and leaf rimmed with ice.", + "3": "The sun filters through the bare branches, casting intricate shadows on the forest floor.", + "4": "The crunch of frozen leaves echoes sharply in the cold, clear air.", + "5": "The forest glows softly as the sunlight reflects off the frost-laden boughs.", + "6": "A deer pauses in a clearing, its breath visible against the crisp, clear sky.", + "7": "The underbrush is silent, muffled by a layer of frost beneath the blue sky.", + "8": "The chill air carries the faint scent of pine, heightened by the cold clarity.", + "9": "Icicles dangle from branches, glinting in the sunlight that streams through the canopy.", + "10": "The forest feels timeless, its winter beauty framed by the azure sky.", + "11": "The brook is frozen, its icy surface reflecting the pale blue of the sky.", + "12": "The forest is peaceful and serene, its silence amplified by the cold, clear air." + }, + "swamp": { + "1": "The swamp is eerily quiet, the frozen waters reflecting the pale sky.", + "2": "Ice clings to the twisted trees, their forms stark against the blue expanse.", + "3": "The air is crisp and cold, the swamp unusually still beneath the clear sky.", + "4": "Frost crystals shimmer on the reeds, their beauty unexpected in the cold swamp.", + "5": "The frozen ground crunches underfoot, breaking the stillness of the clear day.", + "6": "The chill air carries no smell, the swamp’s usual odor subdued by the frost.", + "7": "The water is frozen solid, its surface reflecting the cloudless winter sky.", + "8": "Thin layers of ice crackle underfoot as sunlight streams through the twisted trees.", + "9": "The swamp feels lifeless, its eerie silence matched by the bright, cold sky.", + "10": "The frost-covered moss glows faintly under the pale blue of the winter sky.", + "11": "The swamp is transformed, its dark waters replaced by gleaming ice beneath the clear sky.", + "12": "The air is sharp and still, the swamp’s haunting beauty amplified by the frosty stillness." + }, + "jungle": { + "1": "The frost clings to broad leaves, an unusual sight under the clear, cold sky.", + "2": "The jungle is quiet, the usual sounds muffled by the crisp, freezing air.", + "3": "Ice glistens on vines and ferns, their vibrant greens dulled by the cold.", + "4": "Sunlight filters through the canopy, highlighting the frosty edges of leaves.", + "5": "The jungle paths are slippery with frost, the air sharp and still.", + "6": "The cold air seems to drain the jungle of its usual vitality under the clear sky.", + "7": "The river is partially frozen, its surface reflecting the bright blue sky.", + "8": "The jungle feels alien, its tropical foliage subdued by the freezing air.", + "9": "Icicles hang from vines, catching the sunlight streaming through the canopy.", + "10": "The cold air is a strange contrast to the lush, green jungle surroundings.", + "11": "The jungle is transformed, its vibrant life muted under the frosty, clear sky.", + "12": "The frost-covered ground crunches underfoot, the cold clear air adding an eerie calm." + }, + "hills": { + "1": "The frost glistens on the rolling hills under the pale blue sky.", + "2": "A biting chill sweeps over the hills, the clear sky stretching endlessly.", + "3": "The sun rises over the frosted hills, casting long shadows in the crisp air.", + "4": "The chill winds ripple through the sparse grass beneath the azure sky.", + "5": "The hills are silent, their contours sharp against the cloudless expanse.", + "6": "Frosted shrubs dot the landscape, sparkling in the bright winter sunlight.", + "7": "The cold air carries the faint sound of rustling grass under the clear sky.", + "8": "The frost-covered hills reflect the sunlight, their beauty stark and serene.", + "9": "A thin layer of frost blankets the hills, glowing softly in the morning light.", + "10": "The cold sharpens the view, each hill crest distinct under the blue sky.", + "11": "The chill wind whispers through the valleys, a lone bird circling above.", + "12": "The sun illuminates the frost-rimed stones, the hills quiet under the bright sky." + }, + "mountains": { + "1": "The peaks glisten with ice, piercing the clear, cold sky.", + "2": "The mountain air is sharp and frigid, the clear sky offering no relief.", + "3": "The sun casts a golden glow over the snow-covered cliffs.", + "4": "Frost clings to the rocky crags, the clear sky a stark contrast to the cold.", + "5": "The valleys below are shrouded in frost, the peaks towering into the azure sky.", + "6": "A chill wind races through the mountain passes under the bright winter sun.", + "7": "The cold air amplifies the echo of falling rocks beneath the cloudless expanse.", + "8": "The mountains stand majestic, their frosted edges gleaming in the clear light.", + "9": "The snow crunches underfoot as the frigid air bites at exposed skin.", + "10": "The peaks are bathed in light, their icy slopes glimmering under the blue sky.", + "11": "Icicles dangle from ledges, catching the sun's rays in the freezing air.", + "12": "The sharp outlines of the mountains dominate the horizon, framed by the clear sky." + }, + "desert": { + "1": "The desert sand is frozen solid, glittering under the clear morning light.", + "2": "A biting cold grips the desert, the clear sky offering stark illumination.", + "3": "The dunes are rimed with frost, their patterns stark against the bright sky.", + "4": "The chill air carries no sound, the vast desert silent beneath the azure expanse.", + "5": "Frost sparkles on the sparse vegetation, the desert transformed by the cold.", + "6": "The sun rises over the frozen dunes, casting long, sharp shadows.", + "7": "The chill is biting, the clear sky revealing endless horizons of frozen sand.", + "8": "The frost-covered desert is otherworldly, its beauty stark and haunting.", + "9": "The frozen desert stretches endlessly, the cold air sharp and still.", + "10": "The cold light glints off icy grains of sand, the desert eerily quiet.", + "11": "The air is frigid, the desert’s usual heat replaced by a biting chill.", + "12": "The frost-coated dunes glimmer faintly, their sharp edges etched by the cold wind." + }, + "coastal": { + "1": "The waves crash against the icy shore, the sky clear and cold above.", + "2": "Frost clings to the rocky coastline, the sea a deep blue under the bright sky.", + "3": "The chill air carries the sharp scent of salt, the horizon clear and vast.", + "4": "Icy sea spray glistens in the sunlight, the beach silent and cold.", + "5": "The clear sky reflects off the frozen waves, the coastal cliffs glittering with frost.", + "6": "The frigid wind whips through the coastal grasses, the cold sea shimmering.", + "7": "The tide is low, the exposed rocks gleaming with frost in the bright light.", + "8": "The ocean is calm, the cold air sharpening the distant horizon.", + "9": "The frost-covered pier creaks in the icy breeze beneath the clear sky.", + "10": "The shoreline glitters, the cold and clear day amplifying every detail.", + "11": "The chill air numbs the senses as sunlight dances off the frozen water.", + "12": "The coastal caves echo faintly, the cold wind whispering through their icy edges." + }, + "volcano": { + "1": "The volcanic slopes are dusted with frost, the cold air strangely serene.", + "2": "Steam rises from the icy ground, the clear sky enhancing the contrast.", + "3": "The chill air carries the faint scent of sulfur, the volcano eerily quiet.", + "4": "Frost covers the jagged rocks, the cold making the volcanic terrain stark and surreal.", + "5": "The clear sky reveals every crack and crevice, the frozen landscape oddly beautiful.", + "6": "The frigid air mingles with the faint warmth of volcanic vents.", + "7": "Icy steam wafts from hidden fissures, the volcano silent beneath the bright sky.", + "8": "The lava fields are eerily still, their surface dusted with shimmering frost.", + "9": "The cold air sharpens the scent of the mineral-rich terrain.", + "10": "The volcano stands silent, its icy slopes gleaming under the clear blue sky.", + "11": "The chill air stings the skin, the volcanic ridges stark against the horizon.", + "12": "The frost-covered ground cracks faintly underfoot, the volcano's dormant power palpable." + }, + "artic": { + "1": "The endless ice reflects the sunlight, the air biting and still.", + "2": "Frost clings to every surface, the clear sky amplifying the frozen expanse.", + "3": "The sun hovers low, casting a pale light over the frozen landscape.", + "4": "The arctic winds are silent, the cold sharp and unforgiving under the clear sky.", + "5": "The ice fields stretch endlessly, their beauty stark and chilling.", + "6": "The frigid air carries the faint crackle of shifting ice beneath the azure sky.", + "7": "The frozen sea glimmers faintly, its surface unbroken under the clear light.", + "8": "The horizon blurs with frost, the cold air numbing every sensation.", + "9": "The ice glows faintly in the sunlight, a frozen expanse beneath the bright sky.", + "10": "The cold air cuts like a blade, the arctic landscape barren and silent.", + "11": "The frost-shrouded tundra is endless, the clear sky offering no warmth.", + "12": "The biting cold defines the landscape, the arctic beauty stark and pristine." + }, + "cursed": { + "1": "The frost glows faintly with an eerie light, the cold air unnatural.", + "2": "The clear sky offers no solace, the cursed ground crackling with frost.", + "3": "Shadows seem sharper in the cold light, the cursed air heavy and biting.", + "4": "The frost-rimed landscape feels wrong, the chill seeping into the bones.", + "5": "The ground glitters with frost, but an unnatural stillness pervades the air.", + "6": "The cursed winds whisper faintly, their chill unnatural beneath the clear sky.", + "7": "The frost seems to pulse faintly, the cold air charged with dread.", + "8": "The horizon feels distant, the cursed cold twisting perceptions under the bright sky.", + "9": "The air is frigid, but the chill carries a hint of unnatural menace.", + "10": "The cursed land feels frozen in time, the frost unyielding beneath the azure sky.", + "11": "The cold sharpens every sound, the cursed silence oppressive and vast.", + "12": "The frost-covered ground seems to resist warmth, the cold radiating a dark energy." + } + } + }, + "Cold Snap": { + "conditions": { + "temperature": { "lte": 40 }, + "precipitation": { "lte": 20 }, + "wind": { "gte": 20, "lte": 50 }, + "humidity": { "lte": 40 }, + "cloudCover": { "gte": 20, "lte": 60 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "The fields are coated in frost, the cold air biting and relentless.", + "2": "Frozen troughs glisten in the pale morning light, the chill unyielding.", + "3": "The farm animals huddle together for warmth under a clear, cold sky.", + "4": "The crisp air cuts through the barns, hay bales stiff with frost.", + "5": "Icicles dangle from rooftops, the cold snap freezing everything in its path.", + "6": "The wind carries a biting chill, the frost-laden ground crunching underfoot.", + "7": "The farm is eerily silent, the cold too sharp for activity.", + "8": "Frost glitters on the plowed soil, the fields hardened by the cold.", + "9": "The water pump is frozen solid, the chill seeping deep into the earth.", + "10": "Frost clings to the wooden fences, the air sharp and uninviting.", + "11": "The cold snap leaves the crops brittle, their leaves coated in ice crystals.", + "12": "The barn doors creak stiffly, the frost reaching even the hinges." + }, + "village": { + "1": "The village well is frozen over, buckets sitting unused by the icy rim.", + "2": "Frost-covered cobblestones glint faintly in the weak morning sun.", + "3": "Smoke rises slowly from chimneys, the air too cold for swift dispersal.", + "4": "Villagers bundle in heavy cloaks, their breaths visible in the freezing air.", + "5": "Icicles hang from every eave, the village transformed by the cold snap.", + "6": "The chill air creeps through cracks in shutters, fires struggling to keep homes warm.", + "7": "The frost stiffens clothes hung out to dry, leaving them frozen solid.", + "8": "Children huddle near hearths, their laughter muted by the biting cold.", + "9": "The air smells faintly of wood smoke, the frost blanketing every surface.", + "10": "Snow crunches underfoot as villagers hurry through the cold streets.", + "11": "The well-worn paths through the village are slick with ice, the air sharp.", + "12": "The sound of cracking ice echoes faintly as the village braces for the chill." + }, + "city": { + "1": "Frost clings to stone walls, the city's bustling life slowed by the cold.", + "2": "Merchants stamp their feet to keep warm, their breath clouding the air.", + "3": "Icicles hang from the gates, the chill reaching even the deepest alleys.", + "4": "Smoke from countless chimneys mingles with the biting cold of the streets.", + "5": "The frozen fountains stand as icy monuments, water halted mid-flow.", + "6": "Cobblestones are treacherous with ice, the city’s rhythm slowed by the cold snap.", + "7": "The markets are subdued, the frosty air making every movement deliberate.", + "8": "The chill air seeps into every crevice, the warmth of inns a welcome reprieve.", + "9": "The bells of the city tower sound dull, their tones softened by the cold.", + "10": "Windows frost over, intricate patterns forming on the glass.", + "11": "The river running through the city begins to freeze, its flow sluggish and icy.", + "12": "Guards pace briskly at the gates, their cloaks frosted at the edges." + }, + "plains": { + "1": "The wind howls across the frozen plains, the grass brittle with frost.", + "2": "The flat expanse is stark and white, frost stretching as far as the eye can see.", + "3": "Herds of animals huddle together, their breaths visible in the frigid air.", + "4": "The frost glimmers on the sparse shrubs, the plains eerily quiet.", + "5": "The air is biting, the open plains offering no shelter from the cold.", + "6": "Frozen streams crisscross the landscape, the chill sharpening every sound.", + "7": "The frost-bound earth is hard and unyielding, the plains desolate and quiet.", + "8": "The cold snap turns the plains into a vast, frozen wasteland.", + "9": "The horizon is clear, the cold air making every distant hill appear sharper.", + "10": "The grasses are stiff with ice, cracking faintly under the cold wind.", + "11": "The sun rises weakly over the frozen landscape, its warmth barely felt.", + "12": "The frost-covered plains stretch endlessly, their silence only broken by the wind." + }, + "forest": { + "1": "Frost coats the branches, the forest still and silent under the cold snap.", + "2": "The trees creak faintly, their bark frosted and brittle in the chill air.", + "3": "The underbrush is dusted with ice, the forest floor hardened by the cold.", + "4": "The frosty canopy lets in pale sunlight, casting shimmering patterns below.", + "5": "The cold air is thick and still, the forest animals hiding from the chill.", + "6": "The sound of cracking ice echoes faintly as streams freeze solid.", + "7": "The frost transforms the forest into a glittering, crystalline expanse.", + "8": "The forest paths are slick with ice, their edges lined with frost-tipped leaves.", + "9": "The chill air amplifies every sound, from snapping twigs to rustling branches.", + "10": "The frost on the trees sparkles faintly, the forest stark and beautiful.", + "11": "The cold snap freezes the forest streams, their flow halted by the chill.", + "12": "The frost-covered forest is eerily quiet, its usual life stilled by the cold." + }, + "swamp": { + "1": "The swamp waters freeze at the edges, the cold snap transforming the mire.", + "2": "Icicles hang from twisted roots, the swamp silent under the icy grip.", + "3": "The chill air carries the faint scent of decay, the swamp eerily quiet.", + "4": "The frosty reeds crackle underfoot, the swamp frozen in time.", + "5": "Mist rises faintly from the icy waters, the chill pervading the swamp.", + "6": "The frost glitters on moss-covered stones, the swamp strangely serene.", + "7": "The cold air dampens the usual swamp sounds, leaving an eerie silence.", + "8": "Frost clings to the tangled roots, the swamp transformed by the cold snap.", + "9": "The icy air stings, the swamp’s usual dampness replaced with brittle frost.", + "10": "The water is sluggish and partially frozen, the swamp adapting to the cold.", + "11": "The frost glows faintly on the murky pools, the swamp eerily still.", + "12": "The cold snap freezes the edges of the swamp, its usual life muted and still." + }, + "jungle": { + "1": "Frost clings to broad leaves, the jungle stilled by the unexpected chill.", + "2": "The vibrant greens of the jungle are muted under a layer of frost.", + "3": "The air is sharp and cold, the jungle’s usual heat replaced with biting chill.", + "4": "Icicles dangle from vines, the jungle transformed by the cold snap.", + "5": "The frosty air dampens the jungle’s usual cacophony, leaving a hushed stillness.", + "6": "Frozen droplets hang from broad fronds, catching the faint sunlight.", + "7": "The jungle floor is coated in frost, each step crunching in the chill air.", + "8": "The cold air sharpens the scents of the jungle, each leaf crisp with frost.", + "9": "The frost glistens on tangled roots, the jungle’s dense growth sparkling.", + "10": "The jungle streams steam faintly, their surfaces freezing in the cold air.", + "11": "The chill air quiets the jungle, its usual life slowed by the cold snap.", + "12": "The frost-covered canopy filters weak sunlight, the jungle strangely subdued." + }, + "hills": { + "1": "Frost lines the rolling hills, the grass stiff and shimmering.", + "2": "The cold wind cuts through the hills, leaving the landscape barren and silent.", + "3": "Shepherds huddle near fires, the frost-covered ground crunching underfoot.", + "4": "The chill air carries no sound, the hills eerily quiet under the cold snap.", + "5": "Patches of ice cling to rocky outcrops, the hills stark and frozen.", + "6": "The frost glitters in the pale sun, the hills bathed in a brittle beauty.", + "7": "The streams trickle sluggishly, their edges frozen solid in the biting cold.", + "8": "The rolling hills are coated in frost, the grass brittle and unyielding.", + "9": "The cold snap stings the air, the hills quiet save for the whistling wind.", + "10": "Frost clings to low shrubs, their leaves sparkling in the icy dawn.", + "11": "The frost has hardened the earth, the hills feeling lifeless and barren.", + "12": "The wind whispers through the frosty grasses, the hills serene yet foreboding." + }, + "mountains": { + "1": "The cold snap tightens its grip, the peaks shimmering with frost.", + "2": "Icicles hang from rocky ledges, the mountain paths treacherous and slick.", + "3": "The thin air bites harshly, the cold making the mountains feel inhospitable.", + "4": "Snow crusts over jagged rocks, the mountains locked in icy silence.", + "5": "The frost etches patterns on sheer cliffs, the cold intensifying at higher altitudes.", + "6": "Mountain streams freeze mid-flow, their surfaces glittering in the weak sun.", + "7": "The cold snap leaves the peaks white and silent, the air sharp and biting.", + "8": "Frosted pine trees line the slopes, their branches bowing under icy weight.", + "9": "The cold air howls through the crags, the mountains echoing its icy song.", + "10": "The frost glints faintly on rocky paths, each step precarious in the cold.", + "11": "Snow clings stubbornly to the cliffs, the cold unrelenting in its grasp.", + "12": "The mountains are stark and majestic, their frozen silence unnerving." + }, + "desert": { + "1": "The cold snap turns the desert sands brittle, frost clinging to the dunes.", + "2": "Icicles form on sparse shrubs, the desert air bitter and cold.", + "3": "The frosty ground crunches underfoot, the desert unrecognizably quiet.", + "4": "The freezing air chills to the bone, the desert’s usual warmth absent.", + "5": "Frost clings to rocky outcrops, the cold transforming the desert landscape.", + "6": "The night’s chill lingers, the desert stark and frosted under the pale sun.", + "7": "The cold snap has muted the desert’s colors, leaving it pale and lifeless.", + "8": "The wind stirs frozen grains of sand, the desert eerie in its icy stillness.", + "9": "Frost edges the sparse vegetation, the desert a study in contradictions.", + "10": "The air is sharp and dry, the frost lending a surreal quality to the desert.", + "11": "The freezing winds swirl over the dunes, the desert devoid of its usual heat.", + "12": "The frost sparkles faintly on the sands, the desert frozen in an unnatural stillness." + }, + "coastal": { + "1": "Frost coats the docks, the cold snap freezing the salty air.", + "2": "The sea mist freezes on contact, the coastline shimmering with ice.", + "3": "Waves crash sluggishly against frosty rocks, their spray freezing midair.", + "4": "The cold wind whips across the coast, leaving frost on every surface.", + "5": "The sand is stiff with frost, the shoreline eerily quiet under the cold snap.", + "6": "Fishing boats are docked, their decks slick with ice from the frigid air.", + "7": "Icicles hang from wooden pilings, the coast transformed by the biting chill.", + "8": "The frost clings to the sea grass, the air sharp with the scent of salt.", + "9": "The waves glitter faintly under a pale sun, the cold air chilling to the bone.", + "10": "The coastline is stark and white, frost creeping over the rocks and sand.", + "11": "The cold snap freezes tidal pools, their surfaces glistening in the light.", + "12": "The air is filled with the sound of cracking ice, the coast locked in frost." + }, + "volcano": { + "1": "The lava fields steam faintly, the cold snap tempering the usual heat.", + "2": "Frost clings to jagged rocks, the cold snap making the volcano seem alien.", + "3": "The air is sharp and cold, the volcanic vents struggling to stay warm.", + "4": "The cold snap freezes the outer layers of lava pools, the contrast stark.", + "5": "Icicles hang precariously near steaming cracks, the volcano eerily silent.", + "6": "The frosty air dulls the usual sulfuric scent, the volcano subdued.", + "7": "Frozen ash crunches underfoot, the volcano’s slopes transformed by the chill.", + "8": "The frost sparkles faintly against the blackened rock, the air biting.", + "9": "The cold snap clashes with the volcano’s heat, creating swirling mists.", + "10": "The lava’s glow is dimmed by frost-laden air, the volcano subdued.", + "11": "The cold freezes small streams of water on the volcano, leaving icy trails.", + "12": "The volcano is eerily quiet, the frost muting its usual fiery demeanor." + }, + "artic": { + "1": "The cold snap deepens the frost, the arctic glistening under weak sunlight.", + "2": "Ice crystals form intricate patterns, the arctic air sharp and pure.", + "3": "The snow crunches loudly underfoot, the cold biting at every exposed surface.", + "4": "The frost clings to the tundra, the arctic a vast, frozen expanse.", + "5": "The cold snap freezes the edges of icebergs, the arctic landscape serene.", + "6": "The wind carries a biting chill, the arctic’s frost unyielding and harsh.", + "7": "The snow sparkles in the pale light, the arctic locked in icy stillness.", + "8": "The cold snap sharpens the horizon, the icy expanse stretching endlessly.", + "9": "Frost-covered ice floes creak faintly, the arctic eerily quiet.", + "10": "The chill air intensifies, the arctic’s frozen beauty amplified by the cold snap.", + "11": "The snow is crisp and untouched, the arctic a pristine, frozen wilderness.", + "12": "The frost gleams under a pale sun, the arctic vast and unrelenting." + }, + "cursed": { + "1": "The cold snap spreads a frost that seems to whisper faint, haunting voices.", + "2": "The frost glows faintly under a strange, unnatural light, the air heavy with unease.", + "3": "Icicles form jagged shapes, their eerie angles casting unsettling shadows.", + "4": "The cold air carries a faint metallic scent, the frost unnatural and foreboding.", + "5": "The frost etches runic patterns into the ground, the cold snap feeling almost alive.", + "6": "Frost-covered bones protrude from the earth, the cursed land amplified by the chill.", + "7": "The cold snap freezes even the air, leaving an oppressive silence across the cursed land.", + "8": "The frost clings to the cursed soil, its sheen strange and otherworldly.", + "9": "Shadows seem to twist in the frosty light, the air sharp and unnerving.", + "10": "The frost spreads in strange patterns, the cold snap feeling deliberate and sinister.", + "11": "The air hums faintly with energy, the frost-coated landscape filled with unease.", + "12": "The cold snap deepens the curse, the frost almost seeming to pulse with dark intent." + } + } + }, + "Constant Drizzle": { + "conditions": { + "temperature": { "gte": 40, "lte": 60 }, + "precipitation": { "gte": 30, "lte": 50 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 70 }, + "visibility": { "gte": 20, "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "A light drizzle dampens the fields, turning soil to slick mud.", + "2": "The steady drizzle seeps into the ground, making the air earthy and cool.", + "3": "Farmhands work under grey skies, their clothes soaked by the persistent rain.", + "4": "The rain softly taps against thatched roofs, the fields misty with moisture.", + "5": "The drizzle clings to crops, droplets shimmering faintly in the dull light.", + "6": "Animals huddle in shelters, the fields sodden under the endless drizzle.", + "7": "Water pools in furrows, the constant rain bringing a chill to the air.", + "8": "The scent of wet hay and earth fills the air, the drizzle unrelenting.", + "9": "The drizzle muffles sounds across the farm, leaving a serene hush.", + "10": "Raindrops bead on plow blades, the farm drenched but serene.", + "11": "The rain leaves the farm muddy and slick, the sky a curtain of grey.", + "12": "Soft rain falls steadily, soaking the ground and darkening the fields." + }, + "village": { + "1": "Cobblestone streets glisten under the drizzle, villagers moving briskly under hoods.", + "2": "The soft patter of rain fills the village square, puddles forming everywhere.", + "3": "Smoke from chimneys rises into the drizzle, the air damp and chilly.", + "4": "Wooden carts leave muddy trails as the rain dampens the dirt roads.", + "5": "The constant drizzle blurs the edges of the village, cloaking it in mist.", + "6": "Villagers gather under awnings, their cloaks dripping in the steady rain.", + "7": "The rainwater trickles off rooftops, splashing onto the cobblestones below.", + "8": "The air smells of wet wood and damp earth, the drizzle soaking everything.", + "9": "Children play in puddles despite the steady drizzle, their laughter faint.", + "10": "The drizzle reduces visibility, turning the village into a quiet, wet haven.", + "11": "Lanterns burn softly in the grey drizzle, their light reflected in puddles.", + "12": "The village well overflows slightly, rain pooling in the courtyard." + }, + "city": { + "1": "Rain cascades off tiled roofs, the streets slick with constant drizzle.", + "2": "Merchants struggle to keep goods dry, their stalls draped in soaked cloth.", + "3": "The drizzle turns cobblestones slippery, the city bustling despite the rain.", + "4": "Smoke mixes with the misty rain, the city wrapped in a damp haze.", + "5": "Rainwater drips from gargoyles, their stone faces streaked with moisture.", + "6": "The air feels heavy and cool, the drizzle softening the city's usual clamor.", + "7": "Horses splash through puddles, their hooves echoing against wet stone walls.", + "8": "Guards patrol the city gates, their cloaks darkened by the steady rain.", + "9": "The market square glistens, vendors shivering under makeshift rain shelters.", + "10": "Rain seeps into every crevice, the city alive with the sound of dripping water.", + "11": "Rain trails down stained-glass windows, the colors dulled by the grey sky.", + "12": "The drizzle leaves the city's alleys dark and slick, the air thick with damp." + }, + "plains": { + "1": "A gentle drizzle dampens the grasslands, the horizon lost in mist.", + "2": "The plains are a sea of wet green, the rain shimmering faintly.", + "3": "Droplets cling to tall grasses, bending them under the drizzle's weight.", + "4": "The rain softens the earth, puddles forming in low spots on the plains.", + "5": "A faint mist rises from the soaked ground, the plains quiet under the drizzle.", + "6": "The air is fresh and cool, the rain turning the plains into a lush expanse.", + "7": "The drizzle blurs the horizon, the plains stretching into a grey haze.", + "8": "Rain runs in tiny rivulets through the grasses, the plains alive with moisture.", + "9": "The drizzle muffles sound, the plains peaceful under the soft rain.", + "10": "Small streams form across the plains, the rain creating new paths for water.", + "11": "The sky remains overcast, the drizzle soaking everything under its reach.", + "12": "The soft rain turns the plains vibrant, each blade of grass glistening." + }, + "forest": { + "1": "The drizzle filters through the canopy, leaves dripping steadily onto the forest floor.", + "2": "The forest is alive with the sound of rain, a symphony of dripping leaves.", + "3": "Moss and bark glisten under the drizzle, the forest soaked in green and grey.", + "4": "The constant rain darkens the undergrowth, the forest thick with damp shadows.", + "5": "The air smells of wet pine and earth, the forest refreshed by the drizzle.", + "6": "The drizzle pools in hollow logs, the forest vibrant despite the rain.", + "7": "Animals move quietly through the rain, their tracks washed clean by the drizzle.", + "8": "Ferns glisten under the rain, the forest floor soft and spongy with moisture.", + "9": "The forest feels enclosed and humid, the drizzle creating a serene atmosphere.", + "10": "The rain clings to spiderwebs, droplets sparkling in the dim forest light.", + "11": "Leaves shiver under the steady drizzle, the forest alive with subtle movement.", + "12": "The rain trickles down tree trunks, the forest a cascade of soft sounds." + }, + "swamp": { + "1": "The drizzle adds to the swamp’s dampness, puddles forming among the reeds.", + "2": "The air is thick and humid, the drizzle making the swamp feel even heavier.", + "3": "Rain pools on broad leaves, the swamp alive with the sound of dripping water.", + "4": "The drizzle makes the swamp mud slick and treacherous, each step a challenge.", + "5": "Frogs croak loudly as the drizzle intensifies, the swamp a chorus of life.", + "6": "Mist rises from the swamp’s surface, the drizzle blending into the dense air.", + "7": "Rain ripples across stagnant pools, the swamp eerily calm under the drizzle.", + "8": "The steady drizzle amplifies the swamp’s earthy scent, the air thick and heavy.", + "9": "The swamp feels timeless under the rain, water dripping endlessly from every surface.", + "10": "The rain darkens the moss and mud, the swamp teeming with quiet activity.", + "11": "The drizzle turns the swamp’s pathways into murky streams, the air humid and warm.", + "12": "Water drips steadily from hanging vines, the swamp alive with the sound of rain." + }, + "jungle": { + "1": "The drizzle cascades through the jungle canopy, the air humid and heavy.", + "2": "Broad leaves collect rain, droplets falling heavily onto the jungle floor.", + "3": "The jungle vibrates with life, the drizzle amplifying the sound of insects and birds.", + "4": "Mist rises from the jungle floor, the constant rain thickening the air.", + "5": "The scent of wet vegetation fills the air, the jungle damp and vibrant.", + "6": "The rain turns jungle trails to mud, each step sinking into the wet earth.", + "7": "Droplets run down vines and bark, the jungle glistening under the steady rain.", + "8": "The jungle feels alive and enclosed, the drizzle creating a humid cocoon.", + "9": "The rain pools in flower petals, the jungle awash with subtle colors.", + "10": "The steady drizzle turns the jungle into a symphony of dripping and rustling.", + "11": "The rain blends with the dense greenery, the jungle vibrant and alive.", + "12": "The jungle floor is dark and slick, the canopy offering little shelter from the rain." + }, + "hills": { + "1": "A fine drizzle sweeps across the rolling hills, softening the landscape.", + "2": "The drizzle clings to wildflowers and grasses, bending them gently.", + "3": "The hills are shrouded in mist, the rain blending the horizon into grey.", + "4": "Raindrops bead on scattered rocks, the drizzle steady and persistent.", + "5": "The air is cool and fresh, the drizzle invigorating the greenery.", + "6": "Shepherds move through the wet hills, their cloaks damp with rain.", + "7": "The drizzle makes the paths slick, puddles forming in the lowlands.", + "8": "The hills echo with the soft patter of rain, a serene and damp calm.", + "9": "Droplets glisten on spiderwebs spun between shrubs in the misty rain.", + "10": "Water flows gently along natural ridges, the rain nourishing the soil.", + "11": "The drizzle turns the hills a vibrant green, the rain washing away dust.", + "12": "Birds take shelter in rocky outcrops, their songs softened by the rain." + }, + "mountains": { + "1": "The drizzle swirls in the mountain air, clinging to rocky cliffs.", + "2": "Clouds hang low over the peaks, the constant rain obscuring the view.", + "3": "The paths are slick with water, the drizzle making every step cautious.", + "4": "The drizzle coats moss-covered rocks, making the terrain treacherous.", + "5": "The mountain air is cold and wet, the drizzle relentless and chilling.", + "6": "Streams cascade faster down the slopes, fed by the steady rain.", + "7": "Mist rises where the rain meets the colder air at higher elevations.", + "8": "Goats huddle in small caves, their coats dampened by the persistent drizzle.", + "9": "The steady rain turns the mountain paths to mud, slowing travelers.", + "10": "Echoes of dripping water resonate through the narrow mountain valleys.", + "11": "The drizzle obscures distant peaks, the mountains shrouded in grey haze.", + "12": "Waterfalls roar louder with the rain, their power amplified by the downpour." + }, + "desert": { + "1": "The drizzle is rare but welcome, softening the parched desert sands.", + "2": "Small puddles form briefly in the desert, the rain quickly absorbed.", + "3": "The scent of wet earth rises as the drizzle moistens the arid ground.", + "4": "Cacti glisten under the drizzle, their spines catching tiny droplets.", + "5": "The rain turns the desert air cooler, a refreshing break from the heat.", + "6": "Tracks in the sand are briefly dampened, the rain erasing their edges.", + "7": "The drizzle coats the desert rocks, creating a sheen on their surface.", + "8": "The rain leaves dark streaks on the dunes, the desert momentarily vibrant.", + "9": "Wind carries the drizzle across the sands, the rain a fleeting visitor.", + "10": "The desert sky darkens with clouds, the rain providing fleeting relief.", + "11": "The drizzle stirs dormant seeds to life, the desert whispering of growth.", + "12": "A faint mist hangs over the dunes, the drizzle softening the harsh landscape." + }, + "coastal": { + "1": "The drizzle mingles with sea spray, the coast cloaked in grey mist.", + "2": "Fishing boats rock gently as the rain streaks the surface of the sea.", + "3": "The cliffs glisten with rain, water dripping into the crashing waves below.", + "4": "The drizzle dampens sandy beaches, the horizon blurred by the rain.", + "5": "Seagulls cry through the misty rain, their wings slick with drizzle.", + "6": "The constant rain leaves the coastal paths muddy and slick underfoot.", + "7": "The ocean mirrors the grey sky, rain rippling across its surface.", + "8": "The scent of salt and rain fills the air, the coastline alive with moisture.", + "9": "Waves churn more violently under the cloudy sky, the drizzle steady.", + "10": "The drizzle clings to fishing nets, the docks quiet under the overcast skies.", + "11": "Rain pools in the crevices of rocky tide pools, blending with the ocean water.", + "12": "The steady drizzle turns the coastal landscape lush and glistening." + }, + "volcano": { + "1": "The drizzle hisses against the warm volcanic rock, steam rising in tendrils.", + "2": "Pools of water shimmer atop cooled lava flows, the rain steady and soft.", + "3": "The volcanic slopes are streaked with wet trails, the drizzle relentless.", + "4": "The air is humid and thick, the drizzle mingling with the heat of the volcano.", + "5": "Rain drips into fissures, creating faint sizzles as it touches the warm stone.", + "6": "The clouds hang heavy over the volcano, the rain softening its rugged peaks.", + "7": "The ash-strewn landscape glistens under the steady rain, a stark contrast.", + "8": "The drizzle clings to volcanic plants, the greenery thriving in the damp.", + "9": "Steam rises from lava flows as the drizzle cools the surface momentarily.", + "10": "The rain darkens the volcanic soil, the ground slick and treacherous.", + "11": "Mist and drizzle obscure the caldera, the volcano quiet under the grey sky.", + "12": "The rain turns the volcanic ridges slippery, each step precarious and slow." + }, + "artic": { + "1": "The drizzle freezes upon contact, turning the arctic terrain into a sheet of ice.", + "2": "The rain falls lightly, turning to snowflakes before touching the frozen ground.", + "3": "The air is icy, the drizzle turning the arctic into a chilling grey expanse.", + "4": "Frozen droplets cling to fur and fabric, the arctic drizzle relentless.", + "5": "The rain mingles with blowing snow, the arctic landscape harsh and wet.", + "6": "The drizzle turns to ice on contact, the arctic shimmering under a thin glaze.", + "7": "The sound of the rain is muffled by the snow, the arctic subdued and quiet.", + "8": "Glacial surfaces glisten under the drizzle, the ice reflecting muted light.", + "9": "The drizzle freezes on exposed skin, the arctic air biting and wet.", + "10": "Icicles grow longer with the steady drizzle, their tips dripping water.", + "11": "The rain soaks into packed snow, the arctic cold intensifying with the moisture.", + "12": "The drizzle mixes with frost, the arctic a land of shimmering cold and grey." + }, + "cursed": { + "1": "The drizzle feels unnatural, each drop heavy with an eerie chill.", + "2": "The cursed ground drinks the rain greedily, the drizzle never easing.", + "3": "Whispers seem to echo in the rain, the cursed land alive with dread.", + "4": "The drizzle darkens the cursed soil, the air heavy with unease.", + "5": "The rain brings no relief, only a deepening of the cursed land’s despair.", + "6": "Drops fall with an unnatural rhythm, the drizzle unsettling and strange.", + "7": "The cursed land reeks of decay, the rain intensifying the morbid scent.", + "8": "Shadows seem to move in the drizzle, the cursed air alive with malice.", + "9": "The cursed sky weeps, the rain carrying a faint coppery tang.", + "10": "The drizzle pools into dark puddles, their surfaces unnervingly still.", + "11": "Each raindrop seems to sap warmth, the cursed land growing colder.", + "12": "The cursed drizzle clings to everything, leaving a sickly, damp sheen." + } + } + }, + "Continuous Torrential Rain": { + "conditions": { + "temperature": { "gte": 50, "lte": 80 }, + "precipitation": { "gte": 80 }, + "wind": { "gte": 30, "lte": 70 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 90 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Fields are flooded, with crops submerged under relentless sheets of rain.", + "2": "Farmers scramble to save livestock as the rain drowns pastures.", + "3": "The rain pounds on wooden barns, threatening to weaken their structure.", + "4": "Rivers near the farm overflow, turning roads into rushing streams.", + "5": "The farmland turns into a vast muddy swamp, making work impossible.", + "6": "Rainwater cascades off roofs, forming large pools around the farmhouse.", + "7": "Thunder accompanies the unyielding rain, shaking windows and doors.", + "8": "Hens cluck nervously, their coop surrounded by standing water.", + "9": "The farm well overflows, contaminated by the endless downpour.", + "10": "Trenches fill quickly, waterlogging the soil and washing away seeds.", + "11": "Drenched scarecrows lean awkwardly, battered by the relentless rain.", + "12": "The fields are unrecognizable, the once fertile land now a flooded plain." + }, + "village": { + "1": "Cobblestone streets turn into rivers as rainwater surges downhill.", + "2": "Villagers wade through knee-deep water, their homes partially flooded.", + "3": "Thatched roofs drip steadily, unable to repel the unceasing rain.", + "4": "Chickens and goats huddle under makeshift shelters to escape the storm.", + "5": "The village square becomes a muddy lake, swallowing market stalls.", + "6": "Buckets overflow as villagers frantically try to collect rainwater.", + "7": "Rain drowns out all sound, save for the occasional crash of thunder.", + "8": "Wooden bridges creak dangerously under the weight of rushing water.", + "9": "The blacksmith’s forge sputters as rain pours through the roof.", + "10": "Children watch helplessly as their toys are swept away in the flood.", + "11": "The chapel bell rings faintly, its tower shrouded by sheets of rain.", + "12": "Villagers form a bucket chain, desperate to divert water from homes." + }, + "city": { + "1": "Rain hammers on rooftops, creating a deafening echo through the narrow alleys.", + "2": "City streets turn to streams, with water gushing into basements and cellars.", + "3": "Merchants struggle to save their wares as the market floods rapidly.", + "4": "The city gates are engulfed by water, trapping people within the walls.", + "5": "Stone steps leading to the castle are slick and treacherous from the rain.", + "6": "The central fountain overflows, blending with the deluge of rainwater.", + "7": "Guard posts are abandoned as rainwater soaks through leather armor.", + "8": "Taverns become overcrowded as people seek refuge from the storm.", + "9": "Rainwater pours into the sewers, threatening to backflow into the streets.", + "10": "Lanterns struggle to stay lit, their flames sputtering in the relentless rain.", + "11": "Vendors’ carts are swept away by the growing flood, goods scattered.", + "12": "The city walls glisten under the constant rain, streaked with rivulets of water." + }, + "plains": { + "1": "The vast plains are reduced to a quagmire, water pooling everywhere.", + "2": "Herds of animals cluster on higher ground to escape the rising waters.", + "3": "Paths vanish beneath the floodwaters, leaving travelers stranded.", + "4": "Grasslands bow under the weight of relentless rain, the horizon obscured.", + "5": "Streams overflow their banks, turning the plains into a sprawling marsh.", + "6": "The sound of rushing water fills the air, drowning out all other noises.", + "7": "Wildflowers are beaten flat by the pounding rain, their colors fading.", + "8": "Lightning flashes in the distance, illuminating the flooded expanse.", + "9": "Rabbits dart to their burrows, only to find them waterlogged and unsafe.", + "10": "Fences are swept away by the flood, carried off into the distance.", + "11": "Mud churns underfoot, clinging to boots and making progress slow.", + "12": "The endless rain creates a dull, grey monotony over the vast plains." + }, + "forest": { + "1": "The forest floor becomes a muddy mess as rainwater streams between roots.", + "2": "Trees drip continuously, their leaves heavy with the weight of the rain.", + "3": "Puddles form in the hollows of fallen logs, rippling with every raindrop.", + "4": "Streams carve new paths through the forest, eroding the soil around them.", + "5": "Rain filters through the canopy, creating a symphony of dripping water.", + "6": "Birds and animals take shelter, leaving the forest eerily silent save for the rain.", + "7": "Moss and fungi thrive, their vibrant colors enhanced by the moisture.", + "8": "Paths disappear beneath the floodwaters, swallowed by the deluge.", + "9": "The rain washes away the scents of the forest, leaving a clean, wet aroma.", + "10": "Leaves shine with water, their surfaces reflecting faint light in the gloom.", + "11": "Rain turns every step treacherous, the forest floor slick and unstable.", + "12": "The unceasing rain creates a mist that clings to the trees like a veil." + }, + "swamp": { + "1": "The swamp overflows, with water rising above the twisted roots of trees.", + "2": "Every step sinks deeper into the muck as the rain saturates the ground.", + "3": "Mosquitoes swarm in the damp air, undeterred by the endless rain.", + "4": "The swamp waters are churned by the rain, sending ripples across the surface.", + "5": "Frogs call out louder than ever, their croaks amplified by the wet air.", + "6": "Pools overflow, merging into a vast, soggy expanse under the deluge.", + "7": "Rain drops plunk rhythmically into stagnant pools, creating an eerie melody.", + "8": "The air grows thicker and more humid, the rain feeding the swamp’s growth.", + "9": "Rotting vegetation floats to the surface as the water levels rise rapidly.", + "10": "The smell of decay is heightened by the unyielding rain soaking the swamp.", + "11": "Leeches and other creatures rise to the surface, disturbed by the flooding.", + "12": "Branches bend under the weight of rainwater, dripping heavily into the swamp." + }, + "jungle": { + "1": "The jungle transforms into a maze of streams, water rushing between trees.", + "2": "Thick vines glisten with rainwater, dripping onto the saturated ground below.", + "3": "The constant rain muffles the sounds of the jungle, leaving a muted calm.", + "4": "Rain gathers in broad leaves, spilling over in rhythmic cascades.", + "5": "Trails disappear as the rain churns the soil into an impassable mire.", + "6": "The thick canopy offers little protection, as rain filters steadily through.", + "7": "Monkeys huddle in the higher branches, their fur slick with rainwater.", + "8": "Insects swarm in the humid air, thriving despite the torrential downpour.", + "9": "The jungle floor is a mosaic of puddles, each reflecting the dense greenery.", + "10": "Lianas sway under the weight of rainwater, dripping onto the ground below.", + "11": "The rain feeds the jungle’s lush growth, the air thick with moisture.", + "12": "Rivers rise rapidly, cutting off paths and stranding jungle dwellers." + }, + "hills": { + "1": "Water cascades down the slopes, carving rivulets into the muddy ground.", + "2": "The grassy hills are slick with rain, and footing becomes treacherous.", + "3": "Shepherds struggle to keep their flocks together in the relentless downpour.", + "4": "Mist clings to the hilltops, blurring the horizon in a grey haze.", + "5": "Streams overflow, turning the valleys into small, temporary lakes.", + "6": "The sound of rushing water fills the air, drowning out all other noises.", + "7": "Boulders loosen from the sodden soil, tumbling down the slopes.", + "8": "Drenched wildflowers cling to the ground, their colors muted by the rain.", + "9": "Low-lying trails are submerged, forcing travelers to higher ground.", + "10": "Torrents of water turn winding paths into flowing streams.", + "11": "Hollows between the hills become stagnant pools, brimming with rainwater.", + "12": "The once picturesque landscape is now a sodden, desolate expanse." + }, + "mountains": { + "1": "Rains batter the rocky crags, sending sheets of water down the cliffs.", + "2": "Narrow mountain trails are washed out, making travel perilous.", + "3": "Thunder echoes through the peaks, adding to the storm’s ferocity.", + "4": "Waterfalls swell dramatically, cascading with deafening force.", + "5": "Fog and rain obscure the peaks, leaving the mountain shrouded in gloom.", + "6": "Rockslides become a constant danger as the deluge loosens the terrain.", + "7": "Streams surge with water, carving deep channels through the mountainside.", + "8": "Caves become refuges for wildlife escaping the torrential rain.", + "9": "The ground becomes slick and unstable, threatening to give way.", + "10": "Icicles melt rapidly under the relentless rain, adding to the floodwaters.", + "11": "The storm’s howling winds drive the rain sideways across the ridges.", + "12": "The air grows thick with moisture, and every breath feels heavy and damp." + }, + "desert": { + "1": "Rain transforms the desert, creating fleeting rivers and muddy patches.", + "2": "Dry riverbeds roar to life, filled with water rushing toward the horizon.", + "3": "Cacti swell as they absorb the rare deluge, their spines glistening.", + "4": "The sand turns to mud, sticking to boots and slowing movement.", + "5": "Temporary oases form in low-lying areas, teeming with life.", + "6": "The rain washes away dust, revealing vibrant colors in the desert rocks.", + "7": "Thunder rumbles over the dunes, an uncommon echo in the arid expanse.", + "8": "Water pools reflect the stormy sky, a rare sight in the barren land.", + "9": "Winds whip up wet sand, creating stinging bursts of gritty rain.", + "10": "Scorpions and snakes flee to higher ground, their burrows flooded.", + "11": "The rain fills cracks in the parched earth, briefly softening its surface.", + "12": "Dunes shift under the weight of water, creating new shapes in the landscape." + }, + "coastal": { + "1": "Waves crash violently against the shore, driven by the relentless storm.", + "2": "Seawater mixes with rain, flooding the coastal lowlands.", + "3": "Fishing boats are tossed in the harbor, their moorings strained to breaking.", + "4": "Cliffs glisten under the rain, waterfalls forming where there were none.", + "5": "Seabirds struggle to stay aloft, buffeted by wind and rain.", + "6": "The storm surge inundates beaches, carrying debris far inland.", + "7": "Rain obscures the horizon, making the sea and sky blend into one grey mass.", + "8": "Coves and inlets fill rapidly, cutting off paths and flooding caves.", + "9": "Lighthouses shine dimly through the driving rain, barely visible.", + "10": "Coastal villages brace against the storm, their structures groaning under the strain.", + "11": "Salt spray adds to the wetness, stinging exposed skin.", + "12": "Docks creak ominously as waves and rain batter them endlessly." + }, + "volcano": { + "1": "Rain hisses as it hits the warm volcanic ground, creating plumes of steam.", + "2": "Rivers of water mix with ash, forming thick, gray mudslides.", + "3": "The rain erodes loose volcanic rock, creating treacherous conditions.", + "4": "Pools form in craters, reflecting the stormy sky above.", + "5": "Steam rises from fissures, blending with the downpour to obscure visibility.", + "6": "Lava flows cool and harden rapidly under the relentless rain.", + "7": "The volcanic terrain becomes slippery and unstable, making movement dangerous.", + "8": "Rivulets of water cut paths through the ash, carving out temporary streams.", + "9": "Sulfuric steam clouds the air, mingling with the rain to create a choking fog.", + "10": "The storm drowns out the distant rumble of the volcano.", + "11": "Ash-laden water pools at the volcano’s base, creating a gritty sludge.", + "12": "Lightning flashes illuminate the stark, rain-soaked volcanic landscape." + }, + "artic": { + "1": "Rain freezes on contact, coating the icy terrain in a slick glaze.", + "2": "Snowfields turn to slush as the rain saturates the frozen ground.", + "3": "Icicles drip steadily, their tips melting under the relentless downpour.", + "4": "Glacial rivers swell, their icy waters roaring through the arctic expanse.", + "5": "The rain obscures the horizon, blending sky and snow into a grey haze.", + "6": "Snowdrifts collapse under the weight of water, creating sudden avalanches.", + "7": "Frozen lakes crack and shift as rainwater pools on their surfaces.", + "8": "Wildlife seeks shelter, their tracks washed away by the unyielding rain.", + "9": "Visibility drops to nothing as rain and mist engulf the icy landscape.", + "10": "The sound of raindrops on ice creates an eerie, hollow echo.", + "11": "The rain freezes on fur and feathers, weighing down animals as they move.", + "12": "Glaciers glisten under the rain, their surfaces carved with fresh rivulets." + }, + "cursed": { + "1": "The rain carries an unnatural chill, freezing hearts as much as the ground.", + "2": "Dark clouds churn above, the rain thick and almost tar-like in consistency.", + "3": "Whispers seem to emanate from the falling rain, haunting those who listen.", + "4": "The ground bubbles where the rain touches, leaving a foul-smelling residue.", + "5": "Shadows move unnaturally in the storm, as if alive and watching.", + "6": "The rain feels heavier, dragging down even the spirits of those caught in it.", + "7": "Strange shapes flicker in the mist, vanishing when approached.", + "8": "The rainwater pools glimmer faintly, as though imbued with some dark magic.", + "9": "Eerie silence accompanies the rain, broken only by the occasional unearthly wail.", + "10": "Footprints appear in the muddy ground, leading nowhere and vanishing quickly.", + "11": "The rain seeps into the ground, leaving behind a lingering sense of dread.", + "12": "Lightning illuminates twisted, spectral forms in the storm clouds." + } + } + }, + "Crisp and Cold": { + "conditions": { + "temperature": { "gte": 20, "lte": 40 }, + "precipitation": { "lte": 50 }, + "wind": { "lte": 50 }, + "humidity": { "lte": 50 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 20 } + }, + "descriptions": { + "farm": { + "1": "The frost glints on the crops as a sharp chill hangs in the air.", + "2": "Fields are blanketed in frost, the cold biting through layers of cloth.", + "3": "The crisp morning air carries the scent of frozen earth.", + "4": "Farm animals huddle together, their breaths visible in the icy air.", + "5": "Hoarfrost clings to fences and tools, glittering under pale sunlight.", + "6": "The cold intensifies as the sun sets, freezing the water in the troughs.", + "7": "A faint crunch accompanies every step on the frost-hardened ground.", + "8": "The wind carries a sharp chill, stirring the skeletal remains of last season's crops.", + "9": "Frozen puddles crackle underfoot as farmers prepare for the day.", + "10": "The crisp air is silent, broken only by the occasional lowing of cattle.", + "11": "The frost creeps up the farmhouse windows, forming intricate patterns.", + "12": "Icicles dangle from the barn roof, their tips glinting in the light." + }, + "village": { + "1": "Smoke rises steadily from chimneys as villagers seek warmth indoors.", + "2": "Frost covers the cobblestones, making the narrow streets slick.", + "3": "The village well is frozen at the edges, its surface shimmering faintly.", + "4": "Children chase each other, their laughter mingling with the crisp air.", + "5": "The scent of wood smoke mingles with the icy breeze.", + "6": "Frozen laundry hangs stiffly on lines, unmoving in the still cold.", + "7": "Villagers huddle in cloaks, their breaths misting in the air.", + "8": "Frost-touched rooftops shine under a cold but clear sky.", + "9": "A stray dog trots through the icy streets, its fur dusted with frost.", + "10": "The blacksmith’s hammer rings out, steam rising from the anvil with every strike.", + "11": "The cold seeps into the stone walls, chilling even the hearth-lit homes.", + "12": "Icicles dangle from eaves, dripping faintly as the sun climbs." + }, + "city": { + "1": "Frost clings to the market stalls as merchants stamp their feet to stay warm.", + "2": "A crisp wind cuts through the city’s narrow alleys, stinging exposed skin.", + "3": "Frozen fountains stand still, their statues dusted with frost.", + "4": "Guards huddle in cloaks, their armor gleaming cold in the pale sun.", + "5": "The cold air sharpens every sound, from horses’ hooves to shouted greetings.", + "6": "Smoke spirals into the crisp sky from countless chimneys.", + "7": "The city gates creak as frost stiffens their hinges.", + "8": "The icy streets are eerily quiet, the usual bustle muted by the cold.", + "9": "Icicles form along the edges of the city walls, their tips sparkling faintly.", + "10": "The frigid air carries the scent of baked bread, drawing people to the tavern.", + "11": "Vendors huddle under blankets, selling steaming mugs to passing customers.", + "12": "The city square is dusted with frost, a pale echo of its summer vibrance." + }, + "plains": { + "1": "The open fields are stark and quiet, the frost-covered grass crunching underfoot.", + "2": "A chill breeze sweeps across the plains, bending the frost-tipped stalks.", + "3": "The horizon shimmers with a faint frost haze under the cold sun.", + "4": "Distant trees stand like frozen sentinels on the frostbitten plains.", + "5": "The endless expanse is silent save for the whistle of the cold wind.", + "6": "Ice glistens on the sparse vegetation, creating a landscape of silver and white.", + "7": "The ground is frozen solid, every step echoing faintly in the stillness.", + "8": "Wildlife is scarce, the bitter cold driving most creatures into burrows.", + "9": "The frost sparkles in the sunlight, a brittle beauty stretching to the horizon.", + "10": "A thin mist rises where frost meets the weak sunlight.", + "11": "The plains seem endless, the cold magnifying their stark emptiness.", + "12": "The wind bites at exposed skin, unrelenting in the wide open space." + }, + "forest": { + "1": "Frost coats the leaves and branches, making the forest shimmer faintly.", + "2": "The crunch of frostbitten leaves echoes in the still, icy woods.", + "3": "The cold air amplifies the creak of frozen branches swaying gently.", + "4": "Icicles hang from tree limbs, their tips dripping faintly in the sunlight.", + "5": "Patches of frost glisten where the sunlight filters through the canopy.", + "6": "A biting chill fills the forest, numbing fingers and toes.", + "7": "Animal tracks lead off into the frost, vanishing among the frozen underbrush.", + "8": "The stream runs sluggishly, its surface rimmed with delicate ice.", + "9": "The woods are silent save for the occasional crackle of frost underfoot.", + "10": "Frozen moss clings to the tree trunks, its vibrant green muted by frost.", + "11": "The cold makes the air sharp, each breath visible in the forest shadows.", + "12": "Birdsong is faint and sporadic, the chill keeping most creatures quiet." + }, + "swamp": { + "1": "Frost glistens on the reeds, turning the swamp into a crystalline maze.", + "2": "Patches of frozen water reflect the weak sunlight in fractured patterns.", + "3": "The air is damp and cold, chilling to the bone as frost coats the ground.", + "4": "The usual swamp odors are muted by the sharp chill in the air.", + "5": "Icicles form on overhanging branches, dipping into the cold, stagnant pools.", + "6": "The ground is hard and uneven, the frost locking the muck in place.", + "7": "Cold mist rises from the swampy ground, mingling with the brittle air.", + "8": "The chill silences the swamp, the usual croaks and calls absent.", + "9": "Frosted vines hang limp from the trees, their weight pulling them low.", + "10": "Frozen bubbles form in the mud, trapped beneath a thin layer of ice.", + "11": "The cold brings a rare stillness, with no movement but the drifting frost.", + "12": "The swamp feels otherworldly, transformed by the icy grip of the cold." + }, + "jungle": { + "1": "The dense canopy traps the cold air, creating a damp and frosty undergrowth.", + "2": "Frost coats the leaves, their vibrant greens dulled to silvery hues.", + "3": "The cold air hangs heavy, muffling the usual jungle cacophony.", + "4": "Steam rises faintly from the damp ground as frost meets lingering warmth.", + "5": "Vines and creepers droop under the weight of frost, their leaves brittle.", + "6": "Puddles freeze over, their surfaces smooth and glassy in the dappled light.", + "7": "The jungle’s moisture condenses into frost, creating a shimmering landscape.", + "8": "The air is eerily still, the cold silencing even the buzzing of insects.", + "9": "The chill freezes the dew on the leaves, turning droplets into tiny jewels.", + "10": "Bird calls are sporadic, the cold driving most creatures into hiding.", + "11": "The ground crackles with frost underfoot, a rare sound in the humid jungle.", + "12": "The cold transforms the jungle, its usual vibrancy muted under a silvery frost." + }, + "hills": { + "1": "The rolling hills are coated with a crisp frost that crunches underfoot.", + "2": "Chilled winds sweep over the slopes, carrying the scent of frozen earth.", + "3": "The frost-covered grass glitters faintly in the weak sunlight.", + "4": "Small patches of snow linger in the shaded hollows of the hills.", + "5": "The air is biting, with a cold clarity that sharpens the distant views.", + "6": "Each step stirs up a faint mist as frost melts beneath warm boots.", + "7": "Thin clouds drift across the pale sky, casting fleeting shadows over the frozen hills.", + "8": "The breeze carries a sharp chill, rippling through frost-tipped blades of grass.", + "9": "The distant treeline glistens with ice, the cold creeping into the valleys below.", + "10": "The sound of a lone bird call echoes, carried by the crisp, cold air.", + "11": "Hoarfrost clings to the rocks, forming intricate patterns along the slopes.", + "12": "The clear sky reveals a stark, frozen beauty across the rolling landscape." + }, + "mountains": { + "1": "The peaks rise stark and cold, their frost-covered slopes gleaming in the sunlight.", + "2": "Icy winds howl through the narrow mountain passes, biting at exposed skin.", + "3": "Snow dusts the rocky crags, clinging stubbornly to the steep inclines.", + "4": "The air is thin and sharp, each breath filling lungs with icy clarity.", + "5": "Frost-laden boulders line the path, glinting faintly in the cold sunlight.", + "6": "The chill is unrelenting, sinking deep into the bones of travelers.", + "7": "The clear sky offers an unobstructed view of the frosted mountain range.", + "8": "Icicles hang precariously from ledges, dripping slowly under the weak sun.", + "9": "The snow crunches loudly underfoot, each step echoing in the stillness.", + "10": "A distant avalanche rumbles faintly, a reminder of the mountain's icy grip.", + "11": "The cold transforms every sound into sharp, crystal-clear echoes.", + "12": "The mountain air is bracing, the frost painting the landscape in stark whites and grays." + }, + "desert": { + "1": "The usually dry sands are crusted with frost, a rare and chilling sight.", + "2": "The cold air turns the dunes into a landscape of icy, windblown ridges.", + "3": "A biting chill seeps through the sand, freezing even the hardy desert plants.", + "4": "The desert silence is amplified by the crisp, cold air.", + "5": "Frosted cacti stand like sentinels against the pale, wintry sky.", + "6": "The freezing temperatures make the once-hot sands crackle underfoot.", + "7": "The early morning sun reveals a thin layer of frost on the dunes.", + "8": "The wind carries a sharp chill, cutting across the wide, open desert.", + "9": "The cold night has left the desert eerily still, the frost glinting in the sun.", + "10": "The frost creates delicate patterns on the rocks, a fleeting desert wonder.", + "11": "The sharp cold is a harsh contrast to the usual desert heat.", + "12": "The frost-covered dunes stretch endlessly, a frozen sea under the clear sky." + }, + "coastal": { + "1": "Frost clings to the rocks and driftwood along the shoreline.", + "2": "The waves crash against the shore, their spray freezing as it meets the icy air.", + "3": "The salt air is sharp and cold, cutting through even the thickest cloaks.", + "4": "Frost coats the docked boats, their sails stiff with ice.", + "5": "The cold wind whips across the water, chilling to the bone.", + "6": "The sea glimmers faintly in the weak sunlight, its surface a mirror of icy waves.", + "7": "The beach is eerily quiet, save for the faint crackle of frost underfoot.", + "8": "Icicles dangle from the edges of fishing nets, glinting in the pale light.", + "9": "The water’s edge is frozen in places, ice creeping up the tide line.", + "10": "Seagulls call faintly, their cries muffled by the dense, cold air.", + "11": "The horizon is clear, the cold making the distant sea appear sharper.", + "12": "The frost on the sand gives the shoreline a ghostly, silver sheen." + }, + "volcano": { + "1": "The unusual frost clashes with the faint warmth radiating from the volcanic ground.", + "2": "Thin tendrils of steam rise where frost meets geothermal heat.", + "3": "The cold air carries a faint sulfur scent, sharp and biting.", + "4": "Icicles hang from jagged rocks, glinting alongside faintly glowing fissures.", + "5": "The chill makes the once-warm rocks icy and treacherous underfoot.", + "6": "Frost clings to the edges of lava tubes, a strange mix of fire and ice.", + "7": "The cold mutes the usually sulfurous air, leaving a sharp, biting clarity.", + "8": "A strange silence fills the volcanic landscape, broken only by the occasional crackle of frost.", + "9": "The frost-covered ground reflects the weak light, giving the volcanic slope an ethereal glow.", + "10": "The cold brings an eerie calm to the usually active volcanic terrain.", + "11": "Pockets of warmth create fleeting patches of mist in the icy air.", + "12": "The icy wind sweeps through the volcanic craters, a stark contrast to the usual heat." + }, + "artic": { + "1": "The air is brutally cold, the frost biting through every layer of clothing.", + "2": "The icy expanse stretches endlessly, glittering under the pale sunlight.", + "3": "Each breath forms a visible mist, freezing almost instantly in the frigid air.", + "4": "The snow crunches loudly underfoot, a constant reminder of the biting cold.", + "5": "Frostbite threatens exposed skin, the cold seeping into every pore.", + "6": "The horizon shimmers faintly, the frost creating a mirage of icy stillness.", + "7": "Icicles grow in abundance, dangling from every available surface.", + "8": "The wind howls across the ice, a relentless force against travelers.", + "9": "Even the strongest fires struggle to stay lit in the frigid cold.", + "10": "The frost sparkles like diamonds under the cloudless, icy sky.", + "11": "Every surface is coated in a thick layer of ice, turning the world into a frozen sculpture.", + "12": "The air feels heavy, the cold turning even sound into a muffled echo." + }, + "cursed": { + "1": "The frost carries an unnatural, malevolent chill that gnaws at the soul.", + "2": "Strange patterns form in the ice, twisting into shapes that seem to watch you.", + "3": "The air is unnaturally cold, sapping strength and filling the mind with dread.", + "4": "Frost-covered ground shifts eerily, as though something stirs beneath it.", + "5": "Whispers seem to echo in the icy wind, growing louder as the chill deepens.", + "6": "The frost glows faintly in the darkness, casting eerie shadows.", + "7": "The cold feels alive, coiling around exposed skin like unseen tendrils.", + "8": "Frozen shapes in the mist resemble twisted figures, frozen in agony.", + "9": "The frost leaves dark stains where it touches, as if cursed by unseen forces.", + "10": "The cold carries faint screams, heard only in the stillest moments.", + "11": "Each breath feels heavier, as though the frost is stealing your vitality.", + "12": "The landscape feels hostile, the frost imbued with a palpable sense of menace." + } + } + }, + "Dense Fog": { + "conditions": { + "temperature": { "gte": 30, "lte": 60 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 80 }, + "visibility": { "lte": 20 } + }, + "descriptions": { + "farm": { + "1": "Thick fog blankets the fields, reducing visibility to mere feet.", + "2": "Crops and tools vanish into the gray mist, the farm eerily silent.", + "3": "A dense fog clings to the ground, dampening sounds and soaking the earth.", + "4": "The barn looms faintly in the fog, its outline barely visible.", + "5": "Fog drifts between the haystacks, creating an ethereal, muted atmosphere.", + "6": "The farmhouse lights struggle to pierce through the dense gray curtain.", + "7": "Every sound seems muffled, the fog turning the farm into a ghostly landscape.", + "8": "The livestock move cautiously, their forms swallowed by the thick mist.", + "9": "Moisture drips from the eaves, the fog condensing on every surface.", + "10": "The air is heavy with dampness, the fog wrapping the farm in an impenetrable shroud.", + "11": "Faint shapes in the distance could be trees or something else entirely.", + "12": "The fog clings to the ground, turning the familiar farm into an alien world." + }, + "village": { + "1": "The cobbled streets are shrouded in dense fog, lanterns glowing faintly.", + "2": "Houses appear as dark, looming shadows through the thick mist.", + "3": "Villagers move cautiously, their figures disappearing into the gray haze.", + "4": "The church bell rings faintly, its sound muffled by the heavy fog.", + "5": "The market square is eerily silent, the fog swallowing all sound.", + "6": "Fog curls around the rooftops, hiding even the tallest steeple.", + "7": "The air feels damp and cold, the mist clinging to cloaks and hair.", + "8": "Footsteps echo strangely, their source obscured by the swirling fog.", + "9": "Lantern light casts eerie shadows, the fog distorting their shapes.", + "10": "The distant sounds of voices and carts are muted, adding to the eerie stillness.", + "11": "The village well is a dark, vague shape in the oppressive gray.", + "12": "The fog wraps around the village, turning it into a scene from a dream." + }, + "city": { + "1": "The city’s spires vanish into the fog, leaving the streets shadowed and still.", + "2": "Merchants light torches to guide their way through the dense mist.", + "3": "Fog clogs the alleys, creating an oppressive, silent atmosphere.", + "4": "The city gates are hidden in the mist, their outlines barely discernible.", + "5": "Streetlamps struggle to illuminate the cobbled roads, their light swallowed by the fog.", + "6": "Shouts and footsteps echo strangely, distorted by the thick haze.", + "7": "Buildings loom like spectral forms, their details erased by the fog.", + "8": "The air is damp and cold, the fog adding a weight to every breath.", + "9": "The sounds of the city feel distant, muted by the oppressive gray.", + "10": "Even the busiest streets feel deserted, the fog consuming everything.", + "11": "Windows glisten with condensed moisture, the city shrouded in stillness.", + "12": "The bell tower is hidden, its chimes drifting faintly through the fog." + }, + "plains": { + "1": "The vast plains are obscured by a dense fog, turning the horizon into a gray void.", + "2": "Grass and wildflowers glisten with dew, barely visible through the mist.", + "3": "The fog hugs the ground, turning the open plains into a ghostly expanse.", + "4": "Distant shapes move in the fog, their forms unrecognizable and eerie.", + "5": "The sky blends into the land, the fog erasing all boundaries.", + "6": "The damp air clings to every surface, leaving a faint chill in its wake.", + "7": "The wind stirs the fog in lazy swirls, revealing and concealing the plains.", + "8": "The distant sound of rushing water is muted, barely audible through the thick mist.", + "9": "The open landscape feels enclosed, the fog creating an unnatural sense of confinement.", + "10": "Footsteps vanish into the soft ground, the fog swallowing every sound.", + "11": "The plains are silent, the fog muffling all life into a heavy stillness.", + "12": "The ground is wet underfoot, the fog leaving the plains soaked and cold." + }, + "forest": { + "1": "The dense fog turns the forest into a maze of shadows and faint outlines.", + "2": "Tree trunks loom like pillars, their tops vanishing into the gray mist.", + "3": "The sound of dripping water fills the air as the fog condenses on the leaves.", + "4": "Every step is cautious, the fog hiding roots and fallen branches.", + "5": "The forest feels alive with whispers, the fog carrying faint echoes.", + "6": "Beams of light struggle to penetrate the canopy, diffused into an ethereal glow.", + "7": "The air is cold and damp, the fog clinging to every surface.", + "8": "Leaves glisten with moisture, the forest heavy with the weight of the fog.", + "9": "The fog swirls lazily between the trees, shifting like a living thing.", + "10": "Bird calls are distant and eerie, softened by the dense mist.", + "11": "The undergrowth is invisible, hidden beneath the thick, enveloping fog.", + "12": "The forest feels endless, the fog concealing all paths and landmarks." + }, + "swamp": { + "1": "The swamp is a murky, gray expanse, the fog thick and cloying.", + "2": "The water reflects faint, ghostly shapes as the fog swirls above it.", + "3": "Branches coated in mist reach out like skeletal fingers from the trees.", + "4": "The air is heavy and damp, the fog mixing with the swamp’s fetid smell.", + "5": "Croaking frogs and buzzing insects echo faintly through the thick mist.", + "6": "Pools of stagnant water glisten faintly under the dense gray veil.", + "7": "The fog makes the swamp feel endless, hiding its boundaries completely.", + "8": "Roots and reeds are obscured by the fog, making every step treacherous.", + "9": "The swamp is eerily silent, the fog absorbing even the smallest sounds.", + "10": "Shadows move within the mist, their origins hidden and unsettling.", + "11": "The air is cold and wet, the fog wrapping around the swamp like a blanket.", + "12": "The ground feels more uncertain, hidden by the dense gray mist." + }, + "jungle": { + "1": "The jungle is shrouded in dense fog, its vibrant colors muted and gray.", + "2": "Moisture drips constantly from the leaves, the fog adding to the jungle’s humidity.", + "3": "The dense mist clings to vines and trees, turning the jungle into a shadowy maze.", + "4": "Animal calls echo faintly, their sources hidden by the impenetrable fog.", + "5": "The air feels suffocating, heavy with moisture and the weight of the fog.", + "6": "The ground is slick and treacherous, the fog hiding every root and stone.", + "7": "Beams of light filter through the canopy, diffused into soft, glowing rays.", + "8": "The vibrant life of the jungle is muffled, the fog muting every sound.", + "9": "The fog wraps around the trees, turning the jungle into an eerie, silent world.", + "10": "Leaves and vines drip with condensation, the air thick with dampness.", + "11": "The jungle feels endless, the fog concealing even the nearest landmarks.", + "12": "The dense mist shifts with every step, making the jungle feel alive and watchful." + }, + "hills": { + "1": "The fog rolls over the hills, shrouding them in a ghostly veil.", + "2": "Peaks and dips of the hills disappear into the dense gray haze.", + "3": "A heavy mist clings to the grass, hiding pathways and landmarks.", + "4": "The wind stirs the fog, creating swirling tendrils across the hillsides.", + "5": "Sounds echo strangely, the fog distorting their direction.", + "6": "The hills feel infinite, the fog masking the horizon entirely.", + "7": "The ground is damp beneath your feet, the mist thickening with every step.", + "8": "Distant figures atop the hills are blurred and spectral through the fog.", + "9": "Trees on the slopes appear as dark, indistinct shapes.", + "10": "The fog is so thick, the crest of the next hill is completely obscured.", + "11": "Moisture drips from rocks and bushes, the air heavy with dampness.", + "12": "A chill clings to the mist, adding an eerie stillness to the hills." + }, + "mountains": { + "1": "The peaks vanish into the fog, their towering presence reduced to mystery.", + "2": "The mountain trail is treacherous, visibility reduced to mere feet.", + "3": "The fog swirls in the cold mountain air, wrapping around jagged cliffs.", + "4": "Echoes of falling rocks sound distant and strange through the mist.", + "5": "Every step feels cautious, the fog masking sharp drops and loose gravel.", + "6": "The mountain seems alive, the fog moving like a living shroud.", + "7": "Cliffs and ridges appear suddenly, looming out of the gray haze.", + "8": "Moisture condenses on rocks, making them slick and perilous.", + "9": "The usual grandeur of the mountains is muted by the dense, damp fog.", + "10": "Breath mists in the air, the chill of the fog biting at exposed skin.", + "11": "The sound of wind is muffled, the mist making the mountains eerily silent.", + "12": "The fog creates ghostly shapes in the distance, their forms shifting with the wind." + }, + "desert": { + "1": "The dense fog turns the desert into an alien, featureless landscape.", + "2": "Sand dunes fade into the mist, their shapes barely discernible.", + "3": "The fog traps the desert heat, making the air stifling and heavy.", + "4": "Footsteps vanish quickly, swallowed by the shifting sands and fog.", + "5": "The sun is a dim, pale disk in the sky, barely piercing the mist.", + "6": "The desert feels endless, the fog blending sky and earth into one.", + "7": "Cacti and rocks loom like spectral forms in the gray expanse.", + "8": "The stillness is unsettling, the fog muffling all sound across the desert.", + "9": "The ground is damp in patches, the fog leaving moisture on the sand.", + "10": "Every dune looks the same, the fog erasing familiar landmarks.", + "11": "The usual dry air feels damp, the mist clinging to skin and clothes.", + "12": "The desert’s usual vibrant colors are muted into shades of gray." + }, + "coastal": { + "1": "The sea vanishes into the fog, waves crashing unseen onto the shore.", + "2": "Boats are faint silhouettes, their masts barely visible through the mist.", + "3": "The air smells strongly of salt, the fog thick and damp.", + "4": "The horizon is gone, the ocean and sky blending into one gray expanse.", + "5": "The cries of gulls are faint, their forms lost in the dense fog.", + "6": "Waves lap against rocks, their sound muffled by the heavy mist.", + "7": "The lighthouse beam struggles to pierce the fog, its light barely visible.", + "8": "Footprints in the sand disappear quickly, the fog clinging to the ground.", + "9": "The air is cool and damp, the fog wrapping the coast in silence.", + "10": "Seaweed glistens with moisture, the shoreline hidden in the gray haze.", + "11": "The distant sound of a ship’s horn echoes eerily through the fog.", + "12": "The fog makes it feel like the sea is closer, its presence overwhelming." + }, + "volcano": { + "1": "The dense fog mingles with volcanic steam, creating an oppressive, sulfurous air.", + "2": "Rocks and craters fade into the mist, their jagged edges barely visible.", + "3": "The heat of the volcano is trapped, making the fog thick and stifling.", + "4": "Every step feels uncertain, the fog hiding fissures and unstable ground.", + "5": "The air is heavy with moisture and the tang of sulfur, the fog suffocating.", + "6": "Lava glows faintly through the mist, its red light distorted and ghostly.", + "7": "The fog clings to the volcano’s slopes, hiding its dangerous terrain.", + "8": "The rumble of the volcano is distant and eerie, muffled by the mist.", + "9": "Ash and fog mix in the air, reducing visibility and leaving a gritty residue.", + "10": "Steam rises from cracks, the fog making it hard to tell what’s solid ground.", + "11": "The volcano feels alive, the fog shifting with every tremor and gust.", + "12": "The usual stark landscape of the volcano is softened by the swirling gray mist." + }, + "artic": { + "1": "The icy expanse is hidden by a dense fog, turning the tundra into a gray wasteland.", + "2": "Snow and ice glisten faintly, their details erased by the heavy mist.", + "3": "The cold is biting, the fog freezing into frost on every surface.", + "4": "Glacial crevices are hidden, making every step perilous in the dense fog.", + "5": "The air is so cold that the fog feels like a frozen shroud.", + "6": "Distant howls echo through the mist, their source hidden and unsettling.", + "7": "Icebergs loom like spectral giants, their forms obscured by the gray haze.", + "8": "The sun is a pale, cold disk, barely visible through the fog.", + "9": "The snow crunches underfoot, the fog wrapping the arctic in an eerie stillness.", + "10": "The frozen landscape feels endless, the fog concealing all sense of direction.", + "11": "The mist clings to the ice, turning it into a slick, treacherous surface.", + "12": "The cold is numbing, the fog making the arctic feel even more desolate." + }, + "cursed": { + "1": "The fog feels alive, curling unnaturally around every object in its path.", + "2": "Whispers seem to emanate from the mist, their source unseen and chilling.", + "3": "The fog glows faintly, an unnatural light seeping through the cursed haze.", + "4": "Shadows move within the mist, their shapes indistinct and unsettling.", + "5": "The air feels heavy and oppressive, the fog clinging to skin like a curse.", + "6": "The fog smells of decay and sulfur, the stench almost unbearable.", + "7": "Every sound is distorted, the fog making even familiar voices seem strange.", + "8": "The cursed ground feels colder, the fog sapping warmth and hope alike.", + "9": "Objects appear and vanish, the fog twisting reality into something nightmarish.", + "10": "The fog leaves an oily residue on everything it touches, adding to its malign presence.", + "11": "The light is dim, even torches struggling to illuminate the cursed fog.", + "12": "The fog feels malevolent, a tangible weight pressing down on the cursed land." + } + } + }, + "Dry and Hot": { + "conditions": { + "temperature": { "gte": 60, "lte": 90 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 40 }, + "humidity": { "lte": 40 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 20, "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "The soil is parched, and crops wilt under the relentless sun.", + "2": "Dust kicks up with every step, settling over the dry fields.", + "3": "Farm animals pant in the heat, seeking shade wherever they can.", + "4": "The air shimmers above the cracked ground, waves of heat distorting the view.", + "5": "Every breath feels dry, the air sapping moisture from everything.", + "6": "Wells run low, and buckets scrape the bottoms as farmers fetch water.", + "7": "Creaking sounds come from the barn as wood dries and warps in the heat.", + "8": "The fields are quiet, save for the rustling of dry stalks in the breeze.", + "9": "The horizon is a dusty blur, the dry air obscuring distant views.", + "10": "Crows circle above, scavenging in the barren heat.", + "11": "The scent of dry hay fills the air, heavy and lingering.", + "12": "Farmers wipe sweat from their brows, their clothes sticking to their skin." + }, + "village": { + "1": "The streets are empty, villagers retreating indoors to escape the heat.", + "2": "A haze of dust lingers in the air, kicked up by passing carts.", + "3": "Shutters are drawn, and wells see heavy use as villagers fetch water.", + "4": "The sound of creaking wood fills the air as buildings shrink in the heat.", + "5": "Villagers fan themselves with makeshift fans, their faces flushed.", + "6": "Children splash in shallow streams, seeking relief from the scorching sun.", + "7": "The village square is deserted, the heat too oppressive for gatherings.", + "8": "Baskets of drying herbs hang from windows, their scent strong in the dry air.", + "9": "Every surface feels warm to the touch, the heat soaking into the buildings.", + "10": "The smell of baking bread mingles with the dry, earthy scent of the village.", + "11": "The sound of hammering fades as blacksmiths pause to wipe their brows.", + "12": "The village well is a hub of activity, everyone seeking a cool drink." + }, + "city": { + "1": "The cobblestone streets radiate heat, making the city feel like an oven.", + "2": "Vendors call out half-heartedly, their energy sapped by the sweltering weather.", + "3": "The marketplace is quieter than usual, the crowds thinner in the oppressive heat.", + "4": "Children play near fountains, splashing in the water to cool off.", + "5": "The smell of hot stone and tar fills the air, mingling with city scents.", + "6": "The clinking of tankards echoes as taverns fill with those seeking respite.", + "7": "Sunlight glares off polished armor, making guards squint as they patrol.", + "8": "The city walls trap the heat, turning the inner streets into a furnace.", + "9": "Every step kicks up dust, the dry roads leaving travelers parched.", + "10": "Shade is a rare commodity, and citizens cluster under awnings and trees.", + "11": "The bell of a water carrier rings out, drawing a thirsty crowd.", + "12": "Sweat glistens on the faces of merchants, their wares wilting in the heat." + }, + "plains": { + "1": "The dry grass crackles underfoot, brittle and yellowed by the sun.", + "2": "A warm wind sweeps across the plains, stirring up clouds of dust.", + "3": "Herds of animals huddle near dwindling water sources under the blazing sun.", + "4": "The sky is a clear, unforgiving expanse, the sun unrelenting.", + "5": "Shadows are sparse, the flat expanse offering little refuge from the heat.", + "6": "A mirage shimmers in the distance, the heat bending light on the horizon.", + "7": "The plains are eerily silent, the wildlife hiding from the scorching heat.", + "8": "Cracks spread across the soil, the earth parched and crying for rain.", + "9": "The wind carries a faint, dry scent of sage and dust.", + "10": "Every blade of grass is dry and sharp, rustling in the hot breeze.", + "11": "Buzzards circle high above, their shadows fleeting over the barren land.", + "12": "The horizon wavers, the heat blurring the boundary between earth and sky." + }, + "forest": { + "1": "The leaves hang limp, the forest unusually quiet in the oppressive heat.", + "2": "Dry twigs snap underfoot, the forest floor crunching with every step.", + "3": "Streams run low, their beds cracked and exposed to the sun.", + "4": "The air is heavy and still, trapped beneath the dense canopy.", + "5": "Even in the shade, the forest feels stifling and oppressive.", + "6": "Animals pant and hide in the undergrowth, avoiding the worst of the heat.", + "7": "The scent of dry pine needles fills the air, carried on a faint breeze.", + "8": "Birdsong is muted, the heat sapping energy from the forest creatures.", + "9": "Leaves rustle faintly, the only movement in the still, hot forest.", + "10": "The bark of trees is warm to the touch, the sun baking the canopy.", + "11": "The forest paths are dusty, each step raising a small puff of dirt.", + "12": "The heat has dried out the moss, turning it brittle underfoot." + }, + "swamp": { + "1": "The swamp is thick with heat, the air heavy and suffocating.", + "2": "Pools of water evaporate under the sun, leaving behind crusted salt lines.", + "3": "The mud cracks in places, the swamp struggling under the dry heat.", + "4": "The usual buzz of insects is quieter, the heat affecting even the pests.", + "5": "Every step is laborious, the heat pressing down like a physical weight.", + "6": "The swamp gases are more pungent, the heat intensifying their stench.", + "7": "The vegetation looks wilted, the oppressive sun taking its toll.", + "8": "The few creatures that move seem sluggish, hiding from the punishing heat.", + "9": "The swamp water steams faintly, its temperature unnaturally warm.", + "10": "Reeds and cattails droop, their tips browning in the relentless sun.", + "11": "The swamp feels lifeless, the heat draining the usual vitality from the air.", + "12": "Shimmering heat waves rise from the swamp, distorting the already eerie landscape." + }, + "jungle": { + "1": "The jungle air is thick and suffocating, the heat clinging to every surface.", + "2": "Leaves glisten with sweat-like moisture, the heat saturating the jungle.", + "3": "Streams run slower, their flow reduced by the oppressive heat.", + "4": "Animals pant and move sluggishly, conserving energy in the relentless heat.", + "5": "The jungle’s vibrant colors seem muted, dulled by the overwhelming dryness.", + "6": "The ground is cracked in places, the soil unable to hold moisture.", + "7": "The canopy traps the heat, creating a humid, stifling atmosphere.", + "8": "Insects drone lazily, their usual fervor dampened by the heat.", + "9": "Every breath feels heavy, the air thick and resistant in the lungs.", + "10": "The jungle paths are dry and dusty, the leaves crunching underfoot.", + "11": "Fruits ripen too quickly, their scent heavy and cloying in the hot air.", + "12": "The jungle feels alive with the heat, the air shimmering with its intensity." + }, + "hills": { + "1": "The grassy slopes are parched, and dust swirls with every gust of wind.", + "2": "Sparse trees offer little shade, their leaves curling under the relentless sun.", + "3": "The soil is dry and cracked, the hills baking in the sweltering heat.", + "4": "Wildflowers wilt and fade, unable to withstand the oppressive dryness.", + "5": "A warm wind rustles the dry grass, carrying the scent of earth and heat.", + "6": "Shepherds and livestock cluster near the few remaining water sources.", + "7": "Shimmering heatwaves rise from the hillsides, distorting the horizon.", + "8": "Birdsong is faint, the creatures of the hills subdued by the harsh sun.", + "9": "Every step sends a small puff of dust into the air, coating boots and legs.", + "10": "The hills feel silent and barren, their usual energy sapped by the heat.", + "11": "The dry grass crunches underfoot, brittle and scorched.", + "12": "The rolling terrain offers little respite, the sun dominating the open landscape." + }, + "mountains": { + "1": "The rocky paths are hot to the touch, reflecting the day’s punishing heat.", + "2": "Snowless peaks gleam under the sun, their surfaces dry and weathered.", + "3": "Streams run thin, the water struggling to trickle down the arid slopes.", + "4": "The mountain winds carry a dry, dusty scent, devoid of moisture.", + "5": "Even at higher elevations, the air feels heavy with heat and dryness.", + "6": "Climbers find the rocks loose and crumbly, the heat eroding their stability.", + "7": "The sparse vegetation is shriveled, the greenery faded under the harsh sun.", + "8": "The clear skies offer no protection, the sun glaring down on exposed ridges.", + "9": "The ground radiates heat, making even shaded areas feel warm and stifling.", + "10": "Wildlife is scarce, the heat driving animals into cooler dens and crevices.", + "11": "Every echo seems muted, the air too dry to carry sound far.", + "12": "The barren slopes seem endless, the heat amplifying the sense of desolation." + }, + "desert": { + "1": "The sand glows with heat, every grain burning against unprotected skin.", + "2": "Dunes shift under the relentless sun, the heat distorting the horizon.", + "3": "The air feels thick and suffocating, every breath dry and labored.", + "4": "Mirages dance across the desert floor, promising relief that never comes.", + "5": "The sparse cacti and shrubs seem brittle, their spines curling in the heat.", + "6": "The wind carries stinging grains of sand, each gust a dry assault.", + "7": "The sky is cloudless, offering no reprieve from the relentless sun.", + "8": "Footsteps sink deeply into the hot sand, the effort draining in the heat.", + "9": "The desert feels eerily silent, the heat suppressing all signs of life.", + "10": "Every shadow is fleeting, the sun dominating the barren landscape.", + "11": "Bones of long-dead creatures bleach under the blazing sun.", + "12": "The desert's vastness feels endless, its heat unrelenting." + }, + "coastal": { + "1": "The shoreline is dry and cracked, the heat making the sand almost unbearable.", + "2": "The salt in the air stings the skin, mingling with the sun’s harsh rays.", + "3": "Even the waves seem sluggish, their motion dulled by the oppressive heat.", + "4": "The coastal breeze offers little relief, carrying warmth instead of coolness.", + "5": "Boats creak in the harbor, their wood expanding under the sweltering sun.", + "6": "The tide pools evaporate, leaving salty residue on the exposed rocks.", + "7": "Seabirds circle lazily, their usual cries muted in the heat.", + "8": "The docks are deserted, fishermen seeking shade from the relentless sun.", + "9": "Palm trees sway faintly, their leaves drooping in the oppressive heat.", + "10": "The water shimmers with heatwaves, the horizon blurred and distant.", + "11": "Every step along the beach stirs up hot sand, sticking to sweat-drenched skin.", + "12": "The coastal village feels silent, the heat pressing down on every inhabitant." + }, + "volcano": { + "1": "The air is thick with ash, the heat magnifying the oppressive atmosphere.", + "2": "Rivulets of molten rock gleam faintly, adding to the sweltering heat.", + "3": "The ground radiates warmth, even the rocks too hot to touch for long.", + "4": "The sulfuric air burns the throat, the heat intensifying the acrid smell.", + "5": "Lava flows pulse with a dull glow, their heat suffocating and relentless.", + "6": "The sky above the volcano is a haze of smoke, filtering the harsh sunlight.", + "7": "Every gust of wind carries the scent of burning stone and ash.", + "8": "The heat distorts vision, the surrounding terrain wavering in the intense air.", + "9": "Cracks spider across the ground, exposing faint glimmers of molten rock beneath.", + "10": "The barren slopes are littered with dry, brittle rock that crunches underfoot.", + "11": "The oppressive heat and ash weigh heavily, making every breath laborious.", + "12": "The volcano's peak glows faintly, the heat felt even from a distance." + }, + "artic": { + "1": "The ice feels strangely warm underfoot, the unusual heat melting the edges.", + "2": "Glaciers glisten under the sun, small streams forming from their melting mass.", + "3": "The snowpack recedes, revealing bare, rocky patches beneath.", + "4": "Every step is a struggle, the sun's rays reflecting off the snow blindingly.", + "5": "Wildlife flees to cooler areas, the heat unusual and threatening.", + "6": "The air is heavy and dry, sapping moisture even from the icy terrain.", + "7": "Ice cracks echo loudly, the heat creating instability in the frozen landscape.", + "8": "The polar winds carry a faint warmth, a disconcerting anomaly in the Arctic chill.", + "9": "Snow feels heavy and slushy, compacting easily underfoot.", + "10": "The icy expanse reflects the sun fiercely, creating a blinding glare.", + "11": "The usual biting cold is replaced by an oppressive, dry heat.", + "12": "The once-stable ice sheets begin to shift and crack under the unrelenting heat." + }, + "cursed": { + "1": "The heat feels unnatural, as though the air itself is alive and hostile.", + "2": "Dry, acrid winds howl through the cursed land, carrying a sense of dread.", + "3": "The parched earth seems to radiate an unnatural heat, draining energy.", + "4": "Shadows flicker strangely, the oppressive heat distorting light unnaturally.", + "5": "The ground cracks and crumbles, glowing faintly with an eerie red heat.", + "6": "Every breath feels wrong, the dry air thick with the taste of ash.", + "7": "The cursed land is silent, save for the occasional creak of scorched trees.", + "8": "Pools of water boil away, leaving cracked and desolate basins behind.", + "9": "The heat clings to the skin, leaving an unshakable sensation of unease.", + "10": "The air smells faintly of sulfur and decay, the heat amplifying the stench.", + "11": "Winds carry whispers, the dry heat making every sound feel oppressive.", + "12": "The sun seems too bright, its light harsh and malevolent over the cursed land." + } + } + }, + "Extremely Windy": { + "conditions": { + "temperature": { "gte": 30, "lte": 70 }, + "precipitation": { "lte": 40 }, + "wind": { "gte": 70 }, + "humidity": { "lte": 50 }, + "cloudCover": { "lte": 50 }, + "visibility": { "gte": 30, "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "Winds rip through the fields, flattening crops and scattering debris.", + "2": "The air is filled with dust and chaff, whipped up by relentless gusts.", + "3": "Farm animals huddle in shelters, uneasy under the howling winds.", + "4": "Fences creak and sway, some collapsing under the strain of the gales.", + "5": "The barn doors slam open and shut, struggling against the ferocious wind.", + "6": "Hay bales are scattered across the farmland, carried by the strong gusts.", + "7": "Tree branches snap and fall, littering the farmstead with debris.", + "8": "The wind howls eerily through cracks in the farmhouse walls.", + "9": "Smoke from the chimney is torn away, dissipating instantly.", + "10": "Every step feels like a battle against the wind's unrelenting force.", + "11": "Loose tools and equipment are carried off into the fields.", + "12": "The sky is clear, but the wind's ferocity dominates the senses." + }, + "village": { + "1": "Shutters bang against windows, barely holding against the relentless wind.", + "2": "Villagers struggle to move through the streets, clutching their cloaks tightly.", + "3": "Market stalls are overturned, their goods scattered across the cobblestones.", + "4": "The wind whips through narrow alleys, creating a deafening roar.", + "5": "Roof tiles are ripped away, clattering loudly as they hit the ground.", + "6": "Children are ushered indoors as the wind tears through the village.", + "7": "The bell in the church tower clangs erratically in the fierce gusts.", + "8": "Smoke from hearths is blown back down chimneys, filling homes with soot.", + "9": "Laundry lines snap, sending clothes flying through the air.", + "10": "Signs swing wildly on their hinges, some breaking free entirely.", + "11": "The wind carries a haunting whistle through the village square.", + "12": "Villagers shout to be heard over the howling wind." + }, + "city": { + "1": "Banners and flags are torn from their poles, flapping violently in the wind.", + "2": "Dust and debris swirl through the crowded streets, stinging eyes and skin.", + "3": "Chimneys rattle and groan, some collapsing under the force of the wind.", + "4": "Citizens clutch their cloaks and hats, struggling to navigate the gusty streets.", + "5": "Carts are overturned, their goods spilling onto the cobblestones.", + "6": "The wind howls through the narrow alleys, amplifying its deafening roar.", + "7": "Streetlamps sway dangerously, threatening to topple over.", + "8": "The city gates creak and groan, straining against the unrelenting gusts.", + "9": "The wind drowns out conversations, forcing people to shout to be heard.", + "10": "Loose shingles and tiles fall from rooftops, clattering loudly below.", + "11": "Papers and parchments are snatched from hands and blown away.", + "12": "The city's bustling energy is subdued by the overwhelming power of the wind." + }, + "plains": { + "1": "The grass ripples like waves, flattened by the powerful gusts.", + "2": "Dust storms rise from the dry earth, making visibility difficult.", + "3": "Birds struggle to fly, buffeted by the unrelenting wind.", + "4": "Small shrubs and bushes are uprooted, tumbling across the open plains.", + "5": "The sound of the wind dominates, a constant roar across the vast expanse.", + "6": "Travelers lean into the wind, fighting to keep their footing.", + "7": "The horizon is hazy, obscured by the dust kicked up by the gale.", + "8": "Wind whips through tents and shelters, threatening to tear them apart.", + "9": "Loose stones and debris are carried across the plains, pelting anything in their path.", + "10": "The sky remains clear, but the wind's ferocity shapes the landscape below.", + "11": "Wild animals seek shelter, avoiding the wind's relentless force.", + "12": "The plains feel vast and empty, the wind stripping away all other sounds." + }, + "forest": { + "1": "Tree branches bend and crack under the strain of the fierce winds.", + "2": "Leaves are torn from the trees, swirling through the air in chaotic patterns.", + "3": "The canopy above sways wildly, sunlight flickering through the moving branches.", + "4": "The wind roars through the trees, a deafening sound in the dense forest.", + "5": "Fallen branches litter the forest floor, scattered by the powerful gusts.", + "6": "Animals retreat into their burrows, avoiding the chaos above.", + "7": "The wind creates eerie sounds as it whistles through the dense foliage.", + "8": "Small trees are uprooted, crashing to the ground with loud thuds.", + "9": "Every step through the forest is met with resistance from the fierce wind.", + "10": "Streams and rivers ripple violently, their surfaces disturbed by the gusts.", + "11": "The forest feels alive, the wind transforming it into a place of chaos.", + "12": "Travelers struggle to navigate, the wind masking familiar landmarks." + }, + "swamp": { + "1": "The wind sends ripples across the murky waters, disturbing the swamp's stillness.", + "2": "Dead branches fall into the water with splashes, broken by the powerful gusts.", + "3": "The reeds and cattails sway wildly, their tops snapping in the wind.", + "4": "The air is filled with the eerie sound of wind whistling through the swamp.", + "5": "Small waves form on the water's surface, breaking against the muddy banks.", + "6": "The swamp creatures are unusually quiet, hiding from the stormy winds.", + "7": "Loose moss and vines are carried away, leaving the trees bare in places.", + "8": "Mud splatters as the wind stirs the water, coating everything nearby.", + "9": "The swamp feels desolate, the wind stripping away its usual sounds.", + "10": "Lanterns flicker and extinguish, their flames no match for the wind.", + "11": "Travelers struggle to keep their footing on the slippery, wind-swept paths.", + "12": "The swamp feels alive, the wind transforming it into a place of turmoil." + }, + "jungle": { + "1": "The dense foliage trembles as the wind forces its way through the jungle.", + "2": "Palm fronds bend and snap, crashing to the ground with loud thuds.", + "3": "The wind creates a cacophony, amplified by the dense vegetation.", + "4": "The air is thick with the scent of churned earth and broken leaves.", + "5": "Vines whip through the air, pulled loose by the unrelenting gusts.", + "6": "Animals screech and chatter, unsettled by the jungle's upheaval.", + "7": "Small plants and flowers are uprooted, scattered across the jungle floor.", + "8": "The wind howls through the canopy, drowning out all other sounds.", + "9": "Rainwater stored in leaves is flung down, creating an artificial drizzle.", + "10": "The ground becomes treacherous as loose debris litters the paths.", + "11": "The normally vibrant jungle feels chaotic, its order disrupted by the wind.", + "12": "Every step through the jungle is met with resistance from the powerful gusts." + }, + "hills": { + "1": "Grass bends sharply under the relentless winds, rippling like waves.", + "2": "Small rocks are dislodged and tumble noisily down the slopes.", + "3": "Shepherds and livestock huddle together for protection against the gusts.", + "4": "The howl of the wind echoes across the rolling terrain, a constant roar.", + "5": "Trees on the hill crests sway dangerously, some snapping under the strain.", + "6": "Travelers lean into the wind, struggling to maintain their footing.", + "7": "Loose soil and debris are carried into the air, pelting anyone exposed.", + "8": "Tents and makeshift shelters threaten to tear free from their moorings.", + "9": "Birds are grounded, unable to fly in the overpowering gusts.", + "10": "The hills feel barren and exposed under the unrelenting force of the wind.", + "11": "Shouts are lost to the howling wind, making communication nearly impossible.", + "12": "Paths and trails are obscured by dust and debris, whipped up by the storm." + }, + "mountains": { + "1": "Wind tears through the mountain passes, creating a deafening roar.", + "2": "Loose stones are dislodged, clattering down the slopes below.", + "3": "Travelers clutch their cloaks tightly, bracing against the freezing gusts.", + "4": "Snow and ice are swept up in the wind, creating blinding conditions.", + "5": "Peaks and ridges whistle sharply as the wind cuts through the rocks.", + "6": "The wind tugs at ropes and gear, making climbing treacherous.", + "7": "Tents in base camps shudder violently, threatening to collapse.", + "8": "Small avalanches are triggered by the relentless battering of the winds.", + "9": "The cold wind bites through even the thickest clothing, numbing the skin.", + "10": "The sky is clear, but the wind’s ferocity makes the mountains feel hostile.", + "11": "Echoes of the wind bounce off the rocky walls, amplifying its power.", + "12": "Navigating narrow ledges becomes nearly impossible as the wind pushes wildly." + }, + "desert": { + "1": "The wind carries sand and grit, stinging exposed skin and eyes.", + "2": "Dunes shift and reshape, their peaks collapsing under the gusts.", + "3": "Visibility drops as a haze of sand envelops the desert landscape.", + "4": "The wind whistles through the empty expanse, a haunting, lonely sound.", + "5": "Loose clothing and fabric whip violently, offering little protection from the sand.", + "6": "Oases are choked with flying debris, their waters muddied by the storm.", + "7": "Footprints and tracks are quickly erased by the shifting sands.", + "8": "Camels and travelers struggle to move forward, leaning into the unrelenting wind.", + "9": "Cacti and sparse vegetation bend under the strain, some snapping.", + "10": "Shelters are buried in sand as the wind drives it into every crevice.", + "11": "The desert feels alive, the wind sculpting its surface with violent force.", + "12": "The sun is dimmed by the airborne dust, casting an eerie light over the landscape." + }, + "coastal": { + "1": "Waves crash violently against the cliffs, their spray carried far by the wind.", + "2": "Ships in the harbor strain against their moorings, tossed by the rough seas.", + "3": "Sea spray fills the air, mixing with the salty tang of the ocean wind.", + "4": "Fishing nets and gear are torn loose, scattered across the shore.", + "5": "The wind howls through the rigging of ships, a mournful, ominous sound.", + "6": "Beach sand is whipped into the air, stinging exposed skin.", + "7": "Waves rise higher than usual, their crests foaming in the turbulent wind.", + "8": "Seagulls are grounded, unable to fight against the gale-force gusts.", + "9": "Dockworkers struggle to secure cargo as the wind threatens to overturn crates.", + "10": "The horizon blurs as wind-driven mist and spray obscure the distance.", + "11": "Shoreline vegetation bends sharply, its roots straining against the force.", + "12": "The wind carries a low, constant roar, drowning out all other sounds." + }, + "volcano": { + "1": "Ash is swept into the air by the fierce winds, darkening the sky further.", + "2": "The wind carries a faint sulfurous scent, mixed with the volcanic heat.", + "3": "Loose rocks and pebbles are dislodged, tumbling down the volcano’s slopes.", + "4": "Lava fields shimmer with heat as gusts of wind add to the eerie atmosphere.", + "5": "Sparks and embers from volcanic vents are carried away by the powerful gusts.", + "6": "Shelters and tents near the base of the volcano flap wildly in the wind.", + "7": "The sound of the wind echoes eerily against the barren, rocky terrain.", + "8": "Ash clouds shift and swirl, making breathing even more difficult.", + "9": "The wind whips across lava flows, creating strange, shimmering patterns.", + "10": "Travelers shield their faces, struggling to navigate the treacherous ground.", + "11": "The volcano feels alive, its heat and the wind’s force combining into chaos.", + "12": "Any vegetation clinging to the slopes is stripped bare by the relentless winds." + }, + "artic": { + "1": "Snow and ice are blasted into the air, creating an almost blinding haze.", + "2": "The wind bites with an icy ferocity, cutting through even the warmest furs.", + "3": "Drifts of snow are reshaped by the relentless gusts, covering tracks and trails.", + "4": "The air is filled with the eerie whistle of wind sweeping across frozen plains.", + "5": "Glacial crevices groan and crack under the strain of the howling wind.", + "6": "Shelters are buried in blowing snow, making them difficult to spot.", + "7": "Exposed skin freezes quickly as the wind chill drops temperatures further.", + "8": "Snow-laden trees bow under the force of the unrelenting gusts.", + "9": "Visibility is reduced to almost nothing as wind-driven snow blankets the landscape.", + "10": "The frozen air is thick with ice crystals, stinging and numbing on contact.", + "11": "Ice floes groan and shift, driven by the power of the wind.", + "12": "The artic becomes a howling wasteland, dominated by the unyielding storm." + }, + "cursed": { + "1": "The wind carries a haunting wail, as if echoing distant cries.", + "2": "Dead trees creak and groan, their skeletal branches snapping in the gale.", + "3": "Ash and dust swirl through the cursed land, choking the air.", + "4": "Whispers seem to ride on the wind, unsettling those who hear them.", + "5": "Dark clouds are driven across the sky, casting shifting shadows over the land.", + "6": "The wind tears at cloaks and hoods, exposing travelers to the eerie chill.", + "7": "Unnatural debris is carried by the wind, adding to the land’s malevolence.", + "8": "Lanterns flicker and extinguish, leaving only the sound of the howling wind.", + "9": "The air smells faintly of decay, carried on the gusts from unknown sources.", + "10": "Shadows seem to twist and move in the wind, playing tricks on the eye.", + "11": "Structures creak ominously, as if the wind were trying to tear them down.", + "12": "The cursed land feels alive, the wind heightening its ominous presence." + } + } + }, + "Foggy": { + "conditions": { + "temperature": { "gte": 30, "lte": 60 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Thick fog blankets the fields, making the scarecrows look like shadowy figures.", + "2": "The barn looms faintly through the haze, its outline barely visible.", + "3": "Livestock huddle close as the fog creeps over the paddocks.", + "4": "Farmers tread cautiously, their voices muffled by the dense mist.", + "5": "Dew collects on the crops as the fog lingers in the chilly air.", + "6": "Tools left outside glisten with moisture under the pale foggy light.", + "7": "The sun rises slowly, but its warmth cannot penetrate the thick mist.", + "8": "Far-off crows call out, their shapes hidden within the fog.", + "9": "The distant farmhouse bell is the only guide in the smothering haze.", + "10": "Chickens cluck nervously, unsure of what hides in the misty expanse.", + "11": "A light breeze shifts the fog slightly, revealing and obscuring the fields.", + "12": "Damp earth smells rise from the ground, mixing with the dense fog." + }, + "village": { + "1": "Cobblestone streets disappear into the dense fog, leaving only faint outlines.", + "2": "Lantern light flickers weakly, swallowed by the surrounding mist.", + "3": "The smithy’s anvil rings out, but the blacksmith is hidden by the fog.", + "4": "Children’s laughter echoes eerily through the foggy village square.", + "5": "Shutters creak as villagers keep their homes closed against the chilling mist.", + "6": "Carts creak through the square, their drivers barely visible in the fog.", + "7": "The sound of footsteps on wet cobblestones grows louder before a figure appears.", + "8": "Dogs bark nervously, their sharp yaps echoing in the heavy fog.", + "9": "The village well stands like a ghostly pillar in the shrouded square.", + "10": "Faint church bells ring, their tones muted by the dense mist.", + "11": "Shadows flit past windows, blurred and ghostly in the foggy light.", + "12": "The smell of baking bread drifts through the mist, comforting and warm." + }, + "city": { + "1": "Fog obscures the city spires, reducing the grand skyline to ghostly shapes.", + "2": "Street vendors call out, their carts hidden in the thick mist.", + "3": "Horses’ hooves clatter on stone streets, echoing in the foggy silence.", + "4": "Guards patrol the gates, their forms barely discernible in the mist.", + "5": "The grand marketplace is eerily quiet, the fog muffling all sound.", + "6": "Oil lamps flicker weakly, their light struggling to pierce the dense haze.", + "7": "The harbor is lost to the mist, ships’ masts peeking faintly above.", + "8": "Criers’ voices sound distant, their figures swallowed by the fog.", + "9": "Alleyways vanish into white nothingness, adding an air of mystery to the city.", + "10": "Beggars huddle in corners, their faces hidden by shadow and mist.", + "11": "The clanging of the city clock tower is the only reliable signal of time.", + "12": "Puddles reflect the dim, gray light of the fog-covered city." + }, + "plains": { + "1": "Fog rolls across the open fields, erasing the horizon.", + "2": "The grass glistens with dew, veiled in a soft white mist.", + "3": "Herds of animals move like shadows in the foggy expanse.", + "4": "The rising sun casts a faint glow through the thick mist.", + "5": "Wind carries the scent of damp earth across the plains.", + "6": "Distant hills are reduced to faint, ghostly outlines.", + "7": "Tracks in the dirt fade into the fog, leading into mystery.", + "8": "The whistle of wind through tall grass is the only sound.", + "9": "Campfires struggle to pierce the shrouding mist over the plains.", + "10": "Travelers feel exposed despite the fog’s concealment.", + "11": "The world feels endless, obscured by the white haze.", + "12": "The plains seem to hold their breath in the stillness of the fog." + }, + "forest": { + "1": "Trees loom like shadowy giants, their tops lost in the fog.", + "2": "The undergrowth is damp and silent, shrouded in a thick white mist.", + "3": "Sunlight filters weakly through the canopy, diffused by the fog.", + "4": "Every sound is amplified, from the snap of twigs to the rustle of leaves.", + "5": "Moss-covered stones glisten with moisture in the ghostly light.", + "6": "Animal tracks lead away, disappearing into the obscuring haze.", + "7": "Birdsong is distant and muffled, adding an eerie calm to the woods.", + "8": "The forest floor feels spongy underfoot, wet with fog’s dew.", + "9": "Streams run quietly, their bubbling masked by the heavy mist.", + "10": "Tree branches creak softly, bending under the fog’s dampness.", + "11": "Travelers tread carefully, wary of unseen obstacles in the mist.", + "12": "The air smells of damp earth and decaying leaves, thick with moisture." + }, + "swamp": { + "1": "Thick fog rises from the marsh, cloaking the landscape in white.", + "2": "Swamp gas glimmers faintly, adding an eerie light to the mist.", + "3": "Waterlogged trees stand like specters, their shapes distorted by the fog.", + "4": "The croaks of frogs echo strangely in the dense haze.", + "5": "Mud squelches underfoot, invisible pools hidden in the mist.", + "6": "The air feels heavy and damp, saturated with the swamp’s decay.", + "7": "Small insects flit through the fog, their buzzing magnified in the quiet.", + "8": "The swamp’s stagnant waters are hidden, marked only by faint ripples.", + "9": "Reeds sway slightly, their movement ghostly in the shrouded light.", + "10": "Branches draped with moss vanish into the thick mist above.", + "11": "The fog clings to the swamp like a second skin, smothering sound.", + "12": "A distant splash breaks the silence, its source unseen in the haze." + }, + "jungle": { + "1": "The dense jungle canopy traps the fog, turning the air thick and still.", + "2": "Dew-covered leaves glisten faintly in the dim, misty light.", + "3": "Animal calls sound muffled, their sources hidden by the heavy fog.", + "4": "Vines and undergrowth are coated in moisture, making travel treacherous.", + "5": "The fog clings to the jungle floor, obscuring roots and hidden dangers.", + "6": "Dripping water echoes faintly, adding to the eerie quiet of the mist.", + "7": "The jungle feels alive, every shadow shifting in the fog’s embrace.", + "8": "Travelers feel lost as the fog erases paths and landmarks.", + "9": "The damp air carries the earthy scent of wet vegetation and decay.", + "10": "Bright flowers appear muted, their colors dulled by the mist.", + "11": "A snake slithers past unseen, its presence betrayed only by a faint rustle.", + "12": "The jungle’s towering trees fade into the fog, creating a world of shadows." + }, + "hills": { + "1": "Fog rolls over the hills, turning their gentle slopes into a featureless expanse.", + "2": "Sheep bells ring faintly, their source lost in the dense mist.", + "3": "Shadows of ancient standing stones loom faintly through the fog.", + "4": "The dew-drenched grass sparkles dimly in the pale, misty light.", + "5": "Hillsides vanish into swirling fog, disorienting any traveler.", + "6": "Winding paths disappear as the fog thickens, leaving wanderers guessing.", + "7": "The cool, damp air carries the scent of wildflowers barely visible in the mist.", + "8": "A distant owl hoots, its cry softened by the oppressive fog.", + "9": "Small streams trickle unseen, their gurgles masked by the thick mist.", + "10": "Wind rustles through unseen hedges, their forms swallowed by the haze.", + "11": "Low-hanging fog cloaks the valleys, hiding trails and landmarks.", + "12": "Even familiar hillsides seem strange and foreboding under the veil of fog." + }, + "mountains": { + "1": "Fog shrouds the peaks, rendering the towering cliffs as looming shadows.", + "2": "Jagged rocks emerge like ghosts through the swirling mist.", + "3": "Treacherous paths are obscured, making the journey perilous and slow.", + "4": "The air is cold and damp, carrying the faint echo of falling stones.", + "5": "Sheer drops vanish into the fog, hiding their deadly depths.", + "6": "Mountain trails twist endlessly as the dense mist veils the way.", + "7": "Distant avalanches rumble, their source masked by the thick fog.", + "8": "The sun is a faint glow above the mist, offering little warmth.", + "9": "Streams cascade down the rocks, their sound amplified in the silence.", + "10": "Birds of prey cry out unseen, their sharp calls echoing in the haze.", + "11": "The fog clings to the mountainside, making even small climbs treacherous.", + "12": "Caves and crevices blend into the mist, their openings hidden from view." + }, + "desert": { + "1": "An eerie fog blankets the sands, turning the dunes into a featureless sea.", + "2": "Cacti emerge as dark shapes in the shifting haze.", + "3": "The sun’s warmth is dulled by the dense mist, leaving the desert uncharacteristically cold.", + "4": "The fog swirls around dry rocks, creating ghostly patterns.", + "5": "Tracks in the sand vanish quickly under the rolling mist.", + "6": "Oases are hidden, their life-giving water cloaked in obscurity.", + "7": "The air is damp and heavy, unusual for the arid desert.", + "8": "Echoes of unseen footsteps haunt travelers through the mist.", + "9": "Wind whistles faintly, moving the fog across the dunes like a veil.", + "10": "Faint cries of scavengers echo through the strange, misty silence.", + "11": "The desert feels otherworldly, its vastness muted by the fog.", + "12": "Sandstorms seem to loom just beyond the fog’s edge, waiting silently." + }, + "coastal": { + "1": "Waves crash unseen as thick fog blankets the shoreline.", + "2": "Fishing boats disappear into the mist, their sails faintly visible.", + "3": "The salty air is heavy with moisture as the fog rolls inland.", + "4": "Seagulls cry out, their forms ghostly above the gray waves.", + "5": "The lighthouse beam cuts weakly through the dense haze.", + "6": "Footsteps on the rocky shore are muffled by the damp air.", + "7": "Driftwood and shells are barely visible along the foggy beach.", + "8": "The horizon vanishes as sea and sky merge into a wall of white.", + "9": "Ships’ bells clang faintly, their sources lost in the mist.", + "10": "Sea spray mingles with the fog, leaving everything damp and cold.", + "11": "The sound of waves grows louder, their approach hidden by the fog.", + "12": "Coastal cliffs loom faintly, their edges blurred by the heavy mist." + }, + "volcano": { + "1": "The volcanic slopes are hidden beneath a thick, unsettling fog.", + "2": "The ground beneath is warm, but the fog keeps the air damp and cool.", + "3": "Occasional tremors rattle unseen rocks in the dense mist.", + "4": "Steam vents hiss, their plumes blending into the unnatural fog.", + "5": "The acrid smell of sulfur permeates the heavy, misty air.", + "6": "Lava flows are hidden, their faint glow barely visible through the fog.", + "7": "The volcano feels alive, its sounds amplified by the enclosing haze.", + "8": "Travelers tread carefully as unseen fissures lie hidden beneath the mist.", + "9": "Ash mixes with the fog, creating an eerie, choking atmosphere.", + "10": "The fog clings to the jagged rocks, veiling the landscape in uncertainty.", + "11": "The rumble of magma shifts echoes faintly through the dense mist.", + "12": "Every step feels precarious as the thick fog hides volcanic dangers." + }, + "artic": { + "1": "The frozen tundra is obscured by a thick, clinging fog.", + "2": "Ice formations loom like specters in the swirling mist.", + "3": "Frost glitters faintly in the pale light breaking through the fog.", + "4": "The cold bites harder as the fog traps the frigid air.", + "5": "Distant howls of wolves echo eerily across the icy expanse.", + "6": "Snow crunches underfoot, its sound muted by the heavy mist.", + "7": "Frozen rivers are hidden, their ice treacherous beneath the fog.", + "8": "The aurora above is a faint glow, barely visible through the haze.", + "9": "The air feels heavier, carrying the scent of snow and ice.", + "10": "Icicles hang like daggers, their sharp edges blurred by the mist.", + "11": "The wind carries frost crystals, adding a shimmer to the dense fog.", + "12": "The Arctic feels endless and empty, shrouded in white silence." + }, + "cursed": { + "1": "Fog hangs unnaturally still, carrying whispers that cannot be placed.", + "2": "Shadowy figures seem to move in the mist, vanishing when approached.", + "3": "The air is thick and oppressive, heavy with a sense of dread.", + "4": "Unseen forces seem to shift the fog, guiding it ominously.", + "5": "A faint chill emanates from the mist, unnatural in its coldness.", + "6": "The fog glows faintly, casting an eerie, otherworldly light.", + "7": "Distant laughter echoes, unsettling in its disembodied nature.", + "8": "The ground beneath feels unstable, hidden by the choking haze.", + "9": "Small lights flicker in the mist, leading travelers astray.", + "10": "A feeling of being watched lingers, unseen eyes hidden by the fog.", + "11": "Voices murmur faintly, indistinct and unsettling in the heavy mist.", + "12": "The cursed fog clings to the skin, leaving a chill that won’t fade." + } + } + }, + "Freezing Fog": { + "conditions": { + "temperature": { "lte": 30 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 35 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Freezing fog settles over the fields, coating the crops in a slick, icy glaze.", + "2": "The barn and fences glisten as frost clings to every surface.", + "3": "Fog drapes over the farmstead, turning paths into icy traps.", + "4": "The animals' breath is visible as they huddle for warmth in the biting fog.", + "5": "Frozen mist swirls around the scarecrow, giving it an eerie, lifelike appearance.", + "6": "The windmill creaks in the freezing fog, its blades coated with ice.", + "7": "Icicles form on the edges of the troughs as the freezing fog intensifies.", + "8": "Every step crunches as frost coats the ground beneath the dense fog.", + "9": "The well handle is slick with frost, making it difficult to draw water.", + "10": "Hoarfrost decorates the chicken coop, the birds reluctant to venture out.", + "11": "The air feels heavy and damp, freezing on contact with skin.", + "12": "A thin layer of frost forms on the farm tools left outside." + }, + "village": { + "1": "The freezing fog cloaks the village, reducing visibility to a few feet.", + "2": "Icicles hang from eaves and lampposts, glittering faintly in the fog.", + "3": "Villagers hurry indoors, their footsteps muffled by the icy mist.", + "4": "The cobblestone streets are dangerously slick under the freezing fog.", + "5": "Windows are rimed with frost, obscuring the warm glow inside.", + "6": "The bell tower looms faintly in the freezing fog, its chimes barely audible.", + "7": "Shutters creak as villagers secure their homes against the cold, damp air.", + "8": "The fog clings to the rooftops, turning the village into a ghostly scene.", + "9": "Frost clings to the signs above shops, their names barely readable.", + "10": "Horses snort clouds of steam as they stamp their hooves on icy ground.", + "11": "The well’s bucket handle is encased in ice, making it difficult to lift.", + "12": "Carts left outside are glazed with frost, their wheels frozen in place." + }, + "city": { + "1": "The freezing fog turns the city's spires into ghostly silhouettes.", + "2": "Street vendors struggle to keep their fires lit in the damp, icy air.", + "3": "Cobblestone streets shimmer with frost, making every step treacherous.", + "4": "The freezing fog muffles the usual city bustle, leaving an eerie quiet.", + "5": "Market stalls are abandoned as merchants retreat from the biting cold.", + "6": "The city's walls are barely visible, swallowed by the thick freezing fog.", + "7": "Guards pace atop frosty ramparts, their breaths visible in the icy air.", + "8": "Chimney smoke lingers low, blending into the oppressive freezing fog.", + "9": "Icy stalactites hang from fountains, their water frozen mid-flow.", + "10": "The freezing fog turns even familiar alleys into a maze of shadows.", + "11": "Frosty patterns cover glass panes, distorting the light within.", + "12": "The bell tolls faintly, its sound muted by the dense, freezing mist." + }, + "plains": { + "1": "The freezing fog blankets the plains, turning the grass into brittle frost.", + "2": "Herds of deer move cautiously, their hooves crunching on the icy ground.", + "3": "The horizon disappears into a sea of freezing mist, leaving a sense of isolation.", + "4": "The air is so cold that frost forms instantly on exposed skin.", + "5": "Wagon tracks freeze over quickly, making navigation nearly impossible.", + "6": "The freezing fog clings to wildflowers, encasing them in delicate ice.", + "7": "Frosted bushes loom as indistinct shapes through the thick mist.", + "8": "The chill bites through even the thickest cloaks, leaving travelers shivering.", + "9": "Streams on the plains freeze over as the fog settles deeper.", + "10": "The sun is a faint glow, offering little warmth through the freezing fog.", + "11": "Animal tracks in the snow are quickly obscured by frost forming in the fog.", + "12": "The plains are eerily silent, the fog muting all distant sounds." + }, + "forest": { + "1": "The freezing fog hangs heavy among the trees, creating an otherworldly stillness.", + "2": "Frost drips from the branches, sparkling faintly in the dim light.", + "3": "Every leaf and twig is encased in ice, crackling underfoot.", + "4": "The fog swirls around tree trunks, hiding their roots in icy shadows.", + "5": "The forest floor is treacherously slick, with roots hidden beneath frost.", + "6": "Birdsong is absent as the freezing fog creates an oppressive silence.", + "7": "Animal dens are hidden, their entrances sealed by layers of frost.", + "8": "A lone wolf howls, its cry muffled by the dense, freezing mist.", + "9": "Streams freeze mid-flow, their surfaces smooth under the fog's icy grip.", + "10": "The fog gives the forest a ghostly feel, every shadow an imagined threat.", + "11": "Mossy boulders are transformed into icy sculptures by the freezing fog.", + "12": "The air is so cold it feels like needles against exposed skin." + }, + "swamp": { + "1": "Freezing fog covers the swamp, turning stagnant pools into icy patches.", + "2": "The ground is slick and treacherous, with frost forming on the marshy surface.", + "3": "Reeds are encased in ice, their tips glittering faintly in the mist.", + "4": "The swamp's usual odors are muted, replaced by the sharp scent of frost.", + "5": "Croaking frogs fall silent, their calls frozen in the icy air.", + "6": "Every step is a risk as hidden puddles freeze over beneath the fog.", + "7": "The fog clings to the mangroves, giving the swamp an eerie, lifeless feel.", + "8": "Branches creak as the freezing mist coats them in a fragile layer of ice.", + "9": "Bubbles of trapped gas freeze in the swamp's shallow pools.", + "10": "The usual buzzing insects are absent, silenced by the biting cold.", + "11": "Frost hangs in the air, shimmering faintly in the dim, foggy light.", + "12": "The swamp is deathly quiet, the freezing fog stifling all sound." + }, + "jungle": { + "1": "Freezing fog is an unnatural sight in the jungle, coating vibrant leaves in frost.", + "2": "Tropical flowers droop under the weight of ice crystals.", + "3": "The dense canopy traps the freezing fog, creating a cold, damp world below.", + "4": "Monkey calls are muted, their silhouettes faint in the swirling mist.", + "5": "The fog clings to vines, turning them into icy tendrils.", + "6": "Brightly colored birds are silent, huddled against the unexpected cold.", + "7": "The jungle floor is slick and hazardous as frost forms on the undergrowth.", + "8": "Streams freeze over, their usual bubbling flow silenced by the frost.", + "9": "The air is unnaturally cold, making every breath visible in the fog.", + "10": "The fog turns vibrant jungle colors into muted shades of gray.", + "11": "The scent of damp earth is replaced by the sharp bite of frost.", + "12": "Every leaf and branch glistens with frost, the jungle transformed into a frozen maze." + }, + "hills": { + "1": "The freezing fog swirls around the rolling hills, obscuring the path ahead.", + "2": "Grass blades glisten with frost as the fog thickens across the hillside.", + "3": "Shepherds struggle to guide their flocks through the icy mist.", + "4": "The fog settles low, turning every hollow into a trap of frost.", + "5": "Stone cairns on the hilltops are coated with a thin layer of ice.", + "6": "The air is heavy with freezing mist, biting at exposed skin.", + "7": "Chilly fog creeps over the hillocks, muting the colors of the landscape.", + "8": "Trees on the hills are spectral shapes, frosted and cloaked in fog.", + "9": "Sound carries strangely as the freezing fog muffles and distorts voices.", + "10": "Streams trickling down the hills freeze mid-flow in the icy air.", + "11": "Wagons struggle on icy trails, their wheels skidding in the mist.", + "12": "Frost forms intricate patterns on stones scattered across the hills." + }, + "mountains": { + "1": "Freezing fog engulfs the mountain pass, turning it into a perilous maze.", + "2": "Rocky crags are cloaked in frost as the fog clings to every surface.", + "3": "Icicles hang precariously from cliffs, hidden within the dense mist.", + "4": "The echo of footsteps is muffled by the thick, freezing fog.", + "5": "Frosted pine trees line the path, their branches heavy with ice.", + "6": "The mountain air is painfully cold, each breath a struggle in the fog.", + "7": "Ridges vanish into the white void, leaving travelers disoriented.", + "8": "Frozen scree crunches underfoot, every step a cautious endeavor.", + "9": "Thin layers of ice make the already treacherous slopes even more hazardous.", + "10": "Faint sunlight filters through the fog, offering little warmth.", + "11": "Caves provide the only shelter as the freezing fog deepens.", + "12": "Wind whips the fog around peaks, creating ghostly shapes in the air." + }, + "desert": { + "1": "The freezing fog turns the arid desert into a landscape of shimmering frost.", + "2": "Sand dunes glisten with frost under the pale light of the fog.", + "3": "Cacti are coated with a thin layer of ice, their needles sparkling faintly.", + "4": "The cold mist hides the horizon, making navigation nearly impossible.", + "5": "Tracks in the sand freeze over quickly, disappearing in the icy fog.", + "6": "The usually dry air feels heavy with freezing moisture.", + "7": "Desert plants droop under the weight of frost, their leaves brittle.", + "8": "The freezing fog is an eerie contrast to the desert's usual warmth.", + "9": "Shifting sands are frozen in place, the desert eerily still.", + "10": "Camels tread cautiously, their breath steaming in the icy air.", + "11": "Oases are hidden in the fog, their waters frozen over.", + "12": "The freezing fog clings to rocks and boulders, making them slick with ice." + }, + "coastal": { + "1": "The freezing fog rolls in from the sea, blanketing the coastline in icy mist.", + "2": "Waves crash silently against frost-covered rocks as the fog thickens.", + "3": "Fishing boats are barely visible, their masts ghostly outlines in the fog.", + "4": "Icicles hang from the docks, swaying gently in the freezing air.", + "5": "Seagulls circle above, their calls muffled by the dense mist.", + "6": "The salty air feels sharp and cold, biting at unprotected skin.", + "7": "Freezing fog clings to seaweed, turning the shore into a slippery hazard.", + "8": "The lighthouse beam is faint and distorted as it cuts through the icy mist.", + "9": "Cliffs are hidden beneath a veil of fog, their edges dangerously slick.", + "10": "Foam from the waves freezes on the sand, forming icy patterns.", + "11": "Shells and driftwood are coated in frost, crunching underfoot.", + "12": "The freezing fog muffles the constant roar of the ocean." + }, + "volcano": { + "1": "The freezing fog mixes with volcanic steam, creating an otherworldly chill.", + "2": "Hot vents hiss beneath the fog, their warmth unable to penetrate the icy air.", + "3": "Lava flows are obscured, their faint glow diffused by the freezing mist.", + "4": "Rocks are slick with frost, turning the treacherous terrain even deadlier.", + "5": "The contrast of freezing fog and volcanic heat creates a surreal landscape.", + "6": "Cracks in the ground emit steam that quickly freezes in the icy fog.", + "7": "Ash and frost coat the ground, a strange blend of hot and cold.", + "8": "The fog muffles the rumble of the volcano, adding to the eerie silence.", + "9": "Volcanic vents hiss and sputter, their heat unable to melt the surrounding frost.", + "10": "Frozen streams of lava glint faintly beneath the fog’s icy grip.", + "11": "The freezing fog makes the sulfurous air even harder to breathe.", + "12": "Volcanic stones crack as frost spreads across their heated surfaces." + }, + "artic": { + "1": "Freezing fog clings to the icy landscape, reducing visibility to mere feet.", + "2": "Every surface glistens with frost as the fog swirls in the cold air.", + "3": "Snowdrifts harden into icy mounds beneath the dense freezing mist.", + "4": "The sound of cracking ice echoes faintly in the still, foggy air.", + "5": "The arctic tundra is transformed into a ghostly expanse of frost and fog.", + "6": "Frozen lakes are obscured, their surfaces treacherously slick under the mist.", + "7": "Auroras above are faint and distorted, hidden behind the freezing fog.", + "8": "Glaciers loom as shadowy giants in the dense, icy mist.", + "9": "The freezing fog settles into crevices, forming frost-laden traps.", + "10": "Breath freezes instantly in the air, adding to the already oppressive cold.", + "11": "Polar bears move cautiously, their forms barely visible in the fog.", + "12": "The freezing mist turns even familiar landmarks into eerie, unrecognizable shapes." + }, + "cursed": { + "1": "The freezing fog carries whispers that seem to echo from nowhere.", + "2": "Frost forms sinister patterns on the ground, almost like runes.", + "3": "The mist feels alive, clinging to travelers and chilling them to the bone.", + "4": "Dark shapes move within the freezing fog, their forms indistinct and menacing.", + "5": "The frost burns as much as it freezes, leaving strange marks on skin.", + "6": "Lanterns flicker as the fog seems to swallow the light unnaturally.", + "7": "Chilling laughter echoes faintly through the dense, freezing mist.", + "8": "Even the ground seems cursed, with frost forming in unnatural, jagged lines.", + "9": "The fog whispers secrets no one dares to repeat, freezing blood in the veins.", + "10": "Shadows loom larger than they should, distorted by the cursed freezing fog.", + "11": "Every step feels heavier, as though the fog is dragging travelers down.", + "12": "The freezing mist leaves an unnatural cold that lingers even after it clears." + } + } + }, + "Freezing Rain": { + "conditions": { + "temperature": { "gte": 20, "lte": 37 }, + "precipitation": { "gte": 50 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Icy rain pelts the fields, leaving crops encased in glistening frost.", + "2": "The barn roof creaks under the weight of freezing rain as icicles form.", + "3": "Frozen puddles spread across the farmyard, slick and treacherous.", + "4": "Icy rain clings to fences, turning wooden posts into glimmering sculptures.", + "5": "Cattle huddle together, their fur slick with freezing rain.", + "6": "The ground turns to ice, making every step across the fields dangerous.", + "7": "Rain freezes upon hitting the soil, creating a thin, glassy crust.", + "8": "Farmhands struggle to move through the icy deluge, tools slipping in their grasp.", + "9": "Freezing rain turns the plowed soil into a hard, unyielding sheet.", + "10": "Windmill blades are frozen solid, creaking as the icy rain coats them.", + "11": "Ice sheaths the farmhouse windows, muting the light inside.", + "12": "The freezing rain lashes against the crops, freezing water droplets on leaves." + }, + "village": { + "1": "Cobblestone streets turn into slick, icy hazards under the freezing rain.", + "2": "Thatch roofs glisten as freezing rain clings to every straw.", + "3": "Villagers struggle to light fires as icy rain dampens wood and hearths.", + "4": "Icicles hang from eaves, growing longer with each passing moment of freezing rain.", + "5": "Frozen rainwater drips from the village well’s canopy, coating its handle in ice.", + "6": "Children press their noses against frost-coated windows, watching the freezing rain.", + "7": "Horses stumble on icy cobblestones as villagers lead them to shelter.", + "8": "The freezing rain coats carts and wagons, halting travel.", + "9": "The church bell rings faintly, muffled by the relentless icy downpour.", + "10": "Frozen water creates a sparkling sheen on stone walls and wooden fences.", + "11": "The village square becomes an ice rink as freezing rain pools and hardens.", + "12": "Villagers rush to protect their thatch roofs from the heavy, icy rain." + }, + "city": { + "1": "Stone buildings glisten under a relentless sheet of freezing rain.", + "2": "The freezing rain turns cobblestone streets into slick, hazardous paths.", + "3": "Merchants abandon their stalls as icy rain coats market goods.", + "4": "The city gates groan under the weight of ice as freezing rain continues.", + "5": "Freezing rain encases the city’s statues in thick, glimmering ice.", + "6": "The bell tower chimes faintly, its ropes frozen and unyielding.", + "7": "Shutters rattle as freezing rain lashes against the buildings.", + "8": "Citizens hurry inside as ice accumulates on their cloaks and boots.", + "9": "City guards struggle to keep their footing on the frozen cobbles.", + "10": "The fountain in the square becomes an icy sculpture, frozen mid-flow.", + "11": "Rooftops glisten as ice forms thick layers, threatening to collapse under the weight.", + "12": "Lanterns dim under the freezing rain, their glass encrusted with ice." + }, + "plains": { + "1": "The open plains become a slick sheet of ice as freezing rain falls steadily.", + "2": "Grass blades freeze into fragile spikes, snapping under the weight of ice.", + "3": "Animals flee the freezing rain, seeking shelter from the icy deluge.", + "4": "Patches of earth glisten like glass as freezing rain coats the landscape.", + "5": "The wind howls across the icy plains, carrying the freezing rain far and wide.", + "6": "Streams freeze mid-flow, their surfaces encased in thick, transparent ice.", + "7": "Freezing rain creates a crystal lattice on wildflowers, turning them into icy sculptures.", + "8": "Paths disappear under the freezing rain, their edges blending into the icy ground.", + "9": "Herds of deer move cautiously, their hooves slipping on the frozen plains.", + "10": "The icy rain turns puddles into glistening traps for the unwary.", + "11": "Frost clings to every blade of grass, the plains shimmering under a coat of ice.", + "12": "The freezing rain turns the horizon into a shimmering, icy mirage." + }, + "forest": { + "1": "Tree branches groan under the weight of ice as freezing rain falls relentlessly.", + "2": "The forest floor becomes a slick, icy trap under the freezing rain.", + "3": "Icicles form along tree boughs, glittering faintly in the muted light.", + "4": "The sound of cracking branches echoes through the icy forest.", + "5": "Leaves become brittle and coated with ice, falling to the frozen ground.", + "6": "Freezing rain creates a glassy shell on every twig and vine in the forest.", + "7": "The forest path is treacherous, hidden beneath a slick layer of ice.", + "8": "Animals huddle in their burrows as freezing rain pelts the forest canopy.", + "9": "The freezing rain amplifies the forest’s silence, each drop a sharp crackle.", + "10": "Moss-covered rocks are coated in frost, their surfaces dangerously slippery.", + "11": "The icy rain freezes streams, turning them into glittering ribbons of ice.", + "12": "The forest is eerily still, every sound muted by the weight of freezing rain." + }, + "swamp": { + "1": "Swamp grasses are weighed down by ice as freezing rain falls steadily.", + "2": "The muddy ground hardens under the icy rain, creating a treacherous crust.", + "3": "Freezing rain coats twisted trees, their bark slick with ice.", + "4": "Pools of water freeze over, their surfaces thin and fragile under the rain.", + "5": "The swamp becomes eerily quiet, its usual sounds muted by freezing rain.", + "6": "Ice sheaths the gnarled roots of mangroves, turning them into glistening shapes.", + "7": "The icy rain turns stagnant pools into reflective, frosted mirrors.", + "8": "Thick vines sag under the weight of ice, snapping with sharp cracks.", + "9": "Reeds and cattails shimmer with frost, their tips heavy with ice.", + "10": "The swamp’s murky waters freeze in patches, creating an uneven icy surface.", + "11": "Boggy trails become frozen ruts, dangerous to navigate in the freezing rain.", + "12": "The freezing rain turns the swamp into a hauntingly beautiful, icy expanse." + }, + "jungle": { + "1": "Freezing rain turns the dense jungle into an icy labyrinth of slick foliage.", + "2": "Leaves droop under the weight of ice, their surfaces glistening in the dim light.", + "3": "Freezing rain coats jungle vines, turning them into brittle, frozen ropes.", + "4": "Animals retreat to sheltered canopies, their calls muffled by the icy rain.", + "5": "The jungle floor becomes treacherous, its muddy trails freezing over.", + "6": "Water drips from frozen leaves, forming sharp icicles beneath the canopy.", + "7": "The icy rain transforms flowers into fragile, crystalline sculptures.", + "8": "Frozen streams crisscross the jungle, their icy surfaces gleaming faintly.", + "9": "Dense jungle vines snap under the weight of ice, their remnants littering the ground.", + "10": "The freezing rain turns the air heavy, every surface coated in frost.", + "11": "The jungle canopy sags under the weight of icy rain, light barely filtering through.", + "12": "The freezing rain creates an eerie, otherworldly silence in the jungle." + }, + "hills": { + "1": "Icy rain turns the rolling hills into a dangerous, glassy expanse.", + "2": "Rocks and boulders glisten under a slick coating of freezing rain.", + "3": "Grass blades freeze into brittle, glass-like structures, crunching underfoot.", + "4": "Shepherds struggle to guide their flocks across the frozen slopes.", + "5": "Icy rain coats shrubs and trees, their branches bending under the weight.", + "6": "Paths become treacherous as the ground turns into an uneven icy crust.", + "7": "The wind carries freezing rain, chilling the air and coating the hillsides.", + "8": "Hikers slip on the icy stones, struggling to find footing.", + "9": "Frozen rainwater collects in low spots, forming slick, reflective pools.", + "10": "The freezing rain amplifies the eerie silence of the icy hills.", + "11": "Streams running through the hills freeze partially, their surfaces glittering.", + "12": "The normally vibrant hills are muted, shrouded in ice and mist." + }, + "mountains": { + "1": "Cliffs and crags gleam under a thick coating of freezing rain.", + "2": "The mountain paths are deadly, glazed with a slick layer of ice.", + "3": "Icy rain turns waterfalls into frozen cascades, hanging eerily in place.", + "4": "Icicles form rapidly along rocky outcroppings, creating a jagged spectacle.", + "5": "Travelers huddle in caves as freezing rain lashes the mountain slopes.", + "6": "Snow-covered peaks become encased in ice, glistening in the dim light.", + "7": "Goats slip and stumble on icy ledges, seeking safer ground.", + "8": "Ropes and climbing gear freeze solid, stiff and difficult to handle.", + "9": "The freezing rain muffles sound, leaving the mountains eerily quiet.", + "10": "Treacherous ice sheets form on mountain passes, halting any travel.", + "11": "Freezing rain coats every surface, turning rocks into slippery traps.", + "12": "The wind howls, driving freezing rain sideways across the rugged terrain." + }, + "desert": { + "1": "The desert sands crust over with ice as freezing rain falls relentlessly.", + "2": "Cacti glisten, their spines encased in delicate layers of ice.", + "3": "Frozen rain pools in the crevices of rocks, creating tiny icy mirrors.", + "4": "The normally dry air is heavy with the chill of freezing rain.", + "5": "Dunes sparkle under a sheen of ice, transformed into frozen waves.", + "6": "Travelers’ boots crunch against the icy sand, each step uncertain.", + "7": "Oases freeze over, the water surfaces coated in delicate ice.", + "8": "The freezing rain chills the desert wind, cutting through cloaks and wraps.", + "9": "Nomadic tents sag under the weight of accumulating ice.", + "10": "Frozen rain coats the sparse desert plants, turning them brittle and fragile.", + "11": "The normally golden sands are muted, covered in a shimmering icy crust.", + "12": "The desert’s stillness is broken only by the sound of freezing rain." + }, + "coastal": { + "1": "Freezing rain lashes the cliffs, coating them in thick, treacherous ice.", + "2": "Waves crash against icy rocks, sending frozen spray into the air.", + "3": "Fishing boats are grounded, their decks slick with freezing rain.", + "4": "The salty air feels sharper as freezing rain falls steadily.", + "5": "Harbor ropes freeze stiff, making them impossible to handle.", + "6": "Seagulls struggle to take flight, their feathers heavy with ice.", + "7": "The freezing rain turns the docks into a hazardous, icy expanse.", + "8": "Icicles hang from the masts of ships, glinting in the pale light.", + "9": "The freezing rain creates a reflective sheen on the wet, icy sands.", + "10": "Cliffsides sparkle as freezing rain hardens over every surface.", + "11": "The ocean roars, its surface dark and cold under the freezing rain.", + "12": "The freezing rain turns the coastal village into a glittering, icy tableau." + }, + "volcano": { + "1": "Freezing rain hisses as it lands on warm volcanic rock, turning to steam.", + "2": "The blackened slopes of the volcano gleam under a layer of icy rain.", + "3": "Lava flows cool rapidly, encased in thin, glittering ice from the freezing rain.", + "4": "Steam rises in swirling plumes where freezing rain meets volcanic heat.", + "5": "Jagged rocks become treacherous as freezing rain coats them with ice.", + "6": "The freezing rain turns volcanic vents into icy geysers, spraying steam and frost.", + "7": "Ash-covered surfaces freeze over, locking in the barren landscape under ice.", + "8": "The cold rain clashes with the volcano’s warmth, creating a surreal mist.", + "9": "Travelers on the slopes slip and fall, the freezing rain making every step perilous.", + "10": "Icicles form along cooled lava tubes, hanging like jagged teeth.", + "11": "The freezing rain creates an eerie, quiet calm over the volatile terrain.", + "12": "Frost forms in patches over the blackened soil, an unnatural sight in the heat." + }, + "artic": { + "1": "The arctic tundra is transformed into a crystalline expanse by freezing rain.", + "2": "Freezing rain glazes the ice sheets, creating a dangerously slick surface.", + "3": "Frozen rain falls steadily, coating glaciers with an additional icy sheen.", + "4": "Polar bears move cautiously, their paws slipping on the frozen ground.", + "5": "The freezing rain adds a glassy layer to the arctic’s already treacherous landscape.", + "6": "Icicles grow rapidly along ice floes, glinting in the pale light.", + "7": "The wind drives freezing rain sideways, stinging exposed skin.", + "8": "Snowdrifts harden as the freezing rain creates an icy crust on their surfaces.", + "9": "The horizon glimmers under the freezing rain, as if encrusted with diamonds.", + "10": "Icebergs take on a translucent glow as freezing rain thickens their surfaces.", + "11": "The freezing rain coats seals and penguins, their fur and feathers slick with ice.", + "12": "Every surface in the arctic shimmers with ice, as the freezing rain continues unabated." + }, + "cursed": { + "1": "Freezing rain falls unnaturally heavy, coating the cursed land in thick, dark ice.", + "2": "Icy rain stings like needles, as if the storm itself were alive and vengeful.", + "3": "Freezing rain freezes in midair before shattering into sharp fragments.", + "4": "The cursed ground glows faintly as freezing rain pools into eerie patterns.", + "5": "Ghostly whispers accompany the freezing rain, as frost forms strange symbols.", + "6": "The freezing rain coats twisted trees, their branches resembling clawed hands.", + "7": "The rain freezes into jagged, black ice, radiating a sinister chill.", + "8": "Figures seem to move within the freezing rain, vanishing when approached.", + "9": "Freezing rain clings unnaturally to cloaks and armor, sapping warmth and energy.", + "10": "The cursed air thickens with icy rain, each drop carrying an ominous hum.", + "11": "Pools of frozen rain reflect warped, otherworldly images.", + "12": "The freezing rain falls in rhythmic pulses, as if echoing an ancient curse." + } + } + }, + "Heavy Rain": { + "conditions": { + "temperature": { "gte": 50, "lte": 80 }, + "precipitation": { "gte": 70 }, + "wind": { "lte": 30 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Fields flood as heavy rain turns soil into thick, clinging mud.", + "2": "The sound of rain pounding on thatched roofs fills the air.", + "3": "Livestock huddle in barns as the heavy rain soaks the pastures.", + "4": "Paths between fields become small rivers, rushing with muddy water.", + "5": "Rain barrels overflow as the downpour shows no sign of stopping.", + "6": "Farmers scramble to cover harvested crops from the relentless rain.", + "7": "Heavy rain washes away topsoil, leaving crops vulnerable.", + "8": "Rain pools in low-lying areas, creating shallow, muddy ponds.", + "9": "Tools left outside rust quickly under the continuous deluge.", + "10": "Storm drains near the barn overflow, spilling water onto the dirt paths.", + "11": "The horizon is barely visible through the sheets of rain.", + "12": "Fences sag under the weight of sodden wood, rain streaming down." + }, + "village": { + "1": "Cobblestone streets flood as rainwater pours down from overflowing gutters.", + "2": "Villagers stay indoors, peering out from rain-streaked windows.", + "3": "Heavy rain drowns the sound of chatter and foot traffic in the village square.", + "4": "Market stalls are abandoned, their wares hastily covered with tarps.", + "5": "Water drips steadily from every eave, forming puddles in the muddy paths.", + "6": "Smoke from chimneys struggles to rise in the rain-heavy air.", + "7": "Children splash in puddles despite the relentless rain, their laughter echoing faintly.", + "8": "Wooden crates and barrels float in the rising water near the docks.", + "9": "The village well overflows, rainwater mixing with the fresh supply.", + "10": "Roof repairs are put to the test as the rain seeps through weak spots.", + "11": "Lanterns flicker in the rain-dimmed daylight, their light barely piercing the gloom.", + "12": "Streams of water cut paths through the muddy lanes, flowing toward the river." + }, + "city": { + "1": "Crowded streets empty as heavy rain drenches the bustling city.", + "2": "Stone buildings glisten under the constant downpour, rain cascading from rooftops.", + "3": "Merchants hurriedly close their shops as rainwater seeps under their doors.", + "4": "Drains struggle to keep up, and water pools in the city square.", + "5": "The city guard patrols in soaked cloaks, their boots splashing in the flooded streets.", + "6": "The clattering of rain against metal and stone fills the narrow alleys.", + "7": "Torches sputter in the rain, their flames struggling against the deluge.", + "8": "Stray animals huddle under carts and awnings for shelter.", + "9": "Couriers dart between buildings, their scrolls protected by waxed cloth.", + "10": "The constant rain dampens the usual lively noise of the marketplace.", + "11": "Citizens dodge streams of water cascading from rooftops and eaves.", + "12": "Rain floods the lower quarters, forcing residents to bail out their homes." + }, + "plains": { + "1": "Endless fields of grass bow under the weight of heavy rain.", + "2": "Streams form quickly, carving temporary paths through the soaked plains.", + "3": "The sky merges with the horizon, obscured by sheets of rain.", + "4": "Herd animals cluster together, their coats dripping wet.", + "5": "Lightning briefly illuminates the rain-soaked expanse of the plains.", + "6": "Water pools in low areas, creating shallow, reflective ponds.", + "7": "Travelers struggle as the rain turns the dirt paths into slippery mud.", + "8": "The grasslands turn into a mire as rainwater saturates the soil.", + "9": "The wind drives rain sideways, stinging exposed skin and soaking cloaks.", + "10": "Shelter is scarce, and the open plains offer no respite from the storm.", + "11": "Flocks of birds scatter as the rain grows heavier, seeking refuge in the distance.", + "12": "The sound of rain on grass is a constant, muffling all other noises." + }, + "forest": { + "1": "Heavy rain drips from the canopy, soaking the forest floor below.", + "2": "Leaves glisten with water, bending under the weight of the downpour.", + "3": "Mossy ground becomes slick and treacherous as rainwater seeps in.", + "4": "The sound of rain blends with the rustle of leaves and distant thunder.", + "5": "Streams swell rapidly, their banks overflowing into the undergrowth.", + "6": "Animals retreat to their dens, leaving the forest eerily quiet.", + "7": "Rain filters through the dense canopy, forming tiny rivulets on the bark.", + "8": "Fungi and moss thrive as the forest becomes saturated with rainwater.", + "9": "Pathways become indistinguishable as rainwater washes away the tracks.", + "10": "The scent of wet earth and decaying leaves fills the air.", + "11": "Branches sag under the weight of rain, some snapping and falling.", + "12": "Raindrops collect on spiderwebs, creating delicate, sparkling patterns." + }, + "swamp": { + "1": "Heavy rain turns the swamp into a labyrinth of rising water and shifting mud.", + "2": "The air is thick with the scent of rain and stagnant water.", + "3": "Mosquitoes buzz relentlessly despite the torrential downpour.", + "4": "Reeds and grasses sway in the wind, their tips barely visible above the waterline.", + "5": "Rain ripples endlessly across the swamp’s murky pools.", + "6": "Croaking frogs and chirping insects provide an unrelenting chorus.", + "7": "Paths through the swamp disappear as water levels rise quickly.", + "8": "Rain drips from twisted trees, the sound amplified by the quiet surroundings.", + "9": "The swamp’s muddy banks collapse, spilling into the flooded water.", + "10": "Shallow pools turn into deeper, treacherous waters with each passing hour.", + "11": "Shadows of creatures moving through the water are visible beneath the rain.", + "12": "The rain churns the swamp into a chaotic, swirling mess of mud and water." + }, + "jungle": { + "1": "Rain pounds on the dense jungle canopy, a constant drumbeat overhead.", + "2": "The jungle floor floods quickly, creating hidden dangers beneath the water.", + "3": "Branches sway heavily, dripping rainwater in streams onto the foliage below.", + "4": "Bright flowers glisten under the heavy rain, their colors vivid against the green.", + "5": "The air is humid and heavy, filled with the scent of wet vegetation.", + "6": "Rivers swell dangerously, their currents pulling debris downstream.", + "7": "The chorus of animals quiets, replaced by the endless roar of rain.", + "8": "Large leaves funnel rain into rivulets, creating miniature waterfalls.", + "9": "Water collects in the jungle’s natural hollows, forming small pools.", + "10": "Footpaths are quickly swallowed by mud as rainwater pours through the undergrowth.", + "11": "Vines drip steadily with rain, their surfaces slick and shiny.", + "12": "The constant rain creates a dim, shadowy atmosphere within the jungle." + }, + "hills": { + "1": "Rain cascades down the slopes, forming muddy rivulets along the winding paths.", + "2": "The hills are shrouded in mist as rain pelts the grassy knolls.", + "3": "Shepherds rush to guide their flocks to shelter as the downpour intensifies.", + "4": "Streams swell and overflow, carving new paths through the soggy terrain.", + "5": "The air is filled with the earthy scent of rain-soaked soil and grass.", + "6": "Hillsides glisten as the rainwater streams down their surfaces.", + "7": "Muddy paths become treacherous, making travel across the hills perilous.", + "8": "Raindrops cling to the wildflowers scattered across the hills.", + "9": "Heavy rain muffles the distant calls of wildlife echoing through the hills.", + "10": "Pools of water collect in the low-lying areas between the hilltops.", + "11": "Sheep huddle together beneath trees, their wool soaked and heavy.", + "12": "The sound of the rain mixes with the occasional roll of thunder overhead." + }, + "mountains": { + "1": "Rainwater rushes down rocky slopes, forming temporary waterfalls.", + "2": "The mountain peaks are obscured by dark storm clouds, rain pounding the rocky paths.", + "3": "Travelers take refuge in caves, avoiding the relentless deluge.", + "4": "Streams and rivers swell dangerously, their currents roaring down the valleys.", + "5": "Rockslides are triggered as rain loosens the sodden earth and gravel.", + "6": "The sound of rain echoes off the cliffs, amplifying the storm's presence.", + "7": "Goats cling to steep slopes, braving the storm’s forceful winds and rain.", + "8": "Rain pools in the crevices of the jagged rocks, glinting under flashes of lightning.", + "9": "The mountain trails turn into muddy, slippery hazards.", + "10": "Moss and lichens thrive in the rain-soaked cracks and crevices.", + "11": "Clouds hang low over the mountains, blending with the torrents of rain.", + "12": "Waterfalls roar with increased intensity, fed by the unyielding rain." + }, + "desert": { + "1": "Rare rainstorms flood the desert, turning dry riverbeds into rushing torrents.", + "2": "The parched ground drinks deeply, creating pools of water in the sand.", + "3": "Cacti glisten with rain, their spines dripping with water.", + "4": "Sand dunes shift as the rain weighs down the fine grains.", + "5": "Clouds unleash a torrent, a rare and dramatic spectacle in the arid expanse.", + "6": "Rain evaporates quickly upon hitting the warm desert ground, creating a steamy haze.", + "7": "Plants and shrubs bloom rapidly, taking advantage of the fleeting moisture.", + "8": "The desert floor becomes a patchwork of puddles and rivulets.", + "9": "Nomads take cover as rain lashes their tents and caravans.", + "10": "The air smells of wet sand and blooming desert flowers.", + "11": "Flash floods carve deep gullies into the sandy terrain.", + "12": "The normally silent desert echoes with the sound of falling rain." + }, + "coastal": { + "1": "Rain lashes the shoreline, waves crashing harder against the rocks.", + "2": "Boats bob violently in the harbor as the storm surges over the coast.", + "3": "The sandy beaches are pounded by rain, leaving a glistening sheen on the surface.", + "4": "Seagulls struggle against the rain-driven wind, their cries lost in the storm.", + "5": "Coastal cliffs drip with water as streams form along their edges.", + "6": "Tidepools swell as rainwater mixes with the seawater.", + "7": "Fishing villages hunker down, their rooftops battered by relentless rain.", + "8": "The air is thick with the mingling scents of saltwater and rain-soaked earth.", + "9": "Seaweed litters the shore, washed up by the tumultuous waves and rain.", + "10": "Paths along the cliffs become slippery, treacherous with mud and water.", + "11": "The horizon vanishes in a gray curtain of rain and mist.", + "12": "Lighthouses shine dimly through the rain, their beams refracted in the storm." + }, + "volcano": { + "1": "Rain sizzles as it meets the warm volcanic rock, creating wisps of steam.", + "2": "Lava flows slow as the heavy rain cools the surface, hardening the rock.", + "3": "Mudslides form quickly on the rain-slick slopes of the volcano.", + "4": "The ground turns into a treacherous mix of mud and volcanic ash.", + "5": "Steam vents hiss louder as rainwater seeps into the heated ground.", + "6": "Rain washes away loose pumice, revealing sharp rocks beneath.", + "7": "The usually stark landscape is briefly softened by rain-soaked mosses and plants.", + "8": "The acrid smell of sulfur mingles with the freshness of falling rain.", + "9": "Pools of water form in the craters, boiling slightly in the heat.", + "10": "Clouds of steam rise wherever the rain meets the lava flows.", + "11": "Travelers on the slopes find their footing slippery and dangerous in the rain.", + "12": "Rain blurs the harsh outlines of the volcanic peak, softening its fiery presence." + }, + "artic": { + "1": "Heavy rain falls, freezing upon contact with the icy ground.", + "2": "Icebergs glisten under the relentless rain, their surfaces slick and treacherous.", + "3": "Rain turns to sleet as the temperatures hover near freezing.", + "4": "Puddles form on the ice, only to freeze into reflective sheets.", + "5": "Snowdrifts become heavy and compacted as rain soaks into the icy layers.", + "6": "The rain freezes on fur cloaks and boots, weighing them down.", + "7": "The polar landscape is veiled in a haze of rain and low-lying clouds.", + "8": "Glaciers groan under the added weight of the rain-soaked snow.", + "9": "Animals seek refuge as the rain pelts the frozen tundra.", + "10": "Icicles grow thicker as the rainwater freezes along their lengths.", + "11": "Footsteps leave icy impressions, filled quickly with water from the rain.", + "12": "The Arctic silence is broken by the constant patter of heavy rain." + }, + "cursed": { + "1": "Rain falls in oily, dark drops, leaving a slick residue on the cursed ground.", + "2": "The heavy rain carries an eerie chill, as if sapping warmth from the air.", + "3": "Mud pools bubble ominously under the relentless rain, emitting faint sulfuric odors.", + "4": "The rain falls unnaturally silent, soaking everything with an oppressive weight.", + "5": "Strange whispers seem to echo through the cursed rainstorm, carried by unseen winds.", + "6": "Puddles form with dark water that reflects nothing of the surroundings.", + "7": "Crops in cursed fields wither even as the rain drenches the soil.", + "8": "The cursed rain corrodes metal left exposed, leaving blackened streaks of rust.", + "9": "The rain seems to chill to the bone, carrying an unnatural sense of dread.", + "10": "Shadows lengthen in the cursed rain, growing darker with each passing moment.", + "11": "The heavy downpour muffles all sound, leaving an unnatural silence in its wake.", + "12": "Rainwater gathers in strange, spiraling patterns on the cursed ground." + } + } + }, + "Humid and Hot": { + "conditions": { + "temperature": { "gte": 80, "lte": 95 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 70 }, + "cloudCover": { "lte": 50 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "The air clings heavily to the skin, making work in the fields unbearable.", + "2": "Farm animals seek shade under trees, panting heavily in the oppressive heat.", + "3": "Sweat drips from brows as farmers struggle to till the sun-baked soil.", + "4": "The air feels thick and stifling, carrying the earthy scent of the fields.", + "5": "Even the crops seem to droop under the oppressive humidity and heat.", + "6": "The farmyard puddles from yesterday’s rain steam under the blazing sun.", + "7": "Children cool off by splashing in shallow creeks near the farmstead.", + "8": "Clouds loom on the horizon, but no rain falls to break the sweltering air.", + "9": "Birds are silent, hiding in the shade as the humid heat dominates the day.", + "10": "The farm well is busy as workers seek water to quench their constant thirst.", + "11": "Milk spoils quickly in the muggy weather, forcing the farmers to work fast.", + "12": "The fields buzz with insects, thriving in the heavy, humid heat." + }, + "village": { + "1": "Villagers fan themselves futilely as the stifling heat fills every corner.", + "2": "Washing hung to dry sags limply, failing to escape the thick, humid air.", + "3": "The market square is nearly empty as people seek shelter from the heat.", + "4": "Children splash in shallow fountains to escape the oppressive weather.", + "5": "Smoke from chimneys barely rises, smothered by the humid atmosphere.", + "6": "The air smells of sweat and earth as villagers move sluggishly through tasks.", + "7": "Doors and windows are left wide open, hoping for even a hint of breeze.", + "8": "The blacksmith's forge glows faintly; work slows to avoid worsening the heat.", + "9": "Cattle in the village pens moo lazily, reluctant to move in the muggy air.", + "10": "Villagers gather by the well, not to gossip but to cool off with the water.", + "11": "The baker’s hearth adds to the stifling heat, making the air nearly unbearable.", + "12": "The streets glisten as sweat-soaked villagers move from shade to shade." + }, + "city": { + "1": "The crowded streets reek of sweat and refuse, amplifying the oppressive heat.", + "2": "Hawkers wipe their brows, their cries sluggish in the sweltering markets.", + "3": "Cobblestones radiate heat, making every step in the city an ordeal.", + "4": "City folk linger near fountains and wells, hoping for relief from the humidity.", + "5": "The air feels heavy and damp, clinging to clothing and skin alike.", + "6": "Garbage heaps emit a sour stench, made worse by the humid conditions.", + "7": "Guards along the city walls rest frequently, their armor sticky with sweat.", + "8": "Sellers with ice and cool drinks find their wares vanishing quickly.", + "9": "Street performers abandon their acts, retreating to find shade and water.", + "10": "Even the stone buildings seem to sweat, dripping condensation in the heat.", + "11": "The clamor of the city is quieter, muffled by the heavy, sticky atmosphere.", + "12": "Windows are thrown open, but no breeze stirs the humid, oppressive air." + }, + "plains": { + "1": "Heatwaves shimmer on the horizon, distorting the view of the distant plains.", + "2": "The tall grasses droop under the weight of the humid air.", + "3": "Wild animals linger near water sources, reluctant to stray far in the heat.", + "4": "Every step through the plains feels labored, the air stifling and heavy.", + "5": "Thunderheads gather in the distance, promising relief that never comes.", + "6": "Even the wind seems to have abandoned the plains to the sweltering sun.", + "7": "Streams run low, their muddy banks cracked under the relentless heat.", + "8": "Insects buzz incessantly, thriving in the moist, warm air.", + "9": "The plains smell of dry earth and wildflowers struggling in the humidity.", + "10": "The distant calls of birds sound faint, muffled by the thick air.", + "11": "Shade from lone trees becomes a precious refuge for both man and beast.", + "12": "The open expanse offers no escape from the relentless, muggy heat." + }, + "forest": { + "1": "The canopy traps the humid air, turning the forest into a green sauna.", + "2": "Leaves glisten with moisture, though no rain has fallen for hours.", + "3": "The forest floor steams as the heat bakes the previous night's rainfall.", + "4": "Birdsong is subdued, the humid air stifling even the animals' spirits.", + "5": "Moss-covered trees drip with condensation, adding to the oppressive atmosphere.", + "6": "Insects swarm relentlessly, drawn by the damp, suffocating air.", + "7": "Travelers wipe sweat from their brows, struggling under the muggy canopy.", + "8": "Every breath feels thick and heavy, saturated with the forest's humid air.", + "9": "The scent of damp earth and rotting wood fills the suffocating air.", + "10": "Streams trickle sluggishly, their waters warmed by the relentless heat.", + "11": "The dense foliage seems to amplify the humid warmth, offering no relief.", + "12": "Wild animals move sluggishly, conserving energy in the oppressive conditions." + }, + "swamp": { + "1": "The swamp is a boiling cauldron of heat and humidity, buzzing with life.", + "2": "Mosquitoes swarm in droves, drawn to the thick, swampy air.", + "3": "Water in the marshes stagnates, warmed by the unrelenting heat.", + "4": "The air smells of decaying vegetation and warm, brackish water.", + "5": "Frogs croak loudly, their voices carrying over the humid stillness.", + "6": "Every step through the swamp feels like wading through warm soup.", + "7": "Even the shadows under the trees offer little respite from the heat.", + "8": "The surface of the water shimmers, reflecting the oppressive sun.", + "9": "Humidity clings to the skin, making even breathing a sticky effort.", + "10": "Reeds and grasses bow under the weight of the muggy air.", + "11": "The swamp buzzes with insects, their droning relentless in the damp heat.", + "12": "Birds skim the water, their movements slow in the oppressive humidity." + }, + "jungle": { + "1": "The jungle's dense foliage traps the humid heat, creating a stifling atmosphere.", + "2": "Sweat pours from every brow as travelers push through the dense jungle.", + "3": "The air smells of wet leaves and blooming flowers, thick and overwhelming.", + "4": "Monkeys and birds call faintly, their energy sapped by the oppressive heat.", + "5": "Rainwater from earlier pools in the jungle floor, steaming under the sun.", + "6": "Every step through the jungle is a challenge, the air heavy and moist.", + "7": "Leaves glisten with condensation, dripping steadily onto the jungle floor.", + "8": "Vines and undergrowth seem to thrive in the stifling heat, growing rapidly.", + "9": "Insects swarm in clouds, their buzzing mixing with the jungle's heat.", + "10": "Travelers find their supplies damp and sticky in the jungle's humidity.", + "11": "The jungle floor is soft and muddy, holding the heat like a steaming pot.", + "12": "Waterfalls cascade languidly, their spray offering brief, cool relief." + }, + "hills": { + "1": "The rolling hills shimmer under the relentless sun, the air heavy with moisture.", + "2": "Even the tallest grass wilts in the suffocating heat, insects buzzing incessantly.", + "3": "Sweat-soaked travelers trudge through the humid air, seeking shade in vain.", + "4": "The hills seem endless, their slopes covered in a heat-hazed blanket of humidity.", + "5": "Dew clings to blades of grass even at midday, refusing to evaporate in the sticky heat.", + "6": "The horizon wavers as heat and humidity rise together in shimmering waves.", + "7": "Streams run sluggishly through the valleys, their water warmed by the oppressive air.", + "8": "Birds flit listlessly between sparse trees, their calls muted by the stifling heat.", + "9": "The smell of warm earth and damp foliage fills the hills as the sun beats down.", + "10": "Every breeze dies in the thick air, leaving travelers gasping in the stillness.", + "11": "The soil feels damp underfoot, holding the heat of the day like a smoldering ember.", + "12": "Even the hills’ peaks offer no relief, the heat sticking to skin like a second layer." + }, + "mountains": { + "1": "The mountain air is unusually thick, the heat clinging to the rocky slopes.", + "2": "Rivulets of water drip from heated stones, pooling in warm crevices along the path.", + "3": "Travelers pause frequently, their breaths labored in the humid, heavy atmosphere.", + "4": "Moss and lichen thrive in the muggy warmth, covering the rocks in slick layers.", + "5": "The sun blazes above, its light reflected off damp stone and trickling streams.", + "6": "Even the higher altitudes feel stifling, the cool breezes swallowed by humid heat.", + "7": "Condensation drips from cliff edges, creating a constant patter in the oppressive stillness.", + "8": "Mountain springs bubble warmly, their usual chill replaced by a tepid heat.", + "9": "The air smells of damp rock and warm earth, filling every breath with heaviness.", + "10": "Birds circle lazily overhead, their energy drained by the humid, oppressive heat.", + "11": "The path is slippery with moisture, every step heavy in the humid ascent.", + "12": "Even the mountain's shadow offers no respite, the air sticky and suffocating." + }, + "desert": { + "1": "The desert heat is suffocating, the sand beneath shimmering with moisture.", + "2": "Mirages of water flicker on the horizon, fueled by the unbearable humidity.", + "3": "Every breath feels labored in the desert’s heavy, moisture-laden air.", + "4": "Cacti and succulents seem wilted, their survival tested by the unrelenting humidity.", + "5": "Sweat evaporates slowly, leaving a sticky layer on skin in the stifling desert air.", + "6": "Sand dunes glisten under the sun, their grains sticky and clinging to bare feet.", + "7": "The desert air carries no breeze, only the stillness of oppressive heat.", + "8": "Even the shadows of dunes feel warm, the ground radiating an uncomfortable heat.", + "9": "The sun feels closer than ever, its rays amplified by the dense, humid air.", + "10": "Insects buzz weakly, their sounds swallowed by the heavy desert atmosphere.", + "11": "The horizon blurs as heat and moisture ripple through the sun-scorched landscape.", + "12": "The desert feels alive with an uncomfortable warmth, oppressive and suffocating." + }, + "coastal": { + "1": "The air hangs heavy over the shoreline, salt and heat mingling in oppressive humidity.", + "2": "Waves lap sluggishly against the shore, their rhythm muffled by the thick air.", + "3": "The horizon shimmers where sea and sky meet, blurred by the humid heat.", + "4": "Seagulls call faintly, their energy sapped by the stifling, moisture-laden air.", + "5": "Fishermen work slowly, the humid air clinging to their skin and nets alike.", + "6": "The sand feels hot and sticky, dampened by the humid coastal atmosphere.", + "7": "Ships in the harbor appear distant, veiled by the shimmering, moist air.", + "8": "The water glistens, reflecting the blazing sun in a haze of sticky warmth.", + "9": "Every breeze feels warm and wet, failing to cool those near the shoreline.", + "10": "Seaweed strewn along the beach wilts under the humid, heavy air.", + "11": "The tide pools steam lightly, warmed by the relentless sun and humid air.", + "12": "The salty air is thick and oppressive, making every movement a chore." + }, + "volcano": { + "1": "The volcanic slopes radiate a stifling heat, the air thick and oppressive.", + "2": "Steam vents hiss constantly, adding moisture to the already humid atmosphere.", + "3": "The ground beneath feels warm, the volcanic heat amplified by the dense air.", + "4": "Every breath carries the sulfurous scent of the volcano, mixed with humid heat.", + "5": "The air shimmers near lava flows, thick with heat and moisture.", + "6": "Even the shadows of rocks offer no relief, the volcanic heat omnipresent.", + "7": "The jungle near the volcano thrives in the humidity, vines curling thickly.", + "8": "Lava pools glow faintly, their heat adding to the suffocating atmosphere.", + "9": "Insects buzz relentlessly, their sound adding to the oppressive heat of the volcano.", + "10": "Climbing feels exhausting, the air heavy and moist against every breath.", + "11": "Even the wind carries heat, rising from the rocky volcanic slopes.", + "12": "Rivulets of molten rock add to the heat, creating a dense, humid atmosphere." + }, + "artic": { + "1": "The air is unusually warm for the Arctic, the snow melting in humid pockets.", + "2": "Ice glistens under the unusual heat, creating a strange, sticky dampness.", + "3": "Puddles form in the snow, their water sluggishly evaporating in the muggy air.", + "4": "Even the cold feels damp as humid air envelopes the icy landscape.", + "5": "Frost clings weakly to surfaces, losing its grip under the moist heat.", + "6": "Snowdrifts settle heavily, their texture damp and sticky in the unusual warmth.", + "7": "Seals and polar bears move sluggishly, affected by the oppressive humidity.", + "8": "Icebergs glisten more than usual, their surfaces reflecting the humid sunlight.", + "9": "The Arctic feels alien as the air grows heavy with moisture and warmth.", + "10": "The usual sharpness of the Arctic air is replaced by a thick, heavy stillness.", + "11": "Meltwater streams form rapidly, trickling through the icy, humid terrain.", + "12": "Every step crunches in damp snow, unusual for the cold Arctic expanse." + }, + "cursed": { + "1": "The cursed land feels alive, the humid heat sticking unnaturally to the skin.", + "2": "Sweat drips constantly, but the oppressive air offers no relief, only dread.", + "3": "Even the shadows feel warm, the cursed atmosphere thick with unnatural heat.", + "4": "Every breath tastes faintly of sulfur, the heat sticky and unrelenting.", + "5": "The ground feels damp but hot, as if cursed by an endless, humid fire.", + "6": "Moaning winds fail to cool, carrying the oppressive heat deeper into the land.", + "7": "The cursed air feels heavy with malice, sticking to every surface like tar.", + "8": "Dark clouds loom but provide no relief, only amplifying the suffocating heat.", + "9": "The cursed soil steams faintly, an unnatural humidity rising from below.", + "10": "Insects buzz louder than they should, their wings sluggish in the cursed humidity.", + "11": "Trees seem to droop unnaturally, their leaves damp with ominous condensation.", + "12": "Every sound feels muffled by the cursed heat, as if the air itself conspires." + } + } + }, + "Icy Winds": { + "conditions": { + "temperature": { "lte": 35 }, + "precipitation": { "lte": 30 }, + "wind": { "gte": 50 }, + "humidity": { "lte": 50 }, + "cloudCover": { "lte": 60 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Icy winds whistle through the barns, rattling loose boards and freezing troughs.", + "2": "The fields are barren, frost clinging to the soil as the icy wind howls.", + "3": "Every breath fogs in the frigid air, the wind cutting through layers of clothing.", + "4": "Animals huddle together in the barn, their breath misting as icy winds seep in.", + "5": "The wind carries a biting chill, freezing water barrels and stiffening ropes.", + "6": "Snow swirls around the edges of the farmstead, driven by relentless icy gusts.", + "7": "The wind tears through fences, scattering straw and sending a chill through the land.", + "8": "Chickens cluck nervously, feathers ruffled by the bitter wind cutting through the coop.", + "9": "The sound of the wind drowns out all else, its icy grip freezing the farm.", + "10": "Icicles form on the eaves, the wind carrying sharp flakes of frost into the barn.", + "11": "Every creak of the windmill is sharper, the icy gusts driving the blades relentlessly.", + "12": "The frost-laden air burns exposed skin as the wind sweeps through the farmland." + }, + "village": { + "1": "Icy winds howl through narrow streets, chilling villagers to the bone.", + "2": "Doors and shutters slam shut as the wind carries frost through the village square.", + "3": "The chill air bites fiercely, icy gusts making every step a struggle.", + "4": "Frost creeps up the windows of cottages, driven by the relentless icy winds.", + "5": "Villagers clutch their cloaks tightly, leaning into the bitter, howling wind.", + "6": "The icy gusts rattle signboards and extinguish lanterns in the narrow streets.", + "7": "Snow whips around the village well, the icy wind howling through its stone walls.", + "8": "Every chimney smokes heavily as villagers huddle inside against the freezing gale.", + "9": "The wind carries a harsh whistle, swirling snow and frost across cobblestones.", + "10": "Laundry hung to dry freezes stiff, snapping in the sharp, icy wind.", + "11": "The village green is deserted, frost-covered benches creaking under the wind's force.", + "12": "Even the strongest fires struggle against the biting cold carried on the wind." + }, + "city": { + "1": "The icy wind snakes through alleys, chilling even the busiest market streets.", + "2": "Stone walls offer little warmth as the bitter wind howls through the city gates.", + "3": "The icy gusts sweep through plazas, extinguishing torches and scattering papers.", + "4": "Citizens rush indoors, clutching scarves and cloaks against the relentless chill.", + "5": "Snow whirls from rooftops, driven by the fierce icy winds whipping through the city.", + "6": "The wind howls around spires, rattling windows and creating eerie whistles.", + "7": "Shops close early as the icy winds make the streets too treacherous to traverse.", + "8": "Frost clings to cobblestones, the icy wind biting through even the thickest boots.", + "9": "The city gates groan under the pressure of the relentless, freezing gale.", + "10": "Warm breath fogs instantly, whipped away by the icy gusts swirling through the city.", + "11": "The chill seeps into every crevice, carried on a relentless, frost-laden wind.", + "12": "Even the busiest taverns grow quiet as the howling wind dominates the night." + }, + "plains": { + "1": "The icy wind roars across the open plains, unbroken by trees or hills.", + "2": "Frost and snow are driven in waves, the wind biting and unrelenting.", + "3": "Travelers wrap their cloaks tightly, struggling against the gale sweeping the plains.", + "4": "The grass bends under the force of the wind, frozen blades snapping underfoot.", + "5": "Each gust carries a bitter chill, scouring the plains with relentless force.", + "6": "Even the animals retreat as the icy wind cuts mercilessly through the open land.", + "7": "The wind whistles a haunting tune, unbroken by trees or shelter on the plains.", + "8": "Snow drifts pile high, carved by the sharp, freezing winds roaring through the plains.", + "9": "The horizon blurs under the force of the relentless icy gusts.", + "10": "Ice crystals form on every surface, the wind carrying frost with every gust.", + "11": "The endless plains amplify the wind's howl, a constant reminder of the bitter cold.", + "12": "Clouds of snow and ice swirl across the plains, driven by the unrelenting gale." + }, + "forest": { + "1": "Icy winds snake through the trees, rattling branches and shaking loose snow.", + "2": "The forest canopy creaks under the weight of frost, the wind howling above.", + "3": "Every step crunches as the wind drives snow into frozen drifts among the trees.", + "4": "Animals retreat to their dens as the icy wind howls through the forest.", + "5": "The wind cuts through the underbrush, carrying frost and cold into every shadow.", + "6": "The forest floor glitters with frost, stirred by the sharp, icy gusts.", + "7": "Branches snap under the relentless wind, sending showers of snow to the ground.", + "8": "The howl of the wind drowns out all other sounds, chilling the forest to its roots.", + "9": "Snow and frost cling to every tree, carried by the sharp, biting wind.", + "10": "The wind stirs the canopy, shaking loose snow in a cascade of icy shards.", + "11": "Even the thickest trees can't block the relentless chill of the wind.", + "12": "Frost covers the forest floor, every breath visible in the freezing, wind-laden air." + }, + "swamp": { + "1": "The icy wind sweeps across the swamp, freezing water and stirring reeds.", + "2": "Pools of water ice over as the wind howls, carrying frost through the swamp.", + "3": "The damp air feels doubly cold as the icy wind whips through the marsh.", + "4": "Mist rises in icy tendrils, carried by the biting wind over the swamp's surface.", + "5": "Every step cracks frozen reeds, the icy wind driving frost into the air.", + "6": "The swamp feels eerily still, broken only by the wind's relentless whistle.", + "7": "Frost creeps over moss and bark as the wind chills the swamp to its core.", + "8": "The wind stirs the stagnant water, carrying a freezing chill through the marsh.", + "9": "Even the thick mud seems frozen under the relentless force of the icy wind.", + "10": "Animals retreat into burrows, their calls silenced by the cold, howling gale.", + "11": "Every gust feels sharper as it cuts across the open pools of the swamp.", + "12": "The air smells of frozen decay, the wind chilling the swamp to the bone." + }, + "jungle": { + "1": "The icy wind clashes with the jungle's warmth, freezing dew on every leaf.", + "2": "Mists rise and swirl, the icy wind cutting through the dense jungle foliage.", + "3": "The chill air feels alien among the lush greenery, the wind howling eerily.", + "4": "Leaves rustle violently as the icy wind forces its way through the jungle.", + "5": "Streams freeze at the edges as the icy wind carries frost into the jungle.", + "6": "The humid jungle air turns to icy mist, driven by the relentless wind.", + "7": "Frost clings to vines and branches, the wind chilling the jungle unnaturally.", + "8": "Birds and insects fall silent as the icy wind sweeps through the trees.", + "9": "Every step is slick with ice and frost, the wind chilling the jungle floor.", + "10": "Even the thickest canopy can't block the relentless, frost-laden gusts.", + "11": "The wind freezes pools and streams, leaving the jungle eerily still and cold.", + "12": "Frost sparkles on jungle leaves, the icy wind carrying a cold unlike any other." + }, + "hills": { + "1": "Icy winds roar over the open hills, chilling everything in their path.", + "2": "Snow whirls across the hilltops, driven by the sharp, cutting wind.", + "3": "The barren hills amplify the howl of the icy gusts.", + "4": "Frost glitters on rocks and grass as the wind sweeps relentlessly.", + "5": "The cold air stings exposed skin, carried by the biting wind.", + "6": "Shepherds huddle against the gales, their cloaks flapping wildly.", + "7": "Frozen streams crackle under the relentless force of the icy wind.", + "8": "Even the hardiest creatures retreat as the wind scours the hills.", + "9": "Grass bends and breaks under the freezing gusts.", + "10": "The hills echo with the eerie whistle of the icy wind.", + "11": "Snow drifts form along the ridges, carved by the relentless gusts.", + "12": "The wind carries frost and chill, freezing everything in its path." + }, + "mountains": { + "1": "Icy winds howl between the peaks, carrying snow and frost.", + "2": "Every step feels heavier as the cold wind cuts through the thin air.", + "3": "Snow is whipped into blinding flurries by the relentless mountain winds.", + "4": "The jagged peaks glisten with frost under the biting wind.", + "5": "Shelter is hard to find as the wind tears through the mountain passes.", + "6": "The icy gale roars through the cliffs, shaking loose icicles and snow.", + "7": "Travelers shield their faces as the wind stings with frozen shards.", + "8": "The wind howls like a living thing, echoing through the crags.", + "9": "Snow and ice swirl endlessly, driven by the ferocious winds.", + "10": "The sharp air cuts through even the thickest furs.", + "11": "Frost forms instantly, coating everything in the wind's path.", + "12": "The mountains feel alive with the force of the relentless icy gale." + }, + "desert": { + "1": "The icy wind clashes with the desert's sands, creating a strange, freezing storm.", + "2": "Frost forms on dunes as the biting wind chills the desert night.", + "3": "The normally warm sands feel frozen underfoot, swept by the icy gusts.", + "4": "The wind howls eerily across the dunes, carrying a biting chill.", + "5": "The cold cuts deeper in the open expanse, the wind relentless and sharp.", + "6": "Sand and frost swirl together, driven by the icy desert wind.", + "7": "The air feels thin and frigid, every gust carrying a stinging chill.", + "8": "The wind creates strange patterns in the frozen sands.", + "9": "Even the toughest camels shiver as the wind bites through the desert night.", + "10": "Stars shine brightly above, their light obscured by the icy wind's swirling frost.", + "11": "The desert is eerily silent except for the whistle of the wind.", + "12": "Frost-covered cacti stand against the relentless, freezing gusts." + }, + "coastal": { + "1": "Icy winds whip across the coastline, churning the waves into frothy chaos.", + "2": "The salt spray freezes mid-air, carried by the biting coastal winds.", + "3": "Fishing boats rock violently in the icy gale, their sails stiff with frost.", + "4": "Waves crash harder against the shore, driven by the relentless wind.", + "5": "Icicles form on rocky outcroppings as the wind carries frost inland.", + "6": "Seagulls struggle against the icy gusts, their cries lost in the roar.", + "7": "The wind howls through the docks, tearing at ropes and nets.", + "8": "Shorelines sparkle with frost, the wind carrying a bitter chill.", + "9": "Frost forms on the sand, crunching underfoot as the wind lashes the coast.", + "10": "The lighthouse beacon flickers in the face of the relentless, freezing wind.", + "11": "Cold, salty air bites through clothing, carried by the relentless gusts.", + "12": "The coastal waters churn violently, foam and frost mixing under the gale." + }, + "volcano": { + "1": "The icy wind feels out of place as it clashes with the volcano's heat.", + "2": "Steam rises where frost and heat meet, the wind carrying a bitter chill.", + "3": "Ash swirls in the freezing gusts, creating an eerie, cold haze.", + "4": "The wind howls through lava tubes, its icy edge chilling the molten rock.", + "5": "Frost clings to cooled lava flows, carried by the relentless wind.", + "6": "Every gust seems to fight the volcano's warmth, creating bursts of steam.", + "7": "The ground cracks with frost under the icy wind's touch.", + "8": "Even the volcano's heat can't keep the biting chill at bay.", + "9": "Smoke and frost mix in the wind, creating a surreal, freezing fog.", + "10": "The wind roars, scattering ash and carrying an unnatural, freezing chill.", + "11": "Lava glows dimly under the frost-covered crust, the wind relentless.", + "12": "The volcano's slopes are eerily quiet, except for the howling icy wind." + }, + "artic": { + "1": "The icy wind dominates the tundra, carrying frost and freezing breath.", + "2": "Snow and ice swirl endlessly under the force of the relentless winds.", + "3": "The air feels thin and sharp, every gust biting deeper into exposed skin.", + "4": "Frost and snow cling to everything as the icy wind sweeps the landscape.", + "5": "The wind carries a deathly chill, freezing all in its path.", + "6": "Ice crystals form instantly in the freezing, wind-laden air.", + "7": "The horizon blurs under the force of the relentless, frost-laden gusts.", + "8": "The wind cuts through even the heaviest furs, leaving nothing untouched.", + "9": "Snowdrifts shift and grow as the wind carves patterns in the ice.", + "10": "The wind's howl is constant, drowning out all other sounds in the arctic wasteland.", + "11": "Glaciers creak and groan as the icy wind sweeps over them.", + "12": "Every step crunches on frost-coated ground, the wind biting at all angles." + }, + "cursed": { + "1": "The icy wind howls with an unnatural, ghostly wail.", + "2": "Frost covers cursed ground, the wind carrying whispers of despair.", + "3": "The air feels heavier with the chill, as if the wind carries a malevolent intent.", + "4": "The wind stings with icy shards, as if cutting deeper into the soul.", + "5": "Even the cursed shadows shiver under the relentless, freezing gusts.", + "6": "The icy wind carries faint, haunting cries, barely audible in the chill.", + "7": "Every breath mists in the cursed air, the wind freezing hope itself.", + "8": "The ground crackles with frost, the wind spreading an unnatural chill.", + "9": "Dark figures seem to dance in the snow, carried by the cursed wind.", + "10": "Every gust feels alive, pressing with an unseen, icy force.", + "11": "The cursed wind carries a deep, chilling dread along with its frost.", + "12": "The icy gusts whisper ancient curses, freezing both air and spirit." + } + } + }, + "Light Drizzle": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "gte": 20, "lte": 50 }, + "wind": { "lte": 60 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "gte": 20, "lte": 70 } + }, + "descriptions": { + "farm": { + "1": "A gentle drizzle dampens the soil, softening the earth.", + "2": "Light rain patters against the farmhouse roof.", + "3": "The drizzle leaves a faint sheen on the fields and crops.", + "4": "Puddles form slowly between the furrows of tilled land.", + "5": "A mild drizzle refreshes the air, cooling the hard-working farmers.", + "6": "Raindrops bead on the leaves of hardy vegetables.", + "7": "The light rain mutes the distant sounds of farm animals.", + "8": "Wet earth clings to boots as the drizzle continues.", + "9": "The soft rain smells of fertile earth and spring growth.", + "10": "A gentle rain glistens on the scarecrow in the fields.", + "11": "The drizzle dampens haystacks but doesn’t soak through.", + "12": "A faint mist rises from the warm earth after the drizzle." + }, + "village": { + "1": "Raindrops glisten on cobblestones as a light drizzle falls.", + "2": "The drizzle gently taps against thatched roofs and shutters.", + "3": "Villagers pull up hoods as they carry on their business.", + "4": "The soft rain turns the dirt streets into muddy tracks.", + "5": "Children splash in shallow puddles forming in the village square.", + "6": "A faint mist rises around the village as the drizzle continues.", + "7": "The blacksmith's forge hisses as raindrops sizzle on hot metal.", + "8": "Raindrops bead on hanging laundry, making the villagers sigh.", + "9": "The drizzle cools the air, dampening the smoke from chimneys.", + "10": "A soft rain collects on the village well’s stone lip.", + "11": "Villagers trade quiet greetings under dripping eaves.", + "12": "The drizzle paints the wooden fences and gates a darker hue." + }, + "city": { + "1": "Raindrops run down stone walls as the drizzle soaks the streets.", + "2": "Merchants cover their stalls as light rain mists the marketplace.", + "3": "The drizzle makes rooftops glisten in the dim light.", + "4": "Horses snort and shake off the rain in the bustling city square.", + "5": "The drizzle leaves faint trails on shop windows and lantern glass.", + "6": "Street performers pack up their instruments as the drizzle persists.", + "7": "Rain trickles into gutters, carrying the city's grime to the sewers.", + "8": "Guards shift uncomfortably under the light rain at their posts.", + "9": "The drizzle cools the busy streets, washing away the day's dust.", + "10": "Taverns grow busier as city folk escape the damp outside.", + "11": "A faint mist rises around the city gates as the drizzle continues.", + "12": "The drizzle softens the sound of carts rolling on cobblestones." + }, + "plains": { + "1": "A light drizzle sweeps across the open plains, soaking the grass.", + "2": "The drizzle mutes the golden hues of the plains with a faint grey.", + "3": "A cool rain moistens the tall grasses, bending them slightly.", + "4": "Herd animals stir uneasily as raindrops streak their coats.", + "5": "The drizzle is barely audible in the vast, open expanse.", + "6": "Small puddles form in hollows among the endless grasslands.", + "7": "A faint mist clings to the ground as the drizzle persists.", + "8": "The rain makes the scent of wet grass and earth more vibrant.", + "9": "The soft rain creates faint ripples in distant watering holes.", + "10": "Wind carries the drizzle far across the rolling plains.", + "11": "Rain beads on travelers’ cloaks as they march through the fields.", + "12": "The drizzle paints the horizon with a soft, muted haze." + }, + "forest": { + "1": "Raindrops filter through the canopy, creating a soft patter below.", + "2": "Leaves glisten with moisture as the drizzle continues.", + "3": "The drizzle brings the scent of wet moss and rich soil.", + "4": "Tiny streams form, trickling along tree roots and fallen logs.", + "5": "The forest grows quiet except for the gentle sound of raindrops.", + "6": "Raindrops cling to spiderwebs, turning them into jeweled nets.", + "7": "The drizzle soaks into the leaf litter, dampening the underbrush.", + "8": "Birds rustle in the trees, shaking off the accumulating rain.", + "9": "A soft mist weaves through the trees as the drizzle lingers.", + "10": "Ferns bow under the weight of raindrops collecting on their fronds.", + "11": "The rain magnifies the earthy scent of the forest floor.", + "12": "The drizzle leaves bark slick and shining in the dim light." + }, + "swamp": { + "1": "Drizzle ripples across stagnant pools, stirring the swamp's surface.", + "2": "The rain mixes with the swamp’s humidity, creating a thick, damp air.", + "3": "Raindrops fall on reeds and cattails, making them quiver slightly.", + "4": "The drizzle sends frogs hopping into the water for shelter.", + "5": "Mud grows slicker underfoot as the drizzle coats the swampy ground.", + "6": "Raindrops create faint ripples in the algae-covered water.", + "7": "The drizzle amplifies the earthy, murky scent of the swamp.", + "8": "Branches drip continuously, forming small pools below.", + "9": "The drizzle muffles the usual swamp sounds, making it eerily quiet.", + "10": "The soft rain turns the air even thicker with moisture.", + "11": "Drizzle clings to hanging moss, weighing it down further.", + "12": "A faint mist rises from the swampy waters as the drizzle lingers." + }, + "jungle": { + "1": "Raindrops scatter through the dense jungle canopy, pattering softly below.", + "2": "The drizzle clings to massive leaves, dripping steadily onto the ground.", + "3": "Bird calls echo faintly through the wet jungle as the rain falls.", + "4": "The air grows heavier and wetter with each passing moment of drizzle.", + "5": "Raindrops turn the jungle’s narrow paths into slick trails.", + "6": "Drizzle beads on vines and flowers, creating a sparkling display.", + "7": "Water pools on the jungle floor, disturbed by small creatures scurrying.", + "8": "The soft rain magnifies the jungle's vibrant, earthy smells.", + "9": "Raindrops hit the broad leaves with a steady rhythm.", + "10": "A misty haze settles around the jungle, carried by the light rain.", + "11": "The drizzle softens the roar of distant waterfalls.", + "12": "Insects hum louder in the damp air, undeterred by the drizzle." + }, + "hills": { + "1": "A gentle drizzle blankets the rolling hills, dampening the grass.", + "2": "Raindrops bead on rocks and wildflowers scattered across the slopes.", + "3": "The soft rain creates a faint shimmer over the green hills.", + "4": "The drizzle seeps into the ground, forming small rivulets.", + "5": "Shepherds pull cloaks tighter as the rain softly falls.", + "6": "A light mist rises from the valleys, blending with the drizzle.", + "7": "Raindrops cling to the leaves of small bushes dotting the hills.", + "8": "The drizzle muffles the chirping of birds sheltering under trees.", + "9": "The rain makes the steep paths slick and muddy.", + "10": "Water collects in shallow depressions on the hillsides.", + "11": "The soft rain leaves the air cool and fresh on the open hills.", + "12": "Drizzle lightly coats the hilltops, adding a subtle gleam." + }, + "mountains": { + "1": "The drizzle cascades over rocky cliffs, forming tiny streams.", + "2": "Mist and light rain mingle to obscure the distant peaks.", + "3": "The drizzle turns rugged mountain trails into slippery paths.", + "4": "Rain drips steadily off jagged ledges and boulders.", + "5": "Sheer rock faces glisten as the drizzle persists.", + "6": "The rain amplifies the cold, cutting through travelers' cloaks.", + "7": "Pine trees sway gently under the weight of the steady drizzle.", + "8": "The soft rain dulls the echoing calls of mountain birds.", + "9": "Raindrops splash into shallow pools nestled in rocky crevices.", + "10": "The drizzle enhances the mineral scent of the rugged terrain.", + "11": "A fine mist accompanies the rain, swirling around the peaks.", + "12": "The light rain veils the sharp edges of the mountain range." + }, + "desert": { + "1": "A rare drizzle dampens the arid sands, darkening the surface.", + "2": "Rain beads on the sparse desert flora, leaving a faint shine.", + "3": "The drizzle briefly cools the desert air, refreshing travelers.", + "4": "Raindrops vanish quickly into the thirsty ground.", + "5": "A soft rain creates tiny indentations on the dunes.", + "6": "The drizzle is carried far by the desert wind, scattering droplets.", + "7": "Dry desert paths grow faintly muddy under the light rain.", + "8": "The scent of wet sand fills the air as the drizzle falls.", + "9": "The rain collects in shallow crevices, forming fleeting puddles.", + "10": "Even the light drizzle seems to vanish under the desert sun.", + "11": "The faint rain gives a glimmer to distant mirages on the horizon.", + "12": "Raindrops briefly streak the barren rock formations before drying." + }, + "coastal": { + "1": "The drizzle mingles with the salty spray of crashing waves.", + "2": "Rain falls lightly over the shoreline, leaving the sand wet and dark.", + "3": "The drizzle makes fishing boats glisten in the faint light.", + "4": "Raindrops streak down the cliffs overlooking the churning sea.", + "5": "Seagulls cry out above, undeterred by the soft rain.", + "6": "The drizzle blurs the line between the misty sea and sky.", + "7": "A light rain dapples the calm surface of tidal pools.", + "8": "The scent of rain mixes with the briny air of the coast.", + "9": "Raindrops streak the windows of seaside huts and taverns.", + "10": "Drizzle softens the roar of distant waves on the rocky shore.", + "11": "The rain leaves the boardwalk slippery and shining.", + "12": "A faint mist clings to the coastal rocks as the drizzle lingers." + }, + "volcano": { + "1": "The drizzle sizzles on the warm volcanic rocks.", + "2": "Rain mingles with the sulfurous mist rising from the ground.", + "3": "Raindrops bead on obsidian shards scattered across the terrain.", + "4": "The drizzle briefly cools the heated earth before evaporating.", + "5": "Steam hisses where the rain touches the volcanic soil.", + "6": "A faint rain dampens the ash covering the rocky slopes.", + "7": "The drizzle clings to jagged lava formations, making them gleam.", + "8": "Raindrops pool in cracks and crevices carved by past eruptions.", + "9": "The light rain mingles with the acrid air of the volcanic field.", + "10": "Drizzle coats the blackened landscape with a faint sheen.", + "11": "The rain fails to quench the warm air rising from hidden vents.", + "12": "The drizzle turns the fine volcanic dust into sticky mud." + }, + "artic": { + "1": "Drizzle freezes upon contact, coating the icy terrain in a thin layer.", + "2": "The rain softens into snow as it meets the frigid air.", + "3": "A light drizzle creates a slick sheen on the frozen ground.", + "4": "Raindrops bead on the thick fur of arctic creatures.", + "5": "The drizzle freezes into delicate icicles hanging from ledges.", + "6": "A fine rain dampens the frosty tundra under a grey sky.", + "7": "Drizzle and cold winds turn travel into a treacherous affair.", + "8": "Raindrops freeze on contact, leaving frost patterns on surfaces.", + "9": "The rain enhances the stark, reflective sheen of icy expanses.", + "10": "The drizzle mixes with snowflakes, blurring the distinction between rain and snow.", + "11": "Rain lightly falls over glaciers, pooling briefly before freezing.", + "12": "The light drizzle turns the arctic air heavier and chillier." + }, + "cursed": { + "1": "The drizzle carries an unsettling chill, soaking into the bones.", + "2": "Raindrops seem to linger unnaturally long on the cursed ground.", + "3": "The soft rain leaves faint whispers in its wake.", + "4": "Drizzle falls heavily, but no puddles form on the barren soil.", + "5": "The rain dampens everything, yet leaves an odd dryness behind.", + "6": "A faint mist rises, turning the drizzle into an eerie haze.", + "7": "Raindrops feel heavier than they should, carrying a strange warmth.", + "8": "The drizzle fills the air with the faint scent of decay.", + "9": "Rain leaves streaks of dark residue on walls and stones.", + "10": "The light rain creates the illusion of movement in the shadows.", + "11": "Raindrops echo unnaturally loud, as if amplified by unseen forces.", + "12": "The drizzle soaks cloaks and spirits alike, leaving a profound unease." + } + } + }, + "Light Mist": { + "conditions": { + "temperature": { "gte": 40, "lte": 60 }, + "precipitation": { "lte": 50 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 80 }, + "cloudCover": { "gte": 60, "lte": 90 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "A light mist drapes the fields, softening the morning light.", + "2": "The mist curls around scarecrows, blurring their outlines.", + "3": "Dew glistens on crops, enhanced by the lingering mist.", + "4": "The barn looms faintly through the thin veil of mist.", + "5": "Farm animals move cautiously as the mist obscures their vision.", + "6": "The mist clings to the fenceposts, creating an ethereal glow.", + "7": "The light mist muffles the usual farmyard sounds.", + "8": "Carts leave faint tracks on the damp, misty ground.", + "9": "The mist gathers near the pond, making its surface shimmer.", + "10": "A faint breeze stirs the mist, giving it a ghostly motion.", + "11": "The fields are silent under the shroud of misty stillness.", + "12": "The mist dissipates slowly as the sun begins to rise." + }, + "village": { + "1": "The village square is quiet under a light veil of mist.", + "2": "Lanterns cast soft halos in the misty pre-dawn air.", + "3": "Cottage chimneys rise faintly through the foggy haze.", + "4": "The cobblestone paths glisten with dampness from the mist.", + "5": "Children's laughter sounds distant through the swirling mist.", + "6": "The mist clings to rooftops, blending with the early shadows.", + "7": "Villagers move like silhouettes in the mist-covered streets.", + "8": "The faint outline of the church spire pierces the mist.", + "9": "Washing lines sag under the weight of mist-soaked cloth.", + "10": "The mist mingles with the smell of baking bread in the air.", + "11": "The well in the center of the village appears ghostly in the mist.", + "12": "The light mist lifts slightly as the morning progresses." + }, + "city": { + "1": "City walls loom like shadows through the misty morning.", + "2": "The light mist dampens the bustling sounds of city life.", + "3": "Market stalls appear as dim shapes in the fog-laden square.", + "4": "The mist gathers in narrow alleyways, obscuring movement.", + "5": "Lamps flicker faintly, their light diffused by the mist.", + "6": "The clinking of hooves on cobblestones echoes softly in the mist.", + "7": "Merchants wipe damp mist from their goods and wares.", + "8": "The tall spires of the castle rise faintly above the misty streets.", + "9": "Windows glisten with a fine layer of moisture from the mist.", + "10": "The city gates are barely visible through the shifting fog.", + "11": "Guards stand watch, their forms blurred by the light mist.", + "12": "The mist lightens as the day warms, retreating into the alleys." + }, + "plains": { + "1": "A light mist rolls across the wide-open grasslands.", + "2": "Herds of deer move silently, half-hidden in the mist.", + "3": "The horizon is lost to the mist, making the plains endless.", + "4": "Grass blades shimmer with dew, coated by the mist.", + "5": "The mist swirls gently as a breeze whispers over the plains.", + "6": "Distant hills are faint silhouettes through the soft mist.", + "7": "The sound of running water is muffled by the misty stillness.", + "8": "The light mist clings to the ground, veiling low bushes and rocks.", + "9": "Shepherds whistle to unseen flocks hidden in the mist.", + "10": "The plains feel eerily quiet under the gentle blanket of mist.", + "11": "The mist glows faintly as the rising sun filters through.", + "12": "A faint path emerges as the mist begins to dissipate." + }, + "forest": { + "1": "A light mist hangs between the trees, softening their outlines.", + "2": "The forest floor glistens with moisture from the clinging mist.", + "3": "Birdsong is muffled as the mist weaves through the canopy.", + "4": "Sunbeams pierce the mist in thin, golden shafts of light.", + "5": "The mist curls around gnarled roots and fallen logs.", + "6": "Leaves drip with moisture, their colors muted by the mist.", + "7": "The forest feels hushed and still under the light mist.", + "8": "Small creatures scurry silently through the misty undergrowth.", + "9": "The mist makes the air damp and heavy with the scent of pine.", + "10": "Moss-covered rocks gleam faintly in the misty light.", + "11": "The mist thickens near the stream, making it hard to see.", + "12": "The forest emerges as the mist lifts slowly in the morning sun." + }, + "swamp": { + "1": "A light mist rises from the marsh, blending with the water's surface.", + "2": "The mist clings to reeds and cattails, creating ghostly shapes.", + "3": "The swamp is eerily quiet as the mist dampens every sound.", + "4": "Frogs croak softly, their forms hidden by the clinging mist.", + "5": "The mist rolls over stagnant pools, making the water shimmer.", + "6": "Will-o'-the-wisps flicker faintly through the thickening mist.", + "7": "The air is damp and heavy, the mist amplifying the swamp's scent.", + "8": "Crocodiles slide into the water, their movement veiled by the mist.", + "9": "The mist swirls around rotting logs and patches of moss.", + "10": "The rising sun illuminates the mist, giving the swamp an eerie glow.", + "11": "The light mist gathers in pockets, making navigation difficult.", + "12": "The swamp is a maze of shadows and mist as the day begins." + }, + "jungle": { + "1": "A light mist clings to the thick foliage of the jungle.", + "2": "Leaves glisten with droplets of mist as sunlight filters through.", + "3": "The mist muffles the calls of distant jungle birds and animals.", + "4": "Vines and branches are barely visible through the swirling mist.", + "5": "The jungle floor is damp and slick under the light mist.", + "6": "The mist intensifies the earthy scent of the dense jungle.", + "7": "Sunlight struggles to pierce the thick canopy and mist below.", + "8": "The mist gathers near a river, creating an otherworldly scene.", + "9": "The jungle is alive with muted sounds, softened by the mist.", + "10": "The mist shifts with the breeze, revealing and hiding paths.", + "11": "Lianas dangle like ghostly shapes in the swirling mist.", + "12": "The jungle awakens slowly as the mist begins to burn off." + }, + "hills": { + "1": "A light mist rolls over the hills, softening their rugged contours.", + "2": "The mist gathers in the hollows, making the slopes look ethereal.", + "3": "Sheep graze silently, their forms barely visible through the mist.", + "4": "The hills feel quiet and still under the veil of mist.", + "5": "The mist clings to wildflowers, enhancing their dew-covered beauty.", + "6": "Paths disappear into the mist, making navigation uncertain.", + "7": "The air is cool and damp, with mist curling around rocky outcrops.", + "8": "The sun struggles to break through the mist draping the hills.", + "9": "The mist lends a dreamlike quality to the rolling landscape.", + "10": "The sound of a distant stream is muffled by the light mist.", + "11": "Hawks soar above, their calls softened by the misty air.", + "12": "The mist slowly dissipates as the morning warms the hills." + }, + "mountains": { + "1": "A thin mist cloaks the peaks, lending the mountains a mysterious air.", + "2": "The mist swirls around rocky crags, revealing glimpses of the heights.", + "3": "Mountain goats appear as ghostly shapes through the mist.", + "4": "The light mist clings to the sheer cliffs, shimmering in the sun.", + "5": "The paths wind steeply through the mist, their edges hard to discern.", + "6": "The mist deepens in the valleys, hiding the trails below.", + "7": "The chill of the mountain air is enhanced by the clinging mist.", + "8": "The peaks rise above the mist like islands in a clouded sea.", + "9": "The mist moves with the wind, shifting like a living thing.", + "10": "Eagles glide through the mist, their cries echoing faintly.", + "11": "The light mist turns the jagged peaks into shadowy silhouettes.", + "12": "The mist clears slowly, revealing the majesty of the mountain range." + }, + "desert": { + "1": "A rare light mist blankets the dunes, softening the harsh landscape.", + "2": "The mist clings to sparse vegetation, leaving dewdrops on the leaves.", + "3": "The rolling sands are obscured by the faint, swirling mist.", + "4": "The desert feels unusually cool and quiet under the mist's embrace.", + "5": "Distant dunes appear like mirages through the misty air.", + "6": "Tracks in the sand are quickly blurred by the shifting mist.", + "7": "The mist settles in low areas, giving the desert an eerie stillness.", + "8": "The rising sun struggles to pierce the mist covering the sands.", + "9": "The air feels damp and heavy, a rare sensation in the arid desert.", + "10": "The mist hides the desert's harsh edges, making it seem almost gentle.", + "11": "Faint shadows of dunes ripple through the misty horizon.", + "12": "The mist evaporates quickly as the desert heat intensifies." + }, + "coastal": { + "1": "A light mist drifts in from the sea, dampening the salty air.", + "2": "The coastline is shrouded in mist, with waves crashing faintly.", + "3": "Ships at anchor are barely visible through the shifting mist.", + "4": "The mist clings to the rocks, making them slick and glistening.", + "5": "Sea birds cry faintly, their forms obscured by the mist.", + "6": "The lighthouse casts a dim glow, its beam lost in the mist.", + "7": "Fishermen's boats emerge as faint shadows in the misty harbor.", + "8": "The mist mingles with the scent of salt and seaweed along the shore.", + "9": "The surf is muted, its roar softened by the misty air.", + "10": "The horizon is lost as the sea and mist blend seamlessly.", + "11": "Footprints on the beach fade quickly under the creeping mist.", + "12": "The mist begins to lift, revealing the sparkling waves beyond." + }, + "volcano": { + "1": "A light mist mixes with the steam rising from fissures in the rock.", + "2": "The mist obscures the jagged volcanic slopes, softening their harshness.", + "3": "The air feels heavy and warm as the mist gathers around the crater.", + "4": "Lava flows faintly glow through the thin veil of mist.", + "5": "The mist clings to blackened rocks, giving them a spectral quality.", + "6": "The volcanic peak looms through the mist like a dark sentinel.", + "7": "The sound of bubbling magma is muted by the misty air.", + "8": "Sulfuric scents mingle with the cool dampness of the mist.", + "9": "The mist swirls as it encounters the heat rising from the volcano.", + "10": "The landscape looks otherworldly under the light shroud of mist.", + "11": "The mist creates a shifting haze, hiding cracks and crevices in the terrain.", + "12": "The mist begins to clear, revealing the rugged volcanic landscape." + }, + "artic": { + "1": "A light mist clings to the icy expanse, diffusing the sunlight.", + "2": "Snow-covered peaks fade into the misty horizon, blurring their edges.", + "3": "The frozen ground sparkles with frost under the thin veil of mist.", + "4": "Ice floes drift silently, their shapes softened by the mist.", + "5": "The air is frigid and damp as the mist gathers around frozen lakes.", + "6": "The mist obscures distant glaciers, making them seem like ghosts.", + "7": "Polar bears move silently, their forms almost hidden in the mist.", + "8": "The mist intensifies the silence, creating an eerie stillness.", + "9": "Snowfall mingles with the mist, creating a surreal atmosphere.", + "10": "The mist swirls faintly, caught in the biting Arctic wind.", + "11": "Icicles gleam faintly through the shifting misty light.", + "12": "The mist begins to lift, revealing the vast, icy wilderness." + }, + "cursed": { + "1": "A light mist swirls unnaturally, carrying faint whispers on the wind.", + "2": "The cursed land is veiled in mist, hiding shadowy figures in the distance.", + "3": "The mist feels cold and heavy, clinging to the ground ominously.", + "4": "Faint shapes move in the mist, vanishing when approached.", + "5": "The mist carries an acrid scent, stinging the eyes and throat.", + "6": "Ruins emerge faintly through the mist, their details blurred and twisted.", + "7": "The light mist turns crimson as the sun sets, casting an eerie glow.", + "8": "Strange, disjointed echoes seem to bounce off the misty air.", + "9": "The mist pools unnaturally in hollows, defying the landscape's shape.", + "10": "The air feels alive, the mist shifting as if with a will of its own.", + "11": "Faint cries or whispers seem to come from within the mist.", + "12": "The mist dissipates slightly, revealing the cursed terrain's desolation." + } + } + }, + "Light Rain": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "gte": 30, "lte": 50 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "Gentle rain falls on the fields, softening the soil.", + "2": "The light rain brings a fresh scent to the farmstead.", + "3": "Rain droplets glisten on the leaves of crops and trees.", + "4": "Farm animals shake off the drizzle as they graze.", + "5": "The rain is steady but light, soaking into the tilled earth.", + "6": "Puddles form in the dirt paths around the barn.", + "7": "The light patter of rain on the roof provides a soothing rhythm.", + "8": "Raindrops tap against wooden fences and barrels.", + "9": "The rain turns the dust on the road into a soft mud.", + "10": "A faint mist rises from the fields as rain cools the warm earth.", + "11": "The sky remains grey, with a light rain falling throughout the day.", + "12": "The soft drizzle creates a serene atmosphere over the farm." + }, + "village": { + "1": "Light rain drips from thatched roofs onto cobblestone streets.", + "2": "Villagers bustle under hoods and cloaks to avoid the drizzle.", + "3": "The rain adds a sheen to the wooden walls of houses.", + "4": "Children splash in puddles forming in the village square.", + "5": "The light rain fills the air with the scent of wet earth and wood.", + "6": "Smoke from chimneys mingles with the cool, damp air.", + "7": "The cobblestones are slick underfoot from the steady rain.", + "8": "Rain droplets cling to hanging laundry, making it sag slightly.", + "9": "The sound of rain mingles with the chatter of villagers in the market.", + "10": "Shutters are drawn as villagers seek shelter from the drizzle.", + "11": "Light rain turns the village paths into muddy trails.", + "12": "The village well overflows slightly, rainwater pooling around its base." + }, + "city": { + "1": "Rain softly patters against stone walls and tiled rooftops.", + "2": "The cobbled streets shine under the drizzle, reflecting torchlight.", + "3": "Merchants hastily cover their stalls with tarps to shield wares.", + "4": "Horses’ hooves splash through shallow puddles in the city square.", + "5": "The rain dampens the usual din of city life, muting conversations.", + "6": "Guardsmen huddle under eaves, trying to stay dry.", + "7": "The air smells fresh and clean as rain washes away the city's dust.", + "8": "Rain drips from the spires of the cathedral into the gutters below.", + "9": "Light rain runs in rivulets down the city’s sloped streets.", + "10": "The marketplace grows quieter as the drizzle persists.", + "11": "Lanterns flicker in the rain, their glow distorted by droplets.", + "12": "The soft rain fills the canals, creating ripples in the stagnant water." + }, + "plains": { + "1": "Rain falls lightly over the open plains, creating a gentle rhythm.", + "2": "The tall grass bends under the weight of tiny raindrops.", + "3": "Patches of wildflowers glisten as rainwater gathers on their petals.", + "4": "A faint mist rises where rain meets the warm earth.", + "5": "The plains stretch endlessly under a grey sky, veiled in rain.", + "6": "The soft rain darkens the soil, nourishing the wild grasses.", + "7": "A light breeze carries the scent of rain across the plains.", + "8": "The rain creates a hushed atmosphere over the vast, open land.", + "9": "Rain puddles form in hoofprints left by passing herds.", + "10": "Birds fluff their feathers as they perch on rain-soaked shrubs.", + "11": "The rain falls steadily, turning dirt tracks into slippery paths.", + "12": "The drizzle makes the plains glisten, stretching as far as the eye can see." + }, + "forest": { + "1": "The forest hums with life as rain taps against the canopy.", + "2": "Raindrops fall through the leaves, creating soft music in the undergrowth.", + "3": "The scent of wet pine and earth fills the forest air.", + "4": "Moss-covered logs and rocks glisten under the gentle rain.", + "5": "The forest floor grows damp as rain seeps through the trees.", + "6": "Small streams form along trails, carrying leaves and twigs downhill.", + "7": "Raindrops hang like jewels on spiderwebs spun between branches.", + "8": "The light rain creates a soothing patter, masking distant sounds.", + "9": "Ferns and flowers thrive, their colors vibrant in the drizzle.", + "10": "The rain’s cool touch enhances the forest’s tranquil atmosphere.", + "11": "Birds flit between branches, shaking off rainwater as they land.", + "12": "The rain gathers in pools around tree roots, nourishing the ancient giants." + }, + "swamp": { + "1": "Rain ripples across the stagnant waters of the swamp.", + "2": "The drizzle turns the swamp into a symphony of plopping droplets.", + "3": "Mist rises as rain cools the humid, murky air.", + "4": "Rain clings to drooping moss, glinting like small crystals.", + "5": "The swamp's muddy paths become even slicker under the light rain.", + "6": "Raindrops fall into hidden pools, creating small, bubbling ripples.", + "7": "The rain mixes with the swamp’s earthy scent, heavy and rich.", + "8": "Croaking frogs seem louder in the hush of the persistent drizzle.", + "9": "Branches above sag under the weight of rain-soaked moss.", + "10": "The light rain shrouds the swamp in a muffled stillness.", + "11": "The surface of the water mirrors the grey, rain-filled sky.", + "12": "Rain slides down the bark of twisted swamp trees, dripping into dark pools." + }, + "jungle": { + "1": "The jungle thrives under the light rain, every leaf glistening.", + "2": "Rain filters through the dense canopy, creating a soft patter below.", + "3": "The air grows humid and fresh as the rain nourishes the jungle.", + "4": "Droplets cling to vines, making them shimmer in the dim light.", + "5": "Rain gathers in broad leaves, spilling off in steady drips.", + "6": "The jungle floor grows damp, and earthy smells intensify.", + "7": "Rain softens the calls of birds and monkeys echoing through the jungle.", + "8": "Streams swell as the drizzle adds to the jungle’s network of water.", + "9": "The rain cools the humid air, creating a pleasant respite.", + "10": "The light rain highlights the vibrant greens of the dense foliage.", + "11": "Butterflies retreat under broad leaves, sheltering from the drizzle.", + "12": "The jungle feels alive, every surface glistening with rainwater." + }, + "hills": { + "1": "Rain softly rolls down the grassy slopes, forming small streams.", + "2": "The hills shimmer as light rain dampens the rolling landscape.", + "3": "Shepherds guide their flocks under the light drizzle.", + "4": "The rain brings out the scent of wildflowers scattered across the hills.", + "5": "Puddles form in the ruts of dirt paths winding through the hills.", + "6": "The light rain streaks the hillsides, darkening the green grass.", + "7": "Droplets cling to the wild shrubs, sparkling in the muted light.", + "8": "The gentle rain soaks into the earth, nourishing the highland soil.", + "9": "Soft rain creates a tranquil ambiance over the rolling terrain.", + "10": "The drizzle fades into mist along the crest of the hills.", + "11": "Light rain dances on the rocky outcrops scattered across the hills.", + "12": "Rain trickles down into narrow gullies, carving tiny paths into the slopes." + }, + "mountains": { + "1": "Rain falls lightly, running in rivulets down the jagged peaks.", + "2": "The misty rain veils the mountains, shrouding them in mystery.", + "3": "Rocky trails become slick under the persistent drizzle.", + "4": "The rain amplifies the echo of distant thunder through the valleys.", + "5": "Waterfalls swell as rain seeps down the mountain faces.", + "6": "The rain settles on the sparse vegetation, glinting in the weak light.", + "7": "Clouds hang low over the peaks, blending into the light rain.", + "8": "Hikers and climbers hunker under rocky overhangs to escape the drizzle.", + "9": "Streams snake through the valleys, their flow fed by the soft rain.", + "10": "The rain cools the rugged terrain, refreshing the mountain air.", + "11": "Raindrops glisten on the icy patches clinging to the higher peaks.", + "12": "The rain whispers against the stone, adding to the eerie stillness." + }, + "desert": { + "1": "Light rain speckles the sand, darkening patches across the dunes.", + "2": "The rare drizzle forms tiny pools in cracks of parched earth.", + "3": "Desert plants greedily absorb the fleeting moisture from the rain.", + "4": "The air is cool and damp as the rain falls gently over the arid land.", + "5": "Dust and rain mix, creating a faint, earthy scent.", + "6": "Rain traces delicate paths down the faces of wind-sculpted dunes.", + "7": "Small streams form briefly before disappearing into the thirsty ground.", + "8": "The rain brings out vibrant colors in the otherwise muted desert.", + "9": "Drizzle dances across the sparse vegetation dotting the desert expanse.", + "10": "The light rain is a rare and welcome reprieve from the usual dryness.", + "11": "Raindrops fall onto hot sands, sizzling and vanishing instantly.", + "12": "The desert shimmers as raindrops reflect faint sunlight through the clouds." + }, + "coastal": { + "1": "Gentle rain merges with sea spray along the rocky shore.", + "2": "The sound of waves blends with the soft patter of rain.", + "3": "Boats bob in the harbor as rain dappled the water's surface.", + "4": "Rain runs in rivulets down the cliffs, joining the crashing surf below.", + "5": "The drizzle freshens the salty air, creating a cool breeze.", + "6": "Rain streaks the windows of seaside cottages overlooking the bay.", + "7": "The light rain forms ripples in the tide pools dotting the shore.", + "8": "Fishermen pull their nets in, hurrying to shelter from the drizzle.", + "9": "The rain mingles with the tide, masking the horizon in a grey haze.", + "10": "The rain creates a gentle rhythm against the hulls of docked ships.", + "11": "Seabirds cry as they circle above, their wings damp from the rain.", + "12": "Rain softens the sandy beaches, leaving scattered puddles near the dunes." + }, + "volcano": { + "1": "Light rain hisses as it meets the warm rocks of the volcanic slopes.", + "2": "Steam rises where rain falls on patches of cooling lava.", + "3": "The rain turns ash and dust into sticky, muddy rivulets.", + "4": "Drizzle dampens the blackened ground, creating a fleeting freshness.", + "5": "Rain clings to jagged rocks, pooling in small, volcanic crevices.", + "6": "The faint patter of rain contrasts with the distant rumble of the volcano.", + "7": "Rain falls on the sparse vegetation struggling to survive the harsh terrain.", + "8": "The drizzle softens the air, mingling with the sulfuric scent of the volcano.", + "9": "Small streams of rainwater wind down the cracked, uneven terrain.", + "10": "The rain cools the fiery landscape, leaving streaks of wet ash in its wake.", + "11": "Rainwater gathers in steaming puddles, evaporating almost as quickly as it forms.", + "12": "The drizzle casts a thin veil over the rugged volcanic slopes." + }, + "artic": { + "1": "Rain falls lightly, freezing as it lands on the icy surface.", + "2": "The drizzle coats the snow with a thin, glassy layer of ice.", + "3": "Gentle rain creates a misty haze over the frozen tundra.", + "4": "Rain mixes with the ice, creating treacherous footing on the icy ground.", + "5": "The rain sparkles like diamonds as it falls under the pale light.", + "6": "Drizzle dampens the snow, darkening its pristine whiteness.", + "7": "Rain freezes on contact with the frosted trees and shrubs.", + "8": "Light rain merges with the biting wind, chilling to the bone.", + "9": "The drizzle leaves delicate icicles hanging from every surface.", + "10": "Rain and sleet combine, coating the arctic terrain in an icy sheen.", + "11": "The light rain softens the snow, making the tundra glisten.", + "12": "Raindrops freeze mid-fall, forming delicate patterns on the ice." + }, + "cursed": { + "1": "Rain falls unnaturally cold, as if leeching warmth from the air.", + "2": "The drizzle seems to whisper as it strikes the cursed ground.", + "3": "Raindrops leave faint, dark stains on the earth that refuse to fade.", + "4": "The rain chills more than it should, carrying an unnatural weight.", + "5": "Drizzle clings to surfaces, creating a faintly luminous sheen.", + "6": "The rain carries a metallic scent, unnatural and unsettling.", + "7": "The drizzle seems to avoid the cursed structures, falling only on the ground.", + "8": "Rain falls heavily on the cursed earth, forming pools that ripple oddly.", + "9": "The rain feels heavier, each drop landing with an ominous thud.", + "10": "Rain mingles with the shadows, darkening the air around the cursed land.", + "11": "The drizzle carries an eerie echo, as if mocking the living.", + "12": "Raindrops seem to freeze midair, dissolving into mist before touching the ground." + } + } + }, + "Light Snowfall": { + "conditions": { + "temperature": { "gte": 20, "lte": 35 }, + "precipitation": { "gte": 30, "lte": 50 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "Light snow dusts the fields, creating a thin, frosty blanket.", + "2": "The barn roof glistens under a delicate layer of freshly fallen snow.", + "3": "Snowflakes drift lazily, settling on the fences and haystacks.", + "4": "Chickens wander cautiously as snow collects lightly in the yard.", + "5": "Snow traces the plow lines in the frozen fields.", + "6": "Farmhands bundle up against the gentle flurry coating their tools.", + "7": "The windmill creaks as snowflakes land softly on its blades.", + "8": "Snow clings to the bare branches of orchard trees.", + "9": "Light snow gathers in the troughs and corners of the barnyard.", + "10": "Hoofprints in the snow lead to the stable, dusted with a fine layer.", + "11": "The fields shimmer with frost as snow continues to fall gently.", + "12": "Soft flakes collect in wagon ruts, blending with frozen earth." + }, + "village": { + "1": "Snowflakes swirl between cottages, landing softly on thatched roofs.", + "2": "Villagers hurry along snow-dusted cobblestone streets.", + "3": "A light flurry settles on carts and barrels in the square.", + "4": "Snow gathers around the well, dusting its stone rim.", + "5": "Lanterns glow warmly as snow drifts gently through the air.", + "6": "Children laugh as they catch snowflakes on their tongues.", + "7": "The village is quiet, blanketed by a soft, white dusting.", + "8": "Snow traces the tops of wooden fences and cottage windowsills.", + "9": "Smoke rises from chimneys as flakes melt on the rooftops.", + "10": "The flurry sparkles in the lamplight, creating an enchanting scene.", + "11": "Snow settles on the edges of market stalls, forming soft mounds.", + "12": "The village bell tower stands dusted in white, snowflakes clinging to its surface." + }, + "city": { + "1": "Snow drifts lazily over the bustling market square.", + "2": "Stone walls glisten as a light snowfall coats the city streets.", + "3": "Carts leave tracks in the snow-covered cobblestones.", + "4": "Snowflakes settle on the statues adorning the city gates.", + "5": "Guards stamp their feet to shake off the accumulating snow.", + "6": "The castle spires glisten under a thin layer of freshly fallen snow.", + "7": "Snow muffles the usual clatter of hooves on stone roads.", + "8": "The city's bridges are dusted with snow, their arches framed in white.", + "9": "Merchants brush snow off their wares as flakes continue to fall.", + "10": "Snow traces the intricate carvings of stone buildings.", + "11": "The light flurry blankets rooftops, softening the city's silhouette.", + "12": "A thin veil of snow clings to the city walls, sparkling faintly." + }, + "plains": { + "1": "Snow drifts across the open plains, settling in soft waves.", + "2": "The grass disappears under a gentle blanket of white.", + "3": "Light snowflakes swirl in the wind, creating fleeting patterns.", + "4": "Small animal tracks dot the snowy expanse.", + "5": "Snow gathers in shallow depressions, forming patches of white.", + "6": "The endless plains are hushed under the soft snowfall.", + "7": "Snow clings to the few scattered bushes dotting the landscape.", + "8": "The plains shimmer under the pale light, dusted with frost.", + "9": "Snow catches on the tall grass, bending it under the weight.", + "10": "Gentle flakes settle into the furrows of old wagon trails.", + "11": "A thin white sheet stretches across the horizon, unbroken and serene.", + "12": "The snowfall softens the contours of the plains, creating a dreamlike view." + }, + "forest": { + "1": "Snowflakes drift lazily, catching on the branches of tall trees.", + "2": "The forest floor is dappled with white, snow gathering in patches.", + "3": "Snow clings to the evergreens, creating a picturesque scene.", + "4": "The air is hushed as snow settles softly on fallen logs and rocks.", + "5": "Snow collects in the hollow of tree stumps, forming tiny mounds.", + "6": "Light snow traces the patterns of moss on tree trunks.", + "7": "The forest path becomes a white ribbon winding through the trees.", + "8": "Snow muffles the crunch of footsteps along forest trails.", + "9": "Tiny snowdrifts gather at the base of trees, nestled among roots.", + "10": "Snowflakes swirl in the gentle breeze, glittering in the filtered light.", + "11": "The canopy above is frosted, creating a lattice of snow and branches.", + "12": "The forest glows faintly as snow blankets the undergrowth." + }, + "swamp": { + "1": "Snow settles on the swamp's twisted trees, a rare and eerie sight.", + "2": "The snowflakes vanish as they touch the dark, still water.", + "3": "Snow clings to the moss hanging from gnarled branches.", + "4": "Thin ice forms on the surface of shallow pools under the snowfall.", + "5": "Snowflakes dust the swamp grasses, glistening in the dim light.", + "6": "Light snow creates a stark contrast against the murky waters.", + "7": "Snow collects on the raised roots of ancient swamp trees.", + "8": "The swamp is quiet, muffled by the falling snow.", + "9": "Frost forms on reeds as snow gently falls around them.", + "10": "The snow lingers on the boardwalks, creating slippery paths.", + "11": "Snow drifts lazily, forming thin sheets on dry patches of the swamp.", + "12": "The snowfall adds a strange beauty to the usually forbidding swamp." + }, + "jungle": { + "1": "Snowflakes fall sparsely, melting as they touch the dense jungle foliage.", + "2": "The snow collects on the jungle canopy, a rare and fleeting sight.", + "3": "Snow gathers on the massive leaves, dripping into puddles below.", + "4": "The jungle's vibrant greens are muted under a thin layer of snow.", + "5": "Snowflakes cling briefly to vines before melting in the humid air.", + "6": "The snowfall creates steam as it meets the warm jungle ground.", + "7": "Light snow catches on the bark of towering jungle trees.", + "8": "The jungle is eerily quiet, muffled by the drifting snow.", + "9": "Snow clings to the hanging moss, creating a strange white veil.", + "10": "The jungle paths are dotted with white patches of fallen snow.", + "11": "Snowflakes swirl in the air, creating a surreal winter jungle scene.", + "12": "The jungle's dense underbrush catches and holds the falling snow." + }, + "hills": { + "1": "Snowflakes gently coat the rolling hills, creating a delicate frost.", + "2": "The hills shimmer faintly under a light dusting of snow.", + "3": "Snow gathers along the tops of grassy mounds and rocks.", + "4": "Soft snowflakes fall, settling into the crevices of the hillsides.", + "5": "The breeze carries snow across the open slopes, scattering flakes.", + "6": "Patches of white speckle the hills, clinging to the sparse vegetation.", + "7": "The hilltops are capped with a thin layer of snow, glistening faintly.", + "8": "Snow trails along narrow paths, outlining the curves of the terrain.", + "9": "Gentle flurries sweep over the hills, settling in shallow hollows.", + "10": "The snowfall highlights the rugged contours of the hilly landscape.", + "11": "Snowflakes catch on low bushes, creating a frosted appearance.", + "12": "The hills are quiet and serene, blanketed by a light, powdery snow." + }, + "mountains": { + "1": "Snow dusts the rugged peaks, creating a stark white contrast.", + "2": "The rocky slopes are lightly powdered with fresh snow.", + "3": "Snow gathers in the crevices and ledges of the mountainside.", + "4": "The peaks glisten under a thin layer of snow, catching the light.", + "5": "Snowflakes drift gently, clinging to jagged outcroppings.", + "6": "Snow trails down the steep paths, outlining the winding trails.", + "7": "The mountain air is crisp as snow settles quietly on the terrain.", + "8": "Snow collects around the base of rocky spires and crags.", + "9": "The slopes are dappled with white as the snowfall continues.", + "10": "Snow dusts the boulders scattered across the mountainside.", + "11": "The rugged mountain terrain is softened by a light layer of snow.", + "12": "Snow settles on the cliffs, creating delicate patterns on the rock face." + }, + "desert": { + "1": "Snowflakes fall lightly, melting as they touch the warm sand.", + "2": "Sparse snow patches form on the dunes, a rare and surreal sight.", + "3": "Snow drifts faintly, leaving streaks of white across the golden desert.", + "4": "The desert's rolling dunes are dotted with patches of soft snow.", + "5": "Snowflakes cling to cacti, creating an unusual frosty outline.", + "6": "The desert winds scatter snow across the barren, sandy plains.", + "7": "Snow gathers in the crevices of rocky outcroppings, a fleeting phenomenon.", + "8": "The desert landscape glimmers faintly under the light snowfall.", + "9": "Snowflakes dance in the air before disappearing into the sand.", + "10": "The desert's sparse vegetation is dusted with delicate snow.", + "11": "Thin trails of snow weave along the ridges of the dunes.", + "12": "The contrast between snow and sand creates a surreal, otherworldly scene." + }, + "coastal": { + "1": "Snowflakes swirl above the waves, melting as they touch the water.", + "2": "The shoreline is dusted with snow, blending with the white foam of the waves.", + "3": "Snow gathers lightly on the rocky coast, creating a frosted edge.", + "4": "The docks glisten with a thin layer of snow, slippery underfoot.", + "5": "Snowflakes settle on the fishing boats moored along the harbor.", + "6": "The coastal cliffs are lightly powdered with snow, standing stark against the sea.", + "7": "Snow falls softly, coating the sand and pebbles of the beach.", + "8": "Sea spray mingles with the falling snow, creating a misty, frosty haze.", + "9": "Snow clings to the ropes and nets of the docked ships.", + "10": "The coastal air is crisp as snow settles on the shoreline vegetation.", + "11": "Snow gathers in the crevices of the rocky tide pools.", + "12": "The snowfall lends an ethereal beauty to the bustling harbor." + }, + "volcano": { + "1": "Snowflakes fall over the blackened rocks, melting quickly near the heat vents.", + "2": "Snow gathers in sheltered crevices, contrasting sharply with the dark terrain.", + "3": "The volcano’s slopes are speckled with snow, creating a striking contrast.", + "4": "Steam rises as snow melts upon contact with warm volcanic stones.", + "5": "The crater rim is lightly dusted with snow, a rare and surreal sight.", + "6": "Snowflakes swirl above the smoldering landscape, melting in midair near fissures.", + "7": "Snow clings to the jagged volcanic rocks, creating a fleeting frost.", + "8": "The volcano’s rugged surface is softened slightly by a layer of snow.", + "9": "Snowfall creates a surreal mix of frost and heat on the volcanic slopes.", + "10": "Thin trails of snow line the cooled lava flows, highlighting their shapes.", + "11": "Snowflakes settle briefly on the ash-laden terrain before melting away.", + "12": "The volcanic peak is crowned with snow, a strange contrast to its fiery nature." + }, + "artic": { + "1": "Snow drifts endlessly across the frozen expanse, piling in soft dunes.", + "2": "The ice glistens under a fresh layer of snow, smooth and unbroken.", + "3": "Snow swirls in the air, adding to the endless white landscape.", + "4": "The Arctic tundra is blanketed by a continuous, gentle snowfall.", + "5": "Snow clings to the jagged edges of icebergs and frozen ridges.", + "6": "The ground is lost beneath a soft, unbroken expanse of snow.", + "7": "Snow trails along the frozen rivers, covering their icy surfaces.", + "8": "Snowflakes dance in the biting wind, settling on the frigid terrain.", + "9": "The Arctic horizon glows faintly as snow falls gently in the distance.", + "10": "Snow piles in drifts against ice ridges and frozen outcroppings.", + "11": "The snowfall blurs the line between ground and sky in the icy wasteland.", + "12": "Snow continues to fall, adding to the eternal winter of the Arctic." + }, + "cursed": { + "1": "Snow falls unnaturally, accompanied by faint whispers in the air.", + "2": "The cursed ground is coated with snow that seems to glow faintly.", + "3": "Snowflakes twist and turn erratically, as if moved by unseen hands.", + "4": "The snow melts and refreezes in strange patterns on the cursed soil.", + "5": "Light snowfall mixes with ash, creating a haunting gray frost.", + "6": "Snow gathers around ancient runes, glowing faintly with an eerie light.", + "7": "The snowfall is silent, yet the air is thick with an oppressive feeling.", + "8": "Snowflakes fall in slow motion, defying the natural order.", + "9": "Snow clings to cursed artifacts, shimmering with a faint, unnatural hue.", + "10": "The cursed landscape appears to absorb the falling snow, leaving strange patterns.", + "11": "Snow falls in spirals, as though guided by an unseen force.", + "12": "The snow glows faintly in the moonlight, casting unnatural shadows." + } + } + }, + "Monsoon Rain": { + "conditions": { + "temperature": { "gte": 70, "lte": 90 }, + "precipitation": { "gte": 80 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 80 }, + "cloudCover": { "gte": 80 }, + "visibility": { "lte": 30 } + }, + "descriptions": { + "farm": { + "1": "Fields are submerged under relentless monsoon rain, crops drenched.", + "2": "Rain falls in thick sheets, turning dirt paths into muddy rivers.", + "3": "The farmstead is soaked, with water pooling in every hollow.", + "4": "The pounding rain overwhelms irrigation channels, flooding the land.", + "5": "Cows and chickens huddle for shelter as the monsoon batters the barn.", + "6": "Heavy rain pelts the rooftops, creating a deafening roar in the farmhouse.", + "7": "Water cascades from thatched roofs, forming streams through the farmyard.", + "8": "The relentless downpour leaves the fields waterlogged and unworkable.", + "9": "Monsoon winds drive the rain sideways, drenching everything in its path.", + "10": "Farmhands struggle to protect stored hay from the unyielding rain.", + "11": "The rain-swollen river overflows, inundating nearby farmland.", + "12": "Puddles grow into ponds, and the farmland becomes a soggy expanse." + }, + "village": { + "1": "Streets turn into streams as monsoon rain drenches the village.", + "2": "Villagers scramble to reinforce their roofs against the pounding rain.", + "3": "The village square is a muddy pool under the unrelenting deluge.", + "4": "Rain batters the wooden shutters, seeping into homes despite best efforts.", + "5": "Children play in the torrents of water flowing down the lanes.", + "6": "Villagers huddle under awnings, watching the storm soak the town.", + "7": "Monsoon rains overflow the village well, flooding the surrounding area.", + "8": "The pounding rain turns the cobbled paths slick and treacherous.", + "9": "Rain pours off thatched roofs, drenching anyone caught outside.", + "10": "Streams of water cascade through the village, washing away loose debris.", + "11": "The incessant rain muffles all other sounds in the sodden village.", + "12": "Monsoon winds scatter rain-soaked leaves across the village square." + }, + "city": { + "1": "The city streets are rivers as monsoon rain fills the gutters.", + "2": "Rain pours down, creating cascading waterfalls from rooftop spouts.", + "3": "City guards struggle to keep order as the monsoon floods the alleys.", + "4": "The marketplace is abandoned, stalls drenched and goods soaked.", + "5": "Rain-soaked banners hang limply from the city walls.", + "6": "Heavy rain creates puddles that quickly become small lakes in courtyards.", + "7": "Horses slip on wet cobblestones as the rain pounds the city streets.", + "8": "Water cascades down stone staircases, pooling at the base of towers.", + "9": "Citizens dart between awnings, trying to avoid the relentless rain.", + "10": "Monsoon winds drive rain against the city's stone walls.", + "11": "The city's sewers overflow, spilling into the streets.", + "12": "The monsoon's roar drowns out the usual clamor of city life." + }, + "plains": { + "1": "The open plains become a vast, sodden expanse under the monsoon.", + "2": "Rain falls in sheets, turning the grasslands into a quagmire.", + "3": "Herds of animals seek higher ground as the plains flood.", + "4": "Monsoon winds whip across the plains, driving the rain sideways.", + "5": "Streams overflow, creating temporary rivers across the plains.", + "6": "Water pools in low-lying areas, turning the plains into a marsh.", + "7": "The endless rain soaks the grasses, making travel treacherous.", + "8": "Thunder rumbles over the plains as the monsoon rain continues.", + "9": "Rain soaks through everything, leaving the plains drenched and muddy.", + "10": "Storm clouds hang heavy, releasing unending torrents over the plains.", + "11": "The soaked grasses glisten under the relentless downpour.", + "12": "Monsoon rains transform the plains into a sodden, waterlogged wasteland." + }, + "forest": { + "1": "Rain drums on the canopy, dripping steadily onto the forest floor.", + "2": "The monsoon turns forest trails into muddy, impassable streams.", + "3": "Leaves glisten with rain, heavy drops falling in steady rhythms.", + "4": "Rain-soaked branches creak under the weight of the downpour.", + "5": "Streams within the forest swell, carving new paths through the underbrush.", + "6": "Animals shelter beneath dense foliage, avoiding the relentless rain.", + "7": "The forest air grows thick with humidity as the rain pours on.", + "8": "Water pools around tree roots, creating small, temporary ponds.", + "9": "Rain filters through the canopy, creating a constant, misty drizzle below.", + "10": "The forest floor becomes a muddy mire as the monsoon continues.", + "11": "Droplets shimmer on spiderwebs, reflecting the gray light of the storm.", + "12": "Rain cascades down mossy trunks, soaking the entire forest." + }, + "swamp": { + "1": "The swamp floods further under the relentless monsoon downpour.", + "2": "Rain ripples across stagnant pools, blending into the murky water.", + "3": "Monsoon rains saturate the swamp, raising water levels dramatically.", + "4": "The air becomes thick and oppressive as rain drenches the swamp.", + "5": "Water drips constantly from overhanging branches, adding to the deluge.", + "6": "Crocodiles and frogs retreat to higher ground as the swamp floods.", + "7": "The swamp's reeds sway under the weight of the pouring rain.", + "8": "Rainwater runs off into the bogs, mixing with the brackish water.", + "9": "Incessant rain turns swamp trails into flooded, treacherous paths.", + "10": "The storm adds to the swamp's eerie atmosphere with its constant roar.", + "11": "Water spills over natural levees, spreading across the swamp's surface.", + "12": "The swamp's wildlife grows quieter, sheltering from the unending rain." + }, + "jungle": { + "1": "Rain pours relentlessly through the dense jungle canopy.", + "2": "Monsoon rains turn jungle paths into muddy rivers.", + "3": "The jungle's vibrant greens are muted under the constant downpour.", + "4": "Rainwater cascades down vines, collecting in pools on the forest floor.", + "5": "Monsoon winds shake the treetops, scattering leaves and water below.", + "6": "The jungle floor becomes a swampy mire, flooded by the rains.", + "7": "Rain drums on broad leaves, creating a symphony of sound in the jungle.", + "8": "Animals retreat to sheltered burrows as the rain intensifies.", + "9": "The air is heavy with humidity, thickened by the relentless rain.", + "10": "Waterfalls form along jungle cliffs, fed by the monsoon’s deluge.", + "11": "The jungle becomes a maze of waterlogged trails and flooded clearings.", + "12": "Monsoon rains soak the jungle, leaving every surface slick and wet." + }, + "hills": { + "1": "Rain pours down the slopes, turning paths into streams.", + "2": "The hills are shrouded in misty rain, visibility reduced.", + "3": "Monsoon rains flood valleys between the hills.", + "4": "Rivulets of water carve new paths down the grassy inclines.", + "5": "Wind drives the rain sideways, battering hilltop trees.", + "6": "Herds of animals seek shelter from the unrelenting downpour.", + "7": "The soil turns to mud, making the hills treacherous to traverse.", + "8": "Rain drips from overhanging branches, forming muddy pools below.", + "9": "Streams overflow, cascading down the hillsides with force.", + "10": "Distant thunder echoes across the drenched hilltops.", + "11": "The rain-soaked landscape glistens under occasional lightning flashes.", + "12": "Heavy rain erodes the trails, exposing rocks and roots." + }, + "mountains": { + "1": "Raging torrents form as monsoon rain cascades down cliffs.", + "2": "Fog and rain obscure the peaks, reducing visibility to mere feet.", + "3": "Waterfalls swell with monsoon runoff, thundering into valleys below.", + "4": "Rain turns mountain paths into treacherous streams.", + "5": "Rockslides threaten as the relentless rain loosens the soil.", + "6": "Mountain goats scramble for shelter under ledges and crags.", + "7": "Chilly rain and strong winds lash against the rocky faces.", + "8": "The sound of the rain echoes eerily through mountain passes.", + "9": "Low-lying clouds dump rain in sheets, soaking the terrain.", + "10": "Streams overflow into roaring rivers that snake through the valleys.", + "11": "Heavy rain creates pools in every crevice of the rocky landscape.", + "12": "Lightning briefly illuminates rain-drenched cliffs and peaks." + }, + "desert": { + "1": "Rare monsoon rains turn sandy dunes into muddy flats.", + "2": "Canyons fill with water as flash floods sweep through the desert.", + "3": "Dry riverbeds transform into rushing streams under the heavy rain.", + "4": "Thunder rumbles across the desert, accompanying the torrential rain.", + "5": "Cacti stand drenched, their roots soaking up the rare deluge.", + "6": "Rain pounds on the desert sands, creating small pools in hollows.", + "7": "The parched desert soil struggles to absorb the relentless rain.", + "8": "Desert wildlife scurries for shelter from the unrelenting downpour.", + "9": "Monsoon winds whip sand and rain into a stinging assault.", + "10": "Dark clouds loom over the desert, releasing a rare, heavy rain.", + "11": "Lightning illuminates drenched dunes and pooling water.", + "12": "The desert transforms briefly into a lush, wet expanse." + }, + "coastal": { + "1": "Monsoon rains lash the coast, swelling the waves to dangerous heights.", + "2": "The shoreline floods as the heavy rain combines with high tides.", + "3": "Strong winds and rain batter the cliffs, sending spray into the air.", + "4": "Rain hammers down on the coastal villages, flooding streets and docks.", + "5": "Fishing boats are pulled ashore to avoid the raging storm.", + "6": "Seabirds struggle against the wind as rain drenches the coastline.", + "7": "The monsoon turns sandy beaches into waterlogged marshes.", + "8": "Rain-swollen streams rush toward the sea, cutting new paths in the sand.", + "9": "Dark storm clouds roll in from the horizon, bringing torrential rain.", + "10": "Lightning reflects off the turbulent waters crashing against the shore.", + "11": "Coastal winds drive the rain inland, soaking everything in their path.", + "12": "The rain creates streams and rivulets that flow into the churning sea." + }, + "volcano": { + "1": "Rain hisses as it strikes the warm volcanic rocks, sending up steam.", + "2": "Water pools in craters, creating temporary lakes in the volcanic terrain.", + "3": "Monsoon rains turn ash-covered slopes into slick, muddy paths.", + "4": "Steam rises as rain meets the heated ground of the volcanic plain.", + "5": "Rockslides threaten as the downpour loosens the volcanic soil.", + "6": "Lava tubes fill with rainwater, creating hidden, treacherous pools.", + "7": "Rain mingles with sulfurous fumes, creating a heavy, choking atmosphere.", + "8": "The monsoon rain washes ash down the slopes in muddy streams.", + "9": "Strong winds drive rain through volcanic valleys, soaking the terrain.", + "10": "Lightning illuminates the volcanic landscape under the heavy downpour.", + "11": "Rain cascades off jagged volcanic ridges, flooding the base of the slopes.", + "12": "Water carves new channels through cooled lava, altering the landscape." + }, + "artic": { + "1": "Monsoon rain freezes upon contact, coating everything in a slick layer of ice.", + "2": "Rain turns to sleet as it meets the frigid Arctic winds.", + "3": "Streams of water carve through the icy landscape under the heavy rain.", + "4": "The rain pools and freezes, creating treacherous patches of ice.", + "5": "Glaciers glisten under the relentless monsoon rain, slick and dangerous.", + "6": "Cold rain seeps into crevices, freezing and expanding the icy terrain.", + "7": "Frozen tundra becomes a mire of icy slush under the heavy rainfall.", + "8": "Rain turns into mist, shrouding the Arctic in a chilling veil.", + "9": "Icy winds whip the rain sideways, creating a biting storm.", + "10": "Rain freezes on contact with metal and stone, coating them in frost.", + "11": "Monsoon rain mixes with snow, creating a freezing slurry.", + "12": "The Arctic tundra gleams with a thin sheen of ice from the rain." + }, + "cursed": { + "1": "Monsoon rain falls black as ink, soaking the cursed ground.", + "2": "Rainwater sizzles as it hits cursed soil, leaving behind strange marks.", + "3": "The cursed rain whispers as it falls, unnerving those who hear it.", + "4": "Pools of rain reflect distorted, otherworldly images of the surroundings.", + "5": "The relentless rain carries a faint, acrid smell, stinging the eyes.", + "6": "Monsoon winds howl, carrying eerie, ghostly cries with the downpour.", + "7": "The cursed rain leaves a sticky, unearthly residue on every surface.", + "8": "Dark clouds roil overhead, releasing rain that feels heavier than normal.", + "9": "The cursed land absorbs the rain but seems none the wetter.", + "10": "Lightning flashes green in the cursed storm, illuminating dark shadows.", + "11": "Rainfall twists into unnatural patterns, defying the usual pull of gravity.", + "12": "The ground bubbles as the cursed rain strikes, emitting a faint glow." + } + } + }, + "Morning Frost": { + "conditions": { + "temperature": { "gte": 20, "lte": 35 }, + "precipitation": { "lte": 40 }, + "wind": { "lte": 35 }, + "humidity": { "gte": 60, "lte": 90 }, + "cloudCover": { "lte": 40 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "The fields glisten with a thin layer of frost as the sun rises.", + "2": "Frost clings to the wooden fence posts and thatched roofs.", + "3": "The morning chill bites as frost covers the plowed soil.", + "4": "Frozen dew sparkles on the tips of grass and haystacks.", + "5": "Icicles hang from the edges of the barn, dripping slowly as they thaw.", + "6": "Frost-covered crops shimmer under the pale morning sun.", + "7": "The air is crisp, and the ground crunches underfoot with a frosty layer.", + "8": "The frost etches delicate patterns on the farmhouse windows.", + "9": "Buckets left outside have a thin layer of ice covering their contents.", + "10": "The frost makes the farmyard paths slick and treacherous.", + "11": "Smoke rises from the farmhouse chimney, a contrast to the frosty air.", + "12": "Horses' breath steams in the chilly morning air, frost clinging to their manes." + }, + "village": { + "1": "The village cobblestones are slippery with a coating of frost.", + "2": "Frost outlines the thatched roofs and wooden beams of the cottages.", + "3": "The morning frost glistens on the village well’s stone surface.", + "4": "Chickens peck cautiously at the frozen ground in the village square.", + "5": "Frost laces the village windows, obscuring the view outside.", + "6": "Villagers bundle in cloaks as frost sparkles on every surface.", + "7": "The frost bites into the exposed wood of market stalls left overnight.", + "8": "Frosty breath hangs in the air as villagers prepare for the day.", + "9": "The frost-covered paths crunch beneath the boots of early risers.", + "10": "Thin ice has formed in the village troughs, requiring a crack to access water.", + "11": "A frosty layer glimmers on the edges of the blacksmith’s tools.", + "12": "The village is quiet, the frost muting sounds as the sun struggles to rise." + }, + "city": { + "1": "Frost clings to the stone walls and cobbled streets of the city.", + "2": "Market stalls left overnight are rimed with a layer of frost.", + "3": "Thin ice forms on the edges of the city’s fountains and waterways.", + "4": "The frost-covered rooftops shine under the first light of day.", + "5": "City gates are stiff with frost, making them harder to open.", + "6": "Horse hooves echo against frost-slick cobblestones in the early hours.", + "7": "The frost bites into exposed ironwork, coating railings in icy patterns.", + "8": "Shopkeepers scrape frost off their windows to prepare for the day.", + "9": "Thin layers of ice form on buckets and barrels in the alleys.", + "10": "Frost glitters on the spires and towers of the city skyline.", + "11": "The chill air carries the scent of fires being lit in city hearths.", + "12": "Frost-covered wagons creak as traders prepare for the day's business." + }, + "plains": { + "1": "The open plains shimmer with frost under the pale morning sun.", + "2": "Frost highlights every blade of grass, turning the plains into a silver expanse.", + "3": "Frozen dew crunches beneath the feet of early travelers on the plains.", + "4": "Small streams running through the plains have thin ice crusts forming.", + "5": "The frost clings to low shrubs and wildflowers, sparkling brightly.", + "6": "The vast plains appear desolate, with frost muting their usual vitality.", + "7": "The frost makes every rise and hollow gleam like polished stone.", + "8": "Animal tracks are frozen into the frosty ground of the plains.", + "9": "Thin tendrils of mist curl over the frost-covered grass.", + "10": "The horizon glows as the frost catches the light of the rising sun.", + "11": "The crisp air carries the distant sound of cracking ice along the plains.", + "12": "The plains are silent and still, frost muting even the smallest sounds." + }, + "forest": { + "1": "The forest floor glistens with frost, crunching underfoot.", + "2": "Frost clings to the branches, turning the forest into a silver maze.", + "3": "Morning light filters through the frosted canopy, creating shimmering beams.", + "4": "The air is cold and crisp, frost glinting on moss-covered rocks.", + "5": "Frozen leaves crackle as frost clings to the forest undergrowth.", + "6": "The forest paths are slick with frost, making travel cautious and slow.", + "7": "Frost etches intricate patterns on tree trunks and exposed roots.", + "8": "Icicles form along the edges of fallen logs and low branches.", + "9": "Frosted pine needles sparkle, their green dulled by the morning chill.", + "10": "A quiet stillness blankets the frost-covered forest, broken only by the sound of falling ice.", + "11": "Streams in the forest are edged with ice, the water sluggishly flowing.", + "12": "Frost clings to spiderwebs, turning them into delicate white lace." + }, + "swamp": { + "1": "The swamp is eerily quiet, frost glinting on reeds and moss.", + "2": "Frozen puddles reflect the pale light, their edges frosted over.", + "3": "Frost clings to the swamp's gnarled roots, adding an unearthly gleam.", + "4": "The swamp mist mingles with frost, creating a chilling atmosphere.", + "5": "Waterlogged logs are rimed with frost, slick and dangerous to step on.", + "6": "The frost-covered swamp emits faint creaks as the cold tightens its grip.", + "7": "Frost edges the swamp's stagnant pools, creating thin sheets of ice.", + "8": "The frost adds a ghostly beauty to the swamp's tangled vines.", + "9": "Cold air mingles with the swamp's usual scents, creating a biting freshness.", + "10": "The swamp grasses glisten with frost, their tips frozen in delicate spikes.", + "11": "The frost bites into the swamp's decaying foliage, slowing its decay.", + "12": "Even the swamp's murky waters seem subdued under the frost's icy grasp." + }, + "jungle": { + "1": "Frost clings to the jungle foliage, an unusual sight in the dense heat.", + "2": "The jungle paths are slick with frost, crunching underfoot.", + "3": "Frozen dew hangs from vines, sparkling in the dim light.", + "4": "Frost edges broad leaves, giving the jungle a surreal appearance.", + "5": "Icicles drip from the jungle canopy, a rare phenomenon in the tropical air.", + "6": "The frost-laden jungle floor crunches with every step.", + "7": "Frost patterns coat the jungle's flowers, dulling their vibrant colors.", + "8": "Chilled air flows through the jungle, frosting even the thickest undergrowth.", + "9": "The jungle's usual sounds are muted under the frost's icy grip.", + "10": "Moisture freezes on the jungle's vines, creating sparkling beads of ice.", + "11": "The frost makes the dense jungle unusually bright as it reflects the rising sun.", + "12": "Frozen mist clings to the jungle, shrouding it in an ethereal chill." + }, + "hills": { + "1": "Frost coats the grassy slopes, glinting in the early morning light.", + "2": "Sheep huddle together as frost sparkles on the rolling hills.", + "3": "The ground is hard and slippery with a layer of morning frost.", + "4": "Frost clings to the roots and rocks scattered across the hills.", + "5": "The frost outlines each blade of grass, giving the hills a silvery hue.", + "6": "Morning sun struggles to melt the frost covering the winding hill paths.", + "7": "The frost-covered hills are eerily silent except for the crunch of footsteps.", + "8": "Shadows stretch long across the frost-covered slopes of the hills.", + "9": "Frostbite nips at travelers ascending the frosty hill paths.", + "10": "The frost sparkles brightly on the rolling hills as the sun rises.", + "11": "Icicles hang from shrubs and rocks scattered across the frosty hills.", + "12": "The frost accentuates the curves of the hills, making them shine like silver waves." + }, + "mountains": { + "1": "Frost glazes the rocky trails, making the ascent treacherous.", + "2": "The mountain peaks shimmer under a fresh coat of frost.", + "3": "Frozen mist clings to the cliffs and boulders in the morning chill.", + "4": "Frost covers the mountain paths, crunching under boots.", + "5": "Icicles hang from overhanging rocks, catching the pale morning light.", + "6": "Frost etches patterns on the jagged stones of the mountain pass.", + "7": "Snow-dusted peaks glisten, the frost extending far down the slopes.", + "8": "The frosty air bites at exposed skin as the mountains gleam silver.", + "9": "The mountain forests are still, every leaf and branch rimmed with frost.", + "10": "Frost-covered cairns mark the treacherous mountain paths.", + "11": "The frost accentuates the rugged beauty of the mountain landscape.", + "12": "Streams are partially frozen, their edges sparkling with frost." + }, + "desert": { + "1": "Morning frost sparkles on the sand dunes, a rare and fleeting sight.", + "2": "Cacti and desert plants are rimmed with frost, their thorns glinting in the light.", + "3": "The cold air turns the desert floor into a shimmering frosty expanse.", + "4": "Frost clings to the rocks and sparse vegetation of the desert.", + "5": "The frost is quickly melting as the sun rises over the desert horizon.", + "6": "Footprints crunch through the frosty desert sand underfoot.", + "7": "Thin frost crystals cover the dry riverbeds, glinting in the sun.", + "8": "The frost gives the desert an unearthly glow in the pale dawn light.", + "9": "Desert stones are cold to the touch, coated in a layer of frost.", + "10": "The frosty desert air carries an eerie stillness, amplifying every sound.", + "11": "Frost-covered dunes reflect the first rays of sunlight, glowing faintly.", + "12": "A chill lingers in the desert air, frost fading quickly as warmth returns." + }, + "coastal": { + "1": "Frost clings to the shoreline, glinting on pebbles and driftwood.", + "2": "The sea spray freezes on the rocks, forming delicate frost patterns.", + "3": "Fishing boats in the harbor are rimmed with frost, their decks icy and slick.", + "4": "Frost covers the coastal grasses and sand dunes in a silvery sheen.", + "5": "Seagulls call out over a frosty shore, their cries echoing in the chill air.", + "6": "Frost sparkles on the ropes and nets left on the docks overnight.", + "7": "Thin ice forms in shallow tidal pools, catching the early sunlight.", + "8": "Frost outlines the weathered edges of seaside cottages and piers.", + "9": "Cold air rolls in with the tide, frosting the rocks along the coastline.", + "10": "The beach glistens with frost, the sand firm and icy underfoot.", + "11": "Icicles hang from the edges of the dock, swaying slightly in the wind.", + "12": "The frosty morning air smells of salt, the chill cutting to the bone." + }, + "volcano": { + "1": "The frosty air contrasts sharply with the warm steam venting from the volcano.", + "2": "Frost clings to the volcanic rocks, melting near the steaming cracks.", + "3": "Frozen mist forms over the caldera, glinting eerily in the morning light.", + "4": "The frost outlines the jagged lava formations, adding an unexpected sparkle.", + "5": "Morning frost glazes over the black volcanic soil, making it slick.", + "6": "Steam vents send plumes of fog into the frosty air, mingling strangely.", + "7": "The frosty landscape around the volcano glimmers with an unearthly beauty.", + "8": "The frost melts in patches near bubbling volcanic springs.", + "9": "Volcanic slopes shimmer with frost, the heat beneath barely detectable.", + "10": "Frost patterns cover the volcanic boulders, a stark contrast to their dark surfaces.", + "11": "The frosty morning air carries the sulfurous scent of the volcano.", + "12": "Steam mingles with the frost, creating a surreal and contrasting landscape." + }, + "artic": { + "1": "The frost thickens the already icy terrain, turning the arctic into a sparkling wonderland.", + "2": "Frost adds a layer of glimmer to the snow-covered tundra.", + "3": "The arctic air is biting, with frost clinging even to the smallest ice crystals.", + "4": "Icebergs glint in the early light, their surfaces frosted over from the chill night.", + "5": "The frost bites deep into the ice and snow, making every surface gleam.", + "6": "Polar animals leave frosted tracks across the glacial surface.", + "7": "The frozen horizon shimmers as frost catches the pale sunlight.", + "8": "Every breath freezes instantly in the arctic air, joining the frost-covered world.", + "9": "The frost layers the already frozen landscape, adding to its desolation.", + "10": "Icicles grow overnight, adding frost-covered daggers to the arctic cliffs.", + "11": "The frosty air carries a haunting stillness across the icy expanse.", + "12": "Frost glistens on the arctic’s snowy plains, each crystal catching the light." + }, + "cursed": { + "1": "The frost on the cursed ground seems to pulse faintly, glowing in the dark.", + "2": "Frozen shadows linger where frost has settled on cursed stones.", + "3": "The frost here feels unnatural, clinging even to warm skin.", + "4": "Frost outlines cursed runes etched into the stones, glowing faintly in the gloom.", + "5": "The frost carries an eerie chill, as if it leeches the warmth from the soul.", + "6": "Frozen mist swirls over the cursed ground, frost clinging to twisted trees.", + "7": "The frost seems to move, tracing patterns over the cursed soil.", + "8": "Even the frost feels ominous, its beauty a stark contrast to the cursed air.", + "9": "The cursed land is quiet, frost-covered bones scattered across the frozen ground.", + "10": "The frost glows faintly, as though charged by the curse of the land.", + "11": "Frozen tears glisten on cursed statues, the frost adding to their haunting presence.", + "12": "The frost whispers faintly, a chilling sound carried by the cursed wind." + } + } + }, + "Muddy Conditions": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "gte": 50 }, + "wind": { "lte": 45 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 50 }, + "visibility": { "gte": 20, "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "Fields are drenched, with thick mud sticking to boots and tools.", + "2": "Tractors and carts struggle through the sticky, sodden ground.", + "3": "Livestock leave deep hoofprints in the saturated earth.", + "4": "The farmyard is a mess of puddles and thick, clinging mud.", + "5": "Water runs off the fields, pooling into muddy trenches.", + "6": "Rain-soaked furrows make planting and harvesting treacherous.", + "7": "Mud clogs wheels and footwear, slowing progress around the farm.", + "8": "Fence posts lean precariously in the soft, waterlogged ground.", + "9": "The farm smells of damp earth and wet hay, thick mud everywhere.", + "10": "Ruts in the pathways deepen as carts churn the muddy ground.", + "11": "Puddles mix with mud, creating slippery hazards for workers.", + "12": "Mud cakes the barn doors and walls from splashes and rain." + }, + "village": { + "1": "Villagers tread carefully through streets turned to mud trails.", + "2": "Children play in the thick muck, their laughter echoing through the village.", + "3": "Carts struggle to move through the muddy, uneven paths.", + "4": "Mud splashes up onto homes and clothing as people walk through the streets.", + "5": "The village square is a swampy mess, with puddles reflecting the cloudy sky.", + "6": "Wheels and hooves churn the paths into deep, treacherous ruts.", + "7": "Dogs leave muddy pawprints across doorsteps and courtyards.", + "8": "Villagers use wooden planks to create makeshift bridges over the worst mud.", + "9": "Mud drips from cloaks and boots as people return from errands.", + "10": "The well is surrounded by slick mud, making footing difficult.", + "11": "Wooden fences and carts are streaked with splattered mud.", + "12": "Chickens and ducks thrive in the muddy mess, leaving tracks everywhere." + }, + "city": { + "1": "Cobblestone streets are coated in muddy water from the downpour.", + "2": "City gutters overflow, turning alleys into streams of thick muck.", + "3": "The marketplace stalls are splattered with mud kicked up by passersby.", + "4": "Horses struggle to navigate the muddy streets, their hooves slipping often.", + "5": "Boots squelch with every step as the city's paths turn to mire.", + "6": "Mud coats the lower walls of buildings, sprayed by cart wheels.", + "7": "City workers shovel mud from drainage ditches, their progress slow.", + "8": "Children splash through muddy puddles in the poorer quarters.", + "9": "The main gates are clogged with thick mud, making travel difficult.", + "10": "Fine cloaks and robes are ruined as mud splashes onto pedestrians.", + "11": "Street performers pack up early, their usual spots too muddy for a crowd.", + "12": "The city's air is thick with the smell of wet earth and damp stone." + }, + "plains": { + "1": "The open plains are drenched, the grasslands transformed into a muddy expanse.", + "2": "Horses sink into the mud, their hooves struggling to find solid ground.", + "3": "Travelers' wagons leave deep ruts in the soaked terrain.", + "4": "Mud splashes onto boots and cloaks with every step across the plains.", + "5": "The wind carries the smell of wet grass and churned earth.", + "6": "Puddles form in every dip, creating a maze of slippery paths.", + "7": "Mud clogs the wheels of carts, bringing progress to a crawl.", + "8": "The plains seem endless, their usual beauty hidden under thick mud.", + "9": "Every step on the plains sinks slightly, the ground soft and heavy.", + "10": "Water pools in the long grass, turning patches into boggy traps.", + "11": "Sheep and cattle leave deep tracks in the mud as they graze.", + "12": "Muddy streams crisscross the plains, their waters brown and murky." + }, + "forest": { + "1": "Forest trails are sodden, with mud pooling at the bases of trees.", + "2": "Roots and stones are hidden under slick layers of mud, making walking dangerous.", + "3": "The smell of wet leaves and churned earth fills the damp forest air.", + "4": "Mud coats the trunks of trees where animals and travelers pass by.", + "5": "Every step squelches as the forest floor becomes a muddy quagmire.", + "6": "Streams overflow, spreading muddy water across the undergrowth.", + "7": "The forest is eerily silent, save for the occasional drip of water.", + "8": "Puddles reflect the canopy above, each surrounded by soft mud.", + "9": "Mud-slicked leaves cling to boots, slowing progress through the forest.", + "10": "The forest path disappears into a stretch of waterlogged mud.", + "11": "Animal tracks in the mud crisscross the forest floor.", + "12": "Rain-drenched branches sag under their weight, dropping mud-streaked water below." + }, + "swamp": { + "1": "The swamp is a mess of mud and water, nearly impossible to navigate.", + "2": "Mud bubbles and sinks with each step, the swamp alive with motion.", + "3": "Thick mud clogs boots, making every movement in the swamp arduous.", + "4": "The air is heavy with the smell of rotting vegetation and wet earth.", + "5": "Puddles merge with the muddy ground, creating a seamless swampy expanse.", + "6": "Reeds and roots are buried under layers of thick, clinging mud.", + "7": "Every footfall sinks into the swamp, the mud threatening to trap boots.", + "8": "Mosquitoes buzz loudly as the swamp mud sucks at every step.", + "9": "The swamp water is murky, with mud swirling at its edges.", + "10": "Frogs and insects thrive in the swamp's muddy chaos.", + "11": "Mud drips from low-hanging branches and vines, adding to the mess below.", + "12": "The swamp's water flows sluggishly, its surface coated with a muddy film." + }, + "jungle": { + "1": "Jungle paths are overrun with mud, turning every step into a challenge.", + "2": "The mud in the jungle is thick and clinging, coating boots and tools alike.", + "3": "Water drips from leaves into the muddy undergrowth below.", + "4": "The jungle floor is a soup of mud and decaying vegetation.", + "5": "Roots and vines are buried under layers of slick, slippery mud.", + "6": "Puddles in the jungle glow faintly as the canopy traps the dim light.", + "7": "Mud splashes onto foliage, adding brown streaks to the vibrant greens.", + "8": "Animals leave deep tracks in the jungle's soft, muddy floor.", + "9": "Every step in the jungle pulls at boots, the mud holding fast.", + "10": "The jungle smells of wet leaves and mud, the air humid and heavy.", + "11": "Streams overflow, spreading muddy water throughout the jungle paths.", + "12": "The jungle is eerily quiet, save for the sucking sound of footsteps in the mud." + }, + "hills": { + "1": "Rolling hills are waterlogged, with mud sliding down the slopes.", + "2": "Paths through the hills are slick with mud, making travel treacherous.", + "3": "Mud pools at the bases of hills, creating sticky traps for travelers.", + "4": "Every step up the hills churns the soft earth into thick mud.", + "5": "Animals tread cautiously, their hooves sinking into the muddy terrain.", + "6": "Grass struggles to show through the layers of sticky mud.", + "7": "Small streams overflow, turning paths into muddy runoffs.", + "8": "Hillsides are streaked with dark trails where water and mud have flowed.", + "9": "Climbing the hills requires careful steps to avoid sliding on the mud.", + "10": "Mud splashes up onto travelers’ clothes as they navigate the hills.", + "11": "Rocks protrude from the muddy ground, offering rare solid footing.", + "12": "The scent of wet earth dominates the air across the hills." + }, + "mountains": { + "1": "Mountain trails are treacherous, with mud coating the rocky paths.", + "2": "Mud seeps into crevices, making climbing slow and dangerous.", + "3": "Mountain streams overflow, spreading muddy water onto the trails.", + "4": "Travelers leave deep, muddy footprints along the steep mountain paths.", + "5": "Landslides threaten as mud and water weaken the mountainsides.", + "6": "Boots slide on the slick, mud-coated rocks of the mountain trails.", + "7": "The mountain air is damp, with the ground soggy underfoot.", + "8": "Paths between cliffs are treacherous, with mud hiding gaps and loose stones.", + "9": "Mud streaks the mountain face, left by rain and melting snow.", + "10": "Animals struggle to find footing on the muddy, unstable slopes.", + "11": "Small waterfalls cascade down the mountains, carrying mud and debris.", + "12": "The mountains echo with the sound of slipping mud and running water." + }, + "desert": { + "1": "The desert sands are darkened by mud, clumping underfoot.", + "2": "Rare rains have turned parts of the desert into sticky, muddy patches.", + "3": "Tracks are filled with water, creating small muddy pools in the desert.", + "4": "Mud cakes the edges of desert dunes, an unusual sight in the arid expanse.", + "5": "Desert animals avoid the muddy areas, their usual paths slick with moisture.", + "6": "Oasis edges are muddy, the water mixing with sand to form quicksand traps.", + "7": "The air smells of wet sand and earth, unusual for the dry desert.", + "8": "Wagons struggle through patches of thick desert mud.", + "9": "Sand and mud mix, forming strange, uneven textures on the desert floor.", + "10": "Even the sparse desert plants are coated with splashes of wet mud.", + "11": "Desert winds carry a faint hint of dampness, unusual for the arid region.", + "12": "Footsteps leave deep impressions in the mud-covered desert sand." + }, + "coastal": { + "1": "The coastal paths are muddy, with puddles reflecting the gray skies.", + "2": "Sea spray mixes with mud, creating a sticky mess along the shore.", + "3": "Mud clings to fishing nets and boats as they are dragged along the beach.", + "4": "Coastal cliffs drip with mud and water, making paths slick and dangerous.", + "5": "Mud pools at the base of dunes, where water and sand converge.", + "6": "The coastal breeze carries the scent of wet earth and salty air.", + "7": "Tide pools overflow, spreading muddy water across the rocky shore.", + "8": "Boots leave deep tracks in the mud along the coastal trails.", + "9": "Mud coats the hulls of boats pulled up onto the beach.", + "10": "The sea crashes against muddy shores, spreading debris and dirt.", + "11": "Coastal caves are slick with mud, their entrances treacherous to navigate.", + "12": "Villagers lay planks across muddy paths to reach the docks." + }, + "volcano": { + "1": "Rain on volcanic slopes turns ash and dirt into thick, clinging mud.", + "2": "Mudslides threaten as water mixes with loose volcanic soil.", + "3": "Steam rises where rain hits hot ground, adding to the muddy chaos.", + "4": "Paths along the volcano are slick with a mix of mud and ash.", + "5": "Mud pools form in craters, hiding the usually dry terrain.", + "6": "Every step on the volcanic ground sinks slightly into the muddy surface.", + "7": "The air is heavy with the scent of wet earth and sulfur.", + "8": "Lava rocks are streaked with muddy water, making them hazardous to climb.", + "9": "Trails leading up the volcano are washed out by streams of mud.", + "10": "Boots slip on the slick, muddy terrain of the volcanic slopes.", + "11": "Mud splatters onto clothing and equipment with every step uphill.", + "12": "The usually barren landscape is transformed into a muddy expanse." + }, + "artic": { + "1": "Melting ice turns the Arctic ground into a thick, muddy mess.", + "2": "Mud mixes with slush, making every step slippery and cold.", + "3": "Ice floes leave muddy streaks as they melt and refreeze.", + "4": "Patches of frozen mud glisten under the faint Arctic light.", + "5": "Mud pools where snow has melted, forming slippery patches.", + "6": "Boots sink into the freezing mud, slowing movement across the tundra.", + "7": "The Arctic air is damp and biting, with mud coating the landscape.", + "8": "Even the hardy Arctic animals avoid the muddiest areas.", + "9": "Tracks in the mud freeze overnight, creating treacherous paths.", + "10": "Mud splashes up onto sleds and clothing, freezing quickly in the cold.", + "11": "The Arctic tundra feels uncharacteristically soft and unstable underfoot.", + "12": "Muddy patches reflect the dim light, blending with the icy surroundings." + }, + "cursed": { + "1": "The cursed ground oozes with dark, sticky mud that clings unnaturally.", + "2": "The mud in the cursed area seems alive, sucking at boots and dragging them down.", + "3": "Dark puddles form, the mud within them rippling as if disturbed by unseen forces.", + "4": "Every step in the cursed mud is accompanied by a faint, eerie squelch.", + "5": "The mud smells of decay and something unidentifiable but sinister.", + "6": "Thick, black mud coats the cursed ground, with faint whispers heard around it.", + "7": "Mud bubbles and shifts unnaturally, as though hiding something beneath.", + "8": "Even the cursed air feels heavy, with mud pulling at travelers’ steps.", + "9": "Weapons and tools left on the ground are slowly consumed by the creeping mud.", + "10": "Tracks in the cursed mud disappear moments after being made.", + "11": "The mud here seems endless, pulling at every step with supernatural strength.", + "12": "Cursed puddles glow faintly, the mud around them radiating unnatural heat." + } + } + }, + "Overcast": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "lte": 60 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 50, "lte": 80 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "The sky is a uniform gray, casting a somber tone over the fields.", + "2": "Clouds hang low over the farm, dulling the usual vibrant colors.", + "3": "A thick layer of overcast sky makes the day feel heavy and still.", + "4": "The sunlight barely filters through the clouds, leaving a muted glow.", + "5": "Farm animals seem quiet, as if subdued by the oppressive sky.", + "6": "The fields are bathed in a dim, cool light under the overcast sky.", + "7": "Clouds stretch endlessly, leaving no hint of the sun's position.", + "8": "The air feels damp and cool, complementing the heavy cloud cover.", + "9": "Workers toil under the overcast sky, their shadows faint and blurred.", + "10": "Even the breeze seems muted, the overcast sky dampening all sounds.", + "11": "The horizon blends into the sky, the clouds merging with the land.", + "12": "A faint drizzle threatens to fall from the thick gray sky." + }, + "village": { + "1": "The village feels quiet under the oppressive gray of the overcast sky.", + "2": "Chimney smoke struggles to rise through the dense cloud cover.", + "3": "Shadows are faint, the overcast sky scattering the light evenly.", + "4": "Villagers go about their day, glancing occasionally at the gloomy sky.", + "5": "The overcast clouds cast a somber mood over the bustling village.", + "6": "Buildings seem darker under the gray sky, their colors dulled.", + "7": "The village well reflects the cloudy sky, its water appearing darker than usual.", + "8": "The faint smell of damp earth fills the air beneath the overcast sky.", + "9": "Children play quietly, their laughter muffled by the heavy atmosphere.", + "10": "The village square is dimly lit, the overcast sky creating an early dusk.", + "11": "Villagers light lanterns early, their warm glow fighting against the gray sky.", + "12": "A cool breeze drifts through the village, carrying a hint of rain." + }, + "city": { + "1": "The city's rooftops are muted under the overcast sky, their edges softened.", + "2": "Lanterns are lit early, their flickering flames reflecting off cobblestones.", + "3": "The overcast clouds create a uniform gray dome over the bustling city.", + "4": "Market stalls seem dim, their colors muted by the heavy cloud cover.", + "5": "The air feels heavier, the overcast sky pressing down on the city streets.", + "6": "Citizens glance skyward, frowning at the unbroken gray above.", + "7": "The overcast sky makes the city's tall spires seem to pierce the clouds.", + "8": "Horses' hooves clatter softly, their sound dampened by the thick air.", + "9": "Even the city’s vibrant banners appear dull under the heavy clouds.", + "10": "The murmur of city life feels quieter, subdued by the oppressive sky.", + "11": "The smell of rain lingers in the air, though the overcast clouds hold steady.", + "12": "Darkened windows reflect the gray sky, adding to the city’s somber mood." + }, + "plains": { + "1": "The vast plains stretch endlessly under a blanket of gray clouds.", + "2": "The overcast sky makes the plains feel even more expansive and empty.", + "3": "Grass sways gently, its green muted by the dim, even light.", + "4": "The horizon is barely visible, blending with the heavy cloud cover.", + "5": "A faint breeze rustles through the grass, carrying the smell of damp earth.", + "6": "The overcast sky casts no shadows, making the plains feel timeless.", + "7": "Wildflowers stand out faintly against the dull backdrop of the cloudy sky.", + "8": "Herds of animals move quietly, their calls muffled under the heavy clouds.", + "9": "The plains feel still and somber, the overcast sky dimming all colors.", + "10": "Birds circle silently above, their outlines faint against the gray sky.", + "11": "The air feels cool and dense, complementing the thick layer of clouds.", + "12": "Puddles from recent rain reflect the overcast sky, adding to the gloom." + }, + "forest": { + "1": "The canopy is darker than usual, the overcast sky filtering through the leaves.", + "2": "Tree trunks glisten faintly, damp from the cool, cloudy weather.", + "3": "The forest feels hushed, the overcast sky muting the usual sounds of wildlife.", + "4": "Mossy ground appears darker, its green dimmed under the heavy clouds.", + "5": "The forest paths are cloaked in gray light, creating an eerie stillness.", + "6": "Birdsong is sparse, the overcast sky seeming to dampen their energy.", + "7": "Leaves drip with condensation, the overcast sky holding the moisture in the air.", + "8": "The forest air is cool and moist, heavy with the smell of damp earth.", + "9": "Shafts of light barely penetrate the thick cloud cover and forest canopy.", + "10": "The overcast sky casts the forest in perpetual twilight.", + "11": "The dense clouds make the forest shadows deeper and more mysterious.", + "12": "The forest floor is littered with wet leaves, their colors muted under the gray sky." + }, + "swamp": { + "1": "The swamp is dim and quiet, the overcast sky adding to its somber air.", + "2": "Clouds reflect faintly in the still, murky waters of the swamp.", + "3": "The air is thick and cool, with the overcast sky amplifying the swamp's humidity.", + "4": "Dark waters mirror the gray sky, giving the swamp an eerie feel.", + "5": "The overcast sky casts a uniform light, making it hard to tell the time of day.", + "6": "The swamp's vegetation appears darker, its colors dulled under the gray sky.", + "7": "Sounds of croaking frogs and buzzing insects are faint under the heavy clouds.", + "8": "The air smells of wet earth and decaying vegetation, intensified by the clouds above.", + "9": "Muddy paths through the swamp are slick, the overcast sky hinting at rain.", + "10": "The swamp feels endless, its murky waters blending with the cloudy sky.", + "11": "Tall reeds sway gently, their tips brushing against the low-hanging clouds.", + "12": "The overcast sky casts a pale, diffused light across the swamp’s surface." + }, + "jungle": { + "1": "The jungle canopy is dense and dark, the overcast sky adding to its gloom.", + "2": "Leaves glisten faintly under the dim, even light of the cloudy sky.", + "3": "The overcast sky makes the jungle feel suffocating and still.", + "4": "Moisture clings to every surface, the air thick and heavy with dampness.", + "5": "Animal calls are sparse, their usual energy subdued by the gloomy weather.", + "6": "The jungle floor is darker than usual, the clouds blocking most light.", + "7": "Vines drip with condensation, reflecting the faint light of the overcast sky.", + "8": "The jungle’s colors are muted, its vibrant greens dimmed by the heavy clouds.", + "9": "Insects buzz lazily, their hum blending with the faint sound of dripping water.", + "10": "The overcast sky makes the jungle feel timeless, as if frozen in shadow.", + "11": "The dense air smells of wet leaves and damp soil, intensified by the heavy clouds.", + "12": "Streams of water cut through the jungle, reflecting the gray, overcast sky." + }, + "hills": { + "1": "Rolling hills lie under a thick gray sky, their peaks blending with the clouds.", + "2": "The overcast sky casts a pale light over the grassy slopes.", + "3": "A cool breeze whispers through the hills, carrying a faint hint of dampness.", + "4": "The clouds hang low, brushing against the tallest crests of the hills.", + "5": "The air feels heavy as the overcast sky stretches endlessly above.", + "6": "The hills seem muted, their vibrant greens dulled by the gray light.", + "7": "Small streams glisten faintly under the dim, even light of the sky.", + "8": "The horizon is obscured, blending seamlessly with the cloud-filled sky.", + "9": "Shepherds guide their flocks, the overcast sky casting faint, diffuse shadows.", + "10": "The grass sways gently, the overcast sky making the landscape feel still.", + "11": "The hills echo softly with distant calls, the sound dampened by thick clouds.", + "12": "The air smells of earth and grass, amplified by the cool, cloudy weather." + }, + "mountains": { + "1": "The mountain peaks are shrouded in gray clouds, their outlines faint.", + "2": "Cliffs and crags appear darker under the overcast sky.", + "3": "A chill wind carries the scent of stone and damp air through the mountain pass.", + "4": "The overcast sky blurs the sharp edges of the mountain ridges.", + "5": "The clouds hang low, cloaking the highest peaks in a veil of mist.", + "6": "Shadows are faint, the overcast sky casting a muted light across the slopes.", + "7": "The sound of distant waterfalls echoes eerily under the heavy cloud cover.", + "8": "The air is cold and still, the overcast sky amplifying the mountain’s silence.", + "9": "Rocky paths glisten with dampness, reflecting the pale light above.", + "10": "The mountains feel timeless, their peaks disappearing into the gray sky.", + "11": "Eagles circle silently, their dark shapes barely visible against the clouds.", + "12": "The overcast sky casts a somber mood over the towering peaks." + }, + "desert": { + "1": "The desert stretches endlessly under a flat, gray sky.", + "2": "The overcast clouds cast a dull, cool light over the sandy expanse.", + "3": "The air feels dry and heavy, the muted light softening the desert’s harsh edges.", + "4": "Dunes appear lifeless, their golden hues dimmed by the gray sky.", + "5": "The overcast sky makes the desert feel eerie and unending.", + "6": "The horizon is blurred, the clouds blending seamlessly with the desert sands.", + "7": "A faint wind stirs the sand, creating ghostly patterns beneath the heavy sky.", + "8": "Sparse vegetation casts no shadows under the even light of the overcast sky.", + "9": "The desert’s usual heat is absent, replaced by a cool, oppressive stillness.", + "10": "Shimmering mirages are dulled, the overcast sky making them seem unreal.", + "11": "The air smells faintly of dust and stone, carried on a weak breeze.", + "12": "The vast desert feels silent and subdued beneath the thick layer of clouds." + }, + "coastal": { + "1": "Waves crash against the shore, their foam blending with the gray sky above.", + "2": "The horizon is obscured, the sea merging seamlessly with the overcast clouds.", + "3": "The air is damp and salty, heavy with the scent of the ocean.", + "4": "Fishing boats bob quietly, their sails dark against the pale, cloudy sky.", + "5": "The overcast sky casts a somber light over the rocky shoreline.", + "6": "Seabirds call faintly, their cries lost in the muted sound of the waves.", + "7": "The beach appears washed out, the sand a pale gray under the heavy clouds.", + "8": "The sea reflects the dull gray of the sky, its surface calm and flat.", + "9": "Cool gusts carry the smell of seaweed and salt through the air.", + "10": "The overcast sky makes the coastal cliffs appear taller and more imposing.", + "11": "The tide creeps in silently, the water dark and mirror-like beneath the clouds.", + "12": "The coastal village seems hushed, its sounds absorbed by the heavy sky." + }, + "volcano": { + "1": "The dark, overcast sky adds an eerie mood to the smoldering volcanic slopes.", + "2": "Ash and clouds mix, making it difficult to distinguish the horizon.", + "3": "The air smells faintly of sulfur, the overcast sky amplifying the volcano’s presence.", + "4": "The lava’s glow is faint, dulled by the heavy, gray clouds overhead.", + "5": "The volcano’s peak is obscured, disappearing into the thick cloud cover.", + "6": "The ground feels warm underfoot, contrasting with the cool, overcast air.", + "7": "Dark rock formations appear even more menacing beneath the oppressive sky.", + "8": "Steam vents hiss softly, their white plumes blending with the clouds.", + "9": "The overcast sky casts an unnatural light over the volcanic landscape.", + "10": "The volcano feels otherworldly, its power muted but ever-present beneath the gray sky.", + "11": "A faint wind carries ash and dust, the heavy clouds adding to the gloom.", + "12": "The overcast weather deepens the sense of foreboding around the active volcano." + }, + "artic": { + "1": "The icy landscape is muted under a blanket of thick, gray clouds.", + "2": "Snowfields stretch endlessly, blending into the overcast sky.", + "3": "The overcast light gives the ice an eerie, bluish hue.", + "4": "The air is biting cold, the heavy sky amplifying the Arctic’s desolation.", + "5": "Glaciers loom silently, their edges softened by the diffused light.", + "6": "The horizon disappears into the clouds, making the Arctic feel infinite.", + "7": "A faint wind stirs the snow, the overcast sky casting an oppressive stillness.", + "8": "The snow reflects the gray sky, creating a blinding, muted glow.", + "9": "The Arctic feels timeless and cold, the overcast sky adding to its isolation.", + "10": "Icebergs drift quietly, their shadows faint against the pale light.", + "11": "The only sound is the crunch of snow underfoot, the overcast sky silencing all else.", + "12": "The overcast sky makes the icy expanse feel lifeless and endless." + }, + "cursed": { + "1": "The overcast sky seems darker here, its gray tinged with an unnatural hue.", + "2": "A chilling wind carries whispers through the air, the clouds thick and oppressive.", + "3": "The cursed landscape feels frozen in time, the overcast sky adding to its dread.", + "4": "The ground is blackened, the overcast sky casting eerie, uneven shadows.", + "5": "The air feels heavy and foreboding, as if the clouds themselves carry a curse.", + "6": "No light pierces the overcast sky, leaving the land in perpetual twilight.", + "7": "The overcast sky is silent and oppressive, adding to the cursed land’s unease.", + "8": "Strange shapes seem to move within the clouds, adding to the eerie atmosphere.", + "9": "The overcast sky feels alive, shifting and swirling in unnatural patterns.", + "10": "Even the wind feels cursed, carrying faint wails through the heavy air.", + "11": "The land is eerily quiet, the overcast sky muting all sound.", + "12": "The cursed land feels suffocating, the overcast sky pressing down like a weight." + } + } + }, + "Partly Cloudy": { + "conditions": { + "temperature": { "gte": 40, "lte": 60 }, + "precipitation": { "lte": 60 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 40, "lte": 70 }, + "cloudCover": { "gte": 30, "lte": 70 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "The sun peeks through patches of clouds, casting shifting shadows over the fields.", + "2": "A cool breeze rustles the crops as fluffy clouds drift lazily overhead.", + "3": "The sky alternates between sunlight and shade, making the farm feel alive with movement.", + "4": "Soft sunlight filters through the clouds, warming the dirt paths between the barns.", + "5": "A patchwork of light and shadow dances across the open farmland.", + "6": "The day feels mild as the sun and clouds take turns dominating the sky.", + "7": "Birds chirp in the hedgerows, undisturbed by the partly cloudy weather.", + "8": "Gentle sunbeams break through the clouds, adding warmth to the farmstead.", + "9": "The clouds move slowly, providing welcome shade to the grazing animals.", + "10": "Golden light spills across the fields whenever the sun escapes the clouds.", + "11": "The barn roofs glint intermittently as sunlight plays across them.", + "12": "The day feels peaceful, the mix of clouds and sun lending a soft balance." + }, + "village": { + "1": "The cobblestone streets are dappled with light as clouds drift by overhead.", + "2": "Children play in the square under a sky that alternates between sun and shadow.", + "3": "Villagers chat outside their homes, enjoying the pleasant mix of sun and cloud.", + "4": "Soft sunlight filters through the clouds, brightening the thatched rooftops.", + "5": "The village feels calm, the partly cloudy sky adding a gentle ambiance.", + "6": "Smoke from chimneys curls upward, blending into the drifting clouds.", + "7": "A patch of sunlight warms the village green while shadows linger at the edges.", + "8": "Shutters creak as a light breeze accompanies the partly cloudy sky.", + "9": "The bell tower casts a faint shadow as sunlight breaks through the clouds.", + "10": "The air is crisp and fresh, the changing sky keeping the villagers alert.", + "11": "A rooster crows as sunlight streams onto the village square.", + "12": "The clouds shift constantly, casting patterns across the small cottages." + }, + "city": { + "1": "The stone walls of the city shimmer faintly as sunlight breaks through the clouds.", + "2": "Merchants hawk their wares under a sky that shifts between sun and shadow.", + "3": "The city streets are bustling, the partly cloudy weather adding a sense of ease.", + "4": "A watchman squints at the horizon, the clouds giving intermittent relief from the sun.", + "5": "Sunlight dances on the fountains as clouds drift lazily above.", + "6": "The market square is alive with activity, the weather neither too hot nor too cold.", + "7": "Shadows lengthen briefly as a patch of clouds blocks the sun.", + "8": "The city gates glint intermittently, the sunlight catching the ironwork.", + "9": "The rhythm of the city feels steady, the partly cloudy sky adding to its balance.", + "10": "The banners atop the towers flutter in a gentle breeze beneath scattered clouds.", + "11": "Light spills through gaps in the clouds, warming the crowded streets below.", + "12": "The hum of city life continues uninterrupted, the sky a soft mix of sun and cloud." + }, + "plains": { + "1": "The vast plains stretch under a sky of drifting clouds and patches of sunlight.", + "2": "Grass sways gently as the sun filters through scattered clouds above.", + "3": "The plains are alive with the rustling of wind and the shifting light.", + "4": "Distant hills are softened by the partly cloudy sky, adding depth to the horizon.", + "5": "The open expanse alternates between sunlit warmth and cool shadow.", + "6": "Herds of animals move lazily, basking in the mild weather.", + "7": "The plains feel boundless, the sky a canvas of soft clouds and golden light.", + "8": "A lone tree stands silhouetted against the ever-changing sky.", + "9": "The grasses ripple like waves under the gentle breeze and patchy sunlight.", + "10": "Travelers cross the plains, their figures dwarfed by the vast, shifting sky.", + "11": "The clouds cast fleeting patterns on the golden sea of grass.", + "12": "The air feels light and clear, the sun and clouds in perfect harmony." + }, + "forest": { + "1": "Sunlight filters through the leaves, creating a mosaic of light and shadow.", + "2": "The forest floor is dappled with sunbeams that break through the drifting clouds.", + "3": "Birdsong fills the air as the sky above alternates between sun and cloud.", + "4": "The trees sway gently, their tops brushing against the overcast sky.", + "5": "The forest feels alive with movement, the weather adding to its charm.", + "6": "A soft glow illuminates the underbrush whenever the sun escapes the clouds.", + "7": "The canopy filters the changing light, creating a serene, magical atmosphere.", + "8": "The overcast sky adds depth to the forest, highlighting the vibrant greens.", + "9": "Moss-covered trunks glisten faintly as sunlight pierces the shaded glades.", + "10": "The rustle of leaves accompanies the shifting light through the forest.", + "11": "The forest feels cool and inviting, the partly cloudy sky casting a calm light.", + "12": "The play of light and shadow makes the forest path seem almost enchanted." + }, + "swamp": { + "1": "The swamp is quiet, the sun peeking through clouds to warm the muddy ground.", + "2": "Shallow pools of water reflect the patchy sky above, rippling with movement.", + "3": "The air is thick and damp, the partly cloudy sky adding an eerie light.", + "4": "Trees with moss-draped limbs sway gently under a sky of drifting clouds.", + "5": "The swamp feels timeless, the weather adding a surreal glow to its stillness.", + "6": "Sunlight filters through the mist, adding a fleeting warmth to the swamp.", + "7": "Frogs croak in the distance, their calls blending with the shifting weather.", + "8": "The clouds cast fleeting shadows on the dark waters of the swamp.", + "9": "The swamp's earthy smell is strong, the air humid under the changing sky.", + "10": "A soft breeze stirs the reeds, the light shifting between sun and shadow.", + "11": "The swamp feels alive, the partly cloudy sky lending a strange serenity.", + "12": "Dragonflies dart through the air, their wings catching the sun as it breaks through." + }, + "jungle": { + "1": "The dense jungle canopy filters the sunlight, creating patches of golden light.", + "2": "The jungle hums with life, the partly cloudy sky casting soft shadows below.", + "3": "Bright flowers stand out vividly under the diffuse light of the scattered clouds.", + "4": "The air is humid and alive with the sounds of insects and birds.", + "5": "Sunlight streams through breaks in the canopy, illuminating the jungle floor.", + "6": "The jungle feels vibrant, the weather adding a calm warmth to the scene.", + "7": "Shadows flicker across the leaves as the sky shifts between sun and cloud.", + "8": "The air is thick with the scent of earth and plants, amplified by the weather.", + "9": "The jungle’s greens appear even deeper under the muted light of the sky.", + "10": "Vines and branches glisten with moisture, catching the sun’s fleeting rays.", + "11": "The jungle feels ancient and serene, the partly cloudy sky adding to its mystery.", + "12": "The weather brings a sense of peace, the jungle alive with subtle light shifts." + }, + "hills": { + "1": "Rolling hills bask under intermittent sunlight breaking through scattered clouds.", + "2": "Shadows drift across the slopes as clouds lazily cross the sky.", + "3": "Gentle breezes stir the grass, adding to the serene weather.", + "4": "Birds wheel in the open sky, darting between patches of light and shadow.", + "5": "The hills feel calm, the play of light adding depth to the landscape.", + "6": "Sunlight briefly illuminates a distant hill, highlighting its golden grass.", + "7": "A patch of shade offers a brief respite as clouds drift past.", + "8": "Soft light filters through the clouds, warming the grassy knolls.", + "9": "The scattered clouds make the landscape feel peaceful and open.", + "10": "The contrast of light and shadow accentuates the rolling terrain.", + "11": "The sun breaks through momentarily, brightening the hilltops.", + "12": "The air is crisp, with a mild warmth as sunlight peeks through." + }, + "mountains": { + "1": "The peaks glint with sunlight as the clouds shift and scatter.", + "2": "A patchwork of light and shadow dances across the rugged slopes.", + "3": "The sky is calm, with soft clouds adding texture to the horizon.", + "4": "The mountain paths are dappled with sunlight filtering through the clouds.", + "5": "The jagged cliffs alternate between brightness and shadow.", + "6": "A soft breeze carries the crisp mountain air under the partly cloudy sky.", + "7": "The clouds cast fleeting shadows on the snow-capped peaks.", + "8": "Sunlight bathes the valleys, bringing warmth to the cold mountain air.", + "9": "The mountains feel timeless, the weather lending a serene ambiance.", + "10": "Soft light filters over the rocky terrain, highlighting its rugged beauty.", + "11": "The peaks stand tall against a sky of shifting light and shade.", + "12": "Clouds roll past slowly, their shadows crawling over the slopes." + }, + "desert": { + "1": "The sands shimmer under the bright sunlight peeking through scattered clouds.", + "2": "Dunes are thrown into stark relief by the shifting play of light.", + "3": "The air is dry, the scattered clouds providing brief moments of shade.", + "4": "Sunlight glints off grains of sand, adding brilliance to the arid expanse.", + "5": "A lone cactus stands highlighted as sunlight breaks through the clouds.", + "6": "The horizon wavers with heat, the sky above dotted with drifting clouds.", + "7": "Shadows cast by the clouds create shifting patterns across the desert.", + "8": "The desert feels vast and timeless under the soft, partly cloudy sky.", + "9": "A caravan moves steadily, its path dappled by shifting light.", + "10": "The stillness of the desert is broken only by the dance of sun and shadow.", + "11": "Clouds drift lazily, their shadows a welcome reprieve from the glaring sun.", + "12": "The distant dunes glow warmly in the patches of sunlight." + }, + "coastal": { + "1": "Waves sparkle as sunlight breaks through the scattered clouds above.", + "2": "The sea reflects the partly cloudy sky, a patchwork of blue and white.", + "3": "The breeze carries the scent of salt, accompanied by shifting light.", + "4": "Seagulls cry out as they glide through the sunlit patches of sky.", + "5": "The rocky shore alternates between shadowed and sunlit as clouds drift past.", + "6": "The ocean feels vast and tranquil under the partly cloudy weather.", + "7": "Sunlight catches the crests of waves, creating a dazzling spectacle.", + "8": "Clouds cast moving shadows over the turquoise waters below.", + "9": "The air is fresh and cool, with the sun adding a gentle warmth.", + "10": "The interplay of clouds and light makes the coastline feel alive.", + "11": "A fishing boat bobs on the water, illuminated by a break in the clouds.", + "12": "The rhythm of the waves complements the calm of the changing sky." + }, + "volcano": { + "1": "The rugged slopes are illuminated as sunlight breaks through the clouds.", + "2": "The caldera casts a deep shadow under the partly cloudy sky.", + "3": "Faint wisps of smoke rise, blending into the drifting clouds above.", + "4": "The lava rock glints faintly when touched by the intermittent sunlight.", + "5": "The volcanic terrain feels stark yet beautiful under the shifting light.", + "6": "Heat shimmers above the ground, contrasted by the coolness of the scattered clouds.", + "7": "The clouds roll lazily, their shadows accentuating the craggy slopes.", + "8": "The sky feels heavy, but occasional sunbeams highlight the rugged landscape.", + "9": "The volcanic peaks appear even more imposing under the dappled sky.", + "10": "The interplay of light and shadow lends the volcano an eerie majesty.", + "11": "The ash-streaked ground glows faintly in the fleeting patches of sunlight.", + "12": "A faint smell of sulfur lingers in the air under the partly cloudy sky." + }, + "artic": { + "1": "Snowfields glisten under fleeting sunlight as clouds drift overhead.", + "2": "The ice sparkles brightly when touched by the scattered sunlight.", + "3": "The air is crisp and cold, the partly cloudy sky lending some brightness.", + "4": "Shadows shift across the icy terrain, adding depth to the white expanse.", + "5": "The tundra feels peaceful, the soft light enhancing its stark beauty.", + "6": "A chill breeze blows, the sunlight offering little warmth against the frost.", + "7": "The ice glows faintly under the ever-changing light of the sky.", + "8": "Snowdrifts shimmer, their edges softened by the diffuse light of the clouds.", + "9": "The arctic landscape feels boundless under the gentle light of the day.", + "10": "The frozen ground sparkles like a field of diamonds under the scattered sun.", + "11": "Clouds add texture to the endless sky, their shadows crawling over the ice.", + "12": "The quiet of the arctic is broken only by the whisper of the cold wind." + }, + "cursed": { + "1": "The cursed land feels eerie, with clouds casting ominous shadows.", + "2": "Dark patches of light shift across the ground, adding to the foreboding air.", + "3": "The clouds move unnaturally, as if stirred by unseen forces.", + "4": "Sunlight occasionally breaks through, illuminating the desolation.", + "5": "The landscape feels heavy, the scattered clouds adding a strange unease.", + "6": "The air carries a faint chill, the clouds above seeming to watch silently.", + "7": "Ruins glow faintly under fleeting sunlight, their shadows deepened by the clouds.", + "8": "The sky is a patchwork of light and darkness, mirroring the cursed ground.", + "9": "A sense of dread hangs in the air, amplified by the scattered light.", + "10": "The land feels oppressive, the interplay of clouds adding to its mystery.", + "11": "The clouds drift lazily, their shadows deepening the eerie silence.", + "12": "The cursed terrain shifts in the light, as if alive under the changing sky." + } + } + }, + "Persistent Downpour": { + "conditions": { + "temperature": { "gte": 50, "lte": 70 }, + "precipitation": { "gte": 60 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "The fields are waterlogged, with rivulets carving paths through the soil.", + "2": "Animals huddle in shelters as the rain pounds relentlessly on the barn roof.", + "3": "The sound of rain drowns out all other noises across the farmland.", + "4": "Mud clings to boots and cart wheels as the downpour shows no signs of stopping.", + "5": "Puddles form quickly, turning the farmland paths into streams.", + "6": "Rain drips steadily from the eaves of the farmhouse, creating small waterfalls.", + "7": "The crops sag under the constant weight of the falling rain.", + "8": "The air smells rich with earth and rain, the fields turning to mud.", + "9": "Distant thunder rumbles, blending with the steady drumming of the rain.", + "10": "The relentless rain reduces visibility, the farmland shrouded in mist.", + "11": "A soggy scarecrow stands forgotten in the middle of a drenched field.", + "12": "Rain pours from a dark sky, soaking everything in its path." + }, + "village": { + "1": "Rainwater overflows from the thatched roofs, forming muddy streets.", + "2": "Villagers rush between buildings, cloaks soaked from the endless rain.", + "3": "Lanterns struggle to stay lit as the downpour drenches the village square.", + "4": "Children splash through puddles while others huddle under awnings.", + "5": "The blacksmith’s forge hisses as raindrops sneak through gaps in the roof.", + "6": "Chickens cluck irritably, their coop turned into a muddy swamp.", + "7": "Rain drums loudly on the wooden shutters of every home.", + "8": "A dog shakes off water repeatedly, but the downpour is unrelenting.", + "9": "The sound of rain echoes through the narrow lanes of the village.", + "10": "The village well overflows, rainwater mixing with the runoff.", + "11": "Villagers exchange frustrated glances as the unyielding rain soaks everything.", + "12": "The village church bell rings faintly through the persistent rain." + }, + "city": { + "1": "Cobblestone streets turn slick as water streams through the gutters.", + "2": "Merchants struggle to keep their wares dry under sagging tarps.", + "3": "The city gates are shrouded in mist as rain reduces visibility.", + "4": "The steady downpour pools around market stalls, making trade difficult.", + "5": "Guards huddle under their cloaks, trying to shield themselves from the rain.", + "6": "Smoke from chimneys mingles with the rain, creating a hazy skyline.", + "7": "The sound of rainfall competes with the clatter of horse hooves on wet stone.", + "8": "Rainwater cascades off roofs, splashing onto the already saturated streets.", + "9": "Muddy footprints crisscross the city square, evidence of bustling activity despite the weather.", + "10": "Street performers pack up as the rain drives away their audience.", + "11": "The relentless rain creates a rhythmic backdrop to the city's usual chaos.", + "12": "Water drips from the city walls, forming small waterfalls near the gates." + }, + "plains": { + "1": "The wide expanse of grass is soaked, water pooling in the lowlands.", + "2": "The sound of rain hitting the open plains is a constant roar.", + "3": "Streams form quickly, cutting through the tall grass as the rain persists.", + "4": "Herds of animals move slowly, seeking shelter from the relentless downpour.", + "5": "Puddles dot the landscape, reflecting the dark clouds above.", + "6": "The grass is heavy with water, bending under the weight of the rain.", + "7": "Thunder rumbles across the open plains, amplifying the storm’s presence.", + "8": "The ground becomes a quagmire, each step sinking into mud.", + "9": "Rain streaks down, blurring the horizon in all directions.", + "10": "The plains glisten under the relentless rain, water collecting everywhere.", + "11": "Even the tallest grasses droop under the unyielding storm.", + "12": "A lone tree stands defiant, its leaves shedding torrents of water." + }, + "forest": { + "1": "Raindrops drum against the canopy, turning the forest floor into a muddy mess.", + "2": "Water streams down tree trunks, collecting in puddles at their bases.", + "3": "The thick forest mutes the sound of the downpour, creating a muffled roar.", + "4": "Leaves sag heavily under the constant weight of the falling rain.", + "5": "Animals scurry for shelter, their movements masked by the rain’s noise.", + "6": "The air is thick with the scent of wet earth and sodden wood.", + "7": "Streams of water cascade from leaves, forming rivulets along the ground.", + "8": "The forest paths are nearly impassable, churned into sticky mud.", + "9": "Mossy logs glisten in the rain, their surfaces slick and shiny.", + "10": "The canopy offers little protection as rain penetrates the dense foliage.", + "11": "The forest feels alive, every surface shimmering with rainfall.", + "12": "Rain drips steadily from branches, adding to the chorus of the storm." + }, + "swamp": { + "1": "The swamp is a chaotic mix of rising water and thick, sticky mud.", + "2": "Rain creates ripples across the murky waters, obscuring what lies beneath.", + "3": "The downpour intensifies the swamp's natural dampness, flooding low areas.", + "4": "Trees in the swamp drip constantly, adding to the already saturated ground.", + "5": "The air is thick with humidity, the rain blending seamlessly into the swamp’s haze.", + "6": "Croaks and chirps are drowned out by the steady drum of rain.", + "7": "The swamp feels endless under the heavy clouds and persistent rain.", + "8": "Pools of water overflow, creating an interconnected web of streams.", + "9": "The rain turns the swamp into a symphony of dripping and splashing.", + "10": "Vegetation droops under the weight of rain, adding to the swamp's eerie beauty.", + "11": "Mud bubbles as the swamp absorbs the endless torrent of water.", + "12": "The swamp’s stench intensifies, carried on the humid air of the storm." + }, + "jungle": { + "1": "The jungle steams as the rain mixes with the tropical heat.", + "2": "Dense foliage channels the rain into streams, flooding jungle paths.", + "3": "Raindrops strike the jungle canopy like drumbeats, echoing through the trees.", + "4": "The air is heavy, filled with the scent of wet vegetation and rain.", + "5": "Animals call out sporadically, their voices blending with the constant downpour.", + "6": "Vines glisten, dripping water onto the saturated jungle floor.", + "7": "The jungle paths are rivers of mud, treacherous underfoot.", + "8": "Water collects in giant leaves, spilling over when they can hold no more.", + "9": "Thunder rolls faintly, adding to the jungle's already overwhelming noise.", + "10": "The underbrush is slick and dripping, every surface coated in rain.", + "11": "Brightly colored flowers shimmer under the relentless rain.", + "12": "Streams carve through the jungle floor, adding to the labyrinth of vegetation." + }, + "hills": { + "1": "The rain cascades down the slopes, carving small streams into the hillsides.", + "2": "Puddles form in the valleys, while the hilltops remain drenched in mist.", + "3": "The hills are shrouded in rain, with distant peaks barely visible.", + "4": "Grass sways under the relentless downpour, heavy with water.", + "5": "Small creeks overflow, spilling into the lowlands between the hills.", + "6": "The rain turns dirt paths into slippery, treacherous trails.", + "7": "Fog blends with the rain, creating an eerie, muted landscape.", + "8": "The constant rain soaks the hills, leaving a sheen on every surface.", + "9": "Animals seek higher ground as water pools in the low-lying areas.", + "10": "The sound of rain dominates, a steady rhythm against the hillsides.", + "11": "Distant thunder echoes across the drenched landscape.", + "12": "The hills appear as shadowy outlines through the thick curtain of rain." + }, + "mountains": { + "1": "Waterfalls gush from the cliffs as rain cascades down the mountains.", + "2": "The rainclouds hang low, obscuring the peaks in a grey haze.", + "3": "Rocky paths are slick with rain, making travel perilous.", + "4": "Streams form quickly, rushing down the mountainsides in torrents.", + "5": "Sheltered caves drip constantly, offering little respite from the rain.", + "6": "The sound of rain echoes through narrow passes and steep valleys.", + "7": "The air is cold and damp, with every rock glistening under the rain.", + "8": "Misty rain clings to the slopes, making the mountain feel otherworldly.", + "9": "Small landslides occur as the soil struggles to hold against the deluge.", + "10": "Cloudbursts over the peaks send sheets of water tumbling downward.", + "11": "Animals retreat into the sparse shelter offered by the cliffs.", + "12": "The mountains are alive with the sound of rushing water and falling rain." + }, + "desert": { + "1": "The rain transforms the arid desert into a patchwork of muddy rivulets.", + "2": "Dry riverbeds roar to life, carrying rainwater through the barren land.", + "3": "Puddles form quickly on the hard, cracked ground of the desert.", + "4": "Cacti glisten under the rare sight of persistent rain.", + "5": "The air is filled with the earthy scent of wet sand and stone.", + "6": "Small streams carve temporary paths through the desert dunes.", + "7": "The rain turns dust into a sticky mess, clinging to everything.", + "8": "Sparse vegetation drinks deeply, briefly brightening the landscape.", + "9": "The desert, usually quiet, hums with the sound of raindrops hitting sand.", + "10": "Clouds dominate the sky, casting the desert in an unusual gloom.", + "11": "Footprints disappear quickly as rain washes over the desert floor.", + "12": "The horizon shimmers with rain rather than heat, an unusual sight." + }, + "coastal": { + "1": "Waves crash violently against the shore, fueled by the unrelenting rain.", + "2": "The sea and sky blur into one under the heavy downpour.", + "3": "Fishing boats remain anchored, their decks slick with rainwater.", + "4": "Rain streams down rocky cliffs, adding to the roar of the ocean below.", + "5": "The sandy beaches are pockmarked with puddles from the constant rain.", + "6": "Seagulls cry out, their calls barely audible over the sound of rain.", + "7": "Coastal paths turn into muddy trails, slippery and treacherous.", + "8": "The sea churns angrily, matching the dark storm clouds above.", + "9": "Coastal villages seem isolated, shrouded by rain and mist.", + "10": "Rain mixes with salt spray, creating a damp and heavy atmosphere.", + "11": "Driftwood washes ashore, carried by the relentless tide and rain.", + "12": "The ocean is a grey, restless expanse, blending seamlessly with the rain." + }, + "volcano": { + "1": "Rain steams as it strikes the warm volcanic rock, creating a hazy mist.", + "2": "Rivulets of water carve paths through the ashen soil around the volcano.", + "3": "The persistent rain dampens the ever-present smell of sulfur.", + "4": "Dark clouds cling to the volcanic peak, adding to its ominous presence.", + "5": "The lava flows are cooler, steaming under the deluge of rain.", + "6": "Rainwater collects in craters, forming temporary pools near the summit.", + "7": "Paths around the volcano are muddy and slick, treacherous to navigate.", + "8": "Steam rises where the rain meets warm fissures in the volcanic ground.", + "9": "Rain blurs the boundary between the ashen slopes and the grey sky.", + "10": "The volcanic peak looms, shrouded in mist and rain, an eerie silhouette.", + "11": "Gutters of water carry ash and debris down the slopes of the volcano.", + "12": "Lightning flickers within the storm clouds, adding to the volcano's menace." + }, + "artic": { + "1": "The rain freezes into icy pellets as it falls across the frozen landscape.", + "2": "Glacial surfaces glisten under the relentless downpour.", + "3": "Streams of water run over ice, creating slick and treacherous paths.", + "4": "The sound of rain is muffled by the thick layers of snow and ice.", + "5": "Rain turns to sleet as temperatures drop further in the Arctic chill.", + "6": "The icy expanse is dotted with puddles that quickly refreeze.", + "7": "Icicles grow heavier, dripping steadily with melting rainwater.", + "8": "The Arctic is blanketed in grey, the rainclouds adding to its desolation.", + "9": "Small icebergs crack and shift under the weight of persistent rain.", + "10": "The downpour dampens the sound of distant howls across the tundra.", + "11": "Glaciers glisten, streams carving through their icy surfaces.", + "12": "The Arctic feels alive with the sound of water against the frozen terrain." + }, + "cursed": { + "1": "The rain falls thick and heavy, yet evaporates before it touches the cursed ground.", + "2": "Puddles form in unnatural patterns, their surfaces rippling without wind.", + "3": "The cursed land glows faintly under the persistent downpour, eerie and unnatural.", + "4": "Dark figures seem to move in the rain, illusions cast by the cursed air.", + "5": "The rain smells faintly of sulfur, a warning of the land's corruption.", + "6": "Whispers seem to echo in the rain, carried on the cursed wind.", + "7": "Shadows deepen in the rain, the cursed ground absorbing all light.", + "8": "The downpour intensifies the oppressive atmosphere, making breathing difficult.", + "9": "The cursed rain feels heavy and unnatural, chilling to the bone.", + "10": "Mud in the cursed land seems to cling unnaturally, dragging at every step.", + "11": "The rain seems to glow faintly in the darkness, illuminating nothing.", + "12": "A sense of dread grows with every drop, the rain amplifying the curse." + } + } + }, + "Rain with Thunder": { + "conditions": { + "temperature": { "gte": 40, "lte": 70 }, + "precipitation": { "gte": 60 }, + "wind": { "gte": 20, "lte": 50 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "Thunder rumbles over the fields, rain soaking the crops and paths.", + "2": "Bright flashes of lightning illuminate the drenched farmland.", + "3": "Rain hammers against the wooden barns as thunder echoes in the distance.", + "4": "Horses in the stables neigh nervously at the sound of nearby thunder.", + "5": "The farmland is a sodden expanse, with muddy paths leading to overflowing ditches.", + "6": "Raindrops glisten on the wheat, while the sky roars with thunder.", + "7": "Workers hurriedly cover hay bales to protect them from the relentless rain.", + "8": "The farm's windmill creaks against the stormy gusts, lightning illuminating its blades.", + "9": "Thunder rolls, shaking the ground as rain turns dirt paths into streams.", + "10": "Flashes of light highlight rain-soaked scarecrows in the fields.", + "11": "Rain floods the vegetable patches as thunder cracks above.", + "12": "The storm's energy charges the air, with rain and thunder blending into chaos." + }, + "village": { + "1": "Rain lashes at thatched roofs as thunder crashes above the village square.", + "2": "Villagers huddle inside their homes, startled by the loud claps of thunder.", + "3": "Lightning casts eerie shadows on cobblestone streets as rain pours down.", + "4": "The bell tower chimes faintly against the roar of thunder and rain.", + "5": "Smoke from chimneys is washed away as rain drenches the village.", + "6": "Thunder shakes the ground, causing windows to rattle in their frames.", + "7": "The village well overflows as rainwater rushes down the narrow lanes.", + "8": "Children peek out from doorways, mesmerized by the lightning splitting the sky.", + "9": "Rain-soaked chickens scurry for cover as thunder echoes overhead.", + "10": "The village smith pauses, wiping rain from his brow as lightning flashes.", + "11": "Raindrops batter the market stalls, forcing merchants to pack up quickly.", + "12": "The storm creates rivers of water through the dirt streets of the village." + }, + "city": { + "1": "Rain cascades off rooftops, flooding narrow alleys as thunder booms.", + "2": "Lightning illuminates the tall spires of the city, followed by deafening thunder.", + "3": "The marketplace clears as rain and thunder send townsfolk scurrying for shelter.", + "4": "City guards huddle under their cloaks, enduring the storm's fury.", + "5": "The cobbled streets glisten under the downpour, reflecting flashes of lightning.", + "6": "Thunder reverberates through the stone walls, shaking windowpanes.", + "7": "The rain turns the city square into a quagmire of muddy puddles.", + "8": "Taverns fill quickly as citizens escape the relentless storm outside.", + "9": "Horses nervously stomp in their stables, startled by the thunder's roar.", + "10": "Lightning reveals the silhouettes of the city's towering gates through the rain.", + "11": "Rainwater rushes into drains, creating gurgling echoes through the streets.", + "12": "The city's bell tower tolls, barely audible over the storm's chaos." + }, + "plains": { + "1": "Thunder rolls across the open plains, carrying far in the empty expanse.", + "2": "Rain blankets the plains, turning the grasslands into a sea of swaying wet blades.", + "3": "Bright lightning briefly illuminates the horizon, followed by booming thunder.", + "4": "Storm clouds churn over the plains, pouring torrents of rain onto the earth.", + "5": "Puddles form quickly, pooling in the low areas of the vast grasslands.", + "6": "The rain creates a soft patter against the scattered rocks and shrubs.", + "7": "Animals on the plains scatter, startled by the thunderous sky.", + "8": "Distant trees stand silhouetted by lightning, their branches dripping with rain.", + "9": "The vast openness amplifies the storm, with thunder shaking the ground.", + "10": "Rain lashes against lone travelers, drenching their cloaks in moments.", + "11": "Water streams down the gentle slopes of the plains, carving temporary paths.", + "12": "The horizon is obscured by sheets of rain, broken only by flashes of lightning." + }, + "forest": { + "1": "Rain filters through the canopy, creating a rhythmic patter on the forest floor.", + "2": "Thunder echoes eerily among the trees, shaking the forest's stillness.", + "3": "Lightning strikes illuminate the forest, casting strange shadows on wet bark.", + "4": "Rain turns the dirt paths into muddy tracks, difficult to traverse.", + "5": "Animals retreat to their dens, startled by the storm's fury.", + "6": "Water drips steadily from the leaves, pooling around roots and rocks.", + "7": "The forest floor glistens as rain saturates the moss and fallen leaves.", + "8": "The sound of rain is amplified in the dense forest, a constant background hum.", + "9": "Thunder rumbles through the trees, sending birds flying in a panic.", + "10": "The storm creates small streams that weave around the tree trunks.", + "11": "Rain clings to spiderwebs, forming glistening beads in the dim light.", + "12": "Flashes of lightning momentarily reveal the dense and rain-soaked foliage." + }, + "swamp": { + "1": "Rain splashes into murky pools, sending ripples across the swamp's surface.", + "2": "Thunder crashes, the sound dampened by the swamp's dense, humid air.", + "3": "Lightning illuminates the twisted trees and shadowy waters of the swamp.", + "4": "The swamp is filled with the steady drip of rain, mixing with croaking frogs.", + "5": "Water levels rise quickly, submerging roots and low-lying pathways.", + "6": "The rain stirs up the swamp's muddy bottom, turning the water opaque.", + "7": "Thunder startles flocks of birds, sending them scattering into the rain.", + "8": "Insects swarm in the damp air, undeterred by the storm's intensity.", + "9": "Small streams form, carrying debris through the swampy terrain.", + "10": "Lightning flashes reflect off the swamp's water, illuminating eerie shapes.", + "11": "Rain drips from hanging moss, forming a curtain of water in the swamp.", + "12": "The swamp feels alive, the storm heightening every sound and movement." + }, + "jungle": { + "1": "Rain pours through the dense canopy, drenching the jungle floor.", + "2": "Thunder reverberates through the jungle, startling monkeys and birds alike.", + "3": "Lightning flickers, casting brief flashes of light on the thick vegetation.", + "4": "Water drips steadily from the leaves, creating an almost deafening rhythm.", + "5": "The jungle becomes a maze of slippery roots and muddy paths under the rain.", + "6": "Small streams form, rushing through the undergrowth and pooling in low spots.", + "7": "Rain mingles with the sounds of wildlife, creating a cacophony of life and storm.", + "8": "The dense jungle amplifies the thunder, making it seem closer than it is.", + "9": "Plants glisten under the rain, their leaves shining like polished emeralds.", + "10": "The air becomes thick and humid, the rain barely cooling the jungle heat.", + "11": "Lightning reveals vibrant colors hidden in the dense foliage.", + "12": "Rain transforms the jungle into a steaming, churning cauldron of life." + }, + "hills": { + "1": "Thunder rolls over the hills, echoing in the valleys below.", + "2": "Rain cascades down the slopes, turning trails into muddy streams.", + "3": "Lightning flashes illuminate the rolling terrain, momentarily brightening the dark sky.", + "4": "The sound of thunder seems to chase itself across the open hills.", + "5": "Rain slicks the grassy hillsides, making the footing treacherous.", + "6": "A lone tree on a hilltop is silhouetted by a sharp flash of lightning.", + "7": "Thunder reverberates through the hills, startling grazing animals.", + "8": "Rain soaks the soil, filling the small creeks that run through the hills.", + "9": "Lightning dances across the sky, briefly illuminating the undulating landscape.", + "10": "Storm clouds churn above, the downpour soaking every crest and valley.", + "11": "The wind carries the scent of rain and the distant sound of thunder.", + "12": "Water pools at the base of the hills, creating temporary marshy patches." + }, + "mountains": { + "1": "Thunder crashes between the peaks, the sound amplified by the rocky terrain.", + "2": "Rain runs down the cliffs, forming cascading waterfalls.", + "3": "Lightning reveals jagged peaks shrouded in storm clouds.", + "4": "The storm turns narrow mountain paths into dangerous torrents of water.", + "5": "Thunder booms, shaking loose small rocks from the cliffs.", + "6": "Rain pelts against the sheer rock faces, creating a constant drumming sound.", + "7": "The storm brings a chilling wind that cuts through the mountain air.", + "8": "Lightning flashes light up the high-altitude landscape, revealing storm-swept ridges.", + "9": "Rain pools in crevices, cascading down in sudden bursts.", + "10": "The storm reduces visibility, cloaking the peaks in a veil of rain and mist.", + "11": "The roar of thunder seems endless, rolling from one peak to the next.", + "12": "The mountain trails become slick and perilous under the pounding rain." + }, + "desert": { + "1": "Thunder rumbles across the arid landscape, a rare sound in the desert.", + "2": "Rain hisses as it strikes the hot sand, sending up faint steam.", + "3": "Lightning illuminates distant dunes, casting long, fleeting shadows.", + "4": "The storm fills dry riverbeds with sudden torrents of water.", + "5": "Thunder echoes eerily across the open desert, seemingly endless.", + "6": "Rain is absorbed quickly by the parched ground, leaving small puddles behind.", + "7": "Lightning reveals the barren terrain, stark and lifeless in the storm.", + "8": "The rain brings a momentary coolness to the otherwise searing desert air.", + "9": "Storm clouds gather over the dunes, unleashing a rare and powerful downpour.", + "10": "The sound of thunder rolls unbroken across the vast, empty expanse.", + "11": "Rain creates tiny rivulets in the dunes, carving paths through the sand.", + "12": "Flashes of lightning momentarily light up the endless horizon." + }, + "coastal": { + "1": "Thunder crashes over the waves, blending with the roar of the sea.", + "2": "Rain lashes against the cliffs, turning rocky paths slippery and treacherous.", + "3": "Lightning reveals the turbulent ocean, its waves crashing furiously against the shore.", + "4": "Storm clouds hang low, pouring rain onto the sandy beaches below.", + "5": "The storm's fury turns the gentle sea breeze into a howling gale.", + "6": "Rainwater streams down the rocks, cascading into the sea below.", + "7": "Thunder shakes the ground, the sound carried far by the open water.", + "8": "The horizon is obscured by heavy rain, merging sea and sky into one.", + "9": "Lightning reflects off the wet sand, momentarily illuminating the coast.", + "10": "The storm churns the sea into a frothy, unrelenting chaos.", + "11": "The sound of thunder mixes with the crash of waves against the docks.", + "12": "Rain beats down on the shoreline, pooling in the tidal flats." + }, + "volcano": { + "1": "Thunder clashes with the distant rumble of the volcano, a cacophony of nature's fury.", + "2": "Rain sizzles as it hits the warm volcanic rock, sending up wisps of steam.", + "3": "Lightning illuminates the rugged terrain, revealing fissures and jagged stones.", + "4": "The storm adds an eerie ambiance to the already ominous volcanic landscape.", + "5": "Thunder rolls, blending with the distant groans of shifting earth.", + "6": "Rainwater collects in rocky crevices, quickly evaporating in the heat.", + "7": "The storm paints the volcano in flashes of light and shadow.", + "8": "Rain darkens the ash-covered ground, creating slick, treacherous paths.", + "9": "Lightning briefly reveals plumes of steam rising from cracks in the earth.", + "10": "Thunder crashes, echoing off the rocky walls of the volcano's base.", + "11": "Rain turns volcanic dust into a sticky, muddy paste.", + "12": "The storm's energy feels alive, magnified by the volcano's dormant heat." + }, + "artic": { + "1": "Thunder booms across the frozen expanse, a rare and unsettling sound.", + "2": "Rain turns to ice as it strikes the frigid ground, coating everything in a slick layer.", + "3": "Lightning reflects off the glaciers, creating a dazzling display of light.", + "4": "The storm turns the snow into slush, difficult to traverse.", + "5": "Thunder reverberates off the icy cliffs, magnified in the cold stillness.", + "6": "Rainwater freezes in midair, creating a fine mist of ice crystals.", + "7": "The storm fills the arctic with a relentless cacophony of rain and thunder.", + "8": "Lightning streaks across the frozen sky, momentarily lighting up the tundra.", + "9": "Thunder shatters the stillness, its sound carried far by the icy winds.", + "10": "The rain forms icy rivulets, cutting paths through the snowpack.", + "11": "Rain freezes on contact, covering the arctic landscape in a shimmering glaze.", + "12": "The storm transforms the frozen wasteland into a chaotic, icy mire." + }, + "cursed": { + "1": "Thunder roars like the growl of an unseen beast, shaking the cursed ground.", + "2": "Rain falls in dark, oily drops, leaving a slick residue on the ground.", + "3": "Lightning reveals shadowy figures in the storm, vanishing as quickly as they appear.", + "4": "The storm feels alive, its thunder and rain pulsating with a sinister rhythm.", + "5": "Rainwater runs black, pooling in unnatural patterns across the cursed land.", + "6": "Thunder crashes, sending a wave of unease through the desolate terrain.", + "7": "Lightning flashes illuminate the cursed ground, revealing cracks and decay.", + "8": "The rain feels heavier here, almost oppressive, soaking into the tainted earth.", + "9": "Thunder seems to speak, its low rumble carrying whispers on the wind.", + "10": "The storm turns the cursed landscape into a churning morass of dark mud.", + "11": "Rain seeps into the ground, leaving an acrid, sulfurous scent in the air.", + "12": "The cursed storm's lightning feels unnatural, striking in erratic, violent bursts." + } + } + }, + "Scattered Showers": { + "conditions": { + "temperature": { "gte": 40, "lte": 80 }, + "precipitation": { "gte": 20, "lte": 70 }, + "wind": { "lte": 65 }, + "humidity": { "gte": 50, "lte": 70 }, + "cloudCover": { "gte": 50, "lte": 90 }, + "visibility": { "lte": 70 } + }, + "descriptions": { + "farm": { + "1": "Rain falls intermittently, leaving patches of damp earth across the fields.", + "2": "A brief shower waters the crops before the sun peeks out again.", + "3": "Clouds drift by, releasing occasional bursts of rain over the farmland.", + "4": "Scattered showers leave puddles between rows of crops.", + "5": "The rain comes and goes, refreshing the soil in short bursts.", + "6": "A mix of sunshine and rain creates a shimmering haze over the fields.", + "7": "Light showers dot the farm, bringing a cool reprieve to the warm day.", + "8": "Raindrops patter on the barn roof during a fleeting downpour.", + "9": "Clouds briefly darken the sky, showering the fields with rain.", + "10": "The showers are brief but frequent, soaking patches of the farmland.", + "11": "Rain comes in short bursts, followed quickly by moments of sunlight.", + "12": "The sound of a distant shower approaches, wetting the soil before moving on." + }, + "village": { + "1": "A brief shower dampens the cobblestone streets before sunlight returns.", + "2": "Scattered rain leaves patches of wetness on the village square.", + "3": "Villagers hurry to find cover as a passing shower rolls through.", + "4": "Rain falls in bursts, drumming on rooftops and filling barrels.", + "5": "Clouds bring intermittent rain, leaving the village refreshed.", + "6": "The rain comes and goes, creating a rhythm of wet and dry streets.", + "7": "A light shower cools the air, leaving a glistening sheen on wooden buildings.", + "8": "Children splash in puddles left behind by scattered rain.", + "9": "A sudden downpour wets the village briefly before moving on.", + "10": "Rain showers dot the village, creating a pleasant patter on roofs.", + "11": "Rain clouds drift over the village, delivering short, refreshing bursts.", + "12": "A fleeting rain passes, leaving the scent of fresh earth in its wake." + }, + "city": { + "1": "Rain briefly falls on the bustling streets, dampening cobblestones.", + "2": "Scattered showers bring a cool relief to the crowded marketplace.", + "3": "The patter of rain echoes through narrow alleys during a passing shower.", + "4": "A brief downpour sends citizens hurrying under awnings.", + "5": "Scattered rain leaves streaks on the city's stone walls.", + "6": "Showers come and go, wetting the plazas before sunlight breaks through.", + "7": "The rain cools the air in short bursts, refreshing the busy city.", + "8": "Drops of rain glisten on iron gates and cobblestones after a brief shower.", + "9": "Passing rain leaves puddles along the city's thoroughfares.", + "10": "A quick downpour washes dust from the streets before the sun reappears.", + "11": "Rain showers fall sporadically, adding a rhythmic backdrop to city life.", + "12": "Brief bursts of rain refresh the air, leaving the city sparkling under the sun." + }, + "plains": { + "1": "Scattered rain sweeps across the plains, leaving patches of wet grass.", + "2": "Showers come and go, nourishing the vast stretches of open land.", + "3": "Raindrops sparkle on wildflowers after a brief shower.", + "4": "Clouds drift over the plains, releasing sporadic bursts of rain.", + "5": "A light rain refreshes the grasslands, soaking the soil in short spurts.", + "6": "The sound of distant rain approaches, wetting the fields before moving on.", + "7": "Sunlight and rain alternate, creating a shimmering effect on the plains.", + "8": "Patches of rain dot the horizon, leaving the plains both wet and dry.", + "9": "The rain falls briefly, darkening the green grass before passing.", + "10": "Scattered showers sweep across the plains, leaving behind puddles.", + "11": "Rain clouds drift lazily over the plains, dropping intermittent showers.", + "12": "A fleeting rainstorm moves through, refreshing the vast, open land." + }, + "forest": { + "1": "Scattered showers drip through the canopy, dampening the forest floor.", + "2": "Rain falls in bursts, leaving leaves glistening in the fleeting sunlight.", + "3": "Showers come and go, their sound blending with rustling leaves.", + "4": "Raindrops patter against the leaves, refreshing the forest in waves.", + "5": "The forest smells of fresh rain after a brief shower.", + "6": "Light rain falls sporadically, leaving patches of wet earth and moss.", + "7": "Sunlight filters through rain-soaked leaves, creating a dappled glow.", + "8": "The sound of distant rain grows louder, passing quickly through the woods.", + "9": "Showers briefly quench the forest, their presence as fleeting as a breeze.", + "10": "Rain falls gently, its rhythm blending with the forest's natural sounds.", + "11": "The canopy shields much of the rain, but scattered drops still reach the ground.", + "12": "A passing rain leaves the forest refreshed, its foliage glistening with moisture." + }, + "swamp": { + "1": "Scattered rain adds to the swamp’s already soggy ground.", + "2": "Rain showers ripple across stagnant pools, leaving tiny waves behind.", + "3": "The swamp is briefly refreshed by intermittent rain.", + "4": "Raindrops plop into the muddy waters, creating countless small ripples.", + "5": "Rain comes and goes, mixing with the swamp's ever-present dampness.", + "6": "Light rain causes the air to grow even heavier with moisture.", + "7": "Showers fall sporadically, blending into the swamp’s natural humidity.", + "8": "A brief rain creates fresh puddles in the already waterlogged ground.", + "9": "The sound of rain mingles with the swamp's usual chorus of croaks and chirps.", + "10": "Rainwater drips from moss-covered trees, adding to the swamp's eerie ambiance.", + "11": "The rain is light but persistent, soaking the swamp’s twisted roots and vines.", + "12": "Scattered rain douses the swamp, its presence quickly swallowed by the marsh." + }, + "jungle": { + "1": "Rain showers soak the dense foliage, leaving the jungle refreshed.", + "2": "Scattered rain filters through the thick canopy, wetting the undergrowth.", + "3": "Brief bursts of rain add to the jungle's ever-present humidity.", + "4": "Raindrops tap against the broad leaves, creating a rhythmic backdrop.", + "5": "Light rain falls in intervals, mixing with the jungle’s natural sounds.", + "6": "Rain clouds drift over the jungle, releasing short, intense showers.", + "7": "A fleeting rain passes, leaving the jungle shimmering with moisture.", + "8": "The rain falls briefly, creating temporary streams in the dense undergrowth.", + "9": "Sunlight breaks through after a short rain, illuminating the wet foliage.", + "10": "Showers come and go, blending with the jungle’s vibrant symphony.", + "11": "Rainwater collects in the jungle’s dense undergrowth, pooling around roots.", + "12": "A brief rainstorm leaves the jungle’s greenery glistening under filtered sunlight." + }, + "hills": { + "1": "Rain showers sweep over the hills, leaving glistening patches of grass.", + "2": "A brief rain descends on the rolling hills, refreshing the greenery.", + "3": "Scattered showers leave wet streaks across the hilltops.", + "4": "Raindrops patter lightly on the hillsides, pooling in small dips.", + "5": "Rain clouds drift over the hills, releasing bursts of rain here and there.", + "6": "The hills alternate between sunlight and sporadic showers.", + "7": "A brief rainstorm dampens the slopes before quickly passing.", + "8": "Patches of rain soak the valleys between the hills.", + "9": "Rain comes in waves, leaving the hills both wet and dry.", + "10": "Showers briefly darken the soil of the hillsides.", + "11": "Raindrops glisten on wildflowers after a fleeting shower.", + "12": "Scattered showers leave puddles along the footpaths winding through the hills." + }, + "mountains": { + "1": "Rain falls in bursts, cascading down the rocky mountain slopes.", + "2": "Showers drift across the peaks, leaving them glistening under brief sunlight.", + "3": "Scattered rain wets the narrow mountain trails.", + "4": "Raindrops patter against stone and earth before the clouds move on.", + "5": "Intermittent showers create slick rocks on the mountain paths.", + "6": "The mountains alternate between misty rain and fleeting sunlight.", + "7": "Rain clouds briefly shroud the peaks, releasing their moisture before parting.", + "8": "Rain showers rush down the slopes, feeding mountain streams.", + "9": "Raindrops cling to alpine grasses after a short downpour.", + "10": "The sound of scattered rain echoes off the cliffs and crags.", + "11": "A passing shower refreshes the air, leaving a faint mist over the heights.", + "12": "Scattered showers dot the mountainside, leaving it cool and damp." + }, + "desert": { + "1": "A rare burst of rain dampens the desert sands before quickly evaporating.", + "2": "Scattered rain leaves fleeting dark spots on the arid landscape.", + "3": "A brief shower brings a hint of moisture to the parched ground.", + "4": "The rain falls lightly, forming small puddles in the cracked earth.", + "5": "Clouds bring a short rain, their shadow cooling the desert momentarily.", + "6": "Scattered showers sprinkle the dunes, their effect fleeting.", + "7": "Raindrops are quickly absorbed into the dry desert sand.", + "8": "A brief rain darkens the rocky outcrops before the sun returns.", + "9": "Short-lived rain cools the desert air, bringing temporary relief.", + "10": "Rain showers drift over the desert, leaving faint traces of moisture.", + "11": "A fleeting rainstorm brings life to dormant desert flora.", + "12": "Scattered rain fades as quickly as it begins, leaving the desert dry once more." + }, + "coastal": { + "1": "Scattered rain sweeps in from the sea, leaving the coast refreshed.", + "2": "Raindrops fall intermittently, blending with the salty ocean spray.", + "3": "Showers come and go, dampening the sandy shorelines.", + "4": "Clouds drift inland, releasing bursts of rain along the coast.", + "5": "Rain showers fall briefly, darkening the cliffs and rocky beaches.", + "6": "The coastal breeze carries intermittent rain across the shore.", + "7": "A brief downpour soaks the docks before the sun returns.", + "8": "Raindrops sparkle on seashells after a fleeting shower.", + "9": "Rain clouds drift over the water, spilling their contents on the shore.", + "10": "Scattered showers wet the coast, leaving a fresh, salty scent in the air.", + "11": "Rain patters on the waves and shoreline before fading into sunlight.", + "12": "Short rains pass quickly, leaving the coastline glistening under clear skies." + }, + "volcano": { + "1": "Rain falls sporadically, sizzling as it strikes the warm volcanic rock.", + "2": "Showers sweep through, briefly cooling the ash-streaked slopes.", + "3": "Scattered rain dampens the volcanic soil, forming small streams.", + "4": "Raindrops hiss against exposed lava flows during a fleeting shower.", + "5": "Clouds gather over the crater, releasing bursts of rain on the barren landscape.", + "6": "A brief rainstorm wets the rocky slopes, creating rivulets of water.", + "7": "Scattered showers darken the ashen terrain before moving on.", + "8": "Raindrops pool in the crevices of jagged volcanic rocks.", + "9": "Short rain showers cool the air, mingling with rising volcanic steam.", + "10": "Rain falls lightly, disappearing into the warm ground as quickly as it lands.", + "11": "A passing rain cools the volcanic slopes, leaving steam in its wake.", + "12": "The rain falls briefly, its presence marked by a faint hiss on heated stone." + }, + "artic": { + "1": "Scattered rain falls lightly, turning to ice on the frozen ground.", + "2": "Rain showers drift across the tundra, dampening the snow-covered plains.", + "3": "Raindrops glisten on icy surfaces after a fleeting downpour.", + "4": "Intermittent rain coats the arctic in a thin layer of ice.", + "5": "Rain clouds hover briefly, releasing short bursts over the frozen expanse.", + "6": "The cold rain falls lightly, freezing on contact with the ground.", + "7": "Raindrops mix with snow, leaving the arctic landscape slick and shiny.", + "8": "Scattered rain freezes as it lands, forming thin sheets of ice.", + "9": "A brief rainstorm adds a layer of slickness to the icy tundra.", + "10": "Rain comes and goes, creating a shimmering glaze on the frozen ground.", + "11": "Short rains chill the air further, their drops crystallizing upon impact.", + "12": "The arctic is briefly soaked by scattered rain, freezing almost instantly." + }, + "cursed": { + "1": "Scattered rain falls, tinged with an unnatural darkness.", + "2": "Rain showers come in bursts, leaving the cursed ground strangely damp.", + "3": "The rain carries a faint metallic taste, unsettling those beneath it.", + "4": "Raindrops fall sporadically, hissing as they touch the cursed earth.", + "5": "Brief rains seem to absorb light, leaving the air heavy and dark.", + "6": "Scattered showers fall, their droplets unnervingly warm.", + "7": "Rain falls in bursts, each drop feeling heavier than it should.", + "8": "The cursed rain leaves the ground steaming faintly after each brief shower.", + "9": "A passing rainstorm carries whispers with its falling droplets.", + "10": "Raindrops shimmer with an eerie light before vanishing on the ground.", + "11": "Scattered rain feels unnaturally cold, as if sapping warmth from the air.", + "12": "The rain seems alive, pooling unnaturally and vanishing without a trace." + } + } + }, + "Snow Flurries": { + "conditions": { + "temperature": { "gte": 20, "lte": 35 }, + "precipitation": { "gte": 10, "lte": 40 }, + "wind": { "lte": 55 }, + "humidity": { "gte": 50, "lte": 80 }, + "cloudCover": { "gte": 50 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Light snow flurries dance across the fields, barely dusting the crops.", + "2": "The air is alive with snowflakes, settling lightly on the barns and fences.", + "3": "Snow flurries drift through the farmstead, caught in the wind.", + "4": "A soft snow dusts the rooftops, leaving the farm looking frosted.", + "5": "Flurries swirl gently over the fields, giving a brief touch of winter.", + "6": "The snowflakes fall briefly, melting as they touch the ground.", + "7": "Flurries pass over the farm, leaving the livestock shaking off the cold.", + "8": "Snow falls lightly, caught on the branches of bare orchard trees.", + "9": "A fleeting snow passes over the farm, vanishing as quickly as it arrives.", + "10": "Flurries drift lazily, dusting the plowed soil and thatched roofs.", + "11": "A thin veil of snowflakes sweeps over the barns and pastures.", + "12": "Snow flurries fall softly, the breeze carrying them across the fields." + }, + "village": { + "1": "Snow flurries swirl through the narrow streets, dusting cobblestones.", + "2": "Villagers huddle in cloaks as flurries dance in the cold air.", + "3": "Snowflakes gather briefly on rooftops, melting with the next gust of wind.", + "4": "The flurries drift through the market square, catching on wooden stalls.", + "5": "Snow falls lightly, frosting the village's stone walls and thatched roofs.", + "6": "A brief flurry leaves the village streets looking faintly powdered.", + "7": "Snow swirls through alleys and around chimneys, vanishing as it falls.", + "8": "The wind carries snowflakes past shuttered windows and lantern posts.", + "9": "Flurries settle on the rooftops, creating a fleeting blanket of white.", + "10": "A soft snow falls over the village, painting it in faint winter hues.", + "11": "Snow flurries float lightly, clinging to the branches of bare trees.", + "12": "The air is crisp as snowflakes drift gently through the village." + }, + "city": { + "1": "Snow flurries spiral through the bustling streets, vanishing on warm stone.", + "2": "Snow gathers briefly on rooftops before melting under the city’s heat.", + "3": "Flurries sweep through market stalls, dusting goods and wagons.", + "4": "Snowflakes drift around the towering spires, caught in the city wind.", + "5": "A thin snowfall covers the cobblestones before being trampled underfoot.", + "6": "The flurries swirl past taverns and guildhalls, leaving only a faint chill.", + "7": "Snow drifts lightly through the air, caught in the hum of city life.", + "8": "Flurries pass through narrow alleys, briefly settling on window ledges.", + "9": "Snowflakes cling to cloaks and hoods, disappearing as fast as they fall.", + "10": "Snow dances above chimneys, fading in the warmth of rising smoke.", + "11": "The city streets gleam faintly white as flurries sweep over them.", + "12": "A fleeting snowfall kisses the city, leaving no trace on its stone walls." + }, + "plains": { + "1": "Snow flurries race across the open plains, vanishing into the horizon.", + "2": "A light snowfall drifts over the grass, leaving patches of frost.", + "3": "Flurries swirl in the wind, barely touching the rolling fields.", + "4": "Snowflakes fall gently, melting as they land on the open plains.", + "5": "A fleeting snowfall dances across the grasses, painting them white.", + "6": "The plains shimmer briefly as snowflakes settle on the dew-covered ground.", + "7": "Snow flurries drift lazily, carried far by the steady wind.", + "8": "A passing snowfall blankets the plains for only a moment.", + "9": "The air is filled with soft flurries, a fleeting reminder of winter.", + "10": "Snow dances across the vast fields, clinging to nothing for long.", + "11": "The plains are veiled in a fine mist of falling snow.", + "12": "Flurries sweep through, leaving the plains both chilled and refreshed." + }, + "forest": { + "1": "Snow flurries filter through the trees, dusting the forest floor.", + "2": "Flurries swirl between branches, melting on the damp leaves below.", + "3": "A light snowfall gathers on mossy rocks and fallen logs.", + "4": "Snowflakes drift through the canopy, sparkling in the muted light.", + "5": "Flurries dance in the stillness of the forest, settling on bare branches.", + "6": "Snow gathers briefly on the forest floor, mingling with fallen leaves.", + "7": "A soft snowfall cloaks the forest in a fleeting winter chill.", + "8": "Flurries twirl in the air, resting gently on shrubs and bushes.", + "9": "The forest is quiet as snowflakes fall in delicate waves.", + "10": "Snow drifts between trees, clinging to bark and undergrowth.", + "11": "Flurries weave through the branches, creating a whisper of winter.", + "12": "The forest glimmers faintly white as snowflakes settle on its surface." + }, + "swamp": { + "1": "Snow flurries melt as they touch the swamp’s wet ground.", + "2": "Flurries fall lightly, leaving faint traces on gnarled roots.", + "3": "Snowflakes drift through the swamp’s mist, vanishing on contact.", + "4": "A brief snowfall clings to mossy branches before disappearing.", + "5": "Snow swirls around stagnant pools, creating ripples as it lands.", + "6": "Flurries settle briefly on the swamp’s vegetation, a rare sight in the mire.", + "7": "Snowflakes fall through the swamp's canopy, lost in its dampness.", + "8": "The swamp glimmers as flurries catch on hanging vines and moss.", + "9": "Snow dances above the murky water, melting with each touch.", + "10": "Flurries weave through the swamp, their cold breath lingering briefly.", + "11": "Snowfall adds a touch of white to the swamp’s usual murky hues.", + "12": "A fleeting snowstorm leaves faint traces on the swamp’s still surface." + }, + "jungle": { + "1": "Snow flurries struggle to reach the jungle floor, melting in the humidity.", + "2": "Flurries swirl through the jungle’s dense canopy, a fleeting phenomenon.", + "3": "Snowflakes mingle with the jungle mist, vanishing almost instantly.", + "4": "A rare snowfall dusts the highest leaves of the jungle trees.", + "5": "Snow dances through the humid air, melting before it can settle.", + "6": "Flurries drift between vines, disappearing in the jungle's warmth.", + "7": "Snowflakes sparkle briefly on broad leaves before turning to water.", + "8": "A fleeting snowstorm leaves droplets on the jungle’s dense foliage.", + "9": "The jungle air grows cool as flurries fall for a moment.", + "10": "Snow mingles with mist, creating an unusual chill in the jungle.", + "11": "Flurries swirl among the treetops, leaving no trace behind.", + "12": "Snowfall adds a fleeting whiteness to the jungle’s vibrant greenery." + }, + "hills": { + "1": "Snow flurries swirl over the rolling hills, dusting the grass with white.", + "2": "The wind carries flurries across the slopes, fleeting in the chill air.", + "3": "Snowflakes drift gently, clinging to the sparse shrubs dotting the hills.", + "4": "A light snowfall swirls along the ridges, vanishing into the breeze.", + "5": "Snow dances briefly on the hilltops before disappearing into the ground.", + "6": "Flurries sweep across the valleys, leaving a faint frost behind.", + "7": "The air is crisp as snowflakes settle lightly on the hillsides.", + "8": "Snow flurries swirl along the hill crests, caught in the steady wind.", + "9": "A fleeting snow dusts the hills, melting as it touches the ground.", + "10": "The hills shimmer faintly as snowflakes cling to the frosted grass.", + "11": "Snow flurries pass through the hills, leaving no trace behind.", + "12": "Flurries dance in the cool air, briefly covering the hills with frost." + }, + "mountains": { + "1": "Snow flurries whip around the rocky peaks, carried by strong winds.", + "2": "A light snowfall settles on the crags, frosting the jagged stones.", + "3": "Flurries spiral down the cliffs, vanishing into the deep ravines.", + "4": "Snowflakes dance in the mountain air, clinging to ledges and ridges.", + "5": "A fleeting snowstorm sweeps across the peaks, leaving a thin white layer.", + "6": "Flurries swirl through the mountain passes, caught in the howling wind.", + "7": "Snow drifts gently onto the rocky terrain, adding a fleeting touch of winter.", + "8": "The mountains glisten faintly as snowflakes catch on their rugged surfaces.", + "9": "Snow flurries swirl around the peaks, creating a fleeting winter scene.", + "10": "A soft snow falls on the mountain trails, melting quickly on the stone.", + "11": "Flurries dance around the cliffs, leaving a light dusting behind.", + "12": "The peaks shimmer as snowflakes settle briefly before the wind takes them." + }, + "desert": { + "1": "Snow flurries fall lightly, melting as they touch the warm sands.", + "2": "A rare snowfall drifts through the desert, vanishing in the arid heat.", + "3": "Snowflakes swirl in the desert breeze, settling briefly on the dunes.", + "4": "Flurries sweep across the sand, creating a fleeting frost-like effect.", + "5": "Snow dances in the desert air, melting as quickly as it falls.", + "6": "The desert glimmers briefly as snowflakes vanish upon touching the ground.", + "7": "A fleeting snowfall surprises the desert, leaving no trace behind.", + "8": "Snow flurries drift along the dunes, sparkling in the cold night air.", + "9": "A rare chill brings flurries to the desert, quickly disappearing in the heat.", + "10": "The sands appear frosted as snowflakes briefly rest before melting away.", + "11": "Snow swirls gently through the desert, a rare and fleeting phenomenon.", + "12": "Flurries pass over the desert, creating an eerie contrast with the sands." + }, + "coastal": { + "1": "Snow flurries drift over the crashing waves, melting in the salty air.", + "2": "A light snow falls along the shore, dusting the sand briefly.", + "3": "Flurries swirl through the coastal breeze, settling on rocks and piers.", + "4": "Snowflakes dance in the sea air, clinging to the tops of waves.", + "5": "A fleeting snowfall blankets the shoreline, fading with the tide.", + "6": "Snow flurries mix with the mist, creating a ghostly coastal scene.", + "7": "The wind carries snowflakes across the docks, vanishing into the sea spray.", + "8": "Flurries settle on the beach, leaving faint traces before disappearing.", + "9": "Snow drifts over the water, melting as it touches the waves.", + "10": "A light snow frosts the coastal cliffs, a brief touch of winter by the sea.", + "11": "Snow flurries swirl around the fishing boats, clinging to their ropes.", + "12": "The coast shimmers faintly white as flurries dust the rocks and sand." + }, + "volcano": { + "1": "Snow flurries fall around the volcano, quickly melting on the warm ground.", + "2": "Flurries swirl in the heat, vanishing before they reach the lava flows.", + "3": "Snowflakes drift briefly over the volcanic slopes, a fleeting chill in the heat.", + "4": "The air sparkles with snowflakes, quickly disappearing in the volcanic steam.", + "5": "Snow dances near the crater, melting in the rising heat of the volcano.", + "6": "Flurries swirl in the ash-laden air, a stark contrast against the fiery terrain.", + "7": "Snowflakes fall lightly, vanishing as they meet the warm volcanic stone.", + "8": "A rare snowfall drifts over the volcano, disappearing as fast as it came.", + "9": "The volcano steams as flurries fall, leaving no trace of their presence.", + "10": "Snow settles briefly on the cooler rocks, melting in moments.", + "11": "Flurries sweep over the volcano, fading in the intense heat.", + "12": "The volcanic slopes glimmer faintly as snowflakes fall and vanish." + }, + "artic": { + "1": "Snow flurries sweep across the frozen expanse, blending into the icy landscape.", + "2": "The arctic wind carries snowflakes far across the icy tundra.", + "3": "Flurries dance in the air, settling lightly on the frozen ground.", + "4": "Snow swirls through the biting cold, adding a faint shimmer to the ice.", + "5": "Flurries sweep over the arctic, blending seamlessly into the frosty terrain.", + "6": "Snowflakes glisten as they fall, caught in the still arctic air.", + "7": "A fleeting snowfall adds a soft layer to the already frosty ground.", + "8": "Flurries drift over the ice, adding a subtle brilliance to the tundra.", + "9": "Snow dances in the arctic winds, creating a fleeting winter spectacle.", + "10": "The frozen landscape glows faintly as snowflakes settle for a moment.", + "11": "Flurries whirl across the ice, leaving the arctic untouched and serene.", + "12": "Snowfall mixes with the arctic winds, a constant presence in the cold." + }, + "cursed": { + "1": "Snow flurries swirl unnaturally, seeming to avoid certain parts of the land.", + "2": "The cursed air carries flurries, each snowflake falling with an eerie stillness.", + "3": "Flurries fall in spirals, forming strange patterns on the cursed ground.", + "4": "Snowflakes hover briefly in the air, defying natural movement.", + "5": "Snow swirls and vanishes as it nears the darkened ground of the cursed land.", + "6": "Flurries fall silently, their cold touch spreading an unnatural chill.", + "7": "The snow falls in chaotic bursts, as though driven by an unseen force.", + "8": "Flurries seem to whisper as they fall, their shapes twisting unnaturally.", + "9": "Snow dances in defiance of the cursed winds, falling in scattered patterns.", + "10": "Flurries drift hesitantly, vanishing upon contact with the darkened soil.", + "11": "Snowflakes shimmer briefly, their presence fleeting in the cursed atmosphere.", + "12": "The flurries swirl ominously, their descent carrying an unearthly chill." + } + } + }, + "Sleet": { + "conditions": { + "temperature": { "gte": 30, "lte": 40 }, + "precipitation": { "gte": 50, "lte": 80 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "Icy sleet pelts the crops, coating fields with a thin, slippery layer.", + "2": "Sleet falls relentlessly, turning dirt paths into muddy, frozen tracks.", + "3": "The rooftops of farmhouses glisten with frozen sleet in the pale light.", + "4": "A mix of rain and ice lashes against the barn, creating a rhythmic patter.", + "5": "Sleet clings to the fences, transforming them into frosted outlines.", + "6": "The icy drizzle makes tending livestock treacherous on the slippery ground.", + "7": "Farm animals huddle together as the sleet coats the pasture with icy layers.", + "8": "Sleet strikes the wooden shingles, creating a chorus of icy impacts.", + "9": "The fields glisten faintly, each blade of grass encased in icy sleet.", + "10": "Sleet freezes on the tools left outside, encasing them in thin layers of ice.", + "11": "Farmers wrap themselves tightly as sleet turns the landscape into a frozen mirror.", + "12": "The air is filled with the sharp hiss of sleet striking bare earth and stone." + }, + "village": { + "1": "Sleet coats the cobblestone streets, making every step a challenge.", + "2": "Villagers rush to secure shutters as icy sleet peppers the rooftops.", + "3": "Thin layers of ice form on wagons, weighing them down under the relentless sleet.", + "4": "Children watch from windows as sleet transforms the square into a slippery mess.", + "5": "The village well glistens with ice as sleet hardens its wooden frame.", + "6": "Lanterns flicker in the freezing wind, their glass frosted by the falling sleet.", + "7": "Sleet slides off thatched roofs, gathering in icy heaps below.", + "8": "Villagers spread sand on the paths, fighting the encroaching ice from the sleet.", + "9": "Sleet turns the market stalls into icy relics under a slick, frozen coating.", + "10": "The air is filled with the sound of sleet pinging against wooden beams.", + "11": "Sleet creates a glossy sheen on stone walls, reflecting the pale light.", + "12": "The smithy’s anvil glistens under a layer of sleet, untouched by the heat within." + }, + "city": { + "1": "Sleet clogs the gutters, forming icy blockages that send water spilling over.", + "2": "City streets become slick and treacherous as sleet turns to ice underfoot.", + "3": "Sleet bounces off stone buildings, freezing into glimmering icicles on ledges.", + "4": "The marketplace is abandoned, slick with a dangerous glaze of sleet.", + "5": "Citizens huddle under cloaks as sleet makes even short walks perilous.", + "6": "Torches sputter and sizzle as sleet dampens the city’s narrow streets.", + "7": "Sleet rattles against glass windows, freezing into delicate patterns overnight.", + "8": "The city gates creak as sleet forms icy layers on their iron bars.", + "9": "Bridges glisten dangerously as sleet covers their surfaces in slippery frost.", + "10": "The sound of sleet striking cobblestones fills the city, a constant icy patter.", + "11": "Sleet cascades from rooftops, freezing into sharp icicles along the edges.", + "12": "Horse-drawn carts struggle for traction as sleet covers the streets in ice." + }, + "plains": { + "1": "Sleet lashes across the open plains, freezing the tall grasses under icy layers.", + "2": "The horizon shimmers faintly as sleet coats the plains with a thin, glassy layer.", + "3": "Sleet falls in sheets, the icy winds driving it sideways across the flatlands.", + "4": "The ground crunches underfoot as sleet solidifies into a slippery crust.", + "5": "Sparse trees in the plains glisten with ice, their branches bending under the weight.", + "6": "Sleet forms icy rivulets in shallow depressions, freezing into jagged shapes.", + "7": "The plains are eerily quiet, the only sound the relentless hiss of sleet.", + "8": "Sleet pelts the wildflowers, encasing them in translucent ice.", + "9": "The vast expanse of the plains takes on a muted gleam under a coat of sleet.", + "10": "Sleet freezes on the few boulders scattered across the plains, creating icy sculptures.", + "11": "Grass blades bow under the weight of sleet, creating an icy wave across the plains.", + "12": "Sleet coats animal trails, making them treacherous and barely visible." + }, + "forest": { + "1": "Sleet falls steadily, coating the forest canopy with a thin layer of ice.", + "2": "Tree branches groan under the weight of accumulating sleet, cracking ominously.", + "3": "The forest floor turns slick and treacherous as sleet freezes on fallen leaves.", + "4": "Icy sleet clings to mossy trunks, creating a glistening sheen on the bark.", + "5": "Sleet filters through the branches, forming frozen puddles in the underbrush.", + "6": "The forest is silent save for the occasional crash of ice-laden branches breaking.", + "7": "Sleet encrusts the vines, turning them into shimmering icy cords.", + "8": "Snow and sleet mix on the forest trails, making every step precarious.", + "9": "The forest streams freeze partially under the assault of relentless sleet.", + "10": "Sleet coats the forest’s berry bushes, encasing their bright fruit in icy shells.", + "11": "The sound of sleet striking leaves creates a constant whisper throughout the woods.", + "12": "Icy winds drive the sleet deeper into the forest, glazing everything it touches." + }, + "swamp": { + "1": "Sleet turns the swamp’s surface into a patchwork of frozen puddles and icy mud.", + "2": "The icy rain clings to the reeds, bending them under the weight of the sleet.", + "3": "Pools of water begin to crust over as sleet freezes in the chilling air.", + "4": "Sleet clings to twisted branches, making the swamp shimmer in the dull light.", + "5": "The swamp’s boardwalks grow treacherous as sleet turns them into icy traps.", + "6": "Sleet freezes in the swamp’s murky waters, creating jagged patterns on the surface.", + "7": "The air is thick with the hiss of sleet hitting the waterlogged ground.", + "8": "Sleet settles on the swamp’s ferns, making their leaves heavy and brittle.", + "9": "The swamp creatures retreat as sleet turns their world into a frozen maze.", + "10": "Sleet coats the cattails, their fuzzy tops now encased in icy shells.", + "11": "The swamp’s mist mingles with sleet, creating an eerie, frozen atmosphere.", + "12": "Sleet covers the swamp’s tangled roots, making them slippery and treacherous." + }, + "jungle": { + "1": "Sleet falls in sharp bursts, freezing on the jungle’s broad leaves.", + "2": "The canopy glistens as sleet hardens on the jungle’s dense foliage.", + "3": "Sleet filters through the vines, creating icy patches on the jungle floor.", + "4": "Icy rain freezes on the jungle’s flowers, encasing them in fragile frost.", + "5": "Sleet clings to the trunks of towering trees, creating a shimmering layer of ice.", + "6": "The jungle becomes eerily quiet as sleet muffles its usual vibrant sounds.", + "7": "The air is heavy with icy moisture as sleet falls steadily through the jungle.", + "8": "Sleet turns the jungle paths into slick trails, challenging even seasoned travelers.", + "9": "Droplets freeze mid-drip as sleet coats the jungle’s hanging moss.", + "10": "Sleet gathers in icy pools at the bases of the jungle’s massive trees.", + "11": "The jungle’s thick vines glisten under a thin, icy coating of sleet.", + "12": "Sleet transforms the jungle’s dense undergrowth into a slippery, frozen maze." + }, + "hills": { + "1": "Sleet cascades down the hills, freezing on the uneven ground.", + "2": "The grassy slopes glisten as sleet coats each blade of grass.", + "3": "Sleet pelts the hillsides, forming icy streams in the lower valleys.", + "4": "The rolling terrain becomes treacherous as sleet turns paths into frozen slides.", + "5": "Sparse trees bend under the icy weight of relentless sleet.", + "6": "Sleet freezes the rocky outcrops, creating jagged, glimmering formations.", + "7": "The hills echo with the hiss of sleet striking the sparse vegetation.", + "8": "Sleet coats the low shrubs, leaving them encased in brittle ice.", + "9": "The air grows colder as sleet solidifies into an icy crust across the hills.", + "10": "Travelers find the hill paths perilous, covered in a slick, icy sheen.", + "11": "The soft earth of the hills hardens as sleet freezes the surface.", + "12": "Sleet clings to the hilltop stones, making them glitter in the dull light." + }, + "mountains": { + "1": "Sleet lashes the mountain peaks, freezing on jagged rocks and steep paths.", + "2": "The mountain trails become slick with sleet, a danger to even the surefooted.", + "3": "Thin streams of water freeze as sleet coats the mountain’s surface.", + "4": "Sleet forms icicles on the mountain cliffs, glinting faintly in the dim light.", + "5": "The mountain air is filled with the sharp sound of sleet striking stone.", + "6": "Snow and sleet mix, creating a dangerous crust on the mountain slopes.", + "7": "The higher elevations are veiled in icy mist as sleet hardens on every surface.", + "8": "Sleet collects in crevices, turning them into icy traps along the mountain paths.", + "9": "The mountain’s sparse vegetation is encrusted with frozen layers of sleet.", + "10": "Steep ledges shine with ice as sleet freezes instantly upon contact.", + "11": "Sleet batters the mountain caves, creating a steady rhythm on the rocky entrances.", + "12": "The mountain’s winds drive sleet sideways, icing over every exposed surface." + }, + "desert": { + "1": "Sleet transforms the sandy dunes into frozen ridges under a layer of ice.", + "2": "The desert’s sparse vegetation glistens with icy crystals from the sleet.", + "3": "Sleet creates a surreal contrast, freezing the normally arid desert landscape.", + "4": "Sleet coats the desert rocks, making them slick and gleaming in the cold air.", + "5": "Sand and sleet mix, forming a crunchy, frozen surface underfoot.", + "6": "The desert’s usual heat is quelled as sleet falls in a chilling cascade.", + "7": "Sleet freezes in the hollows of the desert, creating tiny icy pools.", + "8": "The desert’s shifting sands are momentarily stilled under a frosty layer of sleet.", + "9": "The desert air is heavy with moisture as sleet glazes the arid landscape.", + "10": "Distant dunes shimmer faintly as sleet creates icy patterns on their surfaces.", + "11": "The desert’s hardy shrubs glisten with icy layers, their tips sparkling.", + "12": "Sleet turns the desert into an alien, frozen expanse under the cold sky." + }, + "coastal": { + "1": "Sleet pelts the rocky shorelines, freezing on the jagged edges of the coast.", + "2": "The waves crash against icy rocks as sleet turns the coast into a frozen scene.", + "3": "Sleet glazes the sandy beaches, creating a slippery, frozen crust.", + "4": "The coastal winds drive sleet into every crevice, frosting the tidepools.", + "5": "Sea spray freezes in midair as sleet transforms the coastline into an icy domain.", + "6": "Fishing boats struggle as sleet freezes their decks and rigging.", + "7": "Sleet slicks the docks, making them hazardous for workers and travelers alike.", + "8": "The salty air is cold and sharp as sleet coats the coastal cliffs in ice.", + "9": "Seabirds huddle for warmth as sleet turns the coastline into a winter tableau.", + "10": "The coastline glimmers faintly as sleet freezes on the wet, exposed rocks.", + "11": "Sleet freezes the ropes and nets, making them stiff and unusable.", + "12": "Icy patches form where sleet collects, turning the shoreline into a slippery trap." + }, + "volcano": { + "1": "Sleet strikes the volcano’s cooled lava flows, freezing into jagged patterns.", + "2": "The warm air near the volcano creates mist as sleet freezes on cooler surfaces.", + "3": "Sleet coats the rocky terrain, creating an unusual icy sheen on volcanic stone.", + "4": "The volcano’s steam vents hiss as sleet instantly freezes around them.", + "5": "Sleet clashes with the volcano’s heat, creating pockets of frozen and steaming ground.", + "6": "The black volcanic rocks glisten as sleet forms icy layers over their surface.", + "7": "Sleet creates slick patches along the rugged paths winding around the volcano.", + "8": "Lava-heated streams remain unfrozen, steaming in contrast to the surrounding sleet.", + "9": "The sleet struggles to settle on the warm rocks but forms icy borders around vents.", + "10": "Fumaroles hiss angrily as sleet freezes near their edges.", + "11": "The sleet hardens rapidly on cooler parts of the volcano, creating icy ridges.", + "12": "The eerie glow of the volcano is muted under the icy veil of sleet." + }, + "artic": { + "1": "Sleet lashes the ice plains, turning the landscape into a glossy, frozen sheet.", + "2": "The arctic winds drive sleet into towering drifts, hardening them with ice.", + "3": "Sleet pelts the frozen tundra, freezing almost instantly upon hitting the ground.", + "4": "The icy ground gleams under a fresh layer of sleet, reflecting the dim light.", + "5": "Sleet freezes in layers, thickening the ice that covers the arctic terrain.", + "6": "The arctic’s few exposed rocks shimmer as sleet coats them in thin ice.", + "7": "Sleet mingles with snow, creating a dense, icy crust on the arctic expanse.", + "8": "The polar air carries sleet sideways, forming jagged, frozen ridges.", + "9": "The sleet hardens on the frozen rivers, adding another layer to the icy surface.", + "10": "The sleet transforms the arctic into a glassy, treacherous expanse of ice.", + "11": "Sleet makes the arctic air even colder, cutting through layers of fur and cloth.", + "12": "The frozen landscape is veiled in a shimmering, icy sheen from relentless sleet." + }, + "cursed": { + "1": "Sleet falls unnaturally thick, freezing into jagged, otherworldly formations.", + "2": "The cursed air carries a metallic tang as sleet coats the twisted landscape.", + "3": "Sleet freezes on cursed ground, creating slick surfaces that glow faintly.", + "4": "The sleet clings to cursed structures, its icy surface reflecting an eerie light.", + "5": "Dark winds drive sleet into unnatural patterns, coating everything in frost.", + "6": "The sleet freezes instantly upon landing, creating sharp, crystalline shapes.", + "7": "A faint whisper seems to accompany the sleet as it coats the cursed land.", + "8": "The sleet hardens on cursed statues, distorting their features further with icy layers.", + "9": "Sleet gathers unnaturally in certain spots, forming icy patches shaped like runes.", + "10": "The cursed sleet seems to sap warmth from the air, intensifying the icy chill.", + "11": "Sleet freezes blood-red puddles on cursed ground, adding to the ominous scene.", + "12": "The cursed land seems to absorb the sleet’s cold, amplifying the frozen terror." + } + } + }, + "Sleet Storm": { + "conditions": { + "temperature": { "gte": 25, "lte": 35 }, + "precipitation": { "gte": 70 }, + "wind": { "gte": 30, "lte": 60 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "The sleet storm pelts the farmstead, coating rooftops and fields in icy layers.", + "2": "Animals huddle in their enclosures as the relentless sleet freezes everything in sight.", + "3": "The farmland becomes treacherously slick, with icy puddles pooling in furrows.", + "4": "Sleet lashes against the barn doors, freezing hinges and locking them tight.", + "5": "The storm turns the farmland into a frozen tableau, with icicles hanging from every fencepost.", + "6": "Farmhands struggle to move as the sleet storm creates a thick crust on the ground.", + "7": "The crops droop under the weight of the freezing sleet, their leaves encased in ice.", + "8": "The farmhouse windows rattle as sleet pounds against them, obscuring the view outside.", + "9": "Frozen mud covers the farm paths, creating slick and dangerous footing.", + "10": "The well freezes over as the storm's icy grip spreads across the farmstead.", + "11": "Icicles form rapidly on the eaves of the farmhouse, growing longer with each passing minute.", + "12": "The sleet storm creates a shimmering crust over the farm, muffling all sounds but the storm." + }, + "village": { + "1": "The village lanes become slick with ice as the sleet storm intensifies.", + "2": "Shutters slam shut across the village, sleet hammering against the wooden frames.", + "3": "Villagers retreat to their homes, the storm freezing the cobbled streets into a treacherous surface.", + "4": "The sleet coats the rooftops, creating a shimmering icy layer that glistens in the dim light.", + "5": "Lanterns struggle to shine as sleet freezes over their glass panes.", + "6": "The village well freezes over, leaving a thick crust of ice on the bucket and rope.", + "7": "Icicles form on the eaves of every house, growing longer as the storm rages on.", + "8": "The central square is eerily silent, covered in a thick layer of frozen sleet.", + "9": "Sleet collects on the signs and lampposts, making them sag under the icy weight.", + "10": "Children watch from windows as the storm ices over the village's worn cobblestones.", + "11": "The storm turns the village fountain into a frozen sculpture of ice.", + "12": "The sound of sleet hitting the roofs echoes through the quiet village streets." + }, + "city": { + "1": "The sleet storm turns the city streets into icy hazards, halting all traffic.", + "2": "Market stalls collapse under the weight of accumulating ice as the storm rages.", + "3": "Stone walls glisten as sleet freezes instantly upon contact.", + "4": "City guards struggle to patrol the frozen streets, their boots slipping on the ice.", + "5": "The storm coats statues and fountains, transforming them into frozen works of art.", + "6": "Windows across the city are obscured as sleet freezes in thick sheets over the glass.", + "7": "The sleet storm turns the city's cobblestones into an icy maze of treacherous footing.", + "8": "Residents huddle indoors, listening to the relentless sound of sleet striking rooftops.", + "9": "The city's bridges become impassable, coated in a thick layer of ice from the storm.", + "10": "Street lanterns flicker dimly as sleet coats their glass panes, diffusing the light.", + "11": "Sleet freezes on banners and flags, leaving them stiff and unmoving in the icy wind.", + "12": "The storm turns fountains and water troughs into frozen blocks of ice, glimmering faintly." + }, + "plains": { + "1": "The open plains become a vast, icy expanse as sleet covers every blade of grass.", + "2": "The storm drives sleet sideways, forming icy ridges along the grassy plains.", + "3": "Wild animals seek shelter as the sleet storm freezes the open landscape.", + "4": "Sleet crusts the grasses, creating a shimmering frost that reflects faint light.", + "5": "The wind carries sleet across the plains, leaving frozen trails in its wake.", + "6": "The once-soft earth hardens under layers of ice from the relentless storm.", + "7": "Sparse trees stand encased in ice, their branches cracking under the weight.", + "8": "Sleet transforms the rolling plains into a glassy, frozen field of icy patches.", + "9": "Travelers across the plains are buffeted by sleet, struggling to maintain their footing.", + "10": "The storm creates icy puddles in the shallow depressions of the plains.", + "11": "Sleet falls heavily, coating the plains in a thick layer of ice that muffles all sound.", + "12": "The grass bends under the weight of sleet, forming icy arcs across the open landscape." + }, + "forest": { + "1": "Sleet turns the forest into a glistening maze of frozen branches and icy roots.", + "2": "The storm coats every leaf and bough, creating an otherworldly crystalline forest.", + "3": "Animals hide as the sleet lashes the canopy, freezing the undergrowth into a slick surface.", + "4": "Sleet crusts the moss and bark, leaving a slick sheen on every surface.", + "5": "Icicles form on the tree branches, hanging low under the storm's icy weight.", + "6": "The forest paths become treacherous as sleet freezes the muddy ground into solid ice.", + "7": "The storm creates an eerie quiet, broken only by the crackle of freezing branches.", + "8": "The forest floor glitters with ice as sleet transforms every leaf and twig.", + "9": "Sleet coats the forest canopy, causing branches to sag under the frozen weight.", + "10": "The storm makes the air heavy with cold, and the sleet freezes the forest in time.", + "11": "The forest streams freeze mid-flow, their surfaces turned to smooth sheets of ice.", + "12": "The sleet storm leaves the forest eerily still, every sound muffled by icy layers." + }, + "swamp": { + "1": "The swamp's murky waters freeze over as sleet creates a thin, icy crust.", + "2": "Sleet coats the gnarled trees, leaving them shimmering in the swamp's eerie light.", + "3": "The storm turns the swamp's muddy paths into slick, frozen trails.", + "4": "Icicles hang from the swamp's moss-draped branches, glinting in the dim light.", + "5": "The swamp’s pools reflect the storm as sleet freezes their surfaces into mirrors.", + "6": "The storm creates a slippery, frozen layer over the swamp's tangled roots.", + "7": "Sleet makes the swamp's air colder, turning its usual dampness into a freezing chill.", + "8": "The swamp is eerily silent as sleet freezes the water, plants, and air alike.", + "9": "Sleet freezes the swamp's reeds and grasses, encasing them in icy shells.", + "10": "The swamp's natural sounds are muffled as sleet covers everything in a cold, icy glaze.", + "11": "The storm's sleet hardens into jagged edges on the swamp's stagnant water.", + "12": "Sleet freezes the swamp's twisted vines, leaving them brittle and gleaming." + }, + "jungle": { + "1": "The sleet storm transforms the lush jungle into a frozen labyrinth of icy leaves.", + "2": "Sleet freezes the jungle's broad leaves, turning them into brittle, icy sheets.", + "3": "The jungle floor becomes slick as sleet freezes on the damp undergrowth.", + "4": "Icicles form rapidly on the jungle's dense canopy, glinting in the storm's dull light.", + "5": "The jungle streams freeze partially as sleet covers the flowing water with ice.", + "6": "Sleet makes the jungle’s vines stiff and brittle, their usual flexibility frozen away.", + "7": "The air in the jungle grows colder as sleet encases everything in a thick, icy layer.", + "8": "The vibrant jungle colors are muted under the relentless storm of freezing sleet.", + "9": "Sleet creates slippery paths through the dense foliage, making travel perilous.", + "10": "The jungle’s creatures grow silent, retreating from the sleet’s freezing assault.", + "11": "The sleet storm leaves the jungle eerily quiet, every sound muffled by the icy layers.", + "12": "The jungle transforms into a shimmering frostscape as sleet covers every surface." + }, + "hills": { + "1": "The rolling hills glisten with ice as sleet freezes on the grassy slopes.", + "2": "Sleet gathers in the dips and hollows of the hills, creating slick puddles of ice.", + "3": "Travelers struggle to ascend the icy hills as sleet turns paths into frozen slides.", + "4": "The storm coats shrubs and trees, leaving them encased in glimmering frost.", + "5": "Icicles form on rocky outcroppings, dripping under the relentless sleet.", + "6": "The storm turns the hills into a treacherous expanse of slippery terrain.", + "7": "The wind drives sleet across the hills, freezing the sparse vegetation.", + "8": "Sleet covers the hilltops, creating a thin layer of ice that crunches underfoot.", + "9": "The storm freezes streams winding through the hills, turning them into icy ribbons.", + "10": "Sleet pelts the hillsides, creating a deafening rattle on exposed rock surfaces.", + "11": "The storm leaves a frozen crust on the hills, making every step a gamble.", + "12": "Low-lying clouds add to the storm's chill, sleet freezing as soon as it touches the ground." + }, + "mountains": { + "1": "The sleet storm lashes the rugged peaks, turning them into icy fortresses.", + "2": "Mountain trails become perilous as sleet freezes into thick sheets of ice.", + "3": "Sleet transforms jagged cliffs into glistening walls of frozen peril.", + "4": "The storm leaves icicles hanging from every crag and overhang.", + "5": "Frozen sleet forms treacherous ledges on the mountain paths.", + "6": "The howling wind drives sleet across the peaks, icing over the stony surfaces.", + "7": "Mountain streams freeze mid-flow, covered by a sheen of icy sleet.", + "8": "Snowfields turn to slush before freezing solid under the relentless sleet storm.", + "9": "The storm's freezing sleet creates a translucent glaze over the mountain rocks.", + "10": "Hikers find themselves stranded as the storm's icy grip locks the mountain trails.", + "11": "The sleet storm howls through mountain passes, leaving frostbite in its wake.", + "12": "The mountains shimmer with ice as sleet freezes everything in sight." + }, + "desert": { + "1": "The rare sleet storm freezes the desert sands, creating an icy crust.", + "2": "Cacti glisten with ice as sleet encases their spines in a frozen shell.", + "3": "The storm turns dunes into slick, treacherous slopes of frozen sand.", + "4": "Sleet falls relentlessly, forming icy puddles in the desert's rare depressions.", + "5": "The storm leaves a thin, glassy coating over desert rocks and stones.", + "6": "Desert shrubs droop under the weight of accumulating ice.", + "7": "Sleet transforms the arid landscape into a shimmering expanse of frost.", + "8": "Frozen sand cracks underfoot as the storm blankets the desert in ice.", + "9": "The desert air grows frigid as the storm's icy sleet freezes every surface.", + "10": "Ice glints in the faint light as sleet freezes atop the desert's barren terrain.", + "11": "The sleet storm leaves a surreal sight: an icy desert under a slate-gray sky.", + "12": "Sand dunes are dusted with a frosty layer, crunching as the wind shifts the frozen granules." + }, + "coastal": { + "1": "The sleet storm turns the shoreline into a frozen wonderland of icy waves and slick rocks.", + "2": "Fishing boats struggle against the freezing sleet, their decks glazed with ice.", + "3": "The sleet freezes salt spray mid-air, creating a frosty haze over the coast.", + "4": "Coastal cliffs glisten with a layer of ice as sleet coats the rocky edges.", + "5": "Icicles form along the dock ropes, the storm making every surface slick and dangerous.", + "6": "The storm churns the sea into icy foam, waves crashing with frozen fury.", + "7": "Frozen sleet collects in tidal pools, creating icy layers on the shallow waters.", + "8": "Seagulls struggle to fly as icy winds and sleet buffet the coastal skies.", + "9": "The storm turns sandy beaches into treacherous sheets of ice and frozen debris.", + "10": "The lighthouse gleams with a frosty coat, its light diffused by freezing sleet.", + "11": "Fishing nets hang stiffly under the weight of accumulating ice from the sleet storm.", + "12": "The relentless storm freezes coastal paths, turning them into slippery ribbons." + }, + "volcano": { + "1": "The sleet storm creates a strange mix of ice and steam as it meets volcanic heat.", + "2": "Frozen sleet clings to volcanic rocks, creating an eerie contrast against the smoldering ground.", + "3": "Steam rises in plumes where sleet strikes hot lava flows, hissing angrily.", + "4": "The storm leaves icy patches on cooler volcanic surfaces, making footing treacherous.", + "5": "Sleet freezes the ash-covered ground, forming a slippery, glassy crust.", + "6": "The storm turns the volcanic landscape into a surreal mix of frost and fire.", + "7": "Icicles form on cooled lava formations, glittering in the storm's dim light.", + "8": "The storm turns lava tubes into slick, icy tunnels of danger.", + "9": "Sleet freezes atop volcanic vents, only to be melted by bursts of heat.", + "10": "Frozen sleet crackles and shatters as it touches still-warm volcanic stone.", + "11": "The volcano's ash fields become treacherous as sleet hardens into icy layers.", + "12": "The storm coats dormant peaks with ice, leaving the volcano eerily silent and cold." + }, + "artic": { + "1": "The sleet storm intensifies the Arctic's chill, freezing even the hardy tundra plants.", + "2": "Frozen sleet forms jagged layers over the permafrost, making travel nearly impossible.", + "3": "The relentless storm lashes the Arctic landscape, coating icebergs in even more ice.", + "4": "Icicles grow rapidly from every ledge and overhang, glinting in the faint light.", + "5": "The Arctic's already-frozen lakes gain an additional sheen of sleet-formed ice.", + "6": "The storm turns snowdrifts into solid ice mounds, impervious to shovels or picks.", + "7": "The icy air grows thicker as sleet forms layers on the Arctic's barren ground.", + "8": "Even the Arctic's tough wildlife retreats as sleet freezes every exposed surface.", + "9": "The storm's icy grip hardens already-frozen rivers into impassable, solid ice paths.", + "10": "Arctic winds carry the sleet far and wide, freezing everything in its path.", + "11": "The sleet storm leaves the Arctic eerily silent, sound muffled by thick, icy layers.", + "12": "The storm encases the Arctic's sparse vegetation in a crystalline sheath of frost." + }, + "cursed": { + "1": "The cursed land's eerie glow reflects off the ice formed by the relentless sleet storm.", + "2": "Sleet freezes over unnatural mists, creating an ominous, ghostly shimmer.", + "3": "The storm turns cursed ruins into icy monuments of forgotten horrors.", + "4": "Icicles hang like fangs from cursed trees, dripping freezing water onto the ground.", + "5": "Sleet hardens the cursed soil, sealing cracks that once oozed ominous vapors.", + "6": "Frozen sleet collects on twisted statues, distorting their eerie forms further.", + "7": "The storm's sleet freezes cursed rivers, creating shimmering, unnaturally-colored ice.", + "8": "Sleet turns cursed ground into a slick, treacherous expanse of gleaming frost.", + "9": "The air feels thick and oppressive as the sleet storm chills the cursed landscape.", + "10": "The storm adds an icy edge to the cursed land's unholy chill.", + "11": "Sleet freezes dark sigils etched in stone, hiding their ominous glow beneath ice.", + "12": "The cursed air grows heavier, sleet freezing the remnants of unnatural energies." + } + } + }, + "Smoky Haze": { + "conditions": { + "temperature": { "gte": 50, "lte": 90 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 40 }, + "humidity": { "lte": 50 }, + "cloudCover": { "lte": 50 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "The smoky haze clings to the fields, shrouding crops in a dull gray mist.", + "2": "Farmers cough as the acrid haze settles over barns and silos.", + "3": "Livestock huddle uneasily, the smoky air thick and oppressive.", + "4": "The haze dulls the sun's light, casting an eerie glow over the farmland.", + "5": "Ashy particles drift through the air, settling on crops and pastures.", + "6": "The acrid smell of smoke hangs heavy, blending with the scent of hay.", + "7": "Plumes of smoke roll across the fields, obscuring distant farmhouses.", + "8": "The smoky haze turns the horizon into a smudged blur.", + "9": "Farmers pause their work, shielding their faces from the thick haze.", + "10": "Smoke curls around the windmill, its blades barely visible.", + "11": "The haze muffles the usual sounds of the farm, leaving an eerie silence.", + "12": "The sky above the farm is a sickly orange, the haze blotting out the blue." + }, + "village": { + "1": "The village square is engulfed in a smoky haze, making landmarks hard to see.", + "2": "Villagers pull scarves over their faces to ward off the choking smoke.", + "3": "The smoky air hangs between the cottages, smothering the usual bustle.", + "4": "Children peer out from doorways, wary of the thick haze rolling through the streets.", + "5": "The haze dulls the light of the torches, leaving the village in a dim twilight.", + "6": "Smoke curls around the bell tower, obscuring its peak from view.", + "7": "Villagers cough and mutter, their voices hushed in the oppressive air.", + "8": "The scent of burning wood overwhelms the usual smells of bread and livestock.", + "9": "The smoky haze turns the cobblestones slick and dark.", + "10": "The sound of footsteps is muffled as smoke swirls along the village paths.", + "11": "The haze reduces visibility, villagers moving cautiously through the streets.", + "12": "Ash drifts onto rooftops, coating them in a fine gray layer." + }, + "city": { + "1": "The smoky haze turns the bustling city into a maze of ghostly outlines.", + "2": "Merchants shout through the thick haze, their voices strained and muffled.", + "3": "The acrid air makes the city gates barely visible from a short distance.", + "4": "Smoke lingers in the narrow alleyways, making them even more foreboding.", + "5": "Citizens hurry through the streets, pulling cloaks tight against the choking haze.", + "6": "The bell tower looms faintly through the smoky air, its chime dulled.", + "7": "The usual clamor of the marketplace is subdued beneath the oppressive haze.", + "8": "Soot settles on statues and fountains, tarnishing their once-pristine surfaces.", + "9": "Guards patrol with lanterns, their lights barely cutting through the smoky gloom.", + "10": "The haze dims the torches lining the streets, leaving the city in a smoky twilight.", + "11": "Smoke fills the air, mingling with the usual scents of the crowded city.", + "12": "The skyline is obscured, the city's spires barely visible through the smoky veil." + }, + "plains": { + "1": "The vast plains are blanketed in a smoky haze, obscuring the horizon.", + "2": "Wildlife flees as the acrid smoke spreads across the open fields.", + "3": "The smoky air turns the golden grasses of the plains a muted gray.", + "4": "The haze swirls with the wind, creating ghostly patterns over the plains.", + "5": "Travelers struggle to find their way as the haze erases familiar landmarks.", + "6": "The horizon is shrouded, the distant hills invisible in the smoky gloom.", + "7": "Smoke settles in the low-lying areas of the plains, creating dense patches.", + "8": "The scent of burning vegetation fills the air, carried far by the wind.", + "9": "Sunlight struggles to break through the thick haze, casting an eerie glow.", + "10": "The haze makes every sound feel distant, the plains unnaturally quiet.", + "11": "Ash drifts down onto the grasses, leaving a fine gray coating.", + "12": "The smoky haze turns the open expanse into a disorienting, endless void." + }, + "forest": { + "1": "The forest is cloaked in a dense smoky haze, the trees barely visible.", + "2": "Smoke drifts between the trunks, creating an eerie, otherworldly effect.", + "3": "The scent of burning wood permeates the air, sharp and acrid.", + "4": "The forest floor is littered with ash, softening every step.", + "5": "The haze dulls the sunlight filtering through the canopy, leaving the forest dim.", + "6": "Wildlife is silent, the usual sounds muffled by the thick smoky air.", + "7": "The haze clings to the leaves, leaving them coated in a fine, sooty layer.", + "8": "Smoke swirls in the undergrowth, making it difficult to navigate the trails.", + "9": "The forest's shadows blend with the smoke, creating a disorienting labyrinth.", + "10": "Every breath is heavy as the smoky air saturates the forest.", + "11": "The haze makes the forest feel endless, the trees blending into the smoky distance.", + "12": "Ash falls like snow, settling on branches and the forest floor alike." + }, + "swamp": { + "1": "The swamp is veiled in a smoky haze, blending with its usual mist.", + "2": "The acrid smoke hangs low over the swamp, mingling with the stench of decay.", + "3": "Smoke drifts through the mangroves, creating ghostly shapes in the gloom.", + "4": "The murky waters reflect the orange tint of the smoky sky.", + "5": "The haze turns the swamp's eerie stillness into an oppressive silence.", + "6": "Smoke clings to the swamp's vegetation, adding a burnt scent to the air.", + "7": "Visibility is near zero as the haze merges with the swamp's natural fog.", + "8": "The usual buzzing of insects is subdued, drowned out by the oppressive haze.", + "9": "The swamp's pools glisten faintly under the smoky veil.", + "10": "Ashy particles settle on the water's surface, breaking its usual stillness.", + "11": "The smoky air turns the swamp's trees into looming, shadowy forms.", + "12": "The haze amplifies the swamp's natural eeriness, creating an ominous atmosphere." + }, + "jungle": { + "1": "The dense jungle is blanketed in smoky haze, the canopy barely visible.", + "2": "Smoke filters through the thick foliage, turning the jungle into a shadowy maze.", + "3": "The acrid smell of smoke overwhelms the usual scents of the jungle.", + "4": "Sunlight struggles to penetrate the canopy, filtered into dim, smoky beams.", + "5": "Wildlife is silent, retreating from the choking haze engulfing the jungle.", + "6": "The jungle's vibrant colors are muted under the thick layer of smoke.", + "7": "Leaves glisten with dew and ash, the haze clinging to every surface.", + "8": "The smoky air feels heavy, making each step through the jungle laborious.", + "9": "The haze turns the jungle's towering trees into shadowy, indistinct forms.", + "10": "Smoke swirls through the undergrowth, making paths nearly impossible to follow.", + "11": "The jungle echoes faintly as the smoky air muffles every sound.", + "12": "Ash drifts down like snow, coating the jungle floor in a fine gray layer." + }, + "hills": { + "1": "The smoky haze obscures the rolling hills, blending them into the horizon.", + "2": "Smoke clings to the hillsides, turning the vibrant grasslands into muted grays.", + "3": "The air carries the acrid scent of burning wood, stinging the eyes.", + "4": "Distant hilltops are invisible, swallowed by the thick haze.", + "5": "Smoke swirls in the valleys, making paths difficult to navigate.", + "6": "The haze muffles the usual sounds of birds and rustling leaves.", + "7": "Ash drifts down onto the hills, settling on rocks and grass.", + "8": "The sky above is a sickly orange, casting a strange glow over the hills.", + "9": "Travelers on the hills cough as they pass through the acrid air.", + "10": "The haze transforms the landscape into a surreal, dreamlike scene.", + "11": "The usual gentle breeze carries the smoky scent far across the hills.", + "12": "The haze lingers in the dips of the hills, creating dense patches of smoke." + }, + "mountains": { + "1": "The smoky haze obscures the mountain peaks, blending them into the sky.", + "2": "Smoke drifts between the jagged cliffs, giving the mountains a foreboding aura.", + "3": "The acrid scent of burning wood fills the thin mountain air.", + "4": "Visibility drops as the haze clings to the winding mountain paths.", + "5": "Smoke swirls around the peaks, masking their grandeur in a gray shroud.", + "6": "The usual crisp mountain air is heavy with the weight of the haze.", + "7": "Ash falls gently onto the rocky slopes, leaving a fine gray layer.", + "8": "The haze dulls the sun’s rays, casting the mountains in dim, muted light.", + "9": "The sound of falling rocks is muffled by the thick smoky air.", + "10": "The mountain trails feel eerie as the haze obscures familiar landmarks.", + "11": "Travelers struggle to breathe in the acrid, smoke-laden air at high altitudes.", + "12": "The haze lingers in the valleys, turning the mountain’s beauty into a ghostly scene." + }, + "desert": { + "1": "The smoky haze mingles with the desert’s heat, creating a suffocating atmosphere.", + "2": "Dunes fade into the distance, shrouded in the thick smoky air.", + "3": "The acrid scent of smoke blends with the usual dryness of the desert wind.", + "4": "The haze turns the desert sun into a dim, orange glow.", + "5": "Visibility is reduced as smoke clings to the shifting sands.", + "6": "The desert feels eerily quiet as the smoky air muffles all sounds.", + "7": "Ash drifts across the dunes, blending with the pale desert sand.", + "8": "Travelers shield their faces against the hot, smoke-laden winds.", + "9": "The smoky haze amplifies the desert’s harshness, making travel even more difficult.", + "10": "The sky above the desert is a strange mix of orange and gray.", + "11": "Smoke curls along the horizon, blurring the distinction between sky and sand.", + "12": "The haze reduces the vast desert expanse into a claustrophobic gray blur." + }, + "coastal": { + "1": "The smoky haze drifts over the coastline, blending the sea and sky into gray.", + "2": "Waves crash mutedly on the shore, the sound muffled by the thick haze.", + "3": "The acrid scent of smoke overpowers the usual salty tang of the sea.", + "4": "Ships in the harbor are barely visible, lost in the dense smoky air.", + "5": "Smoke mingles with sea mist, creating an oppressive atmosphere along the coast.", + "6": "The haze dulls the sunlight, leaving the coastline in a dim orange glow.", + "7": "Ash falls onto the wet sand, leaving streaks of gray on the shore.", + "8": "Sea birds cry faintly, their calls muffled by the thick haze.", + "9": "The smoky air clings to the water, turning the waves into ghostly shapes.", + "10": "The horizon is invisible, the sea and sky blending into one smoky expanse.", + "11": "The haze makes it hard to distinguish between the sea spray and drifting smoke.", + "12": "The usual coastal breeze feels heavy with the acrid scent of burning." + }, + "volcano": { + "1": "The smoky haze is thick with ash, obscuring the volcano's jagged slopes.", + "2": "Smoke billows from the crater, merging with the haze in the air.", + "3": "The acrid scent of sulfur mingles with the heavy smoky atmosphere.", + "4": "Visibility is near zero as the smoke rolls down the volcano’s flanks.", + "5": "The fiery glow of lava is faintly visible through the dense smoky air.", + "6": "The ground feels hot underfoot, the air thick with the weight of ash.", + "7": "Ash falls steadily, coating the blackened rocks in a pale gray layer.", + "8": "The smoky haze makes the volcano feel alive, its breath heavy in the air.", + "9": "The haze reflects the orange glow of the lava, casting eerie shadows.", + "10": "Smoke and ash mix in the air, creating an almost unbreathable environment.", + "11": "The haze swirls with the wind, carrying the scent of burning earth far and wide.", + "12": "The sky above the volcano is darkened, the sun barely piercing the smoky veil." + }, + "artic": { + "1": "The smoky haze clings to the icy tundra, obscuring the horizon in gray.", + "2": "The icy air is tinged with the sharp, acrid scent of smoke.", + "3": "The usual clarity of the arctic is lost in the thick, smoky haze.", + "4": "The snow reflects the dim orange light of the haze, casting an eerie glow.", + "5": "Smoke drifts over frozen lakes, turning their surfaces into shadowy mirrors.", + "6": "The biting cold combines with the acrid air, making every breath a challenge.", + "7": "Ash settles on the pristine snow, leaving dark streaks across the landscape.", + "8": "The haze muffles the crunch of footsteps on the ice, leaving an eerie silence.", + "9": "The arctic winds carry the smoky air far across the barren tundra.", + "10": "Visibility drops as the haze blends with the arctic’s usual icy mist.", + "11": "The haze turns the frozen landscape into a surreal, colorless expanse.", + "12": "Smoke lingers in the valleys between icy ridges, creating dense patches." + }, + "cursed": { + "1": "The smoky haze is thick and unnatural, carrying an acrid, otherworldly scent.", + "2": "Shadows flicker unnaturally in the haze, adding to the cursed atmosphere.", + "3": "The air feels heavy, each breath saturated with the acrid taste of smoke.", + "4": "The haze dulls all light, casting the cursed land in perpetual twilight.", + "5": "Smoke swirls in eerie patterns, as if alive, whispering through the cursed land.", + "6": "The ground beneath the haze feels cold and lifeless, ash covering every surface.", + "7": "Visibility is near zero as the haze clings to the cursed landscape like a shroud.", + "8": "The smoky air amplifies the unnatural silence, making the cursed land even more ominous.", + "9": "Ash falls like snow, tainting everything with a sense of decay and ruin.", + "10": "The haze carries strange sounds, faint whispers lost in the choking air.", + "11": "The cursed land feels suffocating under the smoky haze, as if drained of life.", + "12": "The smoky air hangs heavy with despair, turning the cursed land into a realm of shadows." + } + } + }, + "Steady Rain": { + "conditions": { + "temperature": { "gte": 40, "lte": 60 }, + "precipitation": { "gte": 50, "lte": 80 }, + "wind": { "lte": 70 }, + "humidity": { "gte": 50 }, + "cloudCover": { "gte": 50 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Rain patters steadily on the thatched roofs, soaking the fields.", + "2": "The soil grows muddy as steady rain nourishes the crops.", + "3": "Droplets glisten on the wooden fences and farm tools.", + "4": "Rain drips from the eaves of the barn, forming puddles below.", + "5": "The farm animals huddle under shelters as rain pours down.", + "6": "Fields turn to shallow pools as rainwater collects.", + "7": "Rain pelts the scarecrow, giving it a damp, forlorn look.", + "8": "The steady rhythm of rain fills the air, muffling other sounds.", + "9": "Carts left outside are drenched, water pooling in their beds.", + "10": "The steady rain washes dirt from the farmhouse walls.", + "11": "Raindrops streak the windows, blurring the view of the sodden fields.", + "12": "The path to the well turns slippery and uneven under the rain." + }, + "village": { + "1": "Rain drums on the rooftops, streaming down into cobblestone streets.", + "2": "Villagers scurry with cloaks and hoods, avoiding the steady rain.", + "3": "The marketplace is empty, stalls abandoned to the wet weather.", + "4": "Rain pools in the corners of the village square.", + "5": "Smoke from chimneys mixes with the rain-heavy air.", + "6": "Lanterns flicker in the rain as villagers hurry indoors.", + "7": "Children splash in puddles forming in the dirt paths.", + "8": "Water streams off rooftops, carving tiny rivers through the mud.", + "9": "The bell tower stands dark and wet against the rainy sky.", + "10": "Raindrops create ripples in a trough left outside the smithy.", + "11": "Rain-soaked chickens cluck irritably from their coops.", + "12": "The sound of the rain masks the usual buzz of village life." + }, + "city": { + "1": "Rainwater runs in rivulets along the cobbled streets of the city.", + "2": "Merchants pull tarps over their wares as the rain persists.", + "3": "The steady rain fills the gutters, spilling over into the alleys.", + "4": "Rain slicks the stone steps leading up to the city gates.", + "5": "Guards stand under makeshift awnings, cloaks soaked through.", + "6": "Street performers pack up their instruments, fleeing the rain.", + "7": "Rain blurs the outlines of the city walls and spires.", + "8": "Children dart through the rain, laughing as they splash in puddles.", + "9": "The blacksmith's hammer rings faintly, muffled by the steady rain.", + "10": "Rain creates a sheen on the cobblestones, making them slippery.", + "11": "The aroma of wet stone and rain-dampened wood fills the city.", + "12": "The city square empties as rain steadily soaks the market stalls." + }, + "plains": { + "1": "Rain falls in steady sheets across the wide, open plains.", + "2": "Grasslands glisten under the continuous rainfall.", + "3": "Puddles form in low spots, reflecting the cloudy sky above.", + "4": "Herds of deer seek shelter as the steady rain falls.", + "5": "The sound of rain on the plains is constant, blending with the rustling grass.", + "6": "Drops bead on the tall grass, bowing it under their weight.", + "7": "Rainwater runs along the edges of dirt paths, carving shallow channels.", + "8": "The horizon blurs under the curtain of steady rain.", + "9": "Rain creates small streams that weave through the grasslands.", + "10": "Birds dart between bushes, seeking shelter from the persistent rain.", + "11": "The plains shimmer in the rain, a sea of glistening green.", + "12": "Rain clings to wildflowers, making their colors pop against the gray." + }, + "forest": { + "1": "The forest canopy drips steadily, rain soaking the underbrush.", + "2": "Puddles form on the forest floor as the rain continues unabated.", + "3": "The air is thick with the smell of wet earth and moss.", + "4": "Leaves shine under the rainfall, water cascading down their edges.", + "5": "The steady patter of rain is amplified in the dense forest.", + "6": "Streams swell with rainwater, their currents quickened.", + "7": "Rain drips from the branches, creating a symphony of droplets.", + "8": "The forest floor grows slick and muddy under the continuous rain.", + "9": "Frogs croak loudly in the rain, filling the forest with their calls.", + "10": "Mushrooms sprout quickly in the dampness, dotting the forest floor.", + "11": "The rain muffles distant sounds, creating a cocoon of wet serenity.", + "12": "Mist rises from the forest floor as the rain cools the warm earth." + }, + "swamp": { + "1": "Rain mingles with the swamp's stagnant pools, rippling their surfaces.", + "2": "The swamp becomes a quagmire as the steady rain falls.", + "3": "Mud bubbles and churns as rainwater mixes with the swamp muck.", + "4": "Rain falls heavily on the swamp, blending with its constant dripping.", + "5": "The air is heavy with moisture, the rain barely distinguishable from the swamp's humidity.", + "6": "Crocodiles lie still in the rain, their scales glistening with water.", + "7": "Mosquitoes buzz undeterred as rain falls over the swamp.", + "8": "The rain creates small rivulets that feed into the swamp’s larger pools.", + "9": "Reeds sway under the weight of the falling rain.", + "10": "Rainwater flows sluggishly through the swamp's murky waterways.", + "11": "The swamp's usual odors are muted by the fresh scent of rainfall.", + "12": "Rain blurs the outline of cypress trees, their trunks disappearing into the mist." + }, + "jungle": { + "1": "Rain pours through the jungle canopy, drenching the vibrant foliage.", + "2": "Droplets glisten on broad jungle leaves, reflecting the dim light.", + "3": "The jungle floor becomes a muddy maze as rain saturates the ground.", + "4": "Rainwater cascades down tree trunks, feeding into hidden streams.", + "5": "The steady rain is punctuated by the calls of jungle creatures.", + "6": "Ferns and mosses thrive under the continuous rainfall.", + "7": "The air is thick with moisture, each breath filled with the scent of wet vegetation.", + "8": "Rainwater pools in the hollows of fallen logs and leaves.", + "9": "Insects buzz loudly, their wings heavy with raindrops.", + "10": "The jungle hums with life as the rain invigorates its many inhabitants.", + "11": "The sky above the jungle is hidden, replaced by a curtain of rain.", + "12": "Rain muffles footsteps, making movement through the jungle almost silent." + }, + "hills": { + "1": "Rain runs in rivulets down the slopes, carving small paths in the soil.", + "2": "The steady rain washes over the hills, leaving the grass glistening.", + "3": "Herds of sheep huddle together on the drenched hillside.", + "4": "Raindrops form tiny waterfalls over rocky outcroppings.", + "5": "The hills echo with the sound of rain pattering on leaves and stones.", + "6": "Low clouds cling to the hilltops as the rain persists.", + "7": "Streams swell at the base of the hills, filled by the unending rain.", + "8": "The paths through the hills become muddy and treacherous.", + "9": "Wildflowers glisten with rainwater, adding color to the gray day.", + "10": "Rain drenches the tall grass, bowing it under its weight.", + "11": "The hills are shrouded in mist, the rain blending seamlessly with the fog.", + "12": "Water collects in small pools, reflecting the dark sky above." + }, + "mountains": { + "1": "Rain cascades down the rocky slopes, filling mountain streams.", + "2": "The sound of rain is carried by the wind through the high peaks.", + "3": "Cliffs glisten with water as steady rain drenches the mountains.", + "4": "Clouds hang low over the peaks, obscuring them in misty rain.", + "5": "Small waterfalls form as rainwater rushes over ledges.", + "6": "The ground becomes slick and dangerous under the relentless rain.", + "7": "Mountain paths are transformed into muddy channels.", + "8": "Rain fills alpine meadows, turning them into shallow pools.", + "9": "Goats take shelter under rocky overhangs as rain pours down.", + "10": "Rain mutes the usual mountain echoes, replaced by a constant patter.", + "11": "Mosses and lichens thrive in the damp mountain air.", + "12": "Rain pools in crevices, creating temporary streams down the cliffs." + }, + "desert": { + "1": "Rare rain dampens the desert sands, forming fleeting puddles.", + "2": "The steady rain is absorbed quickly by the parched ground.", + "3": "Cactus spines glisten with droplets of rain.", + "4": "The desert air smells of wet earth, a rare and welcome scent.", + "5": "Rain darkens the sand, tracing lines down dunes.", + "6": "Rainwater gathers in dry riverbeds, creating short-lived streams.", + "7": "Dust turns to mud under the steady fall of rain.", + "8": "Desert shrubs drink deeply, their leaves brightening in the rain.", + "9": "The desert sky remains gray, a rare departure from its usual clarity.", + "10": "Small animals emerge cautiously to drink from rain-fed pools.", + "11": "The patter of rain on sand is almost imperceptible in the vast silence.", + "12": "Rain reveals the scent of desert plants, carried on the moist air." + }, + "coastal": { + "1": "Rain falls steadily, blending with the roar of crashing waves.", + "2": "Seagulls call mournfully as the rain soaks the coastline.", + "3": "The beach is deserted, rain washing away footprints in the sand.", + "4": "Salt spray mixes with rain, making the air damp and heavy.", + "5": "Rain fills tidal pools, creating ripples that spread across their surfaces.", + "6": "Fishing boats rock gently in the harbor, their decks slick with rain.", + "7": "Cliffs glisten as rainwater streams down to the sea below.", + "8": "The ocean seems darker under the overcast sky and steady rain.", + "9": "Coastal trails become muddy, their edges washed away by the rain.", + "10": "Shells and driftwood are scattered by the rain-swollen tides.", + "11": "The horizon blurs as rain and mist merge with the sea.", + "12": "Lighthouses stand resolute, their stones slick with rain." + }, + "volcano": { + "1": "Rain hisses against warm volcanic rock, creating faint steam.", + "2": "Rain gathers in cracks, trickling down the slopes of the volcano.", + "3": "The volcanic landscape darkens as rain drenches the ashen ground.", + "4": "Steady rain mixes with the faint sulfurous smell in the air.", + "5": "Rain turns ash into sticky mud, making the terrain treacherous.", + "6": "Pools of water form in hardened lava flows, reflecting the cloudy sky.", + "7": "The rain dulls the usual heat radiating from the rocky ground.", + "8": "Vegetation clinging to the slopes drinks deeply from the steady rain.", + "9": "Raindrops create patterns in the fine volcanic dust.", + "10": "Rain softens the harsh edges of jagged lava rock.", + "11": "The air cools slightly as the rain continues to fall.", + "12": "The volcano stands shrouded in mist, the rain hiding its summit." + }, + "artic": { + "1": "Rain falls lightly, freezing into a slick glaze over the icy ground.", + "2": "The steady rain turns to sleet, coating the arctic tundra.", + "3": "Icebergs glisten under the steady rain, their surfaces slick and smooth.", + "4": "Rainwater runs over frozen lakes, creating a thin layer of water.", + "5": "The steady rain brings a rare softness to the arctic wilderness.", + "6": "Snow turns to slush as rain falls over the icy expanse.", + "7": "Penguins waddle through the rain, their feathers shedding the droplets.", + "8": "Rain merges with mist, obscuring the distant glacier peaks.", + "9": "The arctic air remains frigid, despite the constant rainfall.", + "10": "Icicles form as rain freezes upon contact with the ground.", + "11": "The rain is a faint drumbeat against the stillness of the ice.", + "12": "Patches of exposed earth appear as rain melts the thinner ice." + }, + "cursed": { + "1": "Rain falls in an unnatural rhythm, almost whispering as it hits the ground.", + "2": "The cursed land absorbs the rain, the puddles dark and foreboding.", + "3": "Raindrops feel heavy, leaving a faint metallic tang in the air.", + "4": "Rain pools in the shadows, avoiding the light of flickering torches.", + "5": "The steady rain echoes eerily, as if voices murmur in its rhythm.", + "6": "Dead trees drip with blackened water, the rain tainted by the curse.", + "7": "The ground becomes a quagmire, clinging unnaturally to boots and wheels.", + "8": "Rain streaks across the sky, refracting strange, dark hues.", + "9": "The air feels thick with dread as rain soaks the cursed soil.", + "10": "Raindrops seem to shimmer briefly, vanishing before hitting the ground.", + "11": "Mist rises where the rain meets the cursed ground, obscuring all sight.", + "12": "Rain falls steadily, its rhythm broken by occasional ghostly sighs." + } + } + }, + "Subtle Snowfall": { + "conditions": { + "temperature": { "gte": 20, "lte": 35 }, + "precipitation": { "gte": 30, "lte": 50 }, + "wind": { "lte": 40 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "gte": 20, "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "A light dusting of snow coats the fields, shimmering faintly.", + "2": "Snowflakes drift gently, settling on hay bales and wooden fences.", + "3": "The subtle snowfall adds a quiet beauty to the barren farmland.", + "4": "Chickens scurry under cover as snow lightly powders the ground.", + "5": "The farmhouse roof collects a thin layer of white, almost unnoticeable.", + "6": "Snow falls softly, blending into the frost-covered fields.", + "7": "A faint dusting of snow highlights the farmer's plow in the yard.", + "8": "The snowfall is so light that it barely covers the bare earth.", + "9": "Snowflakes drift into the barn, settling on the backs of cattle.", + "10": "The orchard trees shimmer as a thin layer of snow settles.", + "11": "The ground looks patchy with snow, creating a checkerboard pattern.", + "12": "Snow gently caresses the scarecrow’s hat in the field." + }, + "village": { + "1": "Snow lightly powders the cobblestones, muffling footsteps.", + "2": "Villagers glance up at the subtle snowfall as they carry on.", + "3": "Snowflakes drift gently between the rooftops, almost unnoticed.", + "4": "A fine layer of snow collects on windowsills and shutters.", + "5": "Children marvel at the delicate snowflakes, catching them in their hands.", + "6": "The village green looks dusted with white, softening its edges.", + "7": "The snowfall is so light it barely gathers in the cracks of the stones.", + "8": "Chimneys release faint wisps of smoke into the snowy air.", + "9": "The snowflakes vanish as they land on warm hearthstones.", + "10": "A thin layer of snow settles over the village well.", + "11": "The snowfall creates a serene stillness in the narrow village lanes.", + "12": "Snow outlines the wooden signposts leading to nearby hamlets." + }, + "city": { + "1": "Snowflakes float lazily, melting as they touch the bustling streets.", + "2": "A thin veil of snow gathers on stone statues in the square.", + "3": "The subtle snowfall barely settles on the roofs of busy markets.", + "4": "Snow outlines the carvings on the city gate, adding a frosty touch.", + "5": "The snowfall is so light it disappears amid the warmth of city life.", + "6": "Snowflakes dance in the lantern light along the city’s main roads.", + "7": "A fine layer of snow highlights the curves of the cathedral spires.", + "8": "The snowfall muffles distant sounds, adding an air of calm.", + "9": "Snowflakes glimmer as they fall into the dark waters of the canal.", + "10": "Cobblestones in the alleys appear lightly dusted with snow.", + "11": "The city watch stamps their feet, brushing snow from their cloaks.", + "12": "The snowfall frames the stone walls of the bustling courtyard." + }, + "plains": { + "1": "Snow drifts gently, disappearing into the vast expanse of the plains.", + "2": "Grass blades peek through a faint dusting of snow.", + "3": "Snowflakes settle on the sparse bushes dotting the plains.", + "4": "The wind carries the soft snow across the open grasslands.", + "5": "Snowfall turns the horizon into a blurry, muted line.", + "6": "Snow gathers in hollows, forming faint patches of white.", + "7": "The snowfall is barely noticeable on the endless flat terrain.", + "8": "Small tracks in the snow hint at wildlife traversing the plains.", + "9": "Snowflakes shimmer as they touch the wide, open fields.", + "10": "The snowfall is so light it seems to vanish before it lands.", + "11": "The plains appear frosted, with snow glittering in the low light.", + "12": "Snow dusts the tops of stones scattered across the grasslands." + }, + "forest": { + "1": "Snowflakes fall gently, resting on mossy branches and leaves.", + "2": "The forest floor is dappled with a fine layer of snow.", + "3": "Snow highlights the gnarled roots of ancient trees.", + "4": "The light snowfall makes the underbrush glimmer faintly.", + "5": "Snowflakes settle silently on the pine needles above.", + "6": "The snowfall is so light it hardly covers the forest path.", + "7": "Snow gathers in tiny clumps on the branches of low bushes.", + "8": "The forest feels hushed, the snow muffling distant sounds.", + "9": "Snowflakes swirl lazily through shafts of light filtering down.", + "10": "The undergrowth sparkles with frost as the snowfall continues.", + "11": "Snow coats the tops of mushrooms peeking through the forest floor.", + "12": "A faint dusting of snow clings to the bark of the towering trees." + }, + "swamp": { + "1": "Snowflakes vanish as they land in the dark swamp water.", + "2": "The snowfall adds an eerie stillness to the foggy swamp.", + "3": "Snow rests on the gnarled roots emerging from the muck.", + "4": "A fine layer of snow outlines the edges of moss-covered rocks.", + "5": "The swamp trees drip with melting snow, their branches sagging.", + "6": "Snow clings to the tops of reeds swaying in the chilly breeze.", + "7": "The snowfall is so light it vanishes in the swamp’s damp air.", + "8": "Snow dusts the tops of lily pads floating in stagnant pools.", + "9": "The swamp feels unusually quiet under the soft blanket of snow.", + "10": "Snowflakes dance in the faint light filtering through the swamp mist.", + "11": "The snowfall creates pale streaks against the dark waters below.", + "12": "Snow covers the abandoned huts and rickety bridges in a thin layer." + }, + "jungle": { + "1": "Snowflakes melt quickly as they touch the warm jungle leaves.", + "2": "The jungle canopy catches most of the light snowfall above.", + "3": "Snow dusts the broad leaves, creating fleeting patterns of white.", + "4": "The snowfall is barely visible against the dense green of the jungle.", + "5": "Snowflakes cling to vines, giving them an unusual pale shimmer.", + "6": "The snow gathers in patches on fallen logs and roots.", + "7": "Snowflakes swirl in the jungle mist, vanishing as they land.", + "8": "The soft snowfall contrasts sharply with the vibrant jungle colors.", + "9": "Snow clings briefly to ferns before disappearing into the damp ground.", + "10": "The snowfall creates faint white patches along jungle trails.", + "11": "Snowflakes catch on spider webs, highlighting their intricate designs.", + "12": "The jungle air feels cooler as the light snow falls silently." + }, + "hills": { + "1": "Snow gently coats the rolling hills, softening their contours.", + "2": "A light dusting of snow makes the paths through the hills shimmer faintly.", + "3": "Snowflakes drift lazily, settling in the hollows of the hills.", + "4": "The snowfall creates a faint white streak along the winding trails.", + "5": "Snow clings to tufts of grass scattered across the hillsides.", + "6": "The subtle snow blends with the frost already coating the rocky outcrops.", + "7": "Snow gently outlines the tops of the hills, barely altering their appearance.", + "8": "Snow gathers in small patches where the wind has calmed.", + "9": "The hills look tranquil under the soft fall of snow.", + "10": "Snowflakes settle on low shrubs dotting the hillsides.", + "11": "The snowy hills appear painted with pale strokes of white.", + "12": "Snow muffles the sounds of wind rustling through the hill grasses." + }, + "mountains": { + "1": "Snow delicately settles on jagged cliffs and rocky ledges.", + "2": "The snowfall creates faint white lines along the mountain ridges.", + "3": "Snowflakes swirl gently around the peaks, barely visible in the mist.", + "4": "A light snow dusts the narrow mountain paths, adding a slippery sheen.", + "5": "Snow clings to the edges of exposed rocks and icy crevices.", + "6": "The mountains seem serene under the quiet fall of snow.", + "7": "Snow softly outlines the towering pines scattered along the slopes.", + "8": "Snow gathers in the shallow hollows of boulders and ledges.", + "9": "The mountain air feels sharper as snowflakes fall steadily.", + "10": "Snow adds a pale glaze to the higher peaks, leaving the valleys untouched.", + "11": "The snowfall creates fleeting patterns on the craggy surfaces.", + "12": "Snowflakes swirl in the wind before settling briefly on the rocky ground." + }, + "desert": { + "1": "Snowflakes melt as they land on the sun-warmed desert sands.", + "2": "The subtle snowfall creates brief white patches on the dunes.", + "3": "Snow clings to the tops of cacti, a strange sight in the barren desert.", + "4": "The snowfall barely alters the golden hue of the desert landscape.", + "5": "Snow gathers fleetingly on rock formations before vanishing.", + "6": "The desert air feels oddly cool under the faint fall of snow.", + "7": "Snowflakes drift through the desert wind, disappearing quickly.", + "8": "The snowfall adds a delicate shimmer to the dry, cracked earth.", + "9": "Snowflakes gather briefly on the sparse desert plants.", + "10": "The desert horizon appears blurred as snow falls gently.", + "11": "Snow outlines the ridges of sand dunes, a rare and fleeting sight.", + "12": "The snowfall creates pale streaks on the desert's jagged rock outcrops." + }, + "coastal": { + "1": "Snowflakes melt as they touch the salt-tinged breeze by the shore.", + "2": "Snow dusts the rocky coastline, blending with the sea spray.", + "3": "A subtle snowfall settles briefly on the beach before vanishing.", + "4": "Snow gathers in small patches along the edges of driftwood.", + "5": "The waves crash softly as snowflakes swirl in the coastal winds.", + "6": "Snow clings to the rooftops of seaside shacks, barely noticeable.", + "7": "The snowfall creates a faint white veil over the rocky shore.", + "8": "Snow gathers in crevices of the cliffs overlooking the sea.", + "9": "The snowfall is light enough to vanish against the dark water.", + "10": "Snowflakes sparkle briefly before melting on the damp sand.", + "11": "Snow outlines the edges of tide pools, creating a delicate frost.", + "12": "The snowfall mingles with the mist rolling in from the ocean." + }, + "volcano": { + "1": "Snowflakes vanish as they approach the warm volcanic slopes.", + "2": "The subtle snowfall clings to the cooler rock formations higher up.", + "3": "Snow gathers faintly along the edges of dormant lava flows.", + "4": "A light dusting of snow contrasts against the dark volcanic terrain.", + "5": "The snowfall is quickly melted by the residual heat near vents.", + "6": "Snowflakes swirl around the peak, settling briefly before disappearing.", + "7": "The subtle snow outlines cracks in the volcanic rock.", + "8": "Snow gathers in cooler pockets, untouched by the volcano’s heat.", + "9": "The volcanic landscape appears briefly softened under the gentle snowfall.", + "10": "Snowflakes catch in the sparse grasses growing on the slopes.", + "11": "The snow melts into steam where it lands on warmer surfaces.", + "12": "Snow highlights the sharp edges of cooled lava formations." + }, + "artic": { + "1": "Snowfall barely alters the already white expanse of the tundra.", + "2": "The snowfall drifts lazily, blending seamlessly with the icy ground.", + "3": "Snowflakes settle softly on the frozen landscape, adding a fresh shimmer.", + "4": "The subtle snowfall glints faintly in the weak arctic light.", + "5": "Snow gathers lightly on the edges of frost-covered rocks.", + "6": "The arctic air feels heavier under the quiet fall of snow.", + "7": "Snowflakes land on the icy surface, creating faint patterns before vanishing.", + "8": "The snowfall barely changes the appearance of the frozen horizon.", + "9": "Snowflakes catch in the fur of animals wandering the tundra.", + "10": "The snowfall adds a delicate layer to the already thick ice.", + "11": "Snow settles in the cracks of glaciers, brightening their edges.", + "12": "The arctic seems even more silent as snow falls softly." + }, + "cursed": { + "1": "Snowflakes fall silently, disappearing as they touch the cursed ground.", + "2": "The snowfall feels unnaturally cold, biting at exposed skin.", + "3": "Snow clings to twisted trees, adding an eerie white sheen.", + "4": "The cursed air feels heavier as snow falls in unnatural stillness.", + "5": "Snow gathers faintly on the ruins, highlighting their decay.", + "6": "The snowfall whispers through the air, carrying a sense of foreboding.", + "7": "Snowflakes seem to hover briefly before settling on the cursed earth.", + "8": "The snowfall outlines skeletal remains scattered across the cursed lands.", + "9": "Snow clings to abandoned structures, giving them a ghostly appearance.", + "10": "The cursed ground resists the snow, leaving it patchy and uneven.", + "11": "Snowflakes dissolve as they touch blackened, cursed stones.", + "12": "The snowfall carries an eerie stillness, amplifying the unnatural quiet." + } + } + }, + "Thunderclouds Looming": { + "conditions": { + "temperature": { "gte": 50, "lte": 80 }, + "precipitation": { "lte": 40 }, + "wind": { "gte": 20, "lte": 70 }, + "humidity": { "gte": 80 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "Dark thunderclouds gather ominously over the open fields.", + "2": "The distant rumble of thunder sends crows scattering from the barn.", + "3": "Farmhands pause, glancing nervously at the looming storm clouds.", + "4": "The fields darken as the clouds roll in, casting long shadows.", + "5": "A faint smell of rain fills the air as clouds gather overhead.", + "6": "The horizon is obscured by the approaching wall of dark clouds.", + "7": "Livestock grow restless under the oppressive weight of the sky.", + "8": "The golden crops seem to dim as thunderclouds blot out the sun.", + "9": "The air feels heavy and electric, promising an impending storm.", + "10": "Thunderclouds swirl above, their edges tinged with an eerie glow.", + "11": "The farmstead creaks in the rising wind as the clouds loom closer.", + "12": "A hushed stillness falls over the farm, broken only by distant thunder." + }, + "village": { + "1": "Villagers glance skyward as thunderclouds gather over the rooftops.", + "2": "The marketplace empties as dark clouds loom ominously above.", + "3": "The air grows thick with tension as the sky darkens with clouds.", + "4": "Children are hurried indoors as the first peals of thunder echo.", + "5": "The village square is bathed in an unnatural twilight under the clouds.", + "6": "Thunderclouds form an oppressive ceiling above the clustered homes.", + "7": "Lanterns flicker against the rising wind and the gathering storm.", + "8": "The scent of rain mixes with woodsmoke as villagers prepare for the storm.", + "9": "Thunderclouds swirl above the chapel’s steeple, casting long shadows.", + "10": "The village well reflects the darkened sky as thunder rolls in the distance.", + "11": "The cobblestone streets glisten under the faint drizzle from the looming clouds.", + "12": "An eerie silence grips the village as the storm draws near." + }, + "city": { + "1": "The towering spires of the city pierce through the encroaching thunderclouds.", + "2": "Merchants hurriedly pack their stalls as dark clouds roll in.", + "3": "A sense of urgency spreads through the streets as thunder rumbles overhead.", + "4": "Smoke from chimneys mingles with the swirling storm clouds above.", + "5": "The city walls appear foreboding under the shadow of the thunderclouds.", + "6": "Guards on the ramparts glance uneasily at the darkening sky.", + "7": "Street performers scatter as a distant bolt of lightning illuminates the clouds.", + "8": "The city square grows quiet under the heavy weight of the looming storm.", + "9": "The first drops of rain patter on cobblestones as the storm clouds gather.", + "10": "Thunderclouds create a stark contrast against the city’s stonework.", + "11": "The hum of city life slows as the clouds block out the sun.", + "12": "The city gates creak in the rising wind as thunder echoes above." + }, + "plains": { + "1": "Thunderclouds stretch endlessly across the flat expanse of the plains.", + "2": "The tall grass sways violently as the storm winds rush through.", + "3": "The horizon blurs under the weight of the dark, gathering clouds.", + "4": "A distant crack of lightning splits the sky above the open plains.", + "5": "The plains take on a grayish hue as thunderclouds block the sunlight.", + "6": "The scent of rain carries on the wind as the clouds draw near.", + "7": "The sky roars with thunder, echoing across the vast emptiness.", + "8": "The first raindrops dot the dry ground as clouds churn above.", + "9": "The endless plains seem even more vast under the heavy sky.", + "10": "Dark clouds gather in swirling patterns, promising a powerful storm.", + "11": "The grass glistens with moisture as the storm approaches.", + "12": "Thunderclouds dominate the horizon, blotting out the setting sun." + }, + "forest": { + "1": "The canopy darkens as thunderclouds gather above the treetops.", + "2": "The distant sound of thunder echoes through the dense woods.", + "3": "Leaves flutter violently in the rising wind beneath the looming clouds.", + "4": "The forest floor grows dim as the storm clouds block the sunlight.", + "5": "Animals scurry to their burrows as thunder rumbles overhead.", + "6": "The scent of wet earth fills the air as rain threatens to fall.", + "7": "Branches creak ominously under the weight of the coming storm.", + "8": "The first raindrops dot the leaves as thunderclouds swirl above.", + "9": "The forest feels heavy and still as the storm draws closer.", + "10": "The wind whistles through the branches, carrying the scent of rain.", + "11": "The path ahead grows faint as darkness settles under the clouds.", + "12": "Thunder rolls above the forest, reverberating through the ancient trees." + }, + "swamp": { + "1": "The swamp grows eerily still as thunderclouds gather overhead.", + "2": "Dark clouds reflect in the murky waters, casting the swamp in shadow.", + "3": "The smell of ozone mingles with the swamp's pungent air as thunder rumbles.", + "4": "Ripples spread across the stagnant water as the wind picks up.", + "5": "The reeds sway violently as the storm approaches with a low growl.", + "6": "The swamp creatures grow quiet, sensing the storm’s approach.", + "7": "Dark clouds swirl above, mirrored in the swamp’s brackish pools.", + "8": "The swamp's eerie calls are silenced under the weight of the looming clouds.", + "9": "Drops of rain begin to dot the swamp’s still waters.", + "10": "The swamp feels suffocating as the storm draws near.", + "11": "The thunderclouds churn above the twisted trees and muddy waters.", + "12": "The swamp air feels charged as thunder cracks in the distance." + }, + "jungle": { + "1": "The thick jungle canopy darkens under the heavy thunderclouds.", + "2": "Distant thunder blends with the jungle’s constant cacophony.", + "3": "Leaves drip with condensation as the air grows heavier under the clouds.", + "4": "Monkeys screech and scatter as a lightning flash illuminates the jungle.", + "5": "The jungle air feels dense and electric as thunder rolls above.", + "6": "Dark clouds press against the treetops, creating a suffocating gloom.", + "7": "The jungle paths grow treacherous as rain threatens to fall.", + "8": "The distant cry of a jaguar echoes as the storm clouds gather.", + "9": "The jungle floor is blanketed in shadows under the swirling thunderclouds.", + "10": "Birds scatter in droves as the storm draws closer.", + "11": "The air is thick with the mingled scents of rain and vegetation.", + "12": "Thunderclouds loom over the jungle, creating an oppressive darkness." + }, + "hills": { + "1": "Dark thunderclouds crest over the rolling hills, casting deep shadows.", + "2": "The air grows heavy as clouds swirl ominously over the grassy slopes.", + "3": "Shepherds hurry their flocks as the looming storm approaches.", + "4": "The gentle hills seem more foreboding under the darkening sky.", + "5": "Thunder echoes across the valleys, promising a coming storm.", + "6": "The wind howls through the hills as storm clouds gather overhead.", + "7": "The once vibrant green hills dim as the clouds block the sunlight.", + "8": "The scent of rain fills the air as dark clouds creep closer.", + "9": "The hills quiver with the distant growl of thunder.", + "10": "A flash of lightning momentarily illuminates the shadowed hills.", + "11": "The horizon is blurred by the encroaching storm clouds.", + "12": "The hills stand silent under the oppressive weight of the sky." + }, + "mountains": { + "1": "Thunderclouds swirl ominously around the jagged mountain peaks.", + "2": "The once-clear sky is shrouded in dark, churning clouds.", + "3": "Lightning dances along the ridges as the storm approaches.", + "4": "The air grows thin and cold as thunder rumbles through the mountains.", + "5": "Travelers seek shelter as the dark clouds gather strength.", + "6": "The mountain paths are cloaked in shadows as the storm descends.", + "7": "The peaks vanish into the blackness of the encroaching thunderclouds.", + "8": "Echoes of thunder reverberate through the narrow mountain passes.", + "9": "The clouds press low, clinging to the slopes with a menacing presence.", + "10": "The mountain air feels charged, thick with impending rain.", + "11": "Dark clouds roll in, obscuring the path ahead.", + "12": "The peaks glisten with the first drops of rain under the stormy sky." + }, + "desert": { + "1": "Dark thunderclouds form a stark contrast against the golden sands.", + "2": "The distant rumble of thunder carries eerily across the empty desert.", + "3": "The dunes shift restlessly under the rising storm winds.", + "4": "The desert feels suffocating as the storm clouds blot out the sun.", + "5": "Lightning flickers on the horizon, illuminating the barren landscape.", + "6": "The air is heavy with moisture, a rare omen in the arid desert.", + "7": "The sand swirls as the storm's edge brushes across the dunes.", + "8": "The desert sky turns a foreboding gray under the looming storm.", + "9": "Clouds churn overhead, casting long shadows over the endless sands.", + "10": "The horizon is obscured by the swirling darkness of the storm.", + "11": "The wind howls as thunderclouds gather strength above the desert.", + "12": "The first raindrops hiss as they meet the hot, dry sands." + }, + "coastal": { + "1": "Thunderclouds loom over the sea, casting dark reflections on the waves.", + "2": "The wind picks up as storm clouds gather along the coastline.", + "3": "The distant sound of crashing waves mixes with the rumble of thunder.", + "4": "Boats rock violently in the harbor as the storm approaches.", + "5": "Seagulls cry and scatter as dark clouds block the horizon.", + "6": "The salty air grows heavy with the promise of rain and lightning.", + "7": "The sea churns as the first gusts of wind whip up frothy waves.", + "8": "Dark clouds swirl above, mirrored in the restless waters below.", + "9": "The coastal cliffs tremble under the low growl of the approaching storm.", + "10": "Lanterns flicker along the docks as the storm gathers strength.", + "11": "The horizon disappears into the blackness of the storm clouds.", + "12": "The coastline is bathed in eerie twilight as thunderclouds roll in." + }, + "volcano": { + "1": "Dark thunderclouds mingle with the ever-present volcanic smoke.", + "2": "The rumble of thunder rivals the growl of the restless volcano.", + "3": "Ash and storm clouds blend, creating an apocalyptic scene above the crater.", + "4": "Lightning flickers ominously in the churning storm above the volcano.", + "5": "The volcano glows faintly beneath the oppressive weight of the storm clouds.", + "6": "The air grows thick and sulfurous as the storm gathers strength.", + "7": "The rocky slopes tremble under the combined weight of thunder and magma.", + "8": "The first raindrops sizzle as they meet the scorching volcanic rock.", + "9": "The storm clouds churn violently, as if fed by the volcano’s heat.", + "10": "The horizon vanishes into a black sea of volcanic ash and thunderclouds.", + "11": "The volcano’s fiery glow flickers dimly beneath the storm’s shadow.", + "12": "The mountain groans under the storm’s wrath, its heat clashing with the cold rain." + }, + "artic": { + "1": "Thunderclouds loom ominously over the frozen tundra, casting deep shadows.", + "2": "The icy winds grow still as the sky darkens with storm clouds.", + "3": "Snow swirls violently as the first gusts of the storm arrive.", + "4": "The horizon blurs as dark clouds block out the weak Arctic sunlight.", + "5": "Thunder cracks loudly, echoing endlessly across the frozen wastes.", + "6": "The frozen ground trembles slightly as the storm gains strength.", + "7": "The icy air feels electric, heavy with the promise of lightning.", + "8": "The snow-covered landscape dims under the shadow of thunderclouds.", + "9": "The Arctic sky churns as clouds and snow clash violently overhead.", + "10": "A distant lightning flash briefly illuminates the barren ice fields.", + "11": "The howling wind is drowned out by the rumble of thunder.", + "12": "The first drops of freezing rain hiss as they meet the ice below." + }, + "cursed": { + "1": "Dark thunderclouds gather unnaturally fast, swirling with eerie green hues.", + "2": "The air feels charged with dark energy as thunder rumbles ominously.", + "3": "Lightning flickers unnaturally, casting grotesque shadows on the cursed ground.", + "4": "The cursed land grows even darker under the oppressive weight of the storm.", + "5": "A strange, sulfuric smell fills the air as the storm approaches.", + "6": "The sky churns with unnatural colors as the thunderclouds swirl menacingly.", + "7": "The cursed ground trembles slightly as the storm gathers strength.", + "8": "Eerie whispers seem to ride the wind beneath the gathering clouds.", + "9": "The horizon disappears into a black vortex of thunder and unnatural light.", + "10": "The storm clouds radiate an unholy glow, casting the land in sickly hues.", + "11": "The cursed sky grows alive with flickering lightning and ghostly shadows.", + "12": "Thunder cracks loudly, accompanied by a strange, guttural echo from the earth." + } + } + }, + "Thunderstorm": { + "conditions": { + "temperature": { "gte": 60, "lte": 80 }, + "precipitation": { "gte": 70 }, + "wind": { "gte": 30, "lte": 70 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Thunder cracks loudly, startling livestock and farmhands alike.", + "2": "The barn shakes under the booming thunder and heavy rainfall.", + "3": "Dark clouds pour relentless rain over the fields, drowning the soil.", + "4": "Lightning illuminates the farm, briefly revealing soaked crops.", + "5": "Horses whinny nervously as thunder echoes across the open land.", + "6": "Rain lashes against the farmhouse windows as the storm intensifies.", + "7": "The wind howls through the barn, slamming doors against their frames.", + "8": "Thunder rolls deeply, blending with the relentless pounding of rain.", + "9": "The fields are awash with water, creating rivulets that run downhill.", + "10": "Lightning strikes a distant tree, splitting it with a sharp crack.", + "11": "The farmstead is engulfed in near darkness under the storm clouds.", + "12": "Buckets overflow as rain pours down in heavy sheets, soaking the earth." + }, + "village": { + "1": "Thunder booms, rattling the shutters of every house in the village.", + "2": "Villagers dart from building to building, seeking shelter from the deluge.", + "3": "The village square floods as rainwater pools in the cobblestones.", + "4": "Lightning illuminates the bell tower, silhouetting it against the stormy sky.", + "5": "The muddy streets are slick with rain, making travel treacherous.", + "6": "The inn’s lanterns flicker in the wind as thunder crashes above.", + "7": "Children peer out from doorways, watching the rain cascade in sheets.", + "8": "The scent of wet earth and smoke fills the air as chimneys sputter.", + "9": "Thunder rolls through the valley, reverberating across the clustered houses.", + "10": "The village well overflows as water streams down the rooftops.", + "11": "Lightning briefly lights the entire village, turning night into day.", + "12": "Rain pounds on the thatched roofs, drowning out conversation indoors." + }, + "city": { + "1": "Thunder reverberates through the stone streets, startling passersby.", + "2": "Rooftops and gutters overflow, spilling water onto the busy roads.", + "3": "Lightning briefly illuminates the towering spires, casting eerie shadows.", + "4": "The marketplace is abandoned as merchants scramble to cover their wares.", + "5": "The city gates creak under the relentless force of the storm winds.", + "6": "Rainwater rushes down the cobblestone streets, forming small rivers.", + "7": "The city square turns into a reflective pool under the torrential downpour.", + "8": "Guards on the walls squint through the sheets of rain and flickering lightning.", + "9": "Thunder shakes the windows of the grand hall, silencing its occupants.", + "10": "Lightning reveals the city skyline, jagged and foreboding against the dark sky.", + "11": "The air is thick with moisture and the metallic tang of lightning strikes.", + "12": "The storm’s roar drowns out the usual din of city life." + }, + "plains": { + "1": "Thunder rolls endlessly across the flat expanse of the plains.", + "2": "Lightning streaks down, setting fire to a distant patch of grass.", + "3": "The rain comes in horizontal sheets, driven by the howling wind.", + "4": "The flatlands are soaked as water pools in every low-lying spot.", + "5": "The horizon flashes brightly with every crack of lightning.", + "6": "Grass bends under the weight of the pounding rain and wind.", + "7": "The plains seem endless under the oppressive dark clouds above.", + "8": "Lightning reveals a lone tree, stark against the stormy backdrop.", + "9": "Thunder echoes in all directions, amplified by the open landscape.", + "10": "The wind whips through the tall grass, hissing like a serpent.", + "11": "The plains are a blur of water and motion as the storm takes hold.", + "12": "Rain turns the dirt tracks into a quagmire, slowing travel to a crawl." + }, + "forest": { + "1": "Thunder shakes the ground as rain filters through the dense canopy.", + "2": "Lightning strikes a nearby tree, splitting it with a deafening crack.", + "3": "The forest floor becomes a slick, muddy mess under the torrential rain.", + "4": "Wind howls through the branches, sending leaves and twigs flying.", + "5": "Animals scurry for shelter as the storm rages above the treetops.", + "6": "The dense foliage muffles the sound of rain but amplifies the thunder.", + "7": "Puddles form along the winding paths, making travel treacherous.", + "8": "The forest darkens further as the storm clouds blot out the sky.", + "9": "Lightning momentarily lights the underbrush, revealing startled wildlife.", + "10": "Rain drums against the leaves, a relentless rhythm against the storm.", + "11": "The forest air is heavy with the scent of rain and wet earth.", + "12": "Thunder rolls through the woods, reverberating off the trunks of ancient trees." + }, + "swamp": { + "1": "The swamp churns as heavy rain mixes with its murky waters.", + "2": "Thunder rumbles deeply, shaking the soggy ground beneath your feet.", + "3": "Lightning illuminates the twisted trees, casting eerie reflections in the water.", + "4": "The rain creates ripples across every puddle, pool, and stagnant pond.", + "5": "The air is thick with moisture, and the storm’s roar drowns out the usual swamp sounds.", + "6": "Wind stirs the reeds violently, sending droplets flying in all directions.", + "7": "Dark clouds press low, nearly touching the tops of the swamp trees.", + "8": "Thunder echoes across the swamp, blending with the croaks of hidden frogs.", + "9": "The swamp feels alive under the storm, trembling with each thunderclap.", + "10": "Rain soaks the moss-covered ground, turning it into a treacherous mire.", + "11": "The swamp is shrouded in a hazy gloom as lightning flickers above.", + "12": "The oppressive storm makes the air feel thick and suffocating, even in the open." + }, + "jungle": { + "1": "Thunder booms above the dense jungle, startling the wildlife into silence.", + "2": "Rain hammers down on the thick canopy, creating a deafening roar.", + "3": "Lightning flickers through the vines, illuminating hidden dangers.", + "4": "The jungle floor becomes a swamp of mud and rushing water under the storm.", + "5": "Wind rattles the broad leaves, sending water cascading in every direction.", + "6": "The storm cloaks the jungle in near-total darkness, broken only by lightning flashes.", + "7": "Branches snap loudly as the wind tears through the dense foliage.", + "8": "The storm churns the air, mixing the smells of rain, earth, and vegetation.", + "9": "Animals call out in distress, their cries barely audible over the storm.", + "10": "Rain turns the jungle paths into slippery, treacherous tracks.", + "11": "Thunder shakes the ground, its sound amplified by the dense vegetation.", + "12": "The storm feels alive, its power pulsing through the steaming jungle." + }, + "hills": { + "1": "Thunder rolls over the hills, echoing in the valleys below.", + "2": "Lightning illuminates the rolling terrain, casting eerie shadows.", + "3": "Rain cascades down the slopes, forming fast-moving rivulets.", + "4": "The wind howls across the hilltops, bending the grass.", + "5": "Thunder cracks sharply, startling creatures hiding in the brush.", + "6": "Dark clouds swirl ominously, blotting out the sky over the hills.", + "7": "Lightning forks across the horizon, lighting up distant peaks.", + "8": "Rain soaks the hillsides, making paths slippery and dangerous.", + "9": "Thunder rumbles continuously, creating a deep, rolling soundscape.", + "10": "Wind carries the scent of wet earth and ozone through the air.", + "11": "The storm’s power is amplified by the exposed heights of the hills.", + "12": "A crack of thunder shakes the ground, causing a rockslide in the distance." + }, + "mountains": { + "1": "Thunder booms between the peaks, echoing endlessly.", + "2": "Lightning strikes a crag, shattering stone with a deafening crack.", + "3": "Rain pours down the cliffs, turning trails into torrents.", + "4": "Winds whip through the mountain passes, carrying icy rain.", + "5": "Thunder rattles the stones, shaking loose small rocks and debris.", + "6": "The storm obscures the peaks, shrouding them in mist and rain.", + "7": "Lightning illuminates jagged ridges, creating fleeting silhouettes.", + "8": "Rain pools in crevices, spilling down to the valleys below.", + "9": "The air is heavy with moisture, and the thunder feels almost alive.", + "10": "Wind-driven rain cuts like knives against exposed skin.", + "11": "The storm is a constant roar, drowning out all other sounds.", + "12": "A bolt of lightning splits a tree on a nearby ledge, sending it tumbling." + }, + "desert": { + "1": "Thunder rolls across the flat expanse, growing louder with every crack.", + "2": "Lightning strikes the sand, leaving smoldering patches of glassy rock.", + "3": "The rain hammers the parched ground, forming fleeting puddles.", + "4": "The storm creates an eerie light show against the barren desert landscape.", + "5": "Winds whip up the sand, blending it with the lashing rain.", + "6": "Thunder echoes across the dunes, amplified by the emptiness.", + "7": "Lightning briefly reveals distant rock formations, jagged and imposing.", + "8": "Rain evaporates almost as quickly as it falls, creating a misty haze.", + "9": "The desert floor becomes a slick, muddy expanse under the relentless storm.", + "10": "Dark clouds churn overhead, heavy with rain and electricity.", + "11": "Wind-driven sand and rain make it impossible to see more than a few feet.", + "12": "The storm’s ferocity feels unnatural in the normally arid landscape." + }, + "coastal": { + "1": "Thunder shakes the shoreline, blending with the roar of crashing waves.", + "2": "Lightning strikes the sea, creating brief bursts of steam and light.", + "3": "Rain lashes the docks, drenching everything in its path.", + "4": "The storm turns the sea into a frothy, turbulent expanse of chaos.", + "5": "Winds howl through the fishing shacks, slamming doors and windows.", + "6": "Thunder reverberates over the water, carrying its sound far inland.", + "7": "Lightning illuminates the horizon, casting the waves in sharp relief.", + "8": "Boats rock violently in the harbor as the storm intensifies.", + "9": "Rainwater flows down to the beach, carving small channels in the sand.", + "10": "The storm surges inland, flooding low-lying areas near the coast.", + "11": "The sea sprays salty mist into the air, mixing with the pounding rain.", + "12": "The storm’s energy feels endless, its fury mirroring the wild ocean." + }, + "volcano": { + "1": "Thunder rolls ominously over the smoldering slopes of the volcano.", + "2": "Lightning streaks across the dark sky, highlighting the volcanic peak.", + "3": "Rain hisses as it strikes the hot rock, turning to steam instantly.", + "4": "The storm feels alive, its power magnified by the volatile landscape.", + "5": "Wind howls through the craters, carrying ash and rain together.", + "6": "Thunder mixes with the low rumble of the restless volcano.", + "7": "Lightning briefly illuminates streams of glowing lava in the distance.", + "8": "Rain struggles to cool the heat of the volcanic ground, creating sizzling sounds.", + "9": "The storm casts an eerie glow over the already dangerous terrain.", + "10": "Dark clouds swirl low, mixing volcanic ash with the storm’s fury.", + "11": "Steam rises in thick plumes wherever rain meets the volcanic heat.", + "12": "The air smells of sulfur and ozone, heavy with the storm’s moisture." + }, + "artic": { + "1": "Thunder rolls across the icy expanse, an unnatural sound in the cold.", + "2": "Lightning reflects off the snow, creating dazzling bursts of light.", + "3": "The storm drives icy rain and sleet against the frozen ground.", + "4": "Winds howl through the tundra, carrying the storm’s fury for miles.", + "5": "Thunder echoes off the glaciers, deep and resonant.", + "6": "Rain freezes instantly upon contact, coating everything in a slick layer of ice.", + "7": "The storm blurs the horizon, making the endless ice fields seem endless.", + "8": "Lightning strikes distant icebergs, cracking them with a sharp report.", + "9": "The freezing rain soaks clothing, turning to ice as it clings.", + "10": "Dark clouds hang low over the tundra, smothering what little light remains.", + "11": "The storm’s cold feels alive, sinking into bones and stealing breath.", + "12": "The air is thick with the storm’s intensity, each breath stinging with frost." + }, + "cursed": { + "1": "Thunder cracks unnaturally, each bolt accompanied by a faint wail.", + "2": "Lightning flashes green, casting the cursed land in a sickly glow.", + "3": "Rain falls in heavy, dark droplets that leave an unsettling residue.", + "4": "The storm feels alive, its winds carrying whispers and moans.", + "5": "Thunder reverberates with an eerie, echoing tone that chills the soul.", + "6": "Lightning illuminates skeletal trees and crumbling ruins in sharp relief.", + "7": "The rain burns slightly, as if tainted by some dark power.", + "8": "Wind carries the scent of decay, mixing with the storm’s moisture.", + "9": "Thunder shakes the cursed ground, sending shudders through the earth.", + "10": "Rainwater pools into oddly shaped patterns that seem to shift unnaturally.", + "11": "The storm’s fury seems directed, its bolts striking only certain points.", + "12": "Dark clouds swirl above, forming faces that seem to scream silently." + } + } + }, + "Tropical Rainstorm": { + "conditions": { + "temperature": { "gte": 55, "lte": 90 }, + "precipitation": { "gte": 70 }, + "wind": { "gte": 40, "lte": 70 }, + "humidity": { "gte": 80 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "Heavy rain drenches the crops, turning fields into pools of water.", + "2": "The downpour batters rooftops and spills over gutters.", + "3": "Farm animals huddle together, seeking shelter from the relentless rain.", + "4": "Mud paths become impassable as rainwater flows freely.", + "5": "The rain’s intensity causes streams to overflow, flooding nearby fields.", + "6": "Winds howl across the open land, rattling fences and sheds.", + "7": "Buckets fill quickly with rainwater, spilling over in minutes.", + "8": "The storm's humid air clings heavily to the skin.", + "9": "Lush green crops bend under the weight of the torrential rain.", + "10": "Lightning flashes briefly reveal the waterlogged landscape.", + "11": "The rain’s roar drowns out all other sounds across the farm.", + "12": "Puddles grow into ponds, swallowing low-lying patches of farmland." + }, + "village": { + "1": "Villagers rush to secure loose thatch roofs against the pounding rain.", + "2": "The storm sends water cascading through the muddy streets.", + "3": "Small streams form along the village paths, flowing toward the outskirts.", + "4": "Children watch the rain from doorways, forbidden to play in the storm.", + "5": "The rain muffles the usual sounds of village life, leaving only its steady patter.", + "6": "Animals in the pens bleat nervously as water pools around them.", + "7": "The humid air makes even the cool rain feel stifling.", + "8": "Water barrels overflow, their tops clattering under the heavy downpour.", + "9": "Smoke from hearthfires struggles against the wind, dissipating quickly.", + "10": "Villagers huddle together indoors, their voices drowned out by the storm.", + "11": "The rain turns the dirt roads into thick, clinging mud.", + "12": "A bolt of lightning briefly illuminates the soaked rooftops." + }, + "city": { + "1": "Rainwater cascades from rooftops, flooding the narrow streets below.", + "2": "Merchants scramble to cover their stalls as the storm intensifies.", + "3": "The heavy rain fills the city’s drains, causing water to spill into the roads.", + "4": "City folk hurry along the streets, cloaks pulled tightly against the wet.", + "5": "The constant patter of rain echoes off the stone walls of buildings.", + "6": "Pools form quickly in the cobblestone alleys, reflecting the gray sky.", + "7": "Chimneys struggle to release smoke, the rain smothering their warmth.", + "8": "The air feels oppressive, thick with moisture from the storm.", + "9": "Marketplaces fall silent as the rain drives everyone indoors.", + "10": "Guards patrolling the gates are drenched, their armor clinking with water.", + "11": "Lanterns flicker in the wind, their light struggling against the storm.", + "12": "The storm batters wooden shutters, rattling them incessantly." + }, + "plains": { + "1": "The vast plains vanish under sheets of rain, their contours obscured.", + "2": "Water collects in shallow depressions, forming temporary ponds.", + "3": "The storm bends tall grasses, creating waves across the flatlands.", + "4": "Winds whip through the open terrain, driving the rain horizontally.", + "5": "Lightning strikes illuminate the vast, waterlogged expanse.", + "6": "The rain muffles distant sounds, creating an eerie stillness between claps of thunder.", + "7": "Small animals scurry for shelter as the storm intensifies.", + "8": "Mud makes travel impossible, clinging to boots and hooves alike.", + "9": "The rain sparkles briefly in flashes of lightning, creating fleeting beauty.", + "10": "A single tree in the distance sways violently in the gusts.", + "11": "The humid storm air clings heavily, making breathing feel labored.", + "12": "Patches of wildflowers drown under the torrential downpour." + }, + "forest": { + "1": "Rain drips heavily from the canopy, soaking the forest floor.", + "2": "The storm fills the air with the rich scent of wet earth and leaves.", + "3": "Leaves rustle wildly as the wind tears through the forest.", + "4": "Small streams form along trails, carrying leaves and twigs downhill.", + "5": "The rain muffles the usual sounds of the forest, creating an unnatural quiet.", + "6": "Puddles grow around tree roots, drowning small plants.", + "7": "Lightning briefly reveals the dense, rain-soaked foliage.", + "8": "The storm’s winds cause branches to creak ominously above.", + "9": "Water collects on large leaves before spilling onto the ground below.", + "10": "Animals shelter in burrows or under thick vegetation, avoiding the storm.", + "11": "The muddy forest floor sucks at boots, making movement difficult.", + "12": "The downpour creates a steady drumming on the forest’s canopy." + }, + "swamp": { + "1": "The storm turns the swamp into a maze of flooded trails and hidden dangers.", + "2": "Rain sends ripples across stagnant pools, disrupting their eerie stillness.", + "3": "The air grows even heavier, thick with moisture and the smell of decay.", + "4": "Water bubbles up from the ground, mixing with the relentless rain.", + "5": "Lightning illuminates twisted trees, their reflections shimmering in pools below.", + "6": "The storm’s winds scatter reeds and cattails, bending them low.", + "7": "Rain drips from moss-covered branches, adding to the swamp’s constant dampness.", + "8": "Paths through the swamp disappear under rising water, leaving only guesswork.", + "9": "The storm sends frogs and insects into a frenzied chorus.", + "10": "Mud and water blend into an indistinguishable, treacherous expanse.", + "11": "Dark clouds seem to press lower here, the swamp almost swallowing the storm.", + "12": "Thunder rumbles ominously, its sound magnified by the stillness of the swamp." + }, + "jungle": { + "1": "The rain hammers the jungle canopy, creating a deafening roar.", + "2": "Thick vegetation traps the humidity, making the air stifling despite the storm.", + "3": "Rainwater streams down vines and branches, soaking the jungle floor.", + "4": "Animals screech and call, their voices heightened by the storm’s fury.", + "5": "The jungle becomes a maze of flooded paths and hidden dangers.", + "6": "Lightning flashes briefly illuminate the dense, dripping foliage.", + "7": "The humid air feels alive, clinging heavily to every surface.", + "8": "Rain pools in leaves before spilling over in torrents.", + "9": "The storm’s winds whip through the jungle, scattering loose vegetation.", + "10": "Small streams swell into raging currents, cutting through the underbrush.", + "11": "The rain muffles distant sounds, isolating travelers in the thick jungle.", + "12": "Thunder rolls through the jungle, reverberating off the dense greenery." + }, + "hills": { + "1": "Heavy rain rushes down the slopes, forming temporary streams.", + "2": "Wind-driven rain bends tall grasses on the hilltops.", + "3": "The storm’s roar echoes through the rolling terrain.", + "4": "Mudslides threaten the steeper hills as rain loosens the soil.", + "5": "Lightning illuminates the hilltops, casting eerie shadows.", + "6": "Small animals scramble for cover as water pools in low spots.", + "7": "Winds whip across the open hills, driving the rain sideways.", + "8": "The storm creates miniature waterfalls on the rocky outcroppings.", + "9": "Visibility drops as the rain obscures distant hilltops.", + "10": "Thunder rumbles across the hills, amplifying the storm’s intensity.", + "11": "Wildflowers bow under the relentless downpour.", + "12": "The muddy paths become treacherous, slipping underfoot." + }, + "mountains": { + "1": "The storm unleashes torrents of water, cascading down the mountain slopes.", + "2": "Winds howl through narrow mountain passes, carrying sheets of rain.", + "3": "Fog forms quickly in the high peaks, mingling with the rain.", + "4": "Thunder cracks echo through the valleys below.", + "5": "Rockslides become a danger as the rain loosens the steep cliffs.", + "6": "Streams and rivers swell with rainwater, carving through the landscape.", + "7": "Lightning illuminates jagged peaks, creating a momentary stark contrast.", + "8": "Travelers find it near impossible to climb, the terrain slick with water.", + "9": "The storm’s winds whip raindrops into icy stings at higher altitudes.", + "10": "Pine trees sway dangerously, their branches heavy with water.", + "11": "The sound of rushing water fills the valleys as the rain persists.", + "12": "Paths become small rivers, impossible to navigate safely." + }, + "desert": { + "1": "Rain falls in rare torrents, flooding the desert’s dry riverbeds.", + "2": "The arid ground struggles to absorb the deluge, creating temporary pools.", + "3": "Thunder shakes the desert, a rare sound in the usually still air.", + "4": "Lightning flashes illuminate vast expanses of rain-slick sand.", + "5": "The rain turns patches of the desert into slippery, muddy terrain.", + "6": "Plants seem to come alive, drinking in the sudden rainfall.", + "7": "The storm’s winds whip the rain into blinding sheets.", + "8": "Distant dunes collapse under the weight of the unexpected water.", + "9": "Rain collects in rocky hollows, forming fleeting oases.", + "10": "The air is thick with moisture, a stark contrast to the usual dryness.", + "11": "Water carves temporary channels through the sandy terrain.", + "12": "The storm creates an eerie atmosphere, turning the desert unrecognizable." + }, + "coastal": { + "1": "Waves crash violently against the shore, driven by the storm’s winds.", + "2": "Rain lashes the coastal cliffs, eroding the edges with relentless force.", + "3": "Fishing boats struggle against the storm, their lights barely visible.", + "4": "Seagulls cry out, struggling to navigate the turbulent skies.", + "5": "The sea swells dangerously, flooding low-lying beaches.", + "6": "Palm trees bend under the force of the wind and rain.", + "7": "Visibility is reduced to a few feet as rain and sea spray combine.", + "8": "Villages near the shore are drenched, their streets awash with seawater.", + "9": "Lightning dances over the open sea, illuminating towering waves.", + "10": "The air carries a heavy, salty tang mixed with the storm’s humidity.", + "11": "Storm surges threaten to inundate the coast, forcing a retreat inland.", + "12": "The coastline becomes a blur of rain, wind, and crashing waves." + }, + "volcano": { + "1": "Rain hisses against the hot rock, sending up wisps of steam.", + "2": "Streams of water flow rapidly down volcanic slopes, carving paths through ash.", + "3": "Lightning illuminates the jagged volcanic landscape in brief flashes.", + "4": "The storm’s winds scatter loose volcanic debris across the area.", + "5": "Steam rises from fissures as rainwater meets underground heat.", + "6": "The air feels thick with humidity and the acrid scent of sulfur.", + "7": "Water mixes with ash, creating slippery and treacherous mudflows.", + "8": "Pools of rain collect in craters, reflecting the stormy sky above.", + "9": "The storm drowns out the usual rumble of the volcano.", + "10": "Visibility is reduced as rain and steam obscure the landscape.", + "11": "Rivulets of water cut through blackened earth, adding to the chaotic scene.", + "12": "The storm adds an eerie intensity to the already foreboding volcano." + }, + "artic": { + "1": "Rain freezes as it falls, coating the icy terrain in a slick layer.", + "2": "The storm’s winds drive the freezing rain sideways across the tundra.", + "3": "Icebergs glisten under the brief flashes of lightning.", + "4": "The air feels dense with moisture, heavy despite the freezing temperatures.", + "5": "Rain pools on top of frozen lakes, creating hazardous patches.", + "6": "Visibility drops as fog mixes with the storm, obscuring the horizon.", + "7": "Snow melts briefly under the deluge, refreezing into jagged shapes.", + "8": "Icicles grow rapidly as rainwater freezes on contact.", + "9": "The relentless rain forms rivers that cut through the icy expanse.", + "10": "Glacial crevasses fill with water, becoming hidden traps.", + "11": "The rain amplifies the chill, seeping into even the thickest clothing.", + "12": "Thunder rolls across the frozen landscape, echoing off distant ice walls." + }, + "cursed": { + "1": "Rain falls black and cold, soaking into the cursed ground ominously.", + "2": "Thunder cracks unnaturally, as if echoing from another realm.", + "3": "Shadows seem to lengthen under the torrential rain, despite the storm's chaos.", + "4": "Pools of water form, reflecting twisted and unfamiliar shapes.", + "5": "The storm feels alive, its winds whispering unintelligible words.", + "6": "Rain carries the faint scent of decay, clinging to everything it touches.", + "7": "Lightning reveals ghostly apparitions in the downpour’s curtain.", + "8": "The cursed ground refuses to absorb the rain, turning into a slick mire.", + "9": "The storm’s wind carries unnatural howls, chilling the soul.", + "10": "Rainwater gathers in strange, glowing pools before sinking into the earth.", + "11": "The air feels heavy, charged with dark energy as the storm rages on.", + "12": "The rain obscures vision, but shadows seem to move within the downpour." + } + } + }, + "Warm and Calm": { + "conditions": { + "temperature": { "gte": 55, "lte": 80 }, + "precipitation": { "lte": 50 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 40, "lte": 70 }, + "cloudCover": { "lte": 50 }, + "visibility": { "gte": 40 } + }, + "descriptions": { + "farm": { + "1": "The fields bask under a golden sun, the air perfectly still.", + "2": "Birds chirp lazily as the gentle warmth settles over the crops.", + "3": "A light breeze sways the tall grass under a serene sky.", + "4": "Farm animals rest contentedly, enjoying the calm weather.", + "5": "The warmth encourages sprouting seeds to push through the soil.", + "6": "A faint scent of wildflowers carries over the gentle breeze.", + "7": "Sunlight glints off the dew, casting tiny rainbows over the fields.", + "8": "The air feels light and soothing, perfect for a day of work.", + "9": "Farmers take advantage of the calm, tending to their chores outdoors.", + "10": "Clouds drift lazily, adding a touch of shade to the warm day.", + "11": "The earth radiates a quiet contentment under the temperate sky.", + "12": "Evening approaches gently, the warmth lingering pleasantly." + }, + "village": { + "1": "Children play in the village square under a bright, warm sun.", + "2": "The streets hum with peaceful activity, the air perfectly calm.", + "3": "Merchants smile as the pleasant warmth encourages shoppers.", + "4": "Windows are thrown open, inviting the mild air indoors.", + "5": "A calm breeze carries the scent of baking bread through the lanes.", + "6": "Villagers gather outside to share stories in the comforting warmth.", + "7": "Smoke from chimneys rises straight up under a tranquil sky.", + "8": "The warmth draws everyone outside, the streets lively yet serene.", + "9": "Sunlight filters through the cottages, casting warm hues over the village.", + "10": "The day feels perfect, with no signs of harsh weather in sight.", + "11": "Gardens bloom vibrantly, basking in the temperate sun.", + "12": "Evening settles slowly, the warmth lingering over the village." + }, + "city": { + "1": "Market squares bustle under a sunlit, calm sky.", + "2": "The warmth encourages citizens to stroll through the city’s streets.", + "3": "A soft breeze rustles banners and brings a sense of peace.", + "4": "Children chase each other near fountains, their laughter echoing.", + "5": "The warmth brings life to every corner, from bustling squares to quiet alleys.", + "6": "Artists sketch in the plazas, inspired by the tranquil weather.", + "7": "Civic officials hold meetings outdoors, enjoying the pleasant air.", + "8": "The city feels alive yet calm, its people moving with purpose but without hurry.", + "9": "Sunlight glints off the cobblestones, warming the city thoroughly.", + "10": "The calm day draws travelers to the city gates, seeking trade or rest.", + "11": "Horses and carts move leisurely through the thoroughfares.", + "12": "Even as the sun sets, the city’s warmth remains welcoming." + }, + "plains": { + "1": "The vast plains stretch under a warm, clear sky.", + "2": "Gentle sunlight touches every blade of grass, painting the landscape golden.", + "3": "Wildflowers bloom, their colors vivid against the serene backdrop.", + "4": "Birds glide gracefully, the air calm and undisturbed.", + "5": "The warmth feels almost tangible, spreading a sense of peace.", + "6": "The horizon shimmers slightly in the pleasant heat.", + "7": "Travelers feel the comfort of the sun on their backs as they cross the plains.", + "8": "The light breeze whispers through the grass, a soothing sound.", + "9": "Herds of animals graze peacefully, undisturbed by harsh weather.", + "10": "The day seems endless and perfect, inviting exploration.", + "11": "Shadows of passing clouds add a touch of variety to the golden plain.", + "12": "As night falls, the plains cool gently, still wrapped in calm." + }, + "forest": { + "1": "Sunlight streams through the canopy, creating patches of warm light.", + "2": "The forest hums with life, encouraged by the gentle warmth.", + "3": "A calm breeze rustles the leaves, adding a soft, soothing rhythm.", + "4": "The warmth seems to awaken the scent of pine and fresh earth.", + "5": "Birdsong fills the air, the calm encouraging creatures to stir.", + "6": "The undergrowth basks in the mild sun, vibrant with life.", + "7": "Shadows and light dance across the forest floor, mesmerizing in their beauty.", + "8": "The gentle warmth invites quiet reflection in the peaceful woods.", + "9": "The forest feels alive but unhurried, every part thriving under the warm sky.", + "10": "A perfect day for foraging, with no storms or harsh winds in sight.", + "11": "The trees sway slightly in the soft breeze, a comforting sight.", + "12": "Dappled light lingers as the sun sets, the warmth gently fading." + }, + "swamp": { + "1": "The swamp feels warmer but oddly calm, its stillness almost serene.", + "2": "Insects buzz lazily, the warmth making them sluggish.", + "3": "Sunlight glistens off stagnant pools, creating a surreal beauty.", + "4": "The warm air carries the scent of moss and rich earth.", + "5": "Even the murky waters seem peaceful under the mild sun.", + "6": "The swamp’s creatures move slowly, content in the calm weather.", + "7": "Reeds sway gently as a soft breeze ripples across the water.", + "8": "The warmth encourages growth, moss and vines thriving in abundance.", + "9": "Despite the swamp’s eerie feel, the weather adds a touch of tranquility.", + "10": "Frogs croak in a steady rhythm, their sounds blending with the calm.", + "11": "The swamp feels quieter, as if even it recognizes the day’s peace.", + "12": "The warmth lingers into the evening, the swamp bathed in golden light." + }, + "jungle": { + "1": "The jungle teems with life, its warmth enhancing the symphony of sounds.", + "2": "Sunlight filters through the dense canopy, creating a golden glow.", + "3": "The air is thick with warmth but pleasantly still.", + "4": "Leaves glisten with moisture, reflecting the soft light.", + "5": "The gentle warmth brings out the vivid colors of the jungle’s flora.", + "6": "Birds and monkeys chatter in the treetops, invigorated by the calm weather.", + "7": "The jungle feels alive, every corner buzzing with quiet energy.", + "8": "Streams flow gently, the sunlight shimmering on their surfaces.", + "9": "A soft breeze carries the earthy scent of the jungle.", + "10": "The thick greenery thrives under the sun, every leaf glowing with health.", + "11": "The warmth draws out colorful insects, flitting among the plants.", + "12": "As night falls, the jungle retains its warmth, alive with nocturnal sounds." + }, + "hills": { + "1": "Gentle warmth envelops the rolling hills under a cloudless sky.", + "2": "The breeze carries the sweet scent of wildflowers across the slopes.", + "3": "Sunlight glints off scattered rocks, warming the grassy terrain.", + "4": "Shepherds relax, their flocks grazing peacefully in the calm air.", + "5": "A perfect day for exploring the gentle inclines of the hills.", + "6": "The stillness of the air enhances the natural beauty of the landscape.", + "7": "Birds chirp lazily, their songs carried by a faint breeze.", + "8": "The warmth brings life to the hills, grasses swaying gently.", + "9": "Dappled light from scattered clouds adds texture to the sunlit hills.", + "10": "Travelers take in the scenery, enjoying the serene atmosphere.", + "11": "The golden light of the sun casts long shadows across the valleys.", + "12": "Evening falls softly, the warmth lingering over the gentle slopes." + }, + "mountains": { + "1": "The mountain peaks glow under the warmth of the high sun.", + "2": "The crisp air is tinged with a calming stillness, perfect for climbing.", + "3": "Rocks warm under the sun, adding a golden hue to the rugged slopes.", + "4": "Streams trickle down the mountainsides, sparkling in the sunlight.", + "5": "The tranquil weather highlights the majesty of the towering peaks.", + "6": "Eagles soar effortlessly, the clear skies offering an endless view.", + "7": "The warmth soothes the usual chill of the higher altitudes.", + "8": "Travelers rest on sun-warmed ledges, taking in the breathtaking view.", + "9": "The gentle warmth makes the rugged paths seem less daunting.", + "10": "Sunbeams pierce through the thin mountain air, illuminating the cliffs.", + "11": "A rare calm settles over the peaks, making the heights feel inviting.", + "12": "Even as the sun sets, the mountains radiate the day’s stored warmth." + }, + "desert": { + "1": "The sun bathes the sands in golden light, the warmth comforting.", + "2": "The dunes seem to shimmer under a tranquil, cloudless sky.", + "3": "The desert air feels calm, with a faint breeze stirring the sand.", + "4": "Shadows cast by rocky outcrops offer a pleasant respite from the sun.", + "5": "The warmth brings a sense of serenity to the arid expanse.", + "6": "Travelers feel the steady heat but appreciate the stillness.", + "7": "Tracks of animals crossing the sands are perfectly preserved in the calm.", + "8": "Cacti stand tall under the warm glow, thriving in the calm weather.", + "9": "The horizon stretches endlessly, undisturbed by storms or winds.", + "10": "Mirages waver faintly in the distance, adding a touch of mystery.", + "11": "The sun’s heat is tempered by the peacefulness of the day.", + "12": "As night approaches, the desert retains a gentle warmth." + }, + "coastal": { + "1": "The sea glistens under the warm sun, waves gently lapping the shore.", + "2": "Seabirds wheel lazily in the sky, their calls mingling with the calm surf.", + "3": "The coastal breeze carries a faint scent of salt and seaweed.", + "4": "Fishing boats drift on calm waters, their sails glowing in the sunlight.", + "5": "The warmth makes the coast feel inviting, perfect for gathering shellfish.", + "6": "The sandy shore is dotted with footprints, left undisturbed by the breeze.", + "7": "The horizon blurs slightly in the comforting heat, the ocean tranquil.", + "8": "The sun’s reflection dances on the water, casting golden sparkles.", + "9": "Villagers relax near the docks, enjoying the temperate weather.", + "10": "Children play along the shore, their laughter carried on the warm air.", + "11": "The steady warmth and calm sea create an idyllic coastal scene.", + "12": "Evening falls gently, the sea mirroring the warm hues of the sky." + }, + "volcano": { + "1": "The volcanic slopes radiate heat, blending with the day’s natural warmth.", + "2": "The air feels heavier near the volcano, though the calm weather soothes.", + "3": "Pockets of steam rise from fissures, adding mystery to the serene day.", + "4": "The rocky terrain basks under the warm glow of the sun.", + "5": "A rare stillness settles over the usually harsh volcanic landscape.", + "6": "Molten streaks from past eruptions glint in the sunlight.", + "7": "The warmth emphasizes the rugged beauty of the volcanic terrain.", + "8": "Travelers tread carefully, the heat rising from both sun and earth.", + "9": "A faint sulfurous scent lingers in the calm, warm air.", + "10": "The sky above the volcano is a brilliant blue, untouched by smoke.", + "11": "The day feels surreal, the volcanic landscape unusually peaceful.", + "12": "As the sun sets, the warmth clings to the dark, rocky slopes." + }, + "artic": { + "1": "The icy expanse shimmers under the rare warmth of the sun.", + "2": "The frozen air feels lighter, softened by the temperate weather.", + "3": "Snow and ice glisten like jewels under the calm, clear sky.", + "4": "The warmth brings out faint cracks in the ice, a rare sound in the stillness.", + "5": "The arctic landscape feels welcoming, despite its usual harshness.", + "6": "Polar bears rest lazily, their fur catching the sun’s gentle warmth.", + "7": "Travelers find the calm weather a welcome relief from icy winds.", + "8": "The horizon is bathed in golden light, softening the sharp edges of ice.", + "9": "Even the glaciers seem to glow under the sun’s rare warmth.", + "10": "The still air carries the faint sound of distant ice cracking.", + "11": "The landscape appears otherworldly, transformed by the day’s tranquility.", + "12": "As the day ends, the arctic remains serene, the warmth lingering." + }, + "cursed": { + "1": "A deceptive warmth hangs in the air, masking the land’s dark secrets.", + "2": "The cursed ground feels unnervingly calm under the mild sun.", + "3": "Shadows linger unnaturally, even as sunlight touches the cursed land.", + "4": "The warmth feels hollow, as if the land itself rejects it.", + "5": "A faint unease spreads, despite the tranquil appearance of the day.", + "6": "The air feels thick with something unseen, though the sky remains clear.", + "7": "The cursed landscape remains still, its eerie silence amplified by the calm.", + "8": "The sunlight fails to penetrate the gloom entirely, leaving an odd dimness.", + "9": "A sense of dread lingers, unfazed by the temperate weather.", + "10": "Travelers tread cautiously, the warmth doing little to ease their fear.", + "11": "The cursed land exudes a quiet menace, even under a calm sky.", + "12": "As night falls, the warmth vanishes abruptly, replaced by a chilling unease." + } + } + }, + "Tropical Heat Showers": { + "conditions": { + "temperature": { "gte": 65, "lte": 90 }, + "precipitation": { "gte": 30, "lte": 70 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 50, "lte": 80 }, + "visibility": { "gte": 30, "lte": 70 } + }, + "descriptions": { + "farm": { + "1": "Brief showers bring relief to the sun-scorched fields.", + "2": "Rain falls in sudden bursts, saturating the thirsty crops.", + "3": "Warm rain drenches the soil, leaving a steaming haze over the fields.", + "4": "Farmers pause as the downpour briefly interrupts their labor.", + "5": "The showers are warm but refreshing, bringing life to the dry ground.", + "6": "Puddles form in the fields, reflecting the overcast yet bright skies.", + "7": "Rainbows appear briefly as tropical rain comes and goes.", + "8": "Livestock find shelter as warm rain pelts the farmland.", + "9": "The heat intensifies between the brief yet heavy showers.", + "10": "A sudden deluge washes away the dust coating the fields.", + "11": "Farmhands laugh as they are caught in the brief, warm rain.", + "12": "The rain ends as suddenly as it began, leaving a humid calm." + }, + "village": { + "1": "Villagers dash for cover as warm rain sweeps through the streets.", + "2": "Puddles form quickly in the dirt paths, reflecting cloudy skies.", + "3": "The roofs drip steadily as a quick tropical shower passes by.", + "4": "Steam rises from the ground after a sudden, warm downpour.", + "5": "Children laugh, splashing in the warm rain that floods the alleys.", + "6": "The rain brings a brief reprieve from the oppressive tropical heat.", + "7": "Clothes hung out to dry are soaked again by the sudden rain.", + "8": "Villagers retreat to their huts, listening to the rain drum on the thatch.", + "9": "The scent of wet earth fills the village as the rain passes.", + "10": "Brief rain showers leave a shimmering sheen over the rooftops.", + "11": "Warm rain refreshes the air, though humidity quickly returns.", + "12": "The rain ceases abruptly, leaving the village in a damp, tropical calm." + }, + "city": { + "1": "Rainwater streams off stone walls, soaking the bustling streets.", + "2": "Merchants cover their goods as a sudden shower sweeps the market.", + "3": "Warm rain pools in cobbled alleys, reflecting the gray sky.", + "4": "The city air feels fresher after a brief tropical downpour.", + "5": "Children play in the puddles, their laughter echoing through the wet streets.", + "6": "Street vendors hurriedly set up tarps as rain drenches the square.", + "7": "The smell of rain-soaked stone fills the air after the passing shower.", + "8": "Rain lashes against windows before giving way to calm once more.", + "9": "A fleeting rainbow arcs over the city after the rain subsides.", + "10": "The humid air thickens as the heat returns after the warm rain.", + "11": "Drains struggle to keep up with the brief but intense showers.", + "12": "The streets glisten as the tropical rain showers leave as quickly as they came." + }, + "plains": { + "1": "The flat expanse is briefly cooled by a warm tropical shower.", + "2": "Rain darkens the tall grasses, leaving them heavy with moisture.", + "3": "A misty haze rises as the rain warms the dry ground.", + "4": "The shower sweeps across the plains, drenching everything in its path.", + "5": "Animals scatter as the sudden rain interrupts the quiet plains.", + "6": "The downpour passes quickly, leaving shimmering droplets on every blade of grass.", + "7": "Brief rain showers leave the plains steamy and humid.", + "8": "The rain brings a soothing rhythm to the otherwise silent expanse.", + "9": "Herds of animals graze, undisturbed by the warm, passing rain.", + "10": "Clouds gather and dissipate quickly, the tropical heat prevailing.", + "11": "The horizon blurs under the soft veil of a fleeting tropical shower.", + "12": "Steam rises from the ground as the heat and rain combine." + }, + "forest": { + "1": "Rain patters against the dense canopy, barely reaching the forest floor.", + "2": "The forest grows muggy as warm rain filters through the leaves.", + "3": "Raindrops glisten on ferns, sparkling in the filtered sunlight.", + "4": "The tropical downpour intensifies the earthy scents of the forest.", + "5": "Leaves drip steadily after a short, warm shower passes through.", + "6": "The sound of rain mingles with the calls of hidden creatures.", + "7": "Humidity clings to the air as the rain brings brief relief.", + "8": "The forest seems to sigh in relief as the warm rain nourishes it.", + "9": "A fine mist rises as the rain warms the undergrowth.", + "10": "The rain passes quickly, leaving the forest floor slick and damp.", + "11": "Tropical rain showers refresh the vibrant greens of the forest.", + "12": "Even in the warm rain, the forest feels alive with energy." + }, + "swamp": { + "1": "The swamp grows even more sodden as warm rain pours down.", + "2": "Raindrops ripple across the murky waters, disturbing the stillness.", + "3": "The rain mingles with the swamp’s earthy scents, creating a heavy atmosphere.", + "4": "Warm rain adds to the swamp’s humidity, thickening the air.", + "5": "Frogs croak loudly as the rain sends ripples across their pools.", + "6": "The swamp’s plants thrive under the brief yet heavy downpour.", + "7": "Rainwater streams through the dense undergrowth, pooling in the mud.", + "8": "Insects buzz louder, energized by the warm rain’s passing.", + "9": "The swamp seems alive with sound as the rain drums on the water.", + "10": "The air grows heavier as the rain adds moisture to the already humid swamp.", + "11": "Steamy mist rises as the rain warms the boggy ground.", + "12": "The swamp returns to its usual quiet as the rain fades away." + }, + "jungle": { + "1": "The jungle thrives as warm rain cascades through the dense canopy.", + "2": "Water drips from thick leaves, pooling on the jungle floor.", + "3": "The rain intensifies the jungle’s earthy, vibrant scents.", + "4": "Brightly colored birds flutter in the rain, their feathers glistening.", + "5": "The jungle floor becomes slick and muddy as the rain passes through.", + "6": "Raindrops sparkle on vines and flowers, creating a lush tapestry.", + "7": "Warm rain invigorates the jungle, its sounds amplified by the humidity.", + "8": "Mist rises as the heat and rain mingle in the dense undergrowth.", + "9": "The brief downpour nourishes the plants, leaving the jungle teeming with life.", + "10": "Rain drums against the canopy, creating a rhythmic, soothing sound.", + "11": "The rain ends as quickly as it began, leaving the jungle vibrant and wet.", + "12": "Humidity rises sharply as the tropical shower leaves the jungle steaming." + }, + "hills": { + "1": "Brief, warm showers sweep over the hills, bringing momentary relief from the heat.", + "2": "Rain falls in sudden bursts, causing small streams to form along the slopes.", + "3": "The hills steam gently as the warm rain meets the sun-baked earth.", + "4": "Grass glistens with moisture after a quick tropical shower passes.", + "5": "Shepherds take shelter as a warm downpour moves swiftly across the hills.", + "6": "The heat intensifies between showers, making the air heavy and humid.", + "7": "Rainbows appear fleetingly over the hills after the showers subside.", + "8": "Wildflowers perk up, rejuvenated by the sudden tropical rains.", + "9": "The rain creates temporary waterfalls over rocky outcrops.", + "10": "Animals emerge cautiously after the warm rain passes.", + "11": "Puddles reflect the cloudy yet bright skies between the showers.", + "12": "The hills are cloaked in a misty haze as the heat and rain combine.", + "13": "Warm rain rolls down the slopes, pooling in the lowlands.", + "14": "The hills steam as rain briefly cools the sunbaked earth.", + "15": "Rain showers bring a fleeting freshness to the grassy knolls.", + "16": "The warm downpour creates small rivulets along the hillside.", + "17": "Thunderous clouds pass quickly, leaving wet grass behind.", + "18": "Patches of mist rise as the rain warms the hilly terrain.", + "19": "The sound of falling rain echoes through the rolling hills.", + "20": "A brief shower leaves the hills glittering in the muted sunlight.", + "21": "Humidity intensifies after the warm rain saturates the ground.", + "22": "The hills bask in the warmth as rain adds to the vibrant greenery.", + "23": "Animals take shelter in small outcroppings during the tropical shower.", + "24": "The rain stops abruptly, leaving only the drip of water on leaves." + }, + "mountains": { + "1": "Warm rain cascades down the rocky slopes, carving temporary streams.", + "2": "The mountains are shrouded in mist as rain pours from heavy clouds.", + "3": "A brief but intense shower soaks the mountain trails.", + "4": "Rain steams off sun-warmed rocks, creating an eerie haze.", + "5": "The mountain air feels thick with humidity after the downpour.", + "6": "Distant thunder rolls as warm rain refreshes the rugged peaks.", + "7": "The rain leaves streaks of water glistening on sheer cliffs.", + "8": "Clouds part after the rain, revealing damp and glistening peaks.", + "9": "Waterfalls swell briefly as tropical rain drenches the mountainside.", + "10": "Rain-fed streams rush down the rocks, their sound filling the air.", + "11": "The mountain's flora glows vibrant green in the post-rain sunlight.", + "12": "The brief tropical shower clears, leaving a damp serenity.", + "13": "Warm rain showers cascade down the mountain slopes, creating temporary streams.", + "14": "The mountain air becomes heavy with humidity after sudden downpours.", + "15": "Mists rise from the valleys as the warm rain meets cooler air.", + "16": "Travelers seek shelter as brief but intense showers sweep through the peaks.", + "17": "Rainwater rushes over rocks, adding a roar to the mountainside.", + "18": "The sun breaks through clouds, creating spectacular vistas after the rain.", + "19": "The tropical showers bring a lushness to the mountain vegetation.", + "20": "Thunder echoes among the peaks during the brief rainstorms.", + "21": "Waterfalls swell temporarily with the influx of warm rain.", + "22": "Birds take flight, their feathers glistening after the passing showers.", + "23": "The mountain paths become slick and treacherous during the sudden rains.", + "24": "Rainbows arch over the mountains, a fleeting sight between showers." + }, + "desert": { + "1": "The parched sands hiss as warm rain briefly falls.", + "2": "Rain evaporates almost as soon as it touches the scorching dunes.", + "3": "Clouds bring fleeting shade and tropical showers to the desert expanse.", + "4": "Small puddles form in the cracks of baked earth before vanishing.", + "5": "Rain darkens patches of sand, creating temporary patterns on the dunes.", + "6": "Desert plants bloom quickly in response to the brief tropical rain.", + "7": "The rain is a welcome relief but leaves the desert more humid.", + "8": "The brief shower leaves a faint smell of damp sand in the air.", + "9": "Rain streaks the arid landscape, a fleeting but refreshing sight.", + "10": "The desert air feels heavy after the warm rain evaporates.", + "11": "Clouds disperse as quickly as they gather, the rain a mere memory.", + "12": "The sun reclaims dominance, evaporating the rain within moments.", + "13": "Rare tropical showers bring brief relief to the arid desert landscape.", + "14": "Rain falls in warm droplets, quickly absorbed by the thirsty sand.", + "15": "The desert air becomes humid after the sudden heat showers.", + "16": "Cacti bloom swiftly following the brief tropical rains.", + "17": "The hot sand sizzles as warm rain evaporates almost instantly.", + "18": "Mirages waver as the rain adds moisture to the desert air.", + "19": "The showers leave ephemeral puddles that reflect the sun.", + "20": "Desert creatures emerge to drink from temporary water sources.", + "21": "The rain brings a rare freshness to the normally dry atmosphere.", + "22": "Clouds gather and disperse quickly, the showers passing in moments.", + "23": "The scent of wet sand permeates the air after the fleeting rain.", + "24": "The heat intensifies after the showers, the desert steaming under the sun." + }, + "coastal": { + "1": "Warm rain ripples across the ocean waves and wets the sandy shore.", + "2": "The smell of salt mixes with rain as it drenches the coastal landscape.", + "3": "Seagulls call out as tropical showers pass over the coastline.", + "4": "Rainwater streams into the ocean, creating momentary fresh currents.", + "5": "The rain leaves the rocky cliffs slick and glistening.", + "6": "Warm rain steams off sunlit tide pools along the shore.", + "7": "The humid air feels charged with energy after the passing rain.", + "8": "The ocean mirrors the dark clouds as rain falls in a steady rhythm.", + "9": "Sailors hurry to secure their boats as the tropical rain intensifies.", + "10": "A fleeting rainbow arches over the coastline as the rain fades.", + "11": "The shore glistens under the tropical sun after the shower clears.", + "12": "The rain ends suddenly, leaving only the sound of crashing waves.", + "13": "Warm tropical showers sweep over the coastline, drenching the sands.", + "14": "The sea and sky blur as rain falls heavily but briefly over the water.", + "15": "Fishermen take cover as sudden showers interrupt their work.", + "16": "Palm trees sway gently in the warm rain along the beach.", + "17": "The scent of salty air mixes with the freshness of the tropical rain.", + "18": "Rainbows form over the ocean as the sun breaks through clouds.", + "19": "Seashells glisten on the wet sands after the passing showers.", + "20": "Warm rain creates ripples in tide pools, disturbing tiny creatures.", + "21": "Boats bob gently in the harbor, rain tapping on their wooden decks.", + "22": "The humidity rises sharply as the heat returns after the rain.", + "23": "Clouds reflect off the calm sea, mirrored during the brief downpours.", + "24": "The coastal village buzzes back to life as the showers pass quickly." + }, + "volcano": { + "1": "Rain hisses as it touches the warm, rocky surface of the volcano.", + "2": "Steam rises in plumes as tropical rain meets volcanic vents.", + "3": "The rain intensifies the smell of sulfur in the air.", + "4": "Lava flows are briefly veiled by mist as warm rain falls.", + "5": "Rain creates temporary rivulets in the blackened volcanic soil.", + "6": "Thunder echoes across the caldera as tropical rain clouds gather.", + "7": "The volcanic landscape glistens briefly under the tropical rain.", + "8": "The downpour brings a humid calm to the otherwise harsh terrain.", + "9": "Rainwater collects in craters, reflecting the ash-laden sky.", + "10": "The heat of the volcano quickly evaporates the brief rain.", + "11": "Steam and rain mingle, obscuring the jagged volcanic features.", + "12": "The rain fades, leaving the volcano steaming under a humid sky.", + "13": "Warm rain sizzles upon contact with the hot volcanic rock.", + "14": "Steam rises as tropical showers meet the warm surfaces of the volcano.", + "15": "Rainwater runs in rivulets, carving paths through ash and lava rock.", + "16": "The air becomes humid and heavy around the volcano during the showers.", + "17": "Brief downpours bring a rare greenery to the volcanic slopes.", + "18": "The showers pass quickly, leaving the volcanic terrain steaming.", + "19": "Travelers feel the heat intensify after the warm rain evaporates.", + "20": "Rainbows appear over the volcano, contrasting with its stark landscape.", + "21": "The scent of wet sulfur fills the air during the tropical showers.", + "22": "Rain pools in volcanic craters, creating temporary hot springs.", + "23": "The ground hisses as the rain evaporates on the warm earth.", + "24": "Clouds cling to the volcanic peak, dropping warm rain as they pass." + }, + "artic": { + "1": "Warm rain turns patches of snow into heavy slush.", + "2": "Rain dampens the icy landscape, creating slick, dangerous footing.", + "3": "The arctic tundra steams lightly as rain falls onto frozen ground.", + "4": "Rain darkens the icy landscape, pooling briefly before freezing.", + "5": "The warm rain brings a rare, momentary thaw to the icy wasteland.", + "6": "Rainwater beads and freezes on exposed ice, forming smooth surfaces.", + "7": "The air grows heavy with mist as warm rain clashes with cold air.", + "8": "Rain carves temporary rivulets in the melting snow.", + "9": "The icy plains shimmer as the tropical rain refreezes quickly.", + "10": "The arctic quiet is broken by the steady patter of warm rain.", + "11": "Fog rises where rain meets frozen rivers, obscuring the icy expanse.", + "12": "The brief warmth of the rain is gone as frost quickly reclaims the land.", + "13": "In an unusual event, warm showers fall upon the icy landscape.", + "14": "Rain melts the surface of the ice, creating a slick sheen.", + "15": "Mist rises as warm rain meets the cold Arctic air.", + "16": "The snow softens under the brief but warm downpour.", + "17": "Animals are puzzled by the tropical rain in the frigid environment.", + "18": "Icebergs glisten under the warm rain showers, melting slightly.", + "19": "The Arctic air becomes unusually humid during the rare showers.", + "20": "Puddles form on top of the ice, reflecting the gray skies.", + "21": "The rain quickly freezes after falling, adding a layer of ice.", + "22": "Explorers are surprised by the warm rain in the typically cold region.", + "23": "Clouds bring both warmth and moisture, a rarity in the Arctic.", + "24": "Steam hovers over the snow as the warm rain evaporates in the cold." + }, + "cursed": { + "1": "The tropical rain feels unnaturally warm and clings to the skin.", + "2": "The cursed lands seem to absorb the rain, leaving no trace behind.", + "3": "Rainwater turns dark and sluggish as it hits the blighted ground.", + "4": "The rain brings an eerie mist that moves unnaturally through the cursed area.", + "5": "The warm rain does little to refresh the oppressive atmosphere.", + "6": "Raindrops fall heavily, each seeming to carry a faint whisper.", + "7": "Dark clouds churn overhead, the rain thick with an unshakable unease.", + "8": "The cursed soil hisses as the rain sinks into its unnatural depths.", + "9": "Warm rain intensifies the smell of decay in the cursed lands.", + "10": "The cursed landscape gleams under the tropical rain, a false sense of calm.", + "11": "Rain pools into stagnant puddles, reflecting the twisted sky.", + "12": "The rain stops abruptly, leaving behind a lingering, heavy silence.", + "13": "Warm rain falls upon the cursed land, but offers no relief.", + "14": "The showers are warm yet unsettling, adding to the eerie atmosphere.", + "15": "Rainwater here feels heavy and strange, as if carrying a dark essence.", + "16": "The cursed grounds steam ominously as warm rain touches them.", + "17": "Plants in the cursed area wither even as the warm rain falls.", + "18": "The rain seems to whisper as it falls, unsettling those who hear it.", + "19": "Puddles form but reflect distorted images, adding to the unease.", + "20": "The air grows thick and oppressive during the tropical showers.", + "21": "The warm rain leaves behind a faint residue, dark and unnatural.", + "22": "Clouds over the cursed land move unnaturally, the rain unpredictable.", + "23": "The showers pass, but the sense of dread lingers in the humid air.", + "24": "Lightning flashes silently during the showers, adding to the ominous feel." + } + } + }, + "Frigid Mist": { + "conditions": { + "temperature": { "lte": 35 }, + "precipitation": { "gte": 30, "lte": 60 }, + "wind": { "lte": 55 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 50, "lte": 80 }, + "visibility": { "lte": 40 } + }, + "descriptions": { + "farm": { + "1": "A cold mist blankets the fields, leaving frost on the crops.", + "2": "The farmstead is shrouded in an icy fog that chills to the bone.", + "3": "Frigid mist creeps into the barns, making the livestock restless.", + "4": "Frozen dew clings to the hay as the mist thickens around the farm.", + "5": "A chilling mist hangs low over the farm, obscuring the distant fences.", + "6": "Cold vapors seep into the farmhouse, making the hearth feel inadequate.", + "7": "The icy mist dulls the morning light, leaving the farm eerily quiet.", + "8": "A veil of frost-laden mist lingers over the plowed fields.", + "9": "The mist wraps the farm in a frozen stillness, muffling all sounds.", + "10": "Shimmering frost forms on the wooden beams of the farm structures.", + "11": "The frigid mist turns the farmstead into a ghostly, silent tableau.", + "12": "Hoarfrost collects on the farm's tools as the cold mist persists." + }, + "village": { + "1": "The village lanes are cloaked in a frigid mist, silencing the usual bustle.", + "2": "Icy fog rolls through the village, dampening the fires in the hearths.", + "3": "The mist clings to the rooftops, making the chimneys' smoke barely visible.", + "4": "Villagers huddle indoors as the cold mist seeps through the wooden walls.", + "5": "Frigid mist curls around the cobbled paths, freezing puddles solid.", + "6": "The bell in the village square tolls faintly through the icy fog.", + "7": "Frosty mist settles on the market stalls, deterring even the hardiest vendors.", + "8": "The village well glistens with ice as the cold mist persists.", + "9": "Frozen vapors hang over the village green, rendering it lifeless.", + "10": "The mist turns the village into a shadowy and silent maze.", + "11": "A thin layer of frost spreads across the stone cottages.", + "12": "The icy mist makes even the bravest villagers wary of venturing out." + }, + "city": { + "1": "Frigid mist drifts through the city streets, muffling the sounds of life.", + "2": "Cold vapors gather at the edges of the city walls, shrouding them in frost.", + "3": "The mist clings to the cobblestones, making footing treacherous.", + "4": "City lamps barely pierce the thick, icy fog hanging over the thoroughfares.", + "5": "The frigid mist turns the market square into a ghostly silhouette.", + "6": "Frost forms on the metal gates as the mist chills the city to its core.", + "7": "Icy fog rolls off the river, enveloping the city's bustling docks.", + "8": "Shadows of towering spires loom faintly through the frozen haze.", + "9": "The cold mist turns the alleys into unwelcoming corridors of shadow.", + "10": "The city's heartbeat slows as the mist creates an eerie stillness.", + "11": "Even the city's guards tread cautiously in the biting, frosty fog.", + "12": "The mist coats the city's statues with a shimmering layer of frost." + }, + "plains": { + "1": "Frigid mist sprawls across the open plains, turning grass into icy blades.", + "2": "The cold fog hovers low, making the horizon seem endless and gray.", + "3": "A bitter chill accompanies the mist that blankets the windswept plains.", + "4": "Hoarfrost clings to every stalk of grass as the icy mist thickens.", + "5": "The plains are silent save for the faint whistle of wind in the mist.", + "6": "Frozen vapors swirl around solitary boulders scattered across the plains.", + "7": "The mist cloaks the flatlands, transforming them into an eerie, frozen sea.", + "8": "Every step crunches on frost-coated grass hidden beneath the cold fog.", + "9": "The frigid mist reduces visibility, making the plains feel desolate.", + "10": "The chill mist moves like a ghostly tide across the endless fields.", + "11": "The plains appear lifeless as the mist turns every surface icy.", + "12": "Frigid vapors rise from hidden streams, blending into the low-hanging fog." + }, + "forest": { + "1": "The forest is shrouded in an icy mist, leaving branches heavy with frost.", + "2": "Frigid fog curls between the trees, muffling the sounds of the forest.", + "3": "The cold mist drips from frozen leaves, collecting in icy puddles below.", + "4": "A frozen stillness envelops the forest as the frigid mist settles.", + "5": "The mist snakes through the underbrush, chilling the forest to its core.", + "6": "Icy fog weaves through the tree trunks, making them appear as ghostly sentinels.", + "7": "Frost forms intricate patterns on fallen logs as the cold mist deepens.", + "8": "The forest canopy traps the icy vapors, creating an ethereal frostbound gloom.", + "9": "The cold mist masks animal sounds, rendering the forest eerily silent.", + "10": "The mist creates a silver sheen on moss-covered stones and roots.", + "11": "Frigid fog clings to the ground, reducing the forest to shadowy outlines.", + "12": "The mist sparkles faintly in the dim light as frost accumulates." + }, + "swamp": { + "1": "The swamp is veiled in icy fog, turning murky waters into frozen mirrors.", + "2": "Frigid mist swirls over the bog, coating reeds with a thin frost.", + "3": "The swamp feels unnaturally quiet as the mist chills the stagnant air.", + "4": "Frost forms on twisted roots jutting from the frozen, misty waters.", + "5": "The icy fog thickens, shrouding the swamp's dark pools in cold mystery.", + "6": "The mist clings to the swamp's decaying vegetation, freezing it solid.", + "7": "Hoarfrost forms on low-hanging branches as the frigid mist deepens.", + "8": "The swamp appears lifeless, its chill mist stilling even the insects.", + "9": "Icy vapors rise from dark, sluggish waters, adding to the swamp's unease.", + "10": "The mist leaves the swamp coated in frost, a stark contrast to its usual dampness.", + "11": "Frigid fog dampens the swamp's usual smells, replacing them with biting cold.", + "12": "The swamp becomes a frozen labyrinth as mist veils its treacherous paths." + }, + "jungle": { + "1": "The jungle is cloaked in icy mist, turning vines into frozen tendrils.", + "2": "Frigid fog winds through the dense jungle, chilling the humid air.", + "3": "Frozen droplets hang from tropical leaves as the mist thickens.", + "4": "The cold mist contrasts sharply with the jungle's usual heat and vibrancy.", + "5": "The jungle floor is slick with frost as the mist lingers overhead.", + "6": "Frigid vapors curl around tree trunks, muffling the jungle's sounds.", + "7": "Hoarfrost clings to towering ferns as the icy mist deepens.", + "8": "The mist hangs heavy in the jungle, freezing even the smallest creeks.", + "9": "The icy fog creates an otherworldly silence in the normally bustling jungle.", + "10": "The jungle canopy traps the frigid mist, creating a chilling gloom.", + "11": "The mist turns the jungle's vibrant greens into muted, frosted hues.", + "12": "The cold fog moves like a phantom, freezing dew drops in its path." + }, + "hills": { + "1": "A chilling mist rolls over the hills, leaving frost on the grass.", + "2": "The mist clings to the slopes, freezing the shrubs in its icy grip.", + "3": "Frigid vapors hang low, turning the rolling hills into a frozen landscape.", + "4": "Hoarfrost gathers on the rocks as the icy mist deepens across the hills.", + "5": "The hills are shrouded in a biting mist, muting all sound and color.", + "6": "Cold fog creeps through the valleys, leaving a thin layer of frost behind.", + "7": "The mist flows like a frozen river over the hilltops, chilling the air.", + "8": "Frost collects on every blade of grass as the frigid mist spreads.", + "9": "The hills are eerily silent, enveloped by a dense, icy fog.", + "10": "Frozen vapors twist around the hills, making them appear ghostly and surreal.", + "11": "A biting mist swirls over the hills, frosting every surface in its path.", + "12": "The frigid mist leaves the hills coated in a shimmering layer of frost." + }, + "mountains": { + "1": "The mountain peaks vanish into the dense, frigid mist.", + "2": "Icy fog clings to the cliffs, making the ascent perilous.", + "3": "The mist flows through mountain passes, freezing every exposed surface.", + "4": "Cold vapors shroud the mountain trails, turning them into frozen pathways.", + "5": "Frigid mist swirls around the jagged rocks, leaving them slick with frost.", + "6": "The peaks are cloaked in a biting fog that numbs the senses.", + "7": "Frozen mist curls into the caves, chilling even the deepest recesses.", + "8": "The mist moves like a silent predator through the mountain valleys.", + "9": "Hoarfrost gathers on the pine trees as the frigid mist deepens.", + "10": "The icy fog muffles all sound, leaving the mountains eerily still.", + "11": "Frost-coated stones glisten faintly through the shifting mist.", + "12": "The mountain air is sharp and bitter, carried by the freezing mist." + }, + "desert": { + "1": "A rare, frigid mist blankets the desert, turning sand into icy crystals.", + "2": "The cold fog moves across the dunes, leaving a layer of frost behind.", + "3": "Frozen vapors creep through the desert, chilling the air unnaturally.", + "4": "Hoarfrost sparkles faintly on the cacti as the mist settles.", + "5": "The desert feels lifeless under the weight of the freezing fog.", + "6": "The frigid mist obscures the horizon, turning the dunes into ghostly shapes.", + "7": "Cold vapors cling to the desert's rocks, frosting their surfaces.", + "8": "The mist swirls through dry riverbeds, chilling the sand to its core.", + "9": "The desert air turns biting as the icy mist flows over the dunes.", + "10": "The usually warm sands are frozen under the mist's frosty touch.", + "11": "The frigid mist creates a surreal, frozen wasteland in the desert.", + "12": "The cold fog transforms the desert into a shimmering expanse of frost." + }, + "coastal": { + "1": "Icy mist drifts from the sea, chilling the coastal cliffs.", + "2": "The frigid fog rolls in from the waves, freezing the rocky shoreline.", + "3": "Frozen vapors hover over the docks, chilling the timbers of the ships.", + "4": "Hoarfrost forms on fishing nets as the cold mist thickens.", + "5": "The mist clings to the coastal vegetation, turning it into frozen sculptures.", + "6": "The frigid mist silences the crashing waves, creating an eerie stillness.", + "7": "The shoreline disappears into the biting fog, chilling the air.", + "8": "The mist carries a salty chill, freezing spray on the coastal rocks.", + "9": "Frost gathers on the sand as the icy fog settles over the beach.", + "10": "The frigid mist turns the coastal waters into a mirror of frost.", + "11": "The cold fog freezes the seaweed, turning it brittle underfoot.", + "12": "The mist transforms the coastline into a frostbound, spectral landscape." + }, + "volcano": { + "1": "The frigid mist mingles with the volcanic steam, creating an eerie chill.", + "2": "Frozen vapors settle on the cooled lava flows, creating a surreal frost.", + "3": "The biting mist contrasts sharply with the lingering volcanic heat.", + "4": "Hoarfrost clings to the volcanic rocks, defying the warmth beneath.", + "5": "The frigid mist swirls through the craters, chilling even the warmest stones.", + "6": "Cold fog blankets the volcano, muting the usual sulfuric odors.", + "7": "The icy mist turns the volcanic ash brittle, crunching underfoot.", + "8": "The mist transforms the blackened landscape into a frost-covered expanse.", + "9": "The frigid fog obscures the fiery glow of molten rock.", + "10": "Frozen vapors hang heavy in the air, chilling the volcanic vents.", + "11": "The cold mist moves like a ghostly tide across the volcanic slopes.", + "12": "The biting fog freezes even the steam rising from fissures in the earth." + }, + "artic": { + "1": "The icy mist thickens, blending seamlessly with the endless snowfields.", + "2": "Frigid fog curls around icebergs, freezing the surrounding waters further.", + "3": "The arctic landscape is shrouded in a biting mist, silencing all sound.", + "4": "Hoarfrost coats the icy cliffs as the mist deepens across the tundra.", + "5": "The frigid fog clings to the frozen ground, chilling the air even more.", + "6": "The icy mist sparkles faintly under the dim arctic light.", + "7": "Frozen vapors drift across the snow, turning the horizon into a blur.", + "8": "The biting fog flows through the glaciers, frosting even the air.", + "9": "The mist mingles with the icy winds, making the arctic bitterly cold.", + "10": "Frost gathers on every surface as the frigid mist persists.", + "11": "The arctic becomes a silent, frozen expanse under the heavy mist.", + "12": "The mist veils the tundra, making even nearby shapes hard to discern." + }, + "cursed": { + "1": "The frigid mist carries an unnatural chill, whispering through the cursed land.", + "2": "Icy fog clings to the cursed ground, emanating an eerie sense of dread.", + "3": "The biting mist feels alive, curling around every object it touches.", + "4": "Frozen vapors rise from the cursed soil, chilling the air with malice.", + "5": "Hoarfrost collects on twisted trees, adding to the cursed land's ominous aura.", + "6": "The frigid mist obscures the cursed terrain, muffling all sound eerily.", + "7": "The mist carries faint whispers, its icy touch numbing the senses.", + "8": "Frosted stones and dead vegetation are shrouded in the unnatural fog.", + "9": "The mist lingers unnaturally, freezing everything in its spectral embrace.", + "10": "The frigid fog twists into unnatural shapes, chilling the cursed ground.", + "11": "The cold mist feels suffocating, heavy with the weight of curses past.", + "12": "Frozen vapors swirl in patterns that hint at dark and malevolent forces." + } + } + }, + "Mild Snow Shower": { + "conditions": { + "temperature": { "gte": 20, "lte": 35 }, + "precipitation": { "gte": 40, "lte": 70 }, + "wind": { "gte": 20, "lte": 50 }, + "humidity": { "gte": 40, "lte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "gte": 30, "lte": 70 } + }, + "descriptions": { + "farm": { + "1": "Snow drifts gently over the fields, dusting crops in a light white blanket.", + "2": "The farmstead is quietly coated by a mild snow shower, softening the landscape.", + "3": "Light flakes settle on the barn roofs and fences, bringing a calm chill.", + "4": "Gentle snow showers leave the farmland with a fresh, icy shimmer.", + "5": "The fields are sprinkled with light snow, crunching faintly underfoot.", + "6": "Soft flakes fall steadily, blanketing the soil without haste.", + "7": "Snow gathers lightly on the farmhouse windows, a fleeting winter touch.", + "8": "The barns and stables appear dusted with sugar under the mild snowfall.", + "9": "The snow flutters down like feathers, layering the plowed fields lightly.", + "10": "Faint snow showers dance across the farm, leaving only a hint of frost.", + "11": "The distant fields are softly veiled by the lightest of winter snows.", + "12": "A tranquil snow shower dusts the scarecrow and barren crops in white." + }, + "village": { + "1": "Snow drifts lazily onto thatched roofs, giving the village a serene look.", + "2": "The mild snowfall frosts the cobblestone streets lightly.", + "3": "Children gather to catch the gentle flakes, laughing in the village square.", + "4": "Snow coats the carts and stalls with a fleeting touch of winter.", + "5": "The light snow barely sticks to the ground, vanishing in patches.", + "6": "Flakes drift quietly, resting on the roofs and chimneys of the village.", + "7": "Snow softens the edges of the buildings, creating a tranquil scene.", + "8": "Mild snow drapes the well and benches in the village center.", + "9": "Soft flurries fall on the villagers, bringing a calm hush to the streets.", + "10": "Snow settles on the stone walls surrounding the quiet homes.", + "11": "The snowfall is light enough to blend with the smoke rising from chimneys.", + "12": "Snowflakes catch on cloaks and boots as villagers go about their day." + }, + "city": { + "1": "Snow falls lightly over the bustling streets, melting on warm stones.", + "2": "The mild snow shower decorates the city’s spires and battlements.", + "3": "Flakes settle on merchant carts and passersby, barely accumulating.", + "4": "The city square sparkles faintly as the light snow lands on the fountains.", + "5": "Snow dusts the statues and market stalls in a fleeting winter touch.", + "6": "The snowfall mingles with city life, softening its usual clamor.", + "7": "Snowflakes dance through the air, landing gently on cobblestone streets.", + "8": "The city gate appears frosted as snow falls lightly on its iron bars.", + "9": "Gentle snow glimmers in the torchlight, blanketing the busy streets softly.", + "10": "Flakes swirl around the towers, leaving a thin white coat on stone.", + "11": "The snowfall is soft, decorating balconies and bridges without weight.", + "12": "Snow drifts onto awnings and rooftops, adding charm to the cityscape." + }, + "plains": { + "1": "The open plains are lightly dusted by a tranquil snow shower.", + "2": "Snowflakes drift endlessly across the wide expanse, leaving faint trails.", + "3": "The snowfall spreads evenly, softening the sharp lines of the plains.", + "4": "Snow covers the tall grasses, flattening them into delicate white sheets.", + "5": "The wind carries flakes gently across the vast and silent landscape.", + "6": "A mild snow shower veils the plains in an ephemeral winter embrace.", + "7": "Light snow settles on the prairie, adding a crisp sparkle to the horizon.", + "8": "The plains stretch endlessly under a dusting of fresh, soft snow.", + "9": "Snow swirls gently, falling without hurry onto the endless grasslands.", + "10": "Flakes catch on wildflowers and shrubs, frosting the plains softly.", + "11": "The light snow hushes the plains, leaving an air of peace and calm.", + "12": "Snow gathers in shallow drifts along the undisturbed plains." + }, + "forest": { + "1": "Snow falls lightly through the trees, catching on branches and needles.", + "2": "The forest floor is gently sprinkled with a thin layer of white.", + "3": "Flakes drift down, adding a wintry sparkle to the forest canopy.", + "4": "Snow gathers softly on the mossy rocks and fallen logs in the woods.", + "5": "The snowfall is light enough to leave patches of green among the white.", + "6": "Snow flutters quietly between the trees, turning the forest serene.", + "7": "A gentle snow shower coats the leaves and boughs in fleeting frost.", + "8": "Flakes settle on the forest undergrowth, decorating it delicately.", + "9": "The snowfall muffles the forest, creating an enchanting winter stillness.", + "10": "Snow clings to ivy and vines, adding a cold shimmer to the forest.", + "11": "The canopy filters the snowfall, leaving soft white patches below.", + "12": "Snow falls in graceful spirals, lightly frosting the forest trails." + }, + "swamp": { + "1": "Snow falls lightly onto the swamp, frosting reeds and stagnant pools.", + "2": "Flakes gather on moss-covered branches, adding a chill to the swamp air.", + "3": "The mild snow settles unevenly, blending with the muddy terrain.", + "4": "Snow clings to the tops of bulrushes, barely disturbing the swamp waters.", + "5": "The swamp takes on an eerie calm as flakes drift down silently.", + "6": "Light snow decorates the gnarled roots and black waters of the swamp.", + "7": "Flakes melt quickly on the warm patches of swamp but linger on logs.", + "8": "The snow falls in sparse, fleeting patterns over the swampy expanse.", + "9": "Snowflakes swirl around the swamp, catching on the edges of cattails.", + "10": "The light snow barely sticks, leaving the swamp misty and damp.", + "11": "Snow gathers faintly on swamp grasses, contrasting with dark waters.", + "12": "A gentle snow shower brings fleeting brightness to the murky swamp." + }, + "jungle": { + "1": "Snow drifts through the dense canopy, melting as it touches warm leaves.", + "2": "Flakes catch briefly on vines and fronds before vanishing in the jungle heat.", + "3": "The jungle floor sees only a faint dusting as snow struggles to persist.", + "4": "Snow falls sparingly, leaving the jungle glistening for but a moment.", + "5": "Light snow swirls around the trees, melting into the humid jungle air.", + "6": "Snow decorates the highest branches, barely reaching the jungle floor.", + "7": "Flakes dissolve quickly on broad leaves, leaving faint traces of frost.", + "8": "The snowfall is fleeting, adding a rare chill to the vibrant jungle.", + "9": "Snow dances through the jungle canopy, a surreal winter touch.", + "10": "Light snow mingles with jungle mist, adding a crisp chill to the air.", + "11": "The snowfall is sparse, lending a strange calm to the verdant jungle.", + "12": "Flakes cling briefly to jungle vines, sparkling before melting away." + }, + "hills": { + "1": "Snowflakes drift lazily over rolling hills, settling into shallow drifts.", + "2": "A light dusting of snow blankets the grassy knolls, sparkling faintly.", + "3": "The hills wear a thin white coat as snowflakes swirl gently overhead.", + "4": "Snow falls softly, gathering in the hollows and folds of the hills.", + "5": "Light snow flurries drift across the slopes, quiet and peaceful.", + "6": "Patches of white dot the hills as snowflakes settle and melt quickly.", + "7": "A calm snow shower drapes the hills in a fragile, icy shimmer.", + "8": "Snow dances through the air, catching on bushes and exposed stones.", + "9": "Flakes whirl over the hillsides, melting as they land on exposed grass.", + "10": "The hills appear faintly frosted, with snow collecting in shallow dips.", + "11": "Light snow glides over the hilltops, vanishing in pockets of shadow.", + "12": "Snow settles unevenly across the hills, dusting ridges and valleys alike." + }, + "mountains": { + "1": "Snow drifts gently through the peaks, dusting rocky outcroppings.", + "2": "The mountain slopes glisten as a mild snow shower settles over them.", + "3": "Light snow collects in crevices and ledges, sparkling faintly in the cold.", + "4": "Snow swirls around the peaks, brushing the cliffs with icy patterns.", + "5": "Thin layers of snow cling to boulders, giving the mountains a calm hush.", + "6": "A steady snow shower dusts the ridges and frozen trails lightly.", + "7": "Flakes drift lazily down the sheer rock faces, softening the jagged peaks.", + "8": "Snow dances in the thin mountain air, layering the ground in quiet white.", + "9": "The wind carries snow flurries over the slopes, settling only briefly.", + "10": "Light snowfall veils the peaks in an ethereal winter mist.", + "11": "Snow gathers softly on the rocky ledges, clinging to the sparse trees.", + "12": "A soft shower of snow leaves the mountains faintly frosted and serene." + }, + "desert": { + "1": "A surreal snow shower falls, melting instantly on the warm desert sands.", + "2": "Flakes drift aimlessly, vanishing before they touch the dry dunes.", + "3": "Light snow swirls above the desert, leaving no trace on the parched earth.", + "4": "The dunes shimmer as faint snow melts into a misty haze.", + "5": "Snow dances briefly in the desert wind, too fleeting to settle.", + "6": "Sparse flakes fall over the cracked ground, disappearing on contact.", + "7": "A light snow shower falls like dust, vanishing into the sun-baked sand.", + "8": "Snowflakes drift through the desert air, strange and short-lived.", + "9": "Snow falls sparsely, a ghostly presence in the otherwise arid desert.", + "10": "Light snow flutters through the desert sky, surreal against golden dunes.", + "11": "The desert sees a fleeting snowfall, gone before it can coat the earth.", + "12": "Flakes glimmer as they fall, melting into the endless desert sands." + }, + "coastal": { + "1": "Light snow falls over the waves, melting as it touches the rolling sea.", + "2": "Snow settles on the beach, clinging briefly to rocks and driftwood.", + "3": "Snowflakes swirl in the salty air, vanishing against the lapping tide.", + "4": "The coastal cliffs gleam faintly as a thin layer of snow gathers.", + "5": "Snowflakes melt into mist where the sea spray meets the falling shower.", + "6": "A soft snow shower blankets the coastal sands in fleeting white.", + "7": "Snow clings to the pier and ship ropes, adding a touch of winter calm.", + "8": "The shoreline sparkles briefly as snowflakes settle on sea-worn stones.", + "9": "Snow dances on the breeze, melting into the waves as it falls.", + "10": "Light snow drifts over fishing boats, leaving a cold, silvery coating.", + "11": "Snowflakes flutter gently, settling on cliffs before vanishing to the sea.", + "12": "Snow falls lightly on the beach, mixing with sand and saltwater spray." + }, + "volcano": { + "1": "Snowflakes drift gently through volcanic ash, melting on warm rock.", + "2": "Light snow swirls over scorched earth, vanishing as it touches the ground.", + "3": "The volcanic slopes wear a thin veil of snow, stark against black stone.", + "4": "Snow falls lightly, clinging briefly to the lava-hardened cliffs.", + "5": "Fleeting snow flurries swirl through the volcanic steam and ash.", + "6": "Light snow dances against the dark rock, a strange winter contrast.", + "7": "Snow drifts fall thinly, melting in pools of smoldering heat.", + "8": "Soft flakes settle on volcanic debris, quickly turning to vapor.", + "9": "Snow mingles with ash, creating a brief frost across scorched terrain.", + "10": "Light snow dusts the jagged rocks, clinging to cooler, shadowed crevices.", + "11": "The volcano is briefly crowned with a snowy cap that melts quickly.", + "12": "Snowflakes settle fleetingly on dark volcanic soil, adding eerie contrast." + }, + "artic": { + "1": "Soft snow falls on icy plains, adding another layer to the frozen tundra.", + "2": "The Arctic is serenely quiet as gentle snowflakes settle endlessly.", + "3": "A mild snow shower drapes the frozen landscape in soft white.", + "4": "Snowflakes drift through the frigid air, landing on icy surfaces silently.", + "5": "The snowfall is light, but it thickens the ice with a soft, clean layer.", + "6": "Snow dances over frozen waters, glittering like crystal in the weak light.", + "7": "Light flakes add texture to the endless whiteness of the Arctic expanse.", + "8": "Snow settles on frozen rocks and ice floes, blending into the pale ground.", + "9": "Flakes swirl through the wind, veiling the Arctic plains faintly.", + "10": "A gentle snowfall refreshes the ice, its flakes soft and persistent.", + "11": "Snow gathers lightly around glaciers, brightening their jagged edges.", + "12": "Soft showers drift through the Arctic chill, settling without sound." + }, + "cursed": { + "1": "Snow falls eerily slow, flakes tinged gray as they land on cursed ground.", + "2": "The snowflakes feel unnaturally cold, leaving a bitter frost behind.", + "3": "Snow drifts lazily, swirling against unseen winds in the cursed air.", + "4": "Flakes settle in twisted patterns, forming symbols before melting away.", + "5": "The cursed land refuses the snow, melting it into an unnatural mist.", + "6": "Snowflakes fall silently, leaving an uneasy stillness across the terrain.", + "7": "Light snow gathers in fleeting drifts, vanishing as shadows pass over it.", + "8": "The snow lands with a whisper, almost as though the land sighs in protest.", + "9": "The snowfall glimmers faintly, an eerie glow following each flake.", + "10": "Snow settles briefly, only to be swallowed up by cracks in the cursed earth.", + "11": "The cursed ground freezes beneath the snow, emitting a faint, hollow creak.", + "12": "Soft snow falls, creating a quiet unease as it vanishes upon touch." + } + } + }, + "Muggy Showers": { + "conditions": { + "temperature": { "gte": 55 }, + "precipitation": { "gte": 30, "lte": 60 }, + "wind": { "gte": 30, "lte": 50 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 50 } + }, + "descriptions": { + "farm": { + "1": "The air is thick with humidity as gentle rain falls over the fields.", + "2": "A muggy drizzle dampens the soil, leaving crops steaming in the heat.", + "3": "Rain patters on the barn roof while heavy, humid air lingers.", + "4": "The fields are slick with rain, steam rising from the warm ground.", + "5": "A steady shower falls, but the thick air makes the rain feel warm.", + "6": "Humid rain leaves puddles in the furrows, turning soil into muck.", + "7": "The muggy air carries the scent of wet earth as showers continue.", + "8": "Light rain falls lazily, barely cooling the dense and humid atmosphere.", + "9": "Raindrops cling to wheat stalks, the heavy air stifling any relief.", + "10": "Cattle move sluggishly as muggy rain falls, turning pastures to mud.", + "11": "A humid drizzle falls on the orchard, mist curling between damp leaves.", + "12": "Rain trickles steadily, but the thick, oppressive air refuses to lift." + }, + "village": { + "1": "The streets are damp as muggy rain falls, steam rising from the cobbles.", + "2": "Villagers wipe sweat as showers fall, the humidity clinging to clothes.", + "3": "A warm drizzle soaks thatched roofs, the air stifling and heavy.", + "4": "Rain trickles down eaves, pooling in humid puddles around the village square.", + "5": "The heavy air carries the scent of damp wood and sweat as rain falls.", + "6": "Muggy showers patter on shutters, leaving a lingering warmth behind.", + "7": "A steady rain falls, the air so humid it feels like a wet blanket.", + "8": "The village feels stifled as rain falls, mist curling in humid alleys.", + "9": "Humid showers soak the laundry lines, leaving clothes damp and warm.", + "10": "Rain drips from rooftops, but the muggy air offers no relief.", + "11": "Villagers seek cover as muggy rain turns roads to slick pathways.", + "12": "Humid rain patters softly, steam rising from the blacksmith’s forge." + }, + "city": { + "1": "Rain drips heavily through the muggy air, soaking crowded streets.", + "2": "The city’s stone walls sweat as humid rain trickles steadily.", + "3": "Muggy showers create pools of warm water in cobbled alleys.", + "4": "A dense drizzle dampens rooftops, the air thick and stifling.", + "5": "The humid rain leaves city windows streaked, the streets steaming.", + "6": "Raindrops fall heavily, but the air remains suffocatingly warm.", + "7": "Market stalls glisten under a muggy rain, the smell of damp cloth lingering.", + "8": "A warm drizzle falls as horses trudge through humid, muddy streets.", + "9": "Humid rain seeps through cracks, puddling on stone steps and paths.", + "10": "Steam rises from city squares as muggy showers soak the cobblestones.", + "11": "The air is thick with mist as warm rain patters against castle walls.", + "12": "Humid rain drips into gutters, the city heavy with warmth and dampness." + }, + "plains": { + "1": "A warm, humid rain falls gently across the grassy plains.", + "2": "The plains shimmer with steam as muggy showers soak the earth.", + "3": "Light rain clings to tall grass, mist rising in the humid air.", + "4": "A steady drizzle falls, the air thick and still over the wide fields.", + "5": "Muggy rain patters softly, the heat leaving no cool relief behind.", + "6": "Raindrops bead on grass blades as the humid air feels oppressive.", + "7": "The plains feel suffocating under a heavy drizzle and thick air.", + "8": "Showers fall, but the humid breeze carries a stifling warmth.", + "9": "Light rain turns paths to mud as mist rises through the muggy air.", + "10": "A warm rain patters endlessly, steam curling along the grassy hills.", + "11": "Rain dampens the plains, the air thick with heat and humidity.", + "12": "Muggy rain falls silently, the horizon blurred in thickening mist." + }, + "forest": { + "1": "Humid rain drips through thick branches, pooling on the forest floor.", + "2": "The air feels heavy as muggy showers soak leaves and moss.", + "3": "Warm rain patters on the canopy, mist rising like steam in the humidity.", + "4": "Droplets trickle through leaves, the thick air amplifying the drizzle.", + "5": "Muggy rain soaks the undergrowth, leaving a stifling stillness behind.", + "6": "Showers fall through the forest, the damp air clinging to skin and bark.", + "7": "Steam rises as rain trickles down trunks, pooling in warm, muddy roots.", + "8": "Rain drips in humid streaks, the forest air thick and oppressively warm.", + "9": "The humid shower leaves moss glistening, mist curling under the canopy.", + "10": "Leaves sag under warm rain, the forest hushed by the heavy atmosphere.", + "11": "Rain collects in ferns, dripping steadily while the air feels dense.", + "12": "A warm drizzle soaks the forest floor, steam rising in humid columns." + }, + "swamp": { + "1": "Muggy rain falls heavily, rippling across stagnant pools of water.", + "2": "The swamp steams as warm rain trickles into murky, humid waters.", + "3": "Light showers mix with thick air, leaving the swamp stifling and wet.", + "4": "The humid rain adds to the swamp’s heavy mist and earthy scent.", + "5": "Droplets patter on reeds, the muggy air alive with the sound of insects.", + "6": "Warm showers fall endlessly, turning the swamp into a steaming mire.", + "7": "Rain seeps into mossy pools, mist curling into the heavy swamp air.", + "8": "The swamp feels suffocating as muggy rain drizzles over murky waters.", + "9": "A warm drizzle dampens the swamp, steam rising in dense, sticky clouds.", + "10": "Raindrops disturb the swamp’s surface, the air thick with humidity.", + "11": "Muggy rain clings to branches, trickling steadily into stagnant pools.", + "12": "A humid shower turns the swamp into a steaming, muddy quagmire." + }, + "jungle": { + "1": "Warm rain falls thick and steady, steaming in the jungle’s humid air.", + "2": "Showers patter through dense foliage, the heavy air amplifying the dampness.", + "3": "Muggy rain soaks the jungle, mist rising in curling tendrils.", + "4": "Droplets bead on massive leaves, the jungle air thick with warmth.", + "5": "A heavy drizzle coats vines and branches, leaving the air sticky and dense.", + "6": "Rain seeps through the canopy, turning the jungle floor into humid mud.", + "7": "The jungle hums with life as muggy rain drizzles endlessly overhead.", + "8": "Warm rain dampens the vines, mist steaming in the oppressive jungle heat.", + "9": "Droplets trickle like sweat from branches, soaking the dense undergrowth.", + "10": "A humid rain falls steadily, leaving the jungle a dripping, steaming mass.", + "11": "Leaves glisten with moisture as showers fall, the jungle air thick and heavy.", + "12": "Rain mingles with mist, the jungle’s humid air clinging to everything." + }, + "hills": { + "1": "Light rain falls on the hills, the humid air clinging to the slopes.", + "2": "The muggy drizzle turns paths slick as mist gathers along the ridges.", + "3": "A warm shower blankets the hills, the air thick and still.", + "4": "Rain beads on tall grass as the hills steam in the humid air.", + "5": "The muggy rain dampens stone outcroppings, leaving a sticky mist behind.", + "6": "Low clouds hang heavy over the hills, dripping rain into the valleys.", + "7": "The air feels heavy as rain falls, pooling in the crevices of the hills.", + "8": "Muggy rain soaks the hillsides, mist clinging stubbornly to the heights.", + "9": "The humid drizzle dampens the trails, fog curling along the ridgelines.", + "10": "A thick warmth lingers as rain patters against the rocky hills.", + "11": "The air feels stifling as warm rain trickles into the hollows of the hills.", + "12": "Muggy rain falls steadily, leaving the hills shrouded in mist and damp." + }, + "mountains": { + "1": "Rain trickles down rocky slopes, the air thick with warmth.", + "2": "Muggy showers cling to the mountain paths, fog curling around peaks.", + "3": "The humid drizzle leaves boulders slick and the air oppressively warm.", + "4": "A dense rain falls, steaming as it hits the rocky mountain face.", + "5": "Clouds hang low, the muggy rain soaking into crevices and cliffs.", + "6": "Rain clings to alpine pines, the air thick and stifling in the mountains.", + "7": "Muggy showers dampen the rocky paths, leaving them treacherously slick.", + "8": "The heavy air carries the scent of wet stone as rain falls steadily.", + "9": "Rain mists into steam along the cliffs, the mountains shrouded in damp fog.", + "10": "The humid rain drips from ledges, pooling in the cracks of the rock.", + "11": "A muggy drizzle blurs the horizon, the mountain air thick and clinging.", + "12": "Low clouds and steady rain make the mountains feel stifling and still." + }, + "desert": { + "1": "Rare rain falls warmly on the desert, the air thick and sticky.", + "2": "A muggy drizzle dampens the sand, steam rising from the parched ground.", + "3": "Rain clings to sparse plants, the desert air suffocatingly humid.", + "4": "A rare shower falls, the desert heat amplifying the muggy atmosphere.", + "5": "The thick air clings to every breath as warm rain falls lightly.", + "6": "Humid rain dampens dunes, steam rising into the heavy desert air.", + "7": "Rain beads on rocky outcrops, the desert air thick and oppressive.", + "8": "Low clouds linger as the muggy drizzle softens the desert's harsh edges.", + "9": "The desert feels suffocating under a warm drizzle and clinging mist.", + "10": "Rare rainfall leaves the desert steaming, the air heavy with moisture.", + "11": "The rain pools in dry riverbeds, but the humid air offers no relief.", + "12": "The muggy rain turns the desert into a surreal, steaming expanse." + }, + "coastal": { + "1": "Humid rain falls softly, blending with the salty tang of the coast.", + "2": "The muggy drizzle turns the shoreline slick and the air heavy.", + "3": "Rain dampens the beach, the humid air sticking to everything it touches.", + "4": "A steady drizzle mixes with the briny air, leaving a warm mist behind.", + "5": "Rain beads on driftwood as the humid air clings to the coastline.", + "6": "Fog rises off the waves as muggy rain falls on the sandy shore.", + "7": "The humid rain dampens sails and rope, mist curling over the docks.", + "8": "Low clouds hover over the water, the air thick with warmth and rain.", + "9": "The air feels heavy as the muggy drizzle coats rocks and tidepools.", + "10": "Rain and mist obscure the horizon, the coastal air thick with humidity.", + "11": "Rain dampens fishing nets as humid air clings to the coastal village.", + "12": "A muggy drizzle leaves the coast slick, mist rolling in from the sea." + }, + "volcano": { + "1": "Muggy rain falls heavily, hissing as it meets hot volcanic stone.", + "2": "Steam rises as humid showers soak the warm, rocky terrain.", + "3": "The air feels stifling as rain drips onto the ash-covered ground.", + "4": "A warm drizzle falls, mist curling above cracks in the volcanic stone.", + "5": "Rain trickles down blackened slopes, the air thick with humidity.", + "6": "The muggy rain clings to the volcanic air, leaving it oppressively warm.", + "7": "Steam and rain mix in a humid haze around the volcano’s base.", + "8": "The humid air is heavy with sulfur as warm rain patters on the stone.", + "9": "Muggy showers blur the jagged outlines of the volcanic landscape.", + "10": "Rain hisses against the warm earth, the air thick with heat and mist.", + "11": "A muggy drizzle leaves the volcanic rock slick and steaming.", + "12": "Rain beads on lava flows, the volcanic air dense and suffocatingly warm." + }, + "artic": { + "1": "Rain falls lightly, freezing as it touches the frigid Arctic ground.", + "2": "The muggy drizzle feels out of place in the biting Arctic cold.", + "3": "Rain clings to icy surfaces, freezing in the bitter Arctic air.", + "4": "A humid drizzle creates a layer of ice over the frozen tundra.", + "5": "The Arctic air feels heavy as warm rain falls on the icy landscape.", + "6": "Rain falls lightly, steaming as it touches patches of frozen ground.", + "7": "The muggy rain creates icy puddles, the Arctic air thick and sharp.", + "8": "Rain turns to mist over snowdrifts, the Arctic air feeling unnaturally warm.", + "9": "Rain freezes on contact, creating a slick sheen on the Arctic ice.", + "10": "Muggy showers leave an icy glaze across the barren, frozen land.", + "11": "A humid drizzle fogs up icy plains, the air thick with moisture.", + "12": "The Arctic feels eerie under warm rain, mist rising from frozen ground." + }, + "cursed": { + "1": "Muggy rain falls, but the cursed air feels heavy with an unnatural warmth.", + "2": "Rain trickles over cursed ground, the air thick and oppressive.", + "3": "A humid drizzle soaks the cursed landscape, the mist feeling unclean.", + "4": "The rain falls, but the cursed air clings with a suffocating heaviness.", + "5": "Humid showers dampen the cursed soil, turning it into a sticky mire.", + "6": "Rain and mist mingle, the cursed air feeling thick and foreboding.", + "7": "Muggy rain pools in cursed hollows, the air heavy with dread.", + "8": "Rain clings to cursed ruins, the humid air amplifying the eerie stillness.", + "9": "The cursed ground drinks the rain, steam rising in the stifling heat.", + "10": "A humid drizzle feels heavier here, the cursed air laden with tension.", + "11": "Rain patters softly, but the cursed air seems to absorb all comfort.", + "12": "Muggy showers fall steadily, the cursed mist curling unnaturally in the air." + } + } + }, + "Icy Drizzle": { + "conditions": { + "temperature": { "lte": 35 }, + "precipitation": { "gte": 60 }, + "wind": { "gte": 30, "lte": 60 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 40, "lte": 70 }, + "visibility": { "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "A thin, icy drizzle coats the fields, leaving crops shimmering with frost.", + "2": "The cold drizzle clings to wooden fences, turning them slick and treacherous.", + "3": "Icy droplets fall steadily, forming a thin layer of ice over tilled soil.", + "4": "Farm tools glisten with a frozen sheen as the icy drizzle continues.", + "5": "A cold drizzle leaves the barn roof and animals' fur lightly iced.", + "6": "Drizzling ice makes the paths between fields slippery and dangerous.", + "7": "Icy rain lightly pelts the crops, hardening the ground beneath.", + "8": "The drizzle turns to freezing droplets, coating haystacks in a frosty layer.", + "9": "Farm animals huddle in their shelters, avoiding the icy rain.", + "10": "The icy drizzle dampens the air, leaving frost on windowpanes and tools.", + "11": "Cold droplets freeze on contact, leaving the farm eerily still and frosty.", + "12": "The icy rain slicks every surface, from fences to plows, in a frozen glaze." + }, + "village": { + "1": "An icy drizzle falls on the cobblestone streets, making them dangerously slick.", + "2": "The drizzle freezes on rooftops, icicles forming along the eaves.", + "3": "Villagers hurry indoors, icy rain coating cloaks and hats with frost.", + "4": "The steady drizzle of icy rain turns paths to sheets of glassy ice.", + "5": "Doors and shutters glisten with a frozen sheen as the drizzle persists.", + "6": "Freezing rain slicks the wells and stone walls, making everything slippery.", + "7": "Icy droplets collect on market stalls, their surfaces frozen and shiny.", + "8": "The icy drizzle dampens the village square, freezing over wooden benches.", + "9": "Frozen puddles dot the village streets, the icy drizzle adding to the chill.", + "10": "The air feels heavy with moisture as icy droplets coat the buildings.", + "11": "Children play briefly, sliding on icy paths before retreating indoors.", + "12": "The village bell tower glistens, its wooden beams covered in icy rain." + }, + "city": { + "1": "The icy drizzle turns cobblestones to slick, treacherous surfaces.", + "2": "Freezing rain coats shop signs and lanterns in a thin layer of ice.", + "3": "The city walls glisten with frost as the icy drizzle continues.", + "4": "Horse-drawn carts struggle on icy streets, wheels slipping on frozen ruts.", + "5": "The drizzle freezes on market stalls, turning cloth covers into brittle sheets.", + "6": "Freezing droplets collect on iron gates, forming a thin glaze of ice.", + "7": "Citizens wrap themselves tightly, icy rain biting at exposed skin.", + "8": "The icy drizzle creates a sheen over statues and stone carvings in the plaza.", + "9": "City gutters overflow, freezing water pooling in icy patches.", + "10": "The icy rain dampens the city's hum, making every step a careful one.", + "11": "A chill settles over the city as icy droplets collect on every surface.", + "12": "The icy drizzle leaves frost clinging to the edges of cloaks and boots." + }, + "plains": { + "1": "The icy drizzle sweeps across the open plains, coating the grasses in frost.", + "2": "Freezing rain turns the flat land into a shimmering, icy expanse.", + "3": "The drizzle freezes on tall grasses, leaving them brittle and glistening.", + "4": "A cold wind carries the icy rain, turning the plains into a slippery field.", + "5": "Icy droplets coat the wildflowers, their petals stiffened by the frost.", + "6": "The flat land glistens under the icy drizzle, patches of frost forming.", + "7": "Freezing rain dampens the earth, leaving puddles that harden quickly.", + "8": "The icy drizzle settles over the plains, leaving a light frost on the soil.", + "9": "The vast plains shimmer as the icy drizzle forms a thin, frozen layer.", + "10": "Frost forms on scattered rocks as icy rain falls steadily on the plains.", + "11": "The grass crunches underfoot as the drizzle freezes into a slick frost.", + "12": "Icy rain drizzles gently, the plains stretching endlessly under a frosty haze." + }, + "forest": { + "1": "Icy rain falls through the canopy, freezing as it touches leaves and bark.", + "2": "The drizzle freezes on branches, turning the forest into a shimmering maze.", + "3": "Freezing droplets cling to moss, hardening into icy crystals.", + "4": "The forest floor glistens as icy rain coats fallen leaves and twigs.", + "5": "Icy drizzle trickles from treetops, freezing into droplets on roots below.", + "6": "The cold drizzle creates an eerie quiet, freezing on every surface it touches.", + "7": "Tree trunks gleam under the frozen drizzle, their bark slick with ice.", + "8": "Freezing rain turns forest paths into icy trails, dangerous to navigate.", + "9": "The forest crackles softly as ice forms on branches in the chilly rain.", + "10": "The steady drizzle coats the forest in frost, the air sharp and cold.", + "11": "Icy rain drips from treetops, leaving frozen puddles on the forest floor.", + "12": "The forest sparkles faintly as the icy drizzle forms a light, frozen glaze." + }, + "swamp": { + "1": "The icy drizzle freezes over murky waters, a thin layer of ice forming.", + "2": "Freezing rain clings to reeds and moss, leaving them stiff and brittle.", + "3": "The swamp fog thickens as icy drizzle coats every surface in frost.", + "4": "Icy droplets fall on stagnant pools, creating shimmering patterns of ice.", + "5": "The cold drizzle makes the swamp eerily quiet, frost covering dead branches.", + "6": "Freezing rain slicks muddy paths, making the swamp treacherous to traverse.", + "7": "The swamp glistens faintly under the icy drizzle, frost clinging to roots.", + "8": "Moss-covered trees shimmer as the freezing rain solidifies in patches.", + "9": "The icy drizzle turns the swamp into a frozen, otherworldly landscape.", + "10": "Frozen droplets bead on vines, their weight dragging them toward the ground.", + "11": "A cold, icy rain soaks the swamp, frost forming on stagnant water surfaces.", + "12": "Icy drizzle dampens the air, the swamp exhaling cold mist into the stillness." + }, + "jungle": { + "1": "Icy rain falls through dense jungle foliage, freezing on wide leaves.", + "2": "The freezing drizzle coats vines and ferns, leaving them glistening with frost.", + "3": "The jungle paths turn slick and icy under the steady drizzle.", + "4": "Freezing rain clings to vibrant flowers, dulling their colors with frost.", + "5": "The icy drizzle creates an unnatural cold, frosting over jungle canopies.", + "6": "Frozen droplets weigh down vines, adding a heavy stillness to the jungle.", + "7": "Icy rain freezes into delicate patterns on the jungle’s broad leaves.", + "8": "The air turns cold and damp as the icy drizzle coats every surface.", + "9": "Freezing rain clings to jungle trees, their trunks shimmering with frost.", + "10": "The cold drizzle hardens puddles along the jungle paths into thin ice.", + "11": "An eerie quiet falls as icy rain freezes the vibrant jungle into stillness.", + "12": "Icy drizzle dampens the jungle’s warmth, leaving frost along the undergrowth." + }, + "hills": { + "1": "Icy drizzle coats the grassy slopes, making footing treacherous.", + "2": "The drizzle freezes on rocky outcrops, creating slick surfaces.", + "3": "Frozen droplets form on the sparse trees dotting the hillside.", + "4": "The cold drizzle hardens into frost along animal trails.", + "5": "Icy rain clings to wildflowers, their petals glistening and fragile.", + "6": "Shepherds struggle to guide their flocks as icy drizzle slicks the hills.", + "7": "The chill of the icy drizzle cuts through the rolling terrain.", + "8": "Freezing rain hardens the soil, making pathways treacherous.", + "9": "Icy droplets cling to bushes, forming a thin layer of frost.", + "10": "The drizzle settles over the hills, coating everything in a cold glaze.", + "11": "Misty rain freezes as it falls, creating a slippery sheen on stones.", + "12": "A light frost forms as the icy drizzle continues across the hills." + }, + "mountains": { + "1": "The icy drizzle freezes on the cliffs, making climbing perilous.", + "2": "Freezing rain glistens on jagged peaks, creating an eerie shimmer.", + "3": "Thin layers of ice form on narrow mountain paths.", + "4": "Icy droplets coat the sparse vegetation clinging to rocky ledges.", + "5": "Frozen drizzle builds up on stone outcroppings, making them dangerously slick.", + "6": "The cold rain freezes into fragile icicles hanging from crags.", + "7": "Icy drizzle coats the mountain passes, creating a hazardous journey.", + "8": "Frost clings to boulders as the freezing drizzle continues.", + "9": "The icy rain freezes mid-air, carried by fierce mountain winds.", + "10": "Drizzle freezes on exposed rock faces, leaving a glassy surface.", + "11": "The icy mist settles over the peaks, reducing visibility to near nothing.", + "12": "Frozen rain turns the mountain terrain into a treacherous icy expanse." + }, + "desert": { + "1": "Icy drizzle falls across the sand, freezing into a fragile crust.", + "2": "The freezing rain settles over cacti, their spines shimmering with ice.", + "3": "Cold droplets harden on scattered stones, making them slick underfoot.", + "4": "Icy rain turns dune slopes into icy cascades, unusual in the arid expanse.", + "5": "The desert feels alien as icy drizzle clings to the sparse vegetation.", + "6": "Freezing rain creates a brittle frost on the dry sand.", + "7": "The drizzle hardens the desert ground, making it temporarily solid.", + "8": "Frozen droplets cling to the sparse desert plants, glinting in the dim light.", + "9": "The air feels heavy with moisture as icy drizzle chills the desert.", + "10": "An unusual frost forms as the freezing rain coats the arid terrain.", + "11": "Icy rain turns desert rocks slippery, making travel hazardous.", + "12": "The chill of the drizzle brings a rare frost to the desert sands." + }, + "coastal": { + "1": "Icy drizzle sweeps in from the sea, freezing on docks and ships.", + "2": "The cold rain freezes on rocks, turning the shore into a treacherous path.", + "3": "Freezing drizzle clings to fishing nets, hardening them with frost.", + "4": "Icy rain creates slick patches on the weathered planks of seaside piers.", + "5": "Frozen droplets shimmer on the seaweed-strewn beach.", + "6": "The drizzle freezes on ship ropes, making them stiff and brittle.", + "7": "Icy rain hardens on coastal cliffs, leaving them dangerously slick.", + "8": "The salty air mixes with icy drizzle, coating everything in a cold glaze.", + "9": "Freezing rain settles over the tidal pools, forming thin sheets of ice.", + "10": "The frosty drizzle chills the coast, leaving frost on driftwood and stones.", + "11": "Waves crash with icy spray as freezing rain falls steadily on the shore.", + "12": "The freezing drizzle leaves a faint frost on beach grasses and shells." + }, + "volcano": { + "1": "The icy drizzle freezes on cooled lava flows, creating slick surfaces.", + "2": "Freezing rain clings to volcanic rocks, hardening into a fragile glaze.", + "3": "The cold drizzle creates an eerie contrast against the dormant volcano's heat.", + "4": "Icy droplets freeze on ash-covered ground, making the terrain perilous.", + "5": "The drizzle hardens into frost on jagged volcanic ridges.", + "6": "Frozen rain glistens on sulfur vents, creating shimmering frost patterns.", + "7": "Freezing drizzle dampens the volcanic landscape, leaving a light glaze.", + "8": "The icy rain freezes on blackened rock, adding a fragile layer of frost.", + "9": "Frost clings to scattered boulders as the freezing drizzle continues.", + "10": "The freezing rain settles on basalt formations, creating slippery footing.", + "11": "The icy drizzle coats lava tubes, turning them into dangerous icy paths.", + "12": "Frost forms on dormant craters, the drizzle adding to the surreal chill." + }, + "artic": { + "1": "Icy drizzle freezes instantly on the tundra, turning it into a frozen wasteland.", + "2": "The freezing rain glazes snowdrifts, making them hard and slippery.", + "3": "Frozen drizzle clings to icebergs, adding layers of frost.", + "4": "The cold drizzle adds a shimmering sheen to the already frozen ground.", + "5": "Icy rain freezes mid-air, creating a glittering mist across the tundra.", + "6": "Freezing droplets harden on the surface of frozen lakes and rivers.", + "7": "The icy drizzle deepens the chill, coating everything in frost.", + "8": "The frozen landscape sparkles faintly as icy rain glazes the ice caps.", + "9": "The drizzle freezes on the tundra grasses, leaving them brittle and icy.", + "10": "Cold drizzle forms icicles on low-lying shrubs and arctic stones.", + "11": "The icy rain freezes into a layer of frost over endless white expanses.", + "12": "The arctic chill intensifies as icy drizzle coats the barren landscape." + }, + "cursed": { + "1": "The icy drizzle carries an unnatural chill, freezing even darkened stones.", + "2": "Frozen rain forms ghostly patterns on cursed ruins and twisted trees.", + "3": "Icy droplets cling to cursed grounds, turning them into eerie frost patches.", + "4": "The drizzle freezes on bones and forgotten relics, leaving a sinister sheen.", + "5": "The cold rain glazes the cursed landscape, frost forming in unnatural patterns.", + "6": "Freezing drizzle whispers across cursed paths, leaving frosted footprints.", + "7": "Icy rain hardens on cursed idols, their surfaces glistening eerily.", + "8": "The freezing drizzle chills the cursed air, coating the land in frost.", + "9": "Frozen droplets cling to warped trees, shimmering with cursed energy.", + "10": "Icy rain turns cursed marshes into treacherous, frozen traps.", + "11": "The cursed grounds glisten under the icy drizzle, exuding a spectral chill.", + "12": "Frost forms unnaturally fast as icy drizzle coats the cursed earth." + } + } + }, + "Frigid Cyclone": { + "conditions": { + "temperature": { "lte": 20 }, + "precipitation": { "gte": 60 }, + "wind": { "gte": 60 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 70 }, + "visibility": { "gte": 40, "lte": 80 } + }, + "descriptions": { + "farm": { + "1": "A frigid cyclone tears through the fields, uprooting crops and freezing the soil.", + "2": "Icy winds spiral across the farmlands, coating tools and barns with frost.", + "3": "Frozen debris whirls in the air as the cyclone sweeps the farmland.", + "4": "The cyclone’s chill freezes irrigation channels and water barrels solid.", + "5": "Farm animals huddle for warmth as icy winds batter the barn.", + "6": "The swirling winds tear shingles from rooftops and frost the crops.", + "7": "An eerie chill grips the fields as the cyclone passes, leaving frost in its wake.", + "8": "Wheat and corn stalks snap under the cyclone’s relentless icy gusts.", + "9": "Frost spirals across fences and plows as the cold cyclone howls past.", + "10": "Frozen dirt and leaves whip through the air, chilling everything they touch.", + "11": "The cyclone's icy force topples scarecrows and layers the fields with frost.", + "12": "Frigid winds howl, coating every surface in a thick sheen of ice." + }, + "village": { + "1": "Icy winds howl through the village, freezing doors and windows shut.", + "2": "Chilled debris swirls in the streets as the frigid cyclone grips the town.", + "3": "Villagers barricade their homes against the cyclone’s icy onslaught.", + "4": "Frozen thatch and timbers crack under the weight of the freezing winds.", + "5": "The well water freezes solid as the frigid cyclone tears through.", + "6": "Icicles form instantly on roofs and eaves as the cyclone passes.", + "7": "The chilling gusts scatter firewood and extinguish hearth fires.", + "8": "Frost spreads rapidly over cobblestones and walls, freezing them solid.", + "9": "The cyclone’s icy winds rip through market stalls, scattering goods.", + "10": "Villagers shiver in their homes as the freezing winds batter the walls.", + "11": "The cyclone’s icy grip freezes livestock troughs and wells in moments.", + "12": "Icy spirals carve patterns into the frost-covered windows." + }, + "city": { + "1": "Frigid winds howl through the alleys, coating stone walls in frost.", + "2": "The cyclone’s icy gusts extinguish lanterns and freeze fountains solid.", + "3": "Stalls and carts overturn as icy debris swirls through the marketplace.", + "4": "Citizens flee indoors as the freezing winds sweep through the streets.", + "5": "Frost spreads across cobblestones, making the streets treacherously slick.", + "6": "The cyclone’s chill freezes iron gates and latches, locking them in place.", + "7": "Icy winds whip banners and signs, tearing them loose from their moorings.", + "8": "A ghostly frost covers statues and spires, glinting in the weak light.", + "9": "Windows shatter under the cyclone’s force, sending icy shards inside.", + "10": "The chilling winds tear through the city, coating every surface in frost.", + "11": "Icy spirals form in the air, turning the bustling city into a frozen wasteland.", + "12": "Freezing winds whistle through the city, layering every corner in ice." + }, + "plains": { + "1": "The frigid cyclone sweeps the open plains, freezing the grasses solid.", + "2": "Icy winds spiral over the flatlands, leaving frost trails in their wake.", + "3": "The cyclone chills the air to the bone, creating an otherworldly frostscape.", + "4": "Frosted plants bend and snap under the cyclone’s relentless icy gusts.", + "5": "The freezing winds pick up soil and debris, hurling it across the plains.", + "6": "The plains become a frozen sea of ice and frost as the cyclone passes.", + "7": "Chilled air rushes across the plains, coating every blade of grass in ice.", + "8": "The cyclone’s icy gusts sweep across the flatlands, howling ominously.", + "9": "Freezing winds create spiraling frost patterns across the open terrain.", + "10": "The chill of the cyclone bites deep, leaving a trail of frost behind.", + "11": "The cyclone freezes everything it touches, turning the plains white with frost.", + "12": "Frosted clouds of dirt and ice swirl in the air as the cyclone rages." + }, + "forest": { + "1": "The frigid cyclone sweeps through the forest, icing every leaf and branch.", + "2": "Frozen mist swirls between the trees, coating the bark in frost.", + "3": "The cyclone’s chill freezes the forest floor, making every step perilous.", + "4": "Icy winds snap smaller branches, scattering frozen wood across the ground.", + "5": "The forest echoes with the cracking of freezing timber as the cyclone passes.", + "6": "Frost coats the underbrush as the freezing winds sweep through the trees.", + "7": "The cyclone chills the forest canopy, causing icy debris to rain down.", + "8": "Icicles form rapidly on tree limbs, glittering in the pale light.", + "9": "The freezing cyclone creates a crystalline frost covering the forest’s depths.", + "10": "Frozen leaves crunch underfoot as the icy cyclone moves through the woods.", + "11": "The forest transforms into a frozen wonderland, eerily still under the cyclone’s chill.", + "12": "Frost and ice cover every branch, turning the forest into a frozen labyrinth." + }, + "swamp": { + "1": "The frigid cyclone freezes swamp water, leaving a thin, brittle layer of ice.", + "2": "Icy winds swirl over the marsh, frosting reeds and moss-covered trees.", + "3": "The cyclone chills the air, freezing patches of stagnant water solid.", + "4": "Frozen mist hangs heavy over the swamp, chilling every surface it touches.", + "5": "The swamp’s murky depths begin to freeze as the cyclone rages on.", + "6": "Icy gusts scatter wet vegetation, leaving frosted debris across the marsh.", + "7": "The cyclone’s chill freezes vines and moss, leaving them brittle and fragile.", + "8": "Frost creeps across mud and water, encasing the swamp in a thin glaze.", + "9": "Frozen droplets form on the swamp’s hanging moss as the cyclone moves through.", + "10": "Icy winds whip through the marsh, snapping reeds and coating them in frost.", + "11": "The swamp becomes a frozen expanse of mud and ice as the cyclone passes.", + "12": "The frigid winds chill the swamp, creating an eerie, frost-covered wasteland." + }, + "jungle": { + "1": "The frigid cyclone chills the humid jungle, freezing vines and leaves.", + "2": "Icy winds tear through the canopy, causing frozen debris to fall.", + "3": "The cyclone’s chill freezes pools and streams, turning them into icy mirrors.", + "4": "Frozen mist spirals through the jungle, coating plants in shimmering frost.", + "5": "Icy droplets cling to broad leaves, making them glint under weak sunlight.", + "6": "The jungle’s warmth is sapped as the cyclone leaves frost in its wake.", + "7": "Vines snap under the weight of ice as the freezing cyclone rages on.", + "8": "The cyclone freezes tree trunks and undergrowth, leaving a crystalline layer of ice.", + "9": "The jungle floor becomes slick with frost as the frigid winds howl through.", + "10": "Icy gusts whistle through the dense foliage, chilling the jungle to its core.", + "11": "The freezing cyclone transforms the lush jungle into a glittering frostscape.", + "12": "Every leaf and branch is coated in ice as the cyclone’s chill grips the jungle." + }, + "hills": { + "1": "Icy winds sweep through the hills, frosting grass and shrubs.", + "2": "Frozen gusts howl over the ridges, chilling the air to the bone.", + "3": "Frost spirals across the slopes, leaving an icy trail behind.", + "4": "The frigid cyclone whips through valleys, freezing streams and soil.", + "5": "Grass and rocks become slick with frost as the icy winds rage.", + "6": "The cyclone creates swirling frost eddies, chilling the hills deeply.", + "7": "Frozen mist hangs in the air, blanketing the hills in eerie frost.", + "8": "The cyclone’s chill leaves icy patterns on boulders and shrubs.", + "9": "The hills become a frozen expanse as icy winds sweep across them.", + "10": "Snow and frost are carried by the cyclone, covering the hillsides.", + "11": "The chilling gusts whip through the hills, freezing everything in their path.", + "12": "Frosted grass crunches underfoot as the frigid cyclone howls past." + }, + "mountains": { + "1": "The frigid cyclone swirls around peaks, freezing stone and snow.", + "2": "Icy winds tear through mountain passes, chilling travelers to the bone.", + "3": "The cyclone sends frozen debris cascading down the cliffs.", + "4": "Snow-covered slopes glisten under the icy grip of the cyclone.", + "5": "The air grows dangerously cold as the cyclone rages through the peaks.", + "6": "Icicles form rapidly on rocky outcrops as the freezing winds pass.", + "7": "The cyclone’s icy force sends shivers down the mountains’ frozen spines.", + "8": "Frozen mist swirls through valleys, obscuring the treacherous trails.", + "9": "The cyclone’s chill turns even the hardiest mountain terrain to ice.", + "10": "The mountains echo with the eerie howl of the freezing winds.", + "11": "Frost spreads over the rugged landscape, turning the peaks white.", + "12": "Snow and frost coat the cliffs as the cyclone sweeps through." + }, + "desert": { + "1": "The frigid cyclone chills the desert, freezing dunes into hard ice.", + "2": "Frozen sand spirals in the air, creating a surreal frost-covered wasteland.", + "3": "The cyclone’s icy winds harden the desert floor, leaving a frosty crust.", + "4": "Cacti and dry brush are covered in frost as the cyclone passes.", + "5": "The desert transforms into an icy expanse, shimmering under the cold winds.", + "6": "Icy gusts whip across the dunes, creating frozen ripples in the sand.", + "7": "The chill freezes small pools, turning the desert into an arctic mirage.", + "8": "Frozen sand and frost coat the landscape as the icy winds rage.", + "9": "The cyclone’s chill creates strange frost patterns on the dunes.", + "10": "The cold winds sweep through, freezing everything they touch in the desert.", + "11": "The desert becomes a frozen expanse, eerily silent under the cyclone’s chill.", + "12": "Frost forms on dry plants, creating a ghostly, icy desert." + }, + "coastal": { + "1": "The frigid cyclone churns icy waves along the shore, freezing the sand.", + "2": "Frozen sea spray coats the docks and boats, leaving a crystalline sheen.", + "3": "The cyclone’s chill freezes shallow waters, trapping sea life in ice.", + "4": "Icy gusts whip through the coastal cliffs, leaving frost on every surface.", + "5": "The freezing winds carry salt and frost, chilling the coastline deeply.", + "6": "Waves crash and freeze mid-air as the cyclone’s chill intensifies.", + "7": "Frozen mist blankets the shoreline, leaving the beach eerily silent.", + "8": "The cyclone sends icy winds over the sea, frosting the surf and shore.", + "9": "The chill freezes tidal pools, creating frosted mirrors along the coast.", + "10": "The cyclone leaves the coastline glazed in ice, from rocks to sands.", + "11": "Frozen debris from the sea is scattered across the icy shoreline.", + "12": "The cyclone’s icy winds turn the coast into a frozen, shimmering expanse." + }, + "volcano": { + "1": "The frigid cyclone clashes with volcanic heat, creating an eerie frost haze.", + "2": "Frozen ash swirls through the air as icy winds rage around the crater.", + "3": "The cyclone’s chill coats lava flows with a thin, temporary frost.", + "4": "Steam and frost mix as icy winds chill the volcanic landscape.", + "5": "The freezing winds create icy patterns on the rugged volcanic rocks.", + "6": "The chill battles the heat, freezing edges of lava pools momentarily.", + "7": "Icy gusts sweep through the volcanic terrain, creating surreal frozen pockets.", + "8": "The volcano’s smoke mingles with frozen mist, creating a strange icy fog.", + "9": "Frozen debris swirls around the volcano’s base as the cyclone howls.", + "10": "The freezing winds chill volcanic vents, coating them in temporary frost.", + "11": "The cyclone creates icy spirals in the volcanic ash, chilling the ground.", + "12": "The icy winds transform the fiery terrain into a paradox of frost and heat." + }, + "artic": { + "1": "The frigid cyclone intensifies the Arctic’s chill, freezing the ice even harder.", + "2": "Icy gusts spiral through the Arctic expanse, coating snow in frost.", + "3": "The cyclone sends frozen mist swirling, obscuring the frozen tundra.", + "4": "The Arctic’s chill deepens as the freezing cyclone passes through.", + "5": "Ice and snow are whipped into frozen spirals by the icy winds.", + "6": "The cyclone adds a bitter edge to the already frigid Arctic air.", + "7": "Frozen icebergs groan under the pressure of the cyclone’s icy force.", + "8": "The Arctic becomes even more desolate as the freezing winds howl.", + "9": "The cyclone’s icy gusts scatter snow and create jagged frost patterns.", + "10": "The Arctic’s vast expanse glitters under the cyclone’s frosty grip.", + "11": "Frozen ice shards swirl in the air, carried by the cyclone’s winds.", + "12": "The Arctic transforms into an even colder, eerily silent frostscape." + }, + "cursed": { + "1": "The frigid cyclone brings an unnatural chill, freezing the cursed ground.", + "2": "Icy winds howl through the cursed land, chilling even the air of dread.", + "3": "Frost spirals across twisted trees and cursed ruins, enhancing their eerie glow.", + "4": "The cyclone’s chill intensifies the cursed aura, freezing shadows in place.", + "5": "Frozen mist swirls unnaturally, blanketing the cursed terrain in frost.", + "6": "The icy winds carry whispers of dread, chilling the cursed land to its core.", + "7": "The cyclone freezes cursed pools, turning their dark waters to jagged ice.", + "8": "Icy winds whip through, frosting cursed sigils and ancient stones.", + "9": "The cursed ground crackles underfoot as the cyclone’s frost spreads.", + "10": "Frosty gusts swirl around cursed ruins, amplifying their sinister aura.", + "11": "Frozen shadows linger unnaturally as the cyclone’s icy winds pass.", + "12": "The cursed land becomes an eerie, frozen wasteland under the cyclone’s chill." + } + } + }, + "Stormy Gusts": { + "conditions": { + "temperature": { "gte": 30, "lte": 60 }, + "precipitation": { "gte": 20, "lte": 60 }, + "wind": { "gte": 50, "lte": 80 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 20, "lte": 60 }, + "visibility": { "lte": 70 } + }, + "descriptions": { + "farm": { + "1": "Winds howl across the fields, rattling wooden fences and shaking shutters.", + "2": "Stormy gusts rip through the farmland, bending crops and scattering hay.", + "3": "The gusts whistle through the barn, shaking loose boards and tossing straw.", + "4": "Trees along the edges of the fields sway violently under the stormy winds.", + "5": "Howling winds scatter tools and debris across the farmland.", + "6": "The farmhouse creaks under the strain of relentless gusts.", + "7": "Stormy winds whip through the orchard, scattering blossoms and leaves.", + "8": "Chickens scatter as gusts toss loose feathers into the air.", + "9": "The gusts carry the scent of rain, though the skies remain cloudy.", + "10": "Windmills spin wildly under the power of the stormy gusts.", + "11": "The gusts scatter loose soil, creating swirling dust clouds over the fields.", + "12": "Rattling windows and howling winds make the farm seem alive with motion." + }, + "village": { + "1": "Stormy gusts rattle wooden shutters and doors, filling the village with noise.", + "2": "Children run to secure belongings as gusts toss loose items into the air.", + "3": "The village square is whipped into chaos, with stalls creaking under the wind.", + "4": "Gusts howl between cottages, sending chills through the narrow streets.", + "5": "Smoke from chimneys is whipped into spirals by the strong winds.", + "6": "The gusts carry the scent of rain and churn dust in the cobblestone streets.", + "7": "Villagers struggle to carry supplies as the winds threaten to upend them.", + "8": "The stormy winds topple barrels and clatter wooden signs.", + "9": "Loose laundry flaps violently in the wind, some breaking free to fly away.", + "10": "The gusts send leaves and straw swirling through the village paths.", + "11": "Doors creak and shutters bang as the wind sweeps through the village.", + "12": "The howling gusts echo through the alleys, making the village seem eerie." + }, + "city": { + "1": "Stormy gusts roar through the streets, tossing debris into the air.", + "2": "Flags and banners whip violently as the gusts tear through the city.", + "3": "The gusts howl between tall buildings, amplifying their power.", + "4": "Merchants struggle to secure their stalls as goods scatter in the wind.", + "5": "Stormy winds send cloaks and hats flying, forcing pedestrians to shield themselves.", + "6": "The city gates creak under the strain of the relentless gusts.", + "7": "The wind carries the scent of distant rain, hinting at a coming storm.", + "8": "Windows rattle and chimneys groan under the force of the stormy winds.", + "9": "Streetlamps sway precariously as the gusts batter the city streets.", + "10": "Loose papers and dust whirl through the crowded market square.", + "11": "The winds whip through alleyways, creating eerie echoes in the city’s heart.", + "12": "Gusts batter rooftops, sending tiles crashing to the ground below." + }, + "plains": { + "1": "Stormy gusts sweep across the plains, flattening tall grass in waves.", + "2": "The wind howls unimpeded, carrying dust and debris across the open expanse.", + "3": "The gusts create swirling eddies of grass and dirt on the flat terrain.", + "4": "Loose rocks and tumbleweeds are carried along by the relentless winds.", + "5": "The plains echo with the eerie howl of stormy gusts rushing through.", + "6": "Storm clouds loom on the horizon as gusts whip across the plains.", + "7": "Animals scatter to shelter as the gusts bend grass and shake shrubs.", + "8": "The wind creates rippling patterns across the golden fields of grass.", + "9": "Dust clouds form as the gusts kick up the dry soil of the plains.", + "10": "The gusts carry the sound of distant thunder, though skies remain clear.", + "11": "Wildflowers bend low under the assault of the relentless winds.", + "12": "The howling gusts chill the air, making the plains feel desolate and empty." + }, + "forest": { + "1": "Stormy gusts rush through the forest, bending trees and snapping branches.", + "2": "Leaves are torn from the canopy, swirling through the forest air.", + "3": "The wind whistles through the trees, creating an eerie, haunting sound.", + "4": "Stormy gusts shake the forest, sending birds and small animals scattering.", + "5": "Dead leaves and twigs are whipped into small whirlwinds on the forest floor.", + "6": "The gusts make the tall pines sway dangerously, groaning under the strain.", + "7": "Stormy winds scatter loose bark and pinecones, carpeting the forest path.", + "8": "The forest is filled with the sounds of rustling leaves and snapping branches.", + "9": "The gusts chill the air, making the forest feel colder and darker than before.", + "10": "Trees creak and sway as the stormy winds rush through the forest.", + "11": "The wind carries the scent of wet earth and distant rain through the woods.", + "12": "Small clearings become chaotic as gusts scatter debris in all directions." + }, + "swamp": { + "1": "Stormy gusts ripple the swamp waters, sending waves through the murky pools.", + "2": "The winds carry a damp chill, stirring up the swamp’s foul-smelling air.", + "3": "Reeds and cattails bend under the force of the relentless gusts.", + "4": "Stormy winds send ripples across the surface of stagnant pools.", + "5": "The gusts carry insects and leaves through the humid swamp air.", + "6": "The wind howls through the mangroves, creating eerie noises in the swamp.", + "7": "Loose moss and branches are scattered as the stormy gusts pass through.", + "8": "The swamp seems alive as the wind sends ripples and rustles through it.", + "9": "Gusts create splashes in the water as they stir the swamp into motion.", + "10": "The stormy winds rattle through the swamp, shaking vines and reeds.", + "11": "The gusts chill the swamp air, sending shivers through even the bravest travelers.", + "12": "Mud and water spray as the gusts churn the swamp’s surface." + }, + "jungle": { + "1": "Stormy gusts rush through the jungle, shaking the dense foliage violently.", + "2": "Leaves and vines whip in the wind, filling the jungle with rustling noise.", + "3": "The gusts send loose fruit and branches crashing to the jungle floor.", + "4": "Stormy winds tear through the jungle, creating paths of scattered leaves.", + "5": "The wind carries the scent of rain and earth as it roars through the canopy.", + "6": "Monkeys and birds scatter, their cries lost in the sound of howling gusts.", + "7": "The stormy winds create swirling eddies of leaves and dust in jungle clearings.", + "8": "Vines and creepers sway under the force of the relentless winds.", + "9": "The gusts carry moisture and chill, making the jungle feel damp and eerie.", + "10": "The jungle canopy sways and creaks as the stormy gusts rip through.", + "11": "Fallen leaves and debris create a carpet of chaos as the winds rage.", + "12": "The gusts echo through the jungle, drowning out all other sounds." + }, + "hills": { + "1": "Stormy gusts whip across the hills, bending grasses and shrubs.", + "2": "The wind howls over the rolling hills, carrying loose dirt into the air.", + "3": "Stormy winds create a chilling sound as they rush through rocky outcrops.", + "4": "Loose stones tumble down slopes as gusts shake the hillside.", + "5": "The gusts scatter leaves and branches across the rugged hill paths.", + "6": "Howling winds sweep across the hills, making travel treacherous.", + "7": "Stormy gusts carry the scent of rain and churn the dry grasslands.", + "8": "Hikers struggle to stay upright as gusts batter the winding hill trails.", + "9": "The hills echo with the eerie whistle of stormy winds passing through.", + "10": "The wind creates swirling eddies of dust and leaves on the exposed slopes.", + "11": "Grasses ripple like waves under the relentless force of the gusts.", + "12": "The gusts make the hills feel colder, chilling travelers to the bone." + }, + "mountains": { + "1": "Stormy winds roar through mountain passes, shaking loose rocks.", + "2": "The gusts carry a biting chill as they rush over the peaks and cliffs.", + "3": "Stormy gusts whistle through narrow crevices, amplifying their power.", + "4": "The wind sends pebbles and dust tumbling down steep mountain slopes.", + "5": "The gusts buffet climbers, forcing them to cling to the rugged terrain.", + "6": "Stormy winds howl through the valleys, drowning out distant sounds.", + "7": "Loose snow is swept into the air by the powerful mountain gusts.", + "8": "The gusts create swirling patterns of mist and snow around the peaks.", + "9": "Stormy winds make the mountain air colder and harder to breathe.", + "10": "The cliffs echo with the haunting sound of relentless gusts.", + "11": "The wind carries the scent of stone and snow as it tears through the range.", + "12": "Travelers brace against the stormy winds, which threaten to topple them." + }, + "desert": { + "1": "Stormy gusts sweep across the desert, kicking up clouds of sand.", + "2": "The wind howls across the dunes, creating rippling waves of golden sand.", + "3": "The gusts carry a biting heat, making the desert feel even more hostile.", + "4": "Loose sand stings exposed skin as the winds whip through the desert.", + "5": "The stormy gusts reshape the dunes, creating new ridges and valleys.", + "6": "The desert air is filled with swirling sand and the sound of howling winds.", + "7": "The gusts carry the faint scent of distant rain, though the sky remains dry.", + "8": "Desert plants bend and creak under the relentless force of the winds.", + "9": "The stormy gusts make the already barren desert seem even more desolate.", + "10": "The winds carry the cries of distant creatures, distorted and eerie.", + "11": "The desert floor is scoured by the gusts, leaving behind smooth, exposed stone.", + "12": "Travelers shield their faces as stormy winds threaten to overwhelm them." + }, + "coastal": { + "1": "Stormy gusts whip through the coastal air, carrying salt and sea spray.", + "2": "Waves crash violently against the shore, driven by the relentless winds.", + "3": "The wind howls across the beach, scattering sand and shells.", + "4": "Stormy gusts rattle fishing boats and whip through the coastal villages.", + "5": "The gusts carry the scent of brine and churn the waters into frothy chaos.", + "6": "Coastal trees bend low as the stormy winds rush past.", + "7": "The horizon is obscured by sea mist, carried inland by the powerful gusts.", + "8": "Stormy winds send foam and seaweed flying across the rocky shore.", + "9": "The sound of the gusts drowns out the cries of gulls above the waves.", + "10": "Fishing nets and sails flap wildly as the winds buffet the coastline.", + "11": "The gusts chill the coastal air, making the sea feel colder and wilder.", + "12": "Cliffs echo with the sound of the relentless stormy winds and crashing waves." + }, + "volcano": { + "1": "Stormy gusts howl across the volcanic slopes, carrying ash and dust.", + "2": "The winds stir the volcanic ash, creating swirling clouds of grit.", + "3": "Stormy gusts whip through the air, carrying the sulfuric scent of the volcano.", + "4": "The wind whistles through cracks and crevices in the volcanic rock.", + "5": "Stormy winds scatter loose rocks and debris down the steep volcanic slopes.", + "6": "The gusts carry a mix of heat and ash, making the air difficult to breathe.", + "7": "The winds make the volcanic terrain feel even more unstable and hostile.", + "8": "Loose ash rises into the air as the stormy winds tear across the volcano.", + "9": "The howling gusts carry distant echoes of volcanic rumblings.", + "10": "Stormy winds obscure visibility, creating a gray haze over the volcanic landscape.", + "11": "The gusts rattle volcanic outcrops, sending small avalanches of ash tumbling.", + "12": "Travelers struggle against the relentless winds, which sting with volcanic grit." + }, + "artic": { + "1": "Stormy gusts sweep across the icy expanse, chilling to the bone.", + "2": "The wind howls over the frozen tundra, creating swirls of drifting snow.", + "3": "Stormy winds bite at exposed skin, making the cold feel even more intense.", + "4": "The gusts carry loose ice particles, stinging like needles against the face.", + "5": "Snow is whipped into a blinding flurry by the relentless artic winds.", + "6": "Stormy gusts create eerie howls as they rush through icy crevices.", + "7": "The wind chills the air to an unbearable freeze, forcing shelter-seekers to hurry.", + "8": "The artic landscape is transformed into a swirling chaos of snow and wind.", + "9": "Stormy winds carry the faint scent of salt from distant, frozen seas.", + "10": "The gusts send snowflakes spinning in chaotic patterns across the tundra.", + "11": "Icicles rattle and snap as the stormy winds buffet frozen outcrops.", + "12": "The wind howls unimpeded across the artic plains, amplifying its icy ferocity." + }, + "cursed": { + "1": "Stormy gusts howl through the cursed land, carrying an unnatural chill.", + "2": "The wind whispers eerie sounds as it tears through the desolate landscape.", + "3": "Stormy winds scatter ash and bone dust across the cursed terrain.", + "4": "The gusts chill the air with an unnatural cold that seems to seep into the soul.", + "5": "Stormy winds stir the shadows, making them dance ominously.", + "6": "The cursed land echoes with the howls of the stormy gusts, like distant wails.", + "7": "Loose debris is carried by the gusts, creating a haunting rattle.", + "8": "The wind carries whispers and faint cries, unsettling all who hear them.", + "9": "Stormy gusts obscure the horizon with swirling mists and unnatural fog.", + "10": "The gusts seem to carry a sinister energy, unsettling those who brave them.", + "11": "The cursed ground trembles slightly as the relentless winds pass through.", + "12": "The wind carries the scent of decay and damp earth, adding to the foreboding." + } + } + }, + "Icy Overcast Stillness": { + "conditions": { + "temperature": { "lte": 30 }, + "precipitation": { "lte": 30 }, + "wind": { "lte": 30 }, + "humidity": { "gte": 50, "lte": 80 }, + "cloudCover": { "gte": 70 }, + "visibility": { "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Frost clings to the fields under a gray, unmoving sky.", + "2": "The still air makes the icy chill more penetrating across the farmstead.", + "3": "Barn roofs glisten with frost as the overcast sky looms heavily.", + "4": "Frozen puddles dot the farmyard, undisturbed by even a whisper of wind.", + "5": "The icy stillness makes the farm animals restless in their pens.", + "6": "The frozen ground cracks underfoot in the oppressive quiet.", + "7": "A gray sky presses down, casting the farm in muted shadows.", + "8": "The frost-covered crops shimmer faintly under the overcast sky.", + "9": "Every breath feels sharper in the unmoving, frigid air.", + "10": "The stillness amplifies the creak of old barn doors in the cold.", + "11": "Icicles hang untouched from the eaves as silence blankets the farm.", + "12": "Frost-covered fences mark a frozen, quiet landscape under a slate sky." + }, + "village": { + "1": "The village square is frozen in silence under a leaden sky.", + "2": "Chimneys release faint plumes of smoke that rise slowly in the still air.", + "3": "The icy overcast stillness wraps the village in a muted chill.", + "4": "Frost-covered cobblestones glisten faintly as villagers tread carefully.", + "5": "The sound of footsteps echoes sharply in the frozen, quiet streets.", + "6": "Windows fog up in the still, cold air as villagers seek warmth indoors.", + "7": "Icicles dangle from rooftops, untouched by any stirring wind.", + "8": "The overcast sky gives the village a somber, muted tone.", + "9": "The frozen well stands unused, its handle encrusted with ice.", + "10": "Children’s laughter is absent as the icy stillness holds sway.", + "11": "The quiet amplifies the occasional creak of shutters in the cold.", + "12": "A thin layer of frost covers every surface, dulling colors to gray." + }, + "city": { + "1": "The city streets are eerily quiet, muffled by the oppressive cold.", + "2": "Frost clings to the edges of market stalls left unattended in the chill.", + "3": "The overcast sky casts the city in a dim, lifeless light.", + "4": "Smoke from chimneys rises in straight lines into the still, icy air.", + "5": "Frozen fountains stand as monuments to the biting cold.", + "6": "The lack of wind intensifies the quiet, making every sound sharper.", + "7": "Stone buildings glisten faintly with frost under the leaden sky.", + "8": "The city gates creak loudly as they shift against the frozen stillness.", + "9": "The cold air chills to the bone, with no breeze to break the monotony.", + "10": "The icy stillness gives the bustling city an almost ghostly atmosphere.", + "11": "Icicles adorn the city walls, untouched by any stirring wind.", + "12": "The overcast sky and frozen streets give the city an abandoned feel." + }, + "plains": { + "1": "The endless plains lie frozen and silent under a heavy gray sky.", + "2": "The icy stillness stretches across the plains, broken only by frost-covered grass.", + "3": "The overcast sky presses low, muting the vast expanse of the plains.", + "4": "Frost crunches loudly underfoot in the frozen stillness.", + "5": "The chill of the plains is amplified by the oppressive silence.", + "6": "Frozen streams thread through the plains, their surfaces like glass.", + "7": "The still air makes the cold on the plains even more unbearable.", + "8": "Shadows stretch long under the dim, gray light of the overcast sky.", + "9": "The plains seem frozen in time, untouched by even a hint of wind.", + "10": "The vast, frost-covered plains echo with the sounds of nothingness.", + "11": "The icy air bites at any exposed skin, unbroken by any breeze.", + "12": "The overcast stillness amplifies the desolate beauty of the frozen plains." + }, + "forest": { + "1": "The forest is eerily silent, the trees standing frozen under a gray sky.", + "2": "Every branch is coated in frost, unmoving in the still, cold air.", + "3": "The overcast sky casts a dim light, making the forest seem endless.", + "4": "Frozen leaves crunch loudly in the oppressive quiet of the forest.", + "5": "Icicles hang from the branches, gleaming faintly in the muted light.", + "6": "The stillness in the forest makes even the smallest sound echo loudly.", + "7": "Frost-covered moss clings to the trunks of ancient, unmoving trees.", + "8": "The forest floor is a patchwork of ice and frozen leaves.", + "9": "The icy overcast stillness makes the forest feel almost otherworldly.", + "10": "The chill seems to seep from the forest itself, wrapping everything in cold.", + "11": "The stillness of the forest is broken only by the occasional creak of ice.", + "12": "The muted gray of the sky mirrors the frost-covered silence of the woods." + }, + "swamp": { + "1": "The swamp lies still and frozen, with icy waters reflecting the gray sky.", + "2": "Frost clings to reeds and branches, muting the usual sounds of the swamp.", + "3": "The overcast sky turns the frozen swamp into a ghostly expanse.", + "4": "Icy puddles dot the swamp, their surfaces unbroken in the still air.", + "5": "The chill in the swamp air feels heavier under the oppressive gray sky.", + "6": "The swamp’s usual murkiness is replaced by a cold, frosty stillness.", + "7": "Frost-covered vines hang motionless in the icy, overcast swamp.", + "8": "The frozen waters make the swamp eerily silent, amplifying every step.", + "9": "The gray sky reflects in icy pools, giving the swamp an endless feel.", + "10": "The swamp’s icy stillness makes even the air feel damp and heavy.", + "11": "Frozen cattails stand like statues under the unmoving overcast sky.", + "12": "The swamp feels timeless, trapped in the icy stillness of the gray day." + }, + "jungle": { + "1": "The jungle is unnaturally quiet, with frost clinging to its dense foliage.", + "2": "The overcast sky casts a dim light over the frost-covered jungle.", + "3": "The usual buzz of the jungle is silenced by the icy overcast stillness.", + "4": "Frozen leaves crackle faintly in the jungle’s muted, cold air.", + "5": "The jungle floor is slick with frost, turning every step into a careful maneuver.", + "6": "Icicles dangle from thick vines, unmoving in the frozen stillness.", + "7": "The overcast chill seems to sap the life from the jungle’s vibrant greens.", + "8": "The frost-covered jungle seems transformed into a wintery labyrinth.", + "9": "The stillness amplifies the faint drip of water freezing on leaves.", + "10": "The jungle feels alien under the icy overcast sky, its warmth drained away.", + "11": "Frozen streams weave through the jungle, reflecting the gray sky above.", + "12": "The icy stillness wraps the jungle in an eerie, almost ethereal quiet." + }, + "hills": { + "1": "Frost coats the rolling hills, the still air heavy under a leaden sky.", + "2": "The overcast sky casts a dim pall over the frozen, quiet landscape.", + "3": "Icy grass crunches underfoot, breaking the perfect stillness of the hills.", + "4": "The hills are blanketed in frost, their gentle slopes eerily silent.", + "5": "The frozen stillness seems to stretch endlessly across the rolling terrain.", + "6": "Gray clouds hang low over the frost-covered hills, casting long shadows.", + "7": "The hills glisten faintly with ice, the air motionless and biting.", + "8": "Frozen streams thread through the hills, their surfaces unbroken by wind.", + "9": "The quiet of the hills feels oppressive, broken only by the crunch of frost.", + "10": "Every breath turns to mist in the frigid stillness of the open hills.", + "11": "The icy chill seeps into the earth, leaving the hills lifeless and still.", + "12": "The hills lie frozen under a heavy gray sky, muted and lifeless." + }, + "mountains": { + "1": "The peaks stand cloaked in frost, their stillness amplified by the icy air.", + "2": "The overcast sky casts the mountains in a somber, muted light.", + "3": "Frosted crags and ledges glisten faintly under the heavy, gray sky.", + "4": "The stillness of the mountains is broken only by the distant crack of ice.", + "5": "The frozen cliffs seem timeless, unmoving beneath the overcast sky.", + "6": "The icy air bites harder as the mountains loom in the oppressive quiet.", + "7": "Icicles hang like sentinels from rocky outcroppings, untouched by wind.", + "8": "The muted light gives the frozen peaks an otherworldly, lifeless feel.", + "9": "The silence in the mountains is broken only by the crunch of snow underfoot.", + "10": "Frost covers every surface, the mountains eerily still beneath the gray sky.", + "11": "The overcast stillness makes the towering peaks feel eternal and frozen.", + "12": "The biting cold clings to the mountains, their majesty dulled by the frost." + }, + "desert": { + "1": "The desert sands are frozen solid under a lifeless gray sky.", + "2": "Frost coats the dunes, their curves sharp and still in the frigid air.", + "3": "The icy stillness makes the frozen desert feel alien and vast.", + "4": "The usual heat is replaced by a biting chill, the sand hard as stone.", + "5": "The overcast sky looms over the frozen expanse, silencing all life.", + "6": "Every grain of sand seems locked in ice, reflecting the muted gray light.", + "7": "Icicles form on cacti, a surreal sight in the frozen desert landscape.", + "8": "The desert feels endless and lifeless under the heavy, unmoving sky.", + "9": "The still air amplifies the biting cold, turning the desert into a frozen wasteland.", + "10": "Frost-covered dunes glisten faintly as the desert lies eerily silent.", + "11": "The frozen sands crunch faintly underfoot, the only sound in the stillness.", + "12": "The overcast chill transforms the desert into a stark, icy expanse." + }, + "coastal": { + "1": "The sea lies frozen and still under a heavy gray sky, waves locked in ice.", + "2": "Frost coats the shoreline, the air thick with icy stillness.", + "3": "The overcast sky turns the coastal landscape into a frozen tableau.", + "4": "The frozen surf glimmers faintly under the muted gray light.", + "5": "The stillness of the coast is broken only by the distant creak of icebergs.", + "6": "Icicles hang from docks and boats, untouched by the frozen air.", + "7": "The cold bites deeply, the coastal waters silent and unmoving.", + "8": "The frost-covered shore feels abandoned beneath the weight of the overcast sky.", + "9": "The usual sound of waves is replaced by a deep, chilling quiet.", + "10": "The gray sea stretches endlessly, its surface slick with frost.", + "11": "The icy air sharpens every detail of the frozen coastal landscape.", + "12": "The overcast sky mirrors the frozen waters, blending sea and sky in gray." + }, + "volcano": { + "1": "The frozen volcano stands eerily silent, its slopes coated in frost.", + "2": "Steam rises faintly from cracks in the ice, a strange contrast to the chill.", + "3": "The overcast sky dulls the once-fiery slopes into muted grays.", + "4": "The volcano’s rugged surface glistens with frost under the still gray sky.", + "5": "The icy air feels alien against the frozen remnants of molten rock.", + "6": "Icicles hang from hardened lava flows, unmoving in the frigid stillness.", + "7": "The volcano feels lifeless, its heat long extinguished by the biting cold.", + "8": "The heavy overcast sky turns the volcano into a frozen, lifeless giant.", + "9": "Frost-covered rocks crack faintly as the cold deepens around the volcano.", + "10": "The usual heat of the volcano is replaced by an oppressive icy stillness.", + "11": "Frozen ash crunches underfoot, the volcano eerily silent and lifeless.", + "12": "The biting chill makes the volcanic landscape feel utterly alien and quiet." + }, + "artic": { + "1": "The Arctic lies frozen in absolute stillness under a leaden gray sky.", + "2": "Every surface glistens with frost, untouched by wind or warmth.", + "3": "The overcast sky gives the icy expanse an endless, lifeless feel.", + "4": "Icicles hang thick from every surface, their stillness amplifying the quiet.", + "5": "The icy air bites deeply, the Arctic silence heavy and oppressive.", + "6": "The frozen landscape reflects the muted light, a mirror of the gray sky.", + "7": "The Arctic feels timeless, locked in an eternal overcast chill.", + "8": "The silence is absolute, broken only by the distant crack of shifting ice.", + "9": "The heavy stillness makes the frozen expanse feel otherworldly and vast.", + "10": "The biting cold and overcast sky turn the Arctic into a gray wasteland.", + "11": "The frost-covered ice sheets stretch endlessly beneath the heavy gray sky.", + "12": "The icy stillness amplifies the Arctic’s desolate beauty and harshness." + }, + "cursed": { + "1": "The cursed land lies frozen under an unnatural, oppressive gray sky.", + "2": "Frost clings to twisted trees and bones, the air heavy and still.", + "3": "The overcast sky feels alive, pressing down on the cursed, icy ground.", + "4": "Icicles hang like jagged teeth from cursed ruins, the silence unsettling.", + "5": "The biting cold seems to seep with malevolence from the cursed earth.", + "6": "The frost-covered ground cracks faintly, breaking the eerie stillness.", + "7": "The cursed landscape is lifeless, the overcast chill heavy with dread.", + "8": "The silence feels sinister, amplified by the frozen, unmoving air.", + "9": "Shadows seem deeper in the gray light, the cursed frost glinting faintly.", + "10": "The icy stillness feels like a curse itself, binding the land in silence.", + "11": "Frozen relics of the cursed past lie untouched under the heavy gray sky.", + "12": "The frost-covered land feels trapped, the overcast sky hiding its torment." + } + } + }, + "Oppressive Stormfront": { + "conditions": { + "temperature": { "gte": 70 }, + "precipitation": { "lte": 40 }, + "wind": { "gte": 60 }, + "humidity": { "gte": 70 }, + "cloudCover": { "gte": 60 }, + "visibility": { "gte": 20, "lte": 60 } + }, + "descriptions": { + "farm": { + "1": "Dark clouds gather ominously, casting the farm into an unnatural gloom.", + "2": "Winds tear through the fields, bending crops as thunder rumbles nearby.", + "3": "A suffocating humidity clings to the air, the stormfront looming overhead.", + "4": "The horizon disappears into swirling gray clouds, warning of an impending deluge.", + "5": "Lightning crackles in the distance, illuminating the farm’s eerie stillness.", + "6": "Cattle grow restless under the oppressive weight of the stormy air.", + "7": "The sky churns with dark clouds, a foreboding prelude to chaos.", + "8": "The smell of rain and ozone saturates the air, the stormfront nearing.", + "9": "The wind carries faint whispers of the storm’s fury, unsettling the farmstead.", + "10": "Shadows lengthen unnaturally as the stormclouds blot out the sun.", + "11": "The barn creaks ominously as gusts of wind announce the storm’s arrival.", + "12": "The air feels heavy and electric, the stormfront poised to strike." + }, + "village": { + "1": "Dark clouds roll over the village, casting an eerie twilight at midday.", + "2": "The villagers retreat indoors as thunder booms ominously overhead.", + "3": "The oppressive stormfront fills the air with an unnatural, stifling tension.", + "4": "Rain begins to lash the rooftops, heralding the storm’s unstoppable advance.", + "5": "Windows rattle as the wind howls through the village, a storm’s fury building.", + "6": "The smell of wet earth and charged air permeates the village square.", + "7": "Lightning illuminates the gathering stormclouds, revealing their immense scale.", + "8": "Animals bray and grow restless, sensing the stormfront’s impending chaos.", + "9": "The cobblestone streets gleam with moisture as the stormclouds close in.", + "10": "Shutters slam shut as the wind picks up, carrying the storm's chill.", + "11": "The air feels suffocatingly heavy, the stormfront looming over the village.", + "12": "The first drops of rain fall like icy warnings of the storm to come." + }, + "city": { + "1": "The towering stormclouds cast the city in darkness, an ominous prelude to chaos.", + "2": "The marketplace clears as thunder echoes through the narrow streets.", + "3": "Gutters overflow as the oppressive stormfront unleashes its first rains.", + "4": "Lightning illuminates the stone walls, revealing the storm’s overwhelming presence.", + "5": "The city’s bells toll, warning citizens of the incoming tempest.", + "6": "The air feels charged and suffocating, the stormfront ready to burst.", + "7": "Flags and banners whip violently in the wind as the storm grows closer.", + "8": "The sky churns above, a swirling mass of gray and black clouds.", + "9": "Street lamps flicker feebly against the encroaching darkness of the storm.", + "10": "The wind howls through alleys, carrying with it the scent of rain and danger.", + "11": "Citizens hurry to shelter as the stormfront’s shadow envelops the city.", + "12": "The oppressive air makes breathing difficult, the stormclouds pressing down." + }, + "plains": { + "1": "The open plains are engulfed by dark, swirling clouds, an endless stormfront.", + "2": "The grass bends under powerful gusts as lightning streaks across the sky.", + "3": "A deep rumble of thunder rolls across the plains, shaking the earth.", + "4": "The horizon disappears under the oppressive weight of stormclouds.", + "5": "Rain begins to sweep across the plains, soaking the ground instantly.", + "6": "The winds tear at the tall grass, creating waves of movement under the storm.", + "7": "The stormfront looms, its shadow consuming the endless stretch of plains.", + "8": "Lightning flashes, illuminating the desolate plains in stark white light.", + "9": "The air feels suffocating and thick, a prelude to the storm’s full fury.", + "10": "Thunder cracks violently, reverberating across the vast, empty fields.", + "11": "The plains seem eerily quiet save for the wind’s eerie whistle.", + "12": "The storm moves relentlessly, blotting out the sky above the plains." + }, + "forest": { + "1": "The forest grows unnaturally dark as the stormclouds gather overhead.", + "2": "Trees creak and sway violently as the stormfront closes in.", + "3": "The air grows damp and heavy, the forest floor slick with anticipation.", + "4": "Thunder rumbles through the trees, shaking loose leaves and branches.", + "5": "Lightning streaks through the canopy, momentarily illuminating the dense woods.", + "6": "The forest feels alive, its stillness broken by the wind’s howls.", + "7": "The oppressive stormfront seems to press the forest into silence.", + "8": "Rain filters through the leaves, creating a deafening patter in the dense woods.", + "9": "The scent of damp earth and ozone fills the air as the storm looms.", + "10": "Animals scurry for cover as the forest braces for the storm's wrath.", + "11": "The forest canopy rustles violently, leaves tearing loose in the wind.", + "12": "The oppressive stormfront makes the once-vivid forest feel eerie and cold." + }, + "swamp": { + "1": "The swamp is cloaked in darkness as stormclouds roll in, suffocating the air.", + "2": "Lightning flashes across stagnant pools, illuminating the stormy gloom.", + "3": "The air grows thick with moisture and tension, the stormfront looming.", + "4": "Thunder cracks, echoing across the swamp and disturbing its eerie stillness.", + "5": "The wind stirs the stagnant water, creating ripples in the murky depths.", + "6": "Rain falls heavily, turning the swamp into a cacophony of splashes and drips.", + "7": "The oppressive stormfront transforms the swamp into a forbidding quagmire.", + "8": "The scent of wet decay intensifies as the stormclouds blot out the sun.", + "9": "Lightning illuminates twisted trees and shadows, revealing the swamp’s dread.", + "10": "The storm’s presence amplifies the swamp’s eerie, otherworldly silence.", + "11": "Rain pelts the swamp relentlessly, adding to its already soggy gloom.", + "12": "The swamp feels alive with the storm, its still waters disrupted by wind." + }, + "jungle": { + "1": "The dense jungle grows dim under the oppressive weight of stormclouds.", + "2": "Leaves tremble in the wind as the stormfront surges toward the jungle.", + "3": "The air is heavy with humidity, the jungle floor growing slick and treacherous.", + "4": "Thunder rolls through the jungle, its roar muffled by dense foliage.", + "5": "Lightning flashes through the canopy, casting wild shadows across the jungle.", + "6": "Rain cascades from the leaves, creating a deafening roar within the jungle.", + "7": "The stormfront turns the vibrant jungle into a dark, foreboding maze.", + "8": "The scent of damp earth and wet leaves saturates the thick jungle air.", + "9": "Animals cry out in alarm as the storm barrels through the jungle.", + "10": "The wind tears through the jungle, sending leaves and branches flying.", + "11": "The oppressive storm makes the dense jungle feel suffocating and dangerous.", + "12": "The stormfront transforms the jungle into a tempestuous, shadowy wilderness." + }, + "hills": { + "1": "Dark clouds loom over the hills, casting long shadows and eerie silence.", + "2": "Winds whip through the grass, carrying the storm’s oppressive weight.", + "3": "The stormfront’s shadow creeps across the rolling hills, blotting out the sun.", + "4": "Thunder echoes across the hills, the sound deep and foreboding.", + "5": "Lightning streaks the sky, illuminating the storm’s relentless advance.", + "6": "The air grows heavy, the hills suffused with an unnatural stillness.", + "7": "Rain begins to lash the hillsides, carving rivulets into the muddy ground.", + "8": "The wind carries the storm’s chill, bending shrubs and stirring unease.", + "9": "The stormfront looms like a curtain, plunging the hills into early darkness.", + "10": "The grasses sway violently as the oppressive stormfront rolls closer.", + "11": "The sound of distant thunder rolls endlessly, warning of impending chaos.", + "12": "Clouds churn above, casting the hills in an unsettling twilight." + }, + "mountains": { + "1": "Thunder reverberates between the peaks, the stormfront closing in fast.", + "2": "Dark clouds swirl ominously around the mountaintops, obscuring the view.", + "3": "Winds howl through the passes, carrying the weight of the storm.", + "4": "The oppressive stormfront encases the mountains in shadow and fear.", + "5": "Lightning arcs across the peaks, illuminating the rugged terrain briefly.", + "6": "Rain lashes the rocky slopes, turning paths into slick hazards.", + "7": "The storm’s fury traps the mountains in a grim, foreboding silence.", + "8": "The sky churns violently, a harbinger of the storm’s unrelenting force.", + "9": "The air is charged with electricity, every breath thick with tension.", + "10": "The stormfront engulfs the mountains, cutting off the world below.", + "11": "Echoes of thunder roll through the valleys, magnified by the jagged cliffs.", + "12": "Clouds descend to the peaks, cloaking the mountains in eerie darkness." + }, + "desert": { + "1": "A wall of stormclouds looms, casting unnatural shadows over the sands.", + "2": "The wind howls, carrying sand and grit as the stormfront approaches.", + "3": "The oppressive heat mixes with the storm’s looming presence, stifling the air.", + "4": "Lightning streaks the distant dunes, the stormfront’s wrath building.", + "5": "Dark clouds swirl overhead, an ominous prelude to the desert’s upheaval.", + "6": "The stormfront’s shadow turns the golden sands a dull gray.", + "7": "The horizon vanishes beneath the churning mass of approaching clouds.", + "8": "The wind-driven storm feels alive, clawing at the desert with unseen hands.", + "9": "The air grows electric, charged with the promise of violent rain.", + "10": "The oppressive stormfront looms heavy, blotting out the sun’s glare.", + "11": "Sand swirls wildly, the storm churning the desert into chaos.", + "12": "Thunder cracks loudly, echoing unnaturally across the barren landscape." + }, + "coastal": { + "1": "Dark stormclouds gather over the sea, their reflection shimmering ominously.", + "2": "Winds whip the shoreline, driving waves into a frenzied roar.", + "3": "The stormfront cloaks the coast in darkness, the sea a turbulent mirror.", + "4": "Thunder rolls across the waves, its sound blending with crashing surf.", + "5": "Rain lashes the coastal cliffs, creating rivulets that run toward the sea.", + "6": "Lightning illuminates the storm-tossed ocean, revealing its violent nature.", + "7": "The oppressive air weighs down on the coast, suffused with salty mist.", + "8": "Waves batter the shore relentlessly as the stormfront draws closer.", + "9": "The storm turns the sea a murky gray, its fury reflected in the waves.", + "10": "Gulls cry in distress, fleeing the churning skies above the coast.", + "11": "The horizon disappears under the stormfront, merging sea and sky in chaos.", + "12": "Wind howls through seaside groves, bending trees under the storm’s weight." + }, + "volcano": { + "1": "The oppressive stormfront clashes with volcanic heat, shrouding the summit.", + "2": "Lightning flashes illuminate rising plumes of ash, blending fire and storm.", + "3": "The stormfront cloaks the volcano in eerie shadows and rumbling echoes.", + "4": "Rain hisses against molten rock, steam rising as the storm takes hold.", + "5": "The wind howls through jagged ridges, carrying ash and the scent of brimstone.", + "6": "Clouds churn violently above, their shadows dancing across volcanic slopes.", + "7": "The oppressive air mingles with volcanic heat, creating a suffocating atmosphere.", + "8": "Thunder reverberates alongside the rumble of the volcano’s unrest.", + "9": "The horizon is obscured as the stormfront merges with volcanic smoke.", + "10": "Lightning arcs between the stormclouds and the volcano’s fiery summit.", + "11": "Rain mingles with falling ash, coating the ground in a thick, dark sludge.", + "12": "The stormfront transforms the volcano into a scene of primal chaos." + }, + "artic": { + "1": "The stormfront cloaks the tundra in darkness, an icy wind cutting deep.", + "2": "Snow swirls wildly as the stormfront rolls in, obscuring all vision.", + "3": "The oppressive stormfront chills the air further, a freezing dread settling.", + "4": "Lightning flashes through the snow-laden sky, an eerie light in the storm.", + "5": "Winds howl across the ice, carrying the storm’s freezing fury.", + "6": "The stormfront looms like a shadowy giant, plunging the arctic into gloom.", + "7": "Snowdrifts build quickly under the relentless assault of wind and storm.", + "8": "The air feels bitterly cold, the stormfront sapping all remaining warmth.", + "9": "Thunder echoes strangely across the frozen expanse, muffled by the snow.", + "10": "The storm turns the arctic into a whirling tempest of ice and shadow.", + "11": "Snow falls heavily, blending seamlessly with the stormclouds above.", + "12": "The oppressive stormfront amplifies the arctic’s isolation and danger." + }, + "cursed": { + "1": "Dark clouds churn unnaturally, glowing faintly with a sinister green hue.", + "2": "The oppressive stormfront feels alive, radiating a malevolent energy.", + "3": "Lightning crackles in twisted patterns, the storm’s fury almost sentient.", + "4": "The stormfront carries a foul stench, as though the land itself recoils.", + "5": "Winds howl with otherworldly voices, their cries unsettling to the soul.", + "6": "Rain falls thick and black, tainting everything it touches as the storm looms.", + "7": "The clouds seem to pulse with an eerie light, the storm unnaturally still.", + "8": "Thunder crashes with a guttural roar, shaking the cursed land violently.", + "9": "The stormfront twists the light, casting strange shadows across the terrain.", + "10": "The air feels heavy with dread, the storm pressing down like a dark force.", + "11": "Lightning illuminates the storm briefly, revealing horrific shapes in the clouds.", + "12": "The oppressive stormfront hangs ominously, exuding a chilling, cursed aura." + } + } + } + }, + "enviroments": { + "farm": { + "name": "Farmhouse or Single Building" + }, + "village": { + "name": "Small Town or Village" + }, + "city": { + "name": "City Sprawl" + }, + "plains": { + "name": "Open Plains or Farmland" + }, + "forest": { + "name": "Forest or Woodland" + }, + "swamp": { + "name": "Swamp or Marsh" + }, + "jungle": { + "name": "Jungle" + }, + "hills": { + "name": "Hills or Highlands" + }, + "mountains": { + "name": "Mountains" + }, + "desert": { + "name": "Desert or Badlands" + }, + "coastal": { + "name": "Coastal or Island" + }, + "volcano": { + "name": "Volcanic Areas" + }, + "artic": { + "name": "Arctic or Glacial" + }, + "cursed": { + "name": "Cursed Lands" + } + }, + "scales": { + "temperature": { + "0": { + "description": "Coldest temperatures ever recorded, life-threateningly frigid." + }, + "5": { + "description": "Unprecedented extreme cold, dangerously icy conditions." + }, + "10": { + "description": "Brutally cold, freezing air and frostbite risks." + }, + "15": { + "description": "Bitterly cold, harsh and difficult to endure without protection." + }, + "20": { + "description": "Exceptionally cold, far below typical winter norms." + }, + "25": { + "description": "Unusually cold, heavy frost and icy winds." + }, + "30": { + "description": "Very cold, chilly air that bites into exposed skin." + }, + "35": { + "description": "Cold but tolerable with appropriate clothing." + }, + "40": { + "description": "Cool and crisp, a refreshing chill in the air." + }, + "45": { + "description": "Mildly cool, comfortable for outdoor activities." + }, + "50": { + "description": "Perfectly moderate, the ideal temperature range." + }, + "55": { + "description": "Warm and pleasant, a touch of heat in the air." + }, + "60": { + "description": "Comfortably warm, ideal for lighter clothing." + }, + "65": { + "description": "Unusually warm, noticeable heat but still pleasant." + }, + "70": { + "description": "Very warm, bordering on hot for some climates." + }, + "75": { + "description": "Hot and sunny, requiring shade and hydration." + }, + "80": { + "description": "Exceptionally hot, heatwaves and discomfort likely." + }, + "85": { + "description": "Dangerously hot, extreme heat that strains the body." + }, + "90": { + "description": "Scorching heat, survival requires active cooling measures." + }, + "95": { + "description": "Near-record heat, unbearable without shelter." + }, + "100": { + "description": "Hottest temperatures ever recorded, deadly and extreme." + } + }, + "humidity": { + "0": { + "description": "Completely arid, the air is bone dry and moisture is nonexistent." + }, + "5": { + "description": "Extremely dry, the environment feels parched and lifeless." + }, + "10": { + "description": "Very dry, skin and eyes may feel tight and uncomfortable." + }, + "15": { + "description": "Quite dry, the air noticeably lacks moisture." + }, + "20": { + "description": "Dry air, typical of desert climates or artificially heated spaces." + }, + "25": { + "description": "Mildly dry, with some moisture present but still arid." + }, + "30": { + "description": "Slightly dry, generally comfortable but lacking humidity." + }, + "35": { + "description": "Balanced dryness, air feels fresh and invigorating." + }, + "40": { + "description": "Comfortably dry, with a slight but pleasant hint of moisture." + }, + "45": { + "description": "Perfectly balanced, air feels neither too dry nor too humid." + }, + "50": { + "description": "Moderately humid, ideal for comfort and ease of breathing." + }, + "55": { + "description": "Slightly humid, with a mild dampness noticeable." + }, + "60": { + "description": "Comfortably humid, air feels rich but not sticky." + }, + "65": { + "description": "Noticeably humid, sweat evaporates more slowly." + }, + "70": { + "description": "Quite humid, air feels heavier and can be mildly oppressive." + }, + "75": { + "description": "Very humid, physical activity starts to feel more taxing." + }, + "80": { + "description": "Extremely humid, air feels dense and sticky to the skin." + }, + "85": { + "description": "Oppressively humid, movement feels like wading through heavy air." + }, + "90": { + "description": "Saturated air, moisture condenses readily, approaching fog." + }, + "95": { + "description": "Almost completely saturated, visibility can diminish noticeably." + }, + "100": { + "description": "Fully saturated, foggy or misty conditions with water-laden air." + } + }, + "wind": { + "0": { + "description": "Completely calm, an unnatural stillness in the air." + }, + "5": { + "description": "Barely perceptible breeze, air feels eerily still." + }, + "10": { + "description": "Gentle stirrings, slight movement in the air." + }, + "15": { + "description": "Very light breeze, barely noticeable and pleasant." + }, + "20": { + "description": "Light breeze, occasional rustling of leaves." + }, + "25": { + "description": "Noticeable breeze, light objects may shift slightly." + }, + "30": { + "description": "Fresh breeze, tree branches show slight movement." + }, + "35": { + "description": "Moderate breeze, pleasant but noticeable resistance while walking." + }, + "40": { + "description": "Brisk wind, small branches begin to sway." + }, + "45": { + "description": "Strong breeze, noticeable movement of larger branches." + }, + "50": { + "description": "Blustery wind, significant movement of objects outdoors." + }, + "60": { + "description": "Gale force, walking becomes difficult, minor damage possible." + }, + "70": { + "description": "Strong gale, structural damage to weak buildings and trees likely." + }, + "80": { + "description": "Severe storm, hazardous conditions with potential widespread damage." + }, + "90": { + "description": "Violent storm, extreme danger and widespread destruction likely." + }, + "100": { + "description": "Hurricane force, catastrophic devastation expected." + } + }, + "precipitation": { + "0": { + "description": "Completely dry, no rain in sight." + }, + "5": { + "description": "Bone dry, clouds may be present but no precipitation." + }, + "10": { + "description": "No measurable rain, air feels dry." + }, + "15": { + "description": "No rainfall, completely dry conditions." + }, + "20": { + "description": "No rain, clouds present but dry." + }, + "25": { + "description": "No rain, air is dry with no mist." + }, + "30": { + "description": "No rainfall, dry and clear." + }, + "35": { + "description": "Dry conditions, no mist or precipitation." + }, + "40": { + "description": "Dry, overcast but no rain." + }, + "45": { + "description": "Light rain, enough to dampen the ground." + }, + "50": { + "description": "Very light rain, may not even require an umbrella." + }, + "55": { + "description": "Light showers, refreshing but mild." + }, + "60": { + "description": "Light rain, enough to wet the ground thoroughly." + }, + "65": { + "description": "Moderate rain, small puddles forming." + }, + "70": { + "description": "Consistent rain, could affect outdoor activities." + }, + "75": { + "description": "Heavy rain, soaking and persistent." + }, + "80": { + "description": "Very heavy rain, visibility may be reduced." + }, + "85": { + "description": "Torrential rain, dangerous for travel." + }, + "90": { + "description": "Extreme rainfall, localized flooding likely." + }, + "95": { + "description": "Severe rainstorm, widespread disruption possible." + }, + "100": { + "description": "Record-breaking rainfall, catastrophic conditions." + } + }, + "cloudCover": { + "0": { + "description": "Crystal clear skies, no clouds in sight." + }, + "5": { + "description": "Nearly clear skies, with faint traces of clouds." + }, + "10": { + "description": "Mostly clear, with a few scattered clouds." + }, + "15": { + "description": "Predominantly clear skies, minimal cloud presence." + }, + "20": { + "description": "Clear skies with light, scattered clouds." + }, + "25": { + "description": "Mostly sunny, clouds starting to form." + }, + "30": { + "description": "Partly clear skies, but clouds are more noticeable." + }, + "35": { + "description": "Partly sunny, with clouds becoming more frequent." + }, + "40": { + "description": "Balanced mix of sun and clouds, transitioning from clear skies." + }, + "45": { + "description": "Partly cloudy, sun still visible through breaks." + }, + "50": { + "description": "Moderate cloud cover, sunlight occasionally dimmed." + }, + "55": { + "description": "Mostly cloudy, sunlight begins to fade." + }, + "60": { + "description": "Light overcast, clouds dominate the sky." + }, + "65": { + "description": "Mostly cloudy, small breaks in the clouds." + }, + "70": { + "description": "Heavy cloud cover, surroundings feel dim." + }, + "75": { + "description": "Dense overcast, sunlight is minimal." + }, + "80": { + "description": "Very heavy overcast, surroundings feel dull and gray." + }, + "85": { + "description": "Thick cloud layer, no sunlight visible." + }, + "90": { + "description": "Dark and oppressive overcast skies." + }, + "95": { + "description": "Severe overcast, impenetrable clouds dominate the sky." + }, + "100": { + "description": "Completely overcast, skies are thick and gloomy." + } + }, + "visibility": { + "100": { + "description": "Perfectly clear visibility, unlimited line of sight." + }, + "95": { + "description": "Crystal clear visibility, no obstructions." + }, + "90": { + "description": "Very poor visibility, only nearby objects are distinguishable." + }, + "85": { + "description": "Poor visibility, large portions of the view are obscured." + }, + "80": { + "description": "Significant visibility reduction, haze or fog dominant." + }, + "75": { + "description": "Limited visibility, nearby objects remain clear but distance is obscured." + }, + "70": { + "description": "Reduced visibility, haze significantly affects distant objects." + }, + "65": { + "description": "Visibility starting to deteriorate, details are blurred." + }, + "60": { + "description": "Fair visibility, light atmospheric interference noticeable." + }, + "55": { + "description": "Slightly reduced visibility, distant objects lose sharpness." + }, + "50": { + "description": "Normal visibility, atmospheric haze starts to soften details." + }, + "45": { + "description": "Above-average visibility, slight haze on the horizon." + }, + "40": { + "description": "Comfortable visibility, atmospheric effects are minimal." + }, + "35": { + "description": "Good visibility, distant objects remain sharply defined." + }, + "30": { + "description": "Great visibility, minor atmospheric effects noticeable." + }, + "25": { + "description": "Clear visibility, slight atmospheric haze may be present." + }, + "20": { + "description": "Near-perfect visibility, distant details are sharp." + }, + "15": { + "description": "Outstanding visibility, nothing obscures the view." + }, + "10": { + "description": "Excellent visibility, ideal conditions." + }, + "5": { + "description": "Crystal clear visibility, no obstructions." + }, + "0": { + "description": "No visibility, completely obscured by dense fog or obstructions." + } + } + } + }; + log('CalenderData initialized in state.CalenderData.CALENDARS & state.CalenderData.WEATHER'); +}); diff --git a/CalenderData/script.json b/CalenderData/script.json new file mode 100644 index 0000000000..50d08929df --- /dev/null +++ b/CalenderData/script.json @@ -0,0 +1,15 @@ +{ + "name": "CalenderData", + "script": "CalenderData.js", + "version": "1.0", + "previousversions": [], + "description": "This is a script which doesnt do anything by itself, but defines the following state variables, which are used by other programs, specifically QuestTracker. state.CalenderData.CALENDARS & state.CalenderData.WEATHER.", + "authors": "Boli", + "roll20userid": "3714078", + "useroptions": [], + "dependencies": [], + "modifies": { + "state.CalenderData": "read,write" + }, + "conflicts": [] +} \ No newline at end of file diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js new file mode 100644 index 0000000000..db3df54080 --- /dev/null +++ b/QuestTracker/1.0/QuestTracker.js @@ -0,0 +1,4896 @@ +// Github: https://github.com/boli32/QuestTracker/blob/main/QuestTracker.js +// By: Boli (Steven Wrighton): Professional Software Developer, Enthusiatic D&D Player since 1993. +// Contact: https://app.roll20.net/users/3714078/boli +// Readme https://github.com/boli32/QuestTracker/blob/main/README.md + + +var QuestTracker = QuestTracker || (function () { + 'use strict'; + const getCalendarAndWeatherData = () => { + let CALENDARS = {}; + let WEATHER = {}; + if (state.CalenderData) { + if (state.CalenderData.CALENDARS) CALENDARS = state.CalenderData.CALENDARS; + if (state.CalenderData.WEATHER) WEATHER = state.CalenderData.WEATHER; + } + return { CALENDARS, WEATHER }; + }; + const { CALENDARS, WEATHER } = getCalendarAndWeatherData(); + const statusMapping = { + 1: 'Unknown', + 2: 'Discovered', + 3: 'Started', + 4: 'Ongoing', + 5: 'Completed', + 6: 'Completed By Someone Else', + 7: 'Failed', + 8: 'Time ran out', + 9: 'Ignored' + }; + const frequencyMapping = { + 1: "Daily", + 2: "Weekly", + 3: "Monthly", + 4: "Yearly" + } + let QUEST_TRACKER_verboseErrorLogging = true; + let QUEST_TRACKER_questsToAutoAdvance = []; + let QUEST_TRACKER_globalQuestData = {}; + let QUEST_TRACKER_globalQuestArray = []; + let QUEST_TRACKER_globalRumours = {}; + let QUEST_TRACKER_Events = {}; + let QUEST_TRACKER_QuestHandoutName = "QuestTracker Quests"; + let QUEST_TRACKER_RumourHandoutName = "QuestTracker Rumours"; + let QUEST_TRACKER_EventHandoutName = "QuestTracker Events"; + let QUEST_TRACKER_WeatherHandoutName = "QuestTracker Weather"; + let QUEST_TRACKER_rumoursByLocation = {}; + let QUEST_TRACKER_readableJSON = true; + let QUEST_TRACKER_pageName = "Quest Tree Page"; + let QUEST_TRACKER_TreeObjRef = {}; + let QUEST_TRACKER_questGrid = []; + let QUEST_TRACKER_jumpGate = true; + let QUEST_TRACKER_BASE_QUEST_ICON_URL = ''; // add your own image here. + let QUEST_TRACKER_ROLLABLETABLE_QUESTS = "qt-quests"; + let QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS = "qt-quest-groups"; + let QUEST_TRACKER_ROLLABLETABLE_LOCATIONS = "qt-locations"; + let QUEST_TRACKER_calenderType = 'gregorian'; + let QUEST_TRACKER_currentDate = CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate; + let QUEST_TRACKER_defaultDate = CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate; + let QUEST_TRACKER_currentWeekdayName = "Thursday"; + let QUEST_TRACKER_Location = 'northern temperate'; + let QUEST_TRACKER_WeatherLocation = 'plains'; + let QUEST_TRACKER_CURRENT_WEATHER = ""; + let QUEST_TRACKER_imperialMeasurements = { + temperature: false, + precipitation: false, + wind: true, + visibility: true + }; + let QUEST_TRACKER_WEATHER_TRENDS = { + dry: 0, + wet: 0, + heat: 0, + cold: 0, + wind: 0, + humid: 0, + visibility: 0, + cloudy: 0 + }; + let QUEST_TRACKER_FORCED_WEATHER_TRENDS = { + dry: false, + wet: false, + heat: false, + cold: false, + wind: false, + humid: false, + visibility: false, + cloudy: false + }; + let QUEST_TRACKER_HISTORICAL_WEATHER = {}; + let QUEST_TRACKER_WEATHER_DESCRIPTION = {}; + let QUEST_TRACKER_WEATHER = true; + const loadQuestTrackerData = () => { + initializeQuestTrackerState(); + QUEST_TRACKER_verboseErrorLogging = state.QUEST_TRACKER.verboseErrorLogging || true; + QUEST_TRACKER_globalQuestData = state.QUEST_TRACKER.globalQuestData; + QUEST_TRACKER_globalQuestArray = state.QUEST_TRACKER.globalQuestArray; + QUEST_TRACKER_globalRumours = state.QUEST_TRACKER.globalRumours; + QUEST_TRACKER_questsToAutoAdvance = state.QUEST_TRACKER.questsToAutoAdvance; + QUEST_TRACKER_rumoursByLocation = state.QUEST_TRACKER.rumoursByLocation; + QUEST_TRACKER_readableJSON = state.QUEST_TRACKER.readableJSON || true; + QUEST_TRACKER_TreeObjRef = state.QUEST_TRACKER.TreeObjRef || {}; + QUEST_TRACKER_questGrid = state.QUEST_TRACKER.questGrid || []; + QUEST_TRACKER_jumpGate = state.QUEST_TRACKER.jumpGate || true; + QUEST_TRACKER_Events = state.QUEST_TRACKER.events || {}; + QUEST_TRACKER_calenderType = state.QUEST_TRACKER.calenderType || 'gregorian'; + QUEST_TRACKER_currentDate = state.QUEST_TRACKER.currentDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate + QUEST_TRACKER_defaultDate = state.QUEST_TRACKER.defaultDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate + QUEST_TRACKER_Location = state.QUEST_TRACKER.location || 'northern temperate'; + QUEST_TRACKER_WeatherLocation = state.QUEST_TRACKER.weatherLocation || 'plains'; + QUEST_TRACKER_currentWeekdayName = state.QUEST_TRACKER.currentWeekdayName || 'Thursday'; + QUEST_TRACKER_WEATHER_TRENDS = state.QUEST_TRACKER.weatherTrends || { + dry: 0, + wet: 0, + heat: 0, + cold: 0, + wind: 0, + humid: 0, + visibility: 0, + cloudy: 0 + }; + QUEST_TRACKER_FORCED_WEATHER_TRENDS = state.QUEST_TRACKER.forcedWeatherTrends || { + dry: false, + wet: false, + heat: false, + cold: false, + wind: false, + humid: false, + visibility: false, + cloudy: false + }; + QUEST_TRACKER_CURRENT_WEATHER = state.QUEST_TRACKER.currentWeather; + QUEST_TRACKER_HISTORICAL_WEATHER = state.QUEST_TRACKER.historicalWeather || {}; + QUEST_TRACKER_WEATHER_DESCRIPTION = state.QUEST_TRACKER.weatherDescription || {}; + QUEST_TRACKER_WEATHER = state.QUEST_TRACKER.weather || true; + QUEST_TRACKER_imperialMeasurements = state.QUEST_TRACKER.imperialMeasurements || { + temperature: false, + precipitation: false, + wind: true, + visibility: true + } + }; + const saveQuestTrackerData = () => { + state.QUEST_TRACKER.verboseErrorLogging = QUEST_TRACKER_verboseErrorLogging; + state.QUEST_TRACKER.globalQuestData = QUEST_TRACKER_globalQuestData; + state.QUEST_TRACKER.globalQuestArray = QUEST_TRACKER_globalQuestArray; + state.QUEST_TRACKER.globalRumours = QUEST_TRACKER_globalRumours; + state.QUEST_TRACKER.questsToAutoAdvance = QUEST_TRACKER_questsToAutoAdvance; + state.QUEST_TRACKER.rumoursByLocation = QUEST_TRACKER_rumoursByLocation; + state.QUEST_TRACKER.readableJSON = QUEST_TRACKER_readableJSON; + state.QUEST_TRACKER.questGrid = QUEST_TRACKER_questGrid; + state.QUEST_TRACKER.jumpGate = QUEST_TRACKER_jumpGate; + state.QUEST_TRACKER.events = QUEST_TRACKER_Events; + state.QUEST_TRACKER.currentDate = QUEST_TRACKER_currentDate; + state.QUEST_TRACKER.defaultDate = QUEST_TRACKER_defaultDate; + state.QUEST_TRACKER.calenderType = QUEST_TRACKER_calenderType; + state.QUEST_TRACKER.location = QUEST_TRACKER_Location; + state.QUEST_TRACKER.weatherLocation = QUEST_TRACKER_WeatherLocation; + state.QUEST_TRACKER.currentWeekdayName = QUEST_TRACKER_currentWeekdayName; + state.QUEST_TRACKER.currentWeather = QUEST_TRACKER_CURRENT_WEATHER; + state.QUEST_TRACKER.weatherTrends = QUEST_TRACKER_WEATHER_TRENDS; + state.QUEST_TRACKER.forcedWeatherTrends = QUEST_TRACKER_FORCED_WEATHER_TRENDS; + state.QUEST_TRACKER.historicalWeather = QUEST_TRACKER_HISTORICAL_WEATHER; + state.QUEST_TRACKER.weatherDescription = QUEST_TRACKER_WEATHER_DESCRIPTION; + state.QUEST_TRACKER.weather = QUEST_TRACKER_WEATHER; + state.QUEST_TRACKER.imperialMeasurements = QUEST_TRACKER_imperialMeasurements + }; + const initializeQuestTrackerState = (forced = false) => { + if (!state.QUEST_TRACKER || Object.keys(state.QUEST_TRACKER).length === 0 || forced) { + state.QUEST_TRACKER = { + verboseErrorLogging: true, + globalQuestData: {}, + globalQuestArray: [], + globalRumours: {}, + questsToAutoAdvance: [], + rumoursByLocation: {}, + generations: {}, + readableJSON: true, + QUEST_TRACKER_TreeObjRef: {}, + jumpGate: true, + events: {}, + calenderType: 'gregorian', + currentDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, + defaultDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, + location: 'northern temperate', + weatherLocation: 'plains', + currentWeather: null, + weatherTrends: { + dry: 0, + wet: 0, + heat: 0, + cold: 0 + }, + forcedWeatherTrends: { + dry: false, + wet: false, + heat: false, + cold: false + }, + historicalWeather: {}, + weather: true, + imperialMeasurements: { + temperature: false, + precipitation: false, + wind: true, + visibility: true + } + }; + if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]) { + const tableQuests = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTS }); + tableQuests.set('showplayers', false); // Hide table from players + } + if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]) { + const tableQuestGroups = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS }); + tableQuestGroups.set('showplayers', false); // Hide table from players + } + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (!locationTable) { + locationTable = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS }); + locationTable.set('showplayers', false); // Hide table from players + createObj('tableitem', { + _rollabletableid: locationTable.id, + name: 'Everywhere', + weight: 1 + }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_QuestHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_QuestHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_RumourHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_RumourHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_EventHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_EventHandoutName }); + } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_WeatherHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_WeatherHandoutName }); + } + Utils.sendGMMessage("QuestTracker has been initialized."); + } + }; + const Utils = (() => { + const H = { + checkType: (input) => { + if (typeof input === 'string') { + if (/^\d{4}-\d{2}-\d{2}$/.test(input)) { + return 'DATE'; + } + return 'STRING'; + } else if (typeof input === 'boolean') { + return 'BOOLEAN'; + } else if (typeof input === 'number') { + return Number.isInteger(input) ? 'INT' : 'STRING'; + } else if (Array.isArray(input)) { + return 'ARRAY'; + } else if (typeof input === 'object' && input !== null) { + return 'OBJECT'; + } else { + return 'STRING'; + } + } + }; + const sendGMMessage = (message) => { + sendChat('Quest Tracker', `/w gm ${message}`); + }; + const sendMessage = (message) => { + sendChat('Quest Tracker', `${message}`); + }; + const sendDescMessage = (message) => { + sendChat('', `/desc ${message}`); + }; + const normalizeKeys = (obj) => { + if (typeof obj !== 'object' || obj === null) return obj; + if (Array.isArray(obj)) return obj.map(item => normalizeKeys(item)); + return Object.keys(obj).reduce((acc, key) => { + const normalizedKey = key.toLowerCase(); + acc[normalizedKey] = normalizeKeys(obj[key]); + return acc; + }, {}); + }; + const stripJSONContent = (content) => { + content = content + .replace(/
    /gi, '') + .replace(/<\/?[^>]+(>|$)/g, '') + .replace(/ /gi, ' ') + .replace(/&[a-z]+;/gi, ' ') + .replace(/\+/g, '') + .replace(/[\r\n]+/g, ' ') + .replace(/\s{2,}/g, ' ') + .trim(); + const start = content.indexOf('{'); + const end = content.lastIndexOf('}'); + if (start === -1 || end === -1) { + log('Error: Valid JSON structure not found after stripping.'); + return '{}'; + } + const jsonContent = content.substring(start, end + 1).trim(); + return jsonContent; + }; + const sanitizeInput = (input, type) => { + if (input === undefined || input === null) { + Utils.sendGMMessage(`Error: Input is undefined or null.`); + return null; + } + switch (type) { + case 'STRING': + if (typeof input !== 'string') { + errorCheck(1, 'msg', null,`Expected a string, but received "${typeof input}`); + return null; + } + return input.replace(/<[^>]*>/g, '').replace(/["<>]/g, '').replace(/(\r\n|\n|\r)/g, '%NEWLINE%'); + case 'ARRAY': + if (!Array.isArray(input)) { + errorCheck(2, 'msg', null,`Expected an array, but received "${typeof input}`); + return [sanitizeInput(input, 'STRING')]; + } + return input.map(item => sanitizeInput(item, H.checkType(item))).filter(item => item !== null); + case 'DATE': + return /^\d{4}-\d{2}-\d{2}$/.test(input) ? input : null; + case 'BOOLEAN': + return typeof input === 'boolean' ? input : input === 'true' || input === 'false' ? input === 'true' : null; + case 'INT': + return Number.isInteger(Number(input)) ? Number(input) : null; + case 'OBJECT': + if (typeof input !== 'object' || Array.isArray(input)) { + errorCheck(3, 'msg', null,`Expected an object, but received "${typeof input}`); + return null; + } + const sanitizedObject = {}; + for (const key in input) { + if (input.hasOwnProperty(key)) { + const sanitizedKey = sanitizeInput(key, 'STRING'); + const fieldType = H.checkType(input[key]); + const sanitizedValue = sanitizeInput(input[key], fieldType); + if (sanitizedKey !== null && sanitizedValue !== null) { + sanitizedObject[sanitizedKey] = sanitizedValue; + } + } + } + return sanitizedObject; + default: + errorCheck(4, 'msg', null,`Unsupported type "${type}`); + return null; + } + }; + const updateHandoutField = (dataType = 'quest') => { + let handoutName; + switch (dataType.toLowerCase()) { + case 'rumour': + handoutName = QUEST_TRACKER_RumourHandoutName; + break; + case 'event': + handoutName = QUEST_TRACKER_EventHandoutName; + break; + case 'weather': + handoutName = QUEST_TRACKER_WeatherHandoutName; + break; + case 'quest': + handoutName = QUEST_TRACKER_QuestHandoutName; + break; + default: + return; + } + const handout = findObjs({ type: 'handout', name: handoutName })[0]; + if (errorCheck(146, 'exists', handout,'handout')) return; + handout.get('gmnotes', (notes) => { + const cleanedContent = Utils.stripJSONContent(notes); + let data; + try { + data = JSON.parse(cleanedContent); + data = normalizeKeys(data); + } catch (error) { + errorCheck(5, 'msg', null,`Failed to parse JSON data from GM notes: ${error.message}`); + return; + } + let updatedData; + switch (dataType.toLowerCase()) { + case 'rumour': + updatedData = QUEST_TRACKER_globalRumours; + break; + case 'event': + updatedData = QUEST_TRACKER_Events; + break; + case 'weather': + updatedData = QUEST_TRACKER_HISTORICAL_WEATHER; + break; + case 'weatherevents': + updatedData = QUEST_TRACKER_Events; + break; + default: + updatedData = QUEST_TRACKER_globalQuestData; + break; + } + const updatedContent = QUEST_TRACKER_readableJSON + ? JSON.stringify(updatedData, null, 2) + .replace(/\n/g, '
    ') + .replace(/ {2}/g, '  ') + : JSON.stringify(updatedData); + handout.set('gmnotes', updatedContent, (err) => { + if (err) { + errorCheck(6, 'msg', null,`Failed to update GM notes for "${handoutName}": ${err.message}`); + switch (dataType.toLowerCase()) { + case 'rumour': + QUEST_TRACKER_globalRumours = JSON.parse(cleanedContent); + break; + case 'event': + QUEST_TRACKER_Events = JSON.parse(cleanedContent); + break; + default: + QUEST_TRACKER_globalQuestData = JSON.parse(cleanedContent); + break; + } + } + }); + }); + saveQuestTrackerData(); + if (dataType === 'rumours') { + Rumours.calculateRumoursByLocation(); + } + }; + const togglereadableJSON = (value) => { + QUEST_TRACKER_readableJSON = (value === 'true'); + saveQuestTrackerData(); + updateHandoutField('quest'); + updateHandoutField('rumour'); + updateHandoutField('event'); + updateHandoutField('weather'); + updateHandoutField('weatherdescription'); + }; + const toggleWeather = (value) => { + QUEST_TRACKER_WEATHER = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleJumpGate = (value) => { + QUEST_TRACKER_jumpGate = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleVerboseError = (value) => { + QUEST_TRACKER_verboseErrorLogging = (value === 'true'); + saveQuestTrackerData(); + }; + const toggleImperial = (type, value) => { + QUEST_TRACKER_imperialMeasurements[type] = (value === 'true'); + saveQuestTrackerData(); + }; + const sanitizeString = (input) => { + if (typeof input !== 'string') { + Utils.sendGMMessage('Error: Expected a string input.'); + return null; + } + const sanitizedString = input.replace(/[^a-zA-Z0-9_ ]/g, '_'); + return sanitizedString; + }; + const inputAlias = (command) => { + const aliases = { + '!qt': '!qt-menu action=main', + '!qt-date advance': '!qt-date action=modify|unit=day|new=1', + '!qt-date retreat': '!qt-date action=modify|unit=day|new=-1' + }; + return aliases[command] || command; + }; + return { + sendGMMessage, + sendDescMessage, + sendMessage, + normalizeKeys, + stripJSONContent, + sanitizeInput, + updateHandoutField, + togglereadableJSON, + toggleWeather, + toggleJumpGate, + toggleVerboseError, + toggleImperial, + sanitizeString, + inputAlias + }; + })(); + const Import = (() => { + const H = { + importData: (handoutName, dataType) => { + let handout = findObjs({ type: 'handout', name: handoutName })[0]; + if (!handout) { + errorCheck(7, 'msg', null,`${dataType} handout "${handoutName}" not found. Please create it.`); + return; + } + handout.get('gmnotes', (notes) => { + const cleanedContent = Utils.stripJSONContent(notes); + try { + let parsedData = JSON.parse(cleanedContent); + const convertKeysToLowerCase = (obj) => { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(item => convertKeysToLowerCase(item)); + } + return Object.keys(obj).reduce((acc, key) => { + const lowercaseKey = key.toLowerCase(); + acc[lowercaseKey] = convertKeysToLowerCase(obj[key]); + return acc; + }, {}); + }; + parsedData = convertKeysToLowerCase(parsedData); + if (dataType === 'Quest') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_globalQuestArray = []; + Object.keys(parsedData).forEach((questId) => { + const quest = parsedData[questId]; + quest.relationships = quest.relationships || { logic: 'AND', conditions: [] }; + QUEST_TRACKER_globalQuestArray.push({ id: questId, weight: quest.weight || 1 }); + }); + QUEST_TRACKER_globalQuestData = parsedData; + } else if (dataType === 'Rumour') { + parsedData = Utils.normalizeKeys(parsedData); + Object.keys(parsedData).forEach((questId) => { + Object.keys(parsedData[questId]).forEach((status) => { + Object.keys(parsedData[questId][status]).forEach((location) => { + let rumours = parsedData[questId][status][location]; + if (typeof rumours === 'object' && !Array.isArray(rumours)) { + parsedData[questId][status][location] = rumours; + } else { + parsedData[questId][status][location] = {}; + } + }); + }); + }); + QUEST_TRACKER_globalRumours = parsedData; + Rumours.calculateRumoursByLocation(); + } else if (dataType === 'Events') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_Events = parsedData; + } else if (dataType === 'Weather') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_HISTORICAL_WEATHER = parsedData; + } else if (dataType === 'Weather Description') { + parsedData = Utils.normalizeKeys(parsedData); + QUEST_TRACKER_WEATHER_DESCRIPTION = parsedData; + } + saveQuestTrackerData(); + Utils.sendGMMessage(`${dataType} handout "${handoutName}" Imported.`); + } catch (error) { + errorCheck(8, 'msg', null,`Error parsing ${dataType} data: ${error.message}`); + } + }); + }, + syncQuestRollableTable: () => { + let questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + const questTableItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const tableItemMap = {}; + questTableItems.forEach(item => { + tableItemMap[item.get('name')] = item; + }); + const questIdsInGlobalData = Object.keys(QUEST_TRACKER_globalQuestData); + questIdsInGlobalData.forEach(questId => { + if (!tableItemMap[questId]) { + createObj('tableitem', { + rollabletableid: questTable.id, + name: questId, + weight: 1 + }); + } + }); + questTableItems.forEach(item => { + const questId = item.get('name'); + if (!QUEST_TRACKER_globalQuestData[questId]) { + item.remove(); + } + }); + }, + validateRelationships: (relationships, questId) => { + const questName = questId.toLowerCase(); + const validateNestedConditions = (conditions) => { + if (!Array.isArray(conditions)) return true; + return conditions.every(condition => { + if (typeof condition === 'string') { + const lowerCondition = condition.toLowerCase(); + if (errorCheck(9, 'exists', QUEST_TRACKER_globalQuestData.hasOwnProperty(lowerCondition),`QUEST_TRACKER_globalQuestData.hasOwnProperty(${lowerCondition})`)) return false; + return true; + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + return validateNestedConditions(condition.conditions); + } + return false; + }); + }; + const conditionsValid = validateNestedConditions(relationships.conditions || []); + const mutuallyExclusive = Array.isArray(relationships.mutually_exclusive) + ? relationships.mutually_exclusive.map(exclusive => exclusive.toLowerCase()) + : []; + mutuallyExclusive.forEach(exclusive => { + if (errorCheck(10, 'exists', QUEST_TRACKER_globalQuestData.hasOwnProperty(exclusive),`QUEST_TRACKER_globalQuestData.hasOwnProperty(${exclusive})`)) return true; + else return false; + }); + }, + cleanUpDataFields: () => { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + H.validateRelationships(quest.relationships || {}, questId); + }); + saveQuestTrackerData(); + Utils.updateHandoutField('quest'); + } + }; + const fullImportProcess = () => { + H.importData(QUEST_TRACKER_QuestHandoutName, 'Quest'); + H.importData(QUEST_TRACKER_RumourHandoutName, 'Rumour'); + H.importData(QUEST_TRACKER_EventHandoutName, 'Events'); + H.importData(QUEST_TRACKER_WeatherHandoutName, 'Weather'); + H.syncQuestRollableTable(); + Quest.cleanUpLooseEnds(); + H.cleanUpDataFields(); + Quest.populateQuestsToAutoAdvance(); + }; + return { + fullImportProcess + }; + })(); + const Quest = (() => { + const H = { + traverseConditions: (conditions, callback) => { + conditions.forEach(condition => { + if (typeof condition === 'string') { + callback(condition); + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + H.traverseConditions(condition.conditions, callback); + if (Array.isArray(condition.mutually_exclusive)) { + condition.mutually_exclusive.forEach(exclusiveQuest => { + callback(exclusiveQuest); + }); + } + } + }); + }, + updateQuestStatus: (questId, status) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + return; + } + const items = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const item = items.find(i => i.get('name') === questId); + if (item) { + item.set('weight', status); + QUEST_TRACKER_globalQuestArray = QUEST_TRACKER_globalQuestArray.map(q => { + if (q.id === questId) { + q.weight = status; + } + return q; + }); + saveQuestTrackerData(); + } + }, + removeQuestFromRollableTable: (questId) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (questTable) { + const item = findObjs({ type: 'tableitem', rollabletableid: questTable.id }) + .find(i => i.get('name') === questId); + if (item) { + item.remove(); + } + } + }, + getExclusions: (questId) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData || !questData.relationships) { + return []; + } + let exclusions = new Set(); + if (Array.isArray(questData.relationships.mutually_exclusive)) { + questData.relationships.mutually_exclusive.forEach(exclusions.add, exclusions); + } + H.traverseConditions(questData.relationships.conditions || [], condition => { + if (typeof condition === 'string') { + exclusions.add(condition); + } + }); + if (questData.group) { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(key => { + const otherQuest = QUEST_TRACKER_globalQuestData[key]; + if (otherQuest.group && otherQuest.group !== questData.group) { + exclusions.add(key); + } + }); + } + return Array.from(exclusions); + }, + modifyRelationshipObject: (currentRelationships, action, relationshipType, newItem, groupnum) => { + switch (relationshipType) { + case 'mutuallyExclusive': + switch (action) { + case 'add': + currentRelationships.mutually_exclusive = typeof currentRelationships.mutually_exclusive === 'string' ? [currentRelationships.mutually_exclusive] : (currentRelationships.mutually_exclusive || []); + if (!currentRelationships.mutually_exclusive.includes(newItem)) { + currentRelationships.mutually_exclusive.push(newItem); + } + break; + case 'remove': + currentRelationships.mutually_exclusive = currentRelationships.mutually_exclusive.filter( + exclusive => exclusive && exclusive !== newItem + ); + break; + default: + break; + } + break; + case 'single': + if (!Array.isArray(currentRelationships.conditions)) { + currentRelationships.conditions = []; + } + if (!currentRelationships.logic) { + currentRelationships.logic = 'AND'; + } + switch (action) { + case 'add': + const baseIndex = currentRelationships.conditions.findIndex(cond => typeof cond === 'object'); + if (baseIndex === -1) { + currentRelationships.conditions.push(newItem); + } else { + currentRelationships.conditions.splice(baseIndex, 0, newItem); + } + break; + case 'remove': + currentRelationships.conditions = currentRelationships.conditions.filter(cond => cond !== newItem); + break; + default: + break; + } + break; + case 'group': + if (groupnum === null || groupnum < 1) { + return currentRelationships; + } + if (groupnum >= currentRelationships.conditions.length || typeof currentRelationships.conditions[groupnum] !== 'object') { + currentRelationships.conditions[groupnum] = { logic: 'AND', conditions: [] }; + } + const group = currentRelationships.conditions[groupnum]; + if (typeof group === 'object' && group.logic && Array.isArray(group.conditions)) { + switch (action) { + case 'add': + if (!group.conditions.includes(newItem)) { + group.conditions.push(newItem); + } + break; + case 'remove': + group.conditions = group.conditions.filter(cond => cond !== newItem); + break; + default: + break; + } + } + break; + case 'logic': + currentRelationships.logic = currentRelationships.logic === 'AND' ? 'OR' : 'AND'; + break; + case 'grouplogic': + if (groupnum !== null && groupnum >= 1 && groupnum < currentRelationships.conditions.length) { + const group = currentRelationships.conditions[groupnum]; + if (typeof group === 'object' && group.logic) { + group.logic = group.logic === 'AND' ? 'OR' : 'AND'; + } + } + break; + case 'removegroup': + if (groupnum !== null && groupnum >= 1 && groupnum < currentRelationships.conditions.length) { + currentRelationships.conditions.splice(groupnum, 1); + } + break; + case 'addgroup': + currentRelationships.conditions.push({ + logic: 'AND', + conditions: [newItem] + }); + break; + default: + break; + } + return currentRelationships; + }, + generateNewQuestId: () => { + const existingQuestIds = Object.keys(QUEST_TRACKER_globalQuestData); + const highestQuestNumber = existingQuestIds.reduce((max, id) => { + const match = id.match(/^quest_(\d+)$/); + if (match) { + const number = parseInt(match[1], 10); + return number > max ? number : max; + } + return max; + }, 0); + const newQuestNumber = highestQuestNumber + 1; + return `quest_${newQuestNumber}`; + }, + removeQuestReferences: (questId) => { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(otherQuestId => { + if (otherQuestId !== questId) { + const otherQuestData = QUEST_TRACKER_globalQuestData[otherQuestId]; + if (!otherQuestData || !otherQuestData.relationships) return; + const { conditions, mutually_exclusive } = otherQuestData.relationships; + if (Array.isArray(conditions) && conditions.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'single', questId); + } + if (Array.isArray(mutually_exclusive) && mutually_exclusive.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'mutuallyExclusive', questId); + } + if (Array.isArray(conditions)) { + conditions.forEach((condition, index) => { + if (typeof condition === 'object' && Array.isArray(condition.conditions)) { + if (condition.conditions.includes(questId)) { + manageRelationship(otherQuestId, 'remove', 'group', questId, index); + } + } + }); + } + } + }); + }, + getAllQuestGroups: () => { + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) return []; + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + return groupItems.map(item => item.get('name')); + }, + removeQuestsFromGroup: (groupTable, groupId) => { + const groupObject = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + if (!groupObject) return; + + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId] || {}; + if (quest.group === groupId) { + delete quest.group; + } + }); + Utils.updateHandoutField('quest'); + }, + getNewGroupId: (groupTable) => { + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + if (!groupItems || groupItems.length === 0) return 1; + let maxWeight = groupItems.reduce((max, item) => Math.max(max, item.get('weight')), 0); + return maxWeight + 1; + } + }; + const manageRelationship = (questId, action, relationshipType, newItem = null, groupnum = null) => { + let questData = QUEST_TRACKER_globalQuestData[questId]; + let currentRelationships = questData.relationships || { logic: 'AND', conditions: [], mutually_exclusive: [] }; + currentRelationships.conditions = currentRelationships.conditions || []; + currentRelationships.mutually_exclusive = currentRelationships.mutually_exclusive || []; + if (action === 'add' && newItem) { + let targetQuest = QUEST_TRACKER_globalQuestData[newItem]; + if (targetQuest && questData.group && !targetQuest.group) { + targetQuest.group = questData.group; + } else if (targetQuest && !questData.group && targetQuest.group) { + questData.group = targetQuest.group; + } + } + let updatedRelationships = H.modifyRelationshipObject(currentRelationships, action, relationshipType, newItem, groupnum); + Utils.updateHandoutField('quest') + }; + const getValidQuestsForDropdown = (questId) => { + const exclusions = H.getExclusions(questId); + const excludedQuests = new Set([questId, ...exclusions]); + const validQuests = Object.keys(QUEST_TRACKER_globalQuestData).filter(qId => { + return !excludedQuests.has(qId); + }); + if (validQuests.length === 0) { + return false; + } + return validQuests; + }; + const addQuest = () => { + const newQuestId = H.generateNewQuestId(); + const defaultQuestData = { + name: 'New Quest', + description: 'Description', + relationships: {}, + hidden: true, + autoadvance: {} + }; + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + QUEST_TRACKER_globalQuestData[newQuestId] = defaultQuestData; + QUEST_TRACKER_globalQuestArray.push({ id: newQuestId, weight: 1 }); + if (questTable) { + createObj('tableitem', { + rollabletableid: questTable.id, + name: newQuestId, + weight: 1, + }); + } + Utils.updateHandoutField('quest') + }; + const removeQuest = (questId) => { + H.removeQuestReferences(questId); + H.removeQuestFromRollableTable(questId); + Rumours.removeAllRumoursForQuest(questId); + delete QUEST_TRACKER_globalQuestData[questId]; + QUEST_TRACKER_globalQuestArray = QUEST_TRACKER_globalQuestArray.filter(quest => quest.id !== questId); + Utils.updateHandoutField('quest'); + }; + const cleanUpLooseEnds = () => { + const processedPairs = new Set(); + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + const mutuallyExclusiveQuests = quest.relationships?.mutually_exclusive || []; + mutuallyExclusiveQuests.forEach(targetId => { + const pairKey = [questId, targetId].sort().join('-'); + if (!processedPairs.has(pairKey)) { + processedPairs.add(pairKey); + const targetQuest = QUEST_TRACKER_globalQuestData[targetId]; + if (targetQuest) { + const targetMutuallyExclusive = new Set(targetQuest.relationships?.mutually_exclusive || []); + if (!targetMutuallyExclusive.has(questId)) { + manageRelationship(targetId, 'add', 'mutuallyExclusive', questId); + Utils.sendGMMessage(`Added missing mutually exclusive relationship from ${targetId} to ${questId}.`); + } + } + } + }); + }); + }; + const populateQuestsToAutoAdvance = () => { + QUEST_TRACKER_questsToAutoAdvance = Object.keys(QUEST_TRACKER_globalQuestData).filter(questId => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + const currentStatus = getStatusNameByQuestId(questId, QUEST_TRACKER_globalQuestArray); + return ( + quest.autoadvance && + Object.keys(quest.autoadvance).length > 0 && + currentStatus !== 'Completed' && + currentStatus !== 'Completed By Someone Else' && + currentStatus !== 'Failed' + ); + }); + saveQuestTrackerData(); + }; + const getStatusNameByQuestId = (questId, questArray) => { + let quest = questArray.find(q => q.id === questId); + if (quest) { + return statusMapping[quest.weight] || 'Unknown'; + } + return 'Unknown'; + }; + const getQuestStatus = (questId) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + return 1; + } + const questItem = findObjs({ type: 'tableitem', rollabletableid: questTable.id }).find(item => item.get('name') === questId); + if (!questItem) { + return 1; + } + return questItem.get('weight'); + }; + const manageQuestObject = ({ action, field, current, old = '', newItem }) => { + const quest = QUEST_TRACKER_globalQuestData[current]; + switch (field) { + case 'status': + H.updateQuestStatus(current, newItem); + QuestPageBuilder.updateQuestStatusColor(current, newItem); + Rumours.calculateRumoursByLocation(); + break; + case 'hidden': + if (action === 'update') { + quest.hidden = !quest.hidden; + QuestPageBuilder.updateQuestVisibility(current, quest.hidden); + } + break; + case 'autoadvance': + if (action === 'add') { + const correctCapitalization = Object.values(statusMapping).find(status => status.toLowerCase() === old.toLowerCase()); + if (correctCapitalization) { + old = correctCapitalization; + } + quest.autoadvance = quest.autoadvance || {}; + quest.autoadvance[old] = newItem; + } else if (action === 'remove') { + old = old.toLowerCase(); + if (quest.autoadvance) { + const keyToRemove = Object.keys(quest.autoadvance).find(key => key.toLowerCase() === old); + if (keyToRemove) { + delete quest.autoadvance[keyToRemove]; + if (Object.keys(quest.autoadvance).length === 0) { + delete quest.autoadvance; + } + } + } + } + break; + case 'name': + if (action === 'add') { + quest.name = newItem; + QuestPageBuilder.updateQuestText(current, newItem); + } else if (action === 'remove') { + quest.name = ''; + } + break; + case 'description': + if (action === 'add') { + quest.description = newItem; + QuestPageBuilder.updateQuestTooltip(current, newItem); + } else if (action === 'remove') { + quest.description = ''; + } + break; + case 'group': + if (action === 'add') { + quest.group = newItem; + } else if (action === 'remove') { + delete quest.group; + } + break; + default: + errorCheck(11, 'msg', null,`Unsupported action for type ( ${field} )`); + break; + } + Utils.updateHandoutField('quest'); + }; + const manageGroups = (action, newItem = null, groupId = null) => { + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) { + errorCheck(12, 'msg', null,`Quest groups table not found.`) + return; + } + switch (action) { + case 'add': + const allGroups = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).map(item => item.get('name').toLowerCase()); + if (allGroups.includes(Utils.sanitizeString(newItem.toLowerCase()))) return; + const newWeight = H.getNewGroupId(groupTable); + if (newWeight === undefined || newWeight === null) return; + let newGroup = createObj('tableitem', { + rollabletableid: groupTable.id, + name: newItem, + weight: newWeight + }); + break; + case 'remove': + if (groupId === 1) return; + let groupToRemove = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + H.removeQuestsFromGroup(groupTable, groupId); + groupToRemove.remove(); + break; + case 'update': + const groupList = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).map(item => item.get('name').toLowerCase()); + if (groupList.includes(Utils.sanitizeString(newItem.toLowerCase()))) return; + let groupToUpdate = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }).find(item => item.get('weight') == groupId); + if (groupToUpdate) { + groupToUpdate.set('name', newItem); + } + break; + } + }; + const autoAdvance = (autoAdvanceData) => { + Object.keys(autoAdvanceData).forEach((questId) => { + const questStatuses = autoAdvanceData[questId]; + const validStatuses = Object.keys(questStatuses) + .filter((status) => questStatuses[status]) + .map((status) => { + const statusValue = Object.keys(statusMapping).find( + (key) => statusMapping[key].toLowerCase() === status.toLowerCase() + ); + return statusValue ? { statusName: status, statusValue: parseInt(statusValue, 10) } : null; + }) + .filter((value) => value !== null); + if (validStatuses.length === 0) { + return; + } + const highestStatus = validStatuses.reduce((max, current) => + current.statusValue > max.statusValue ? current : max + ); + const quest = QUEST_TRACKER_globalQuestData[questId]; + const currentStatus = Object.keys(statusMapping).find( + (key) => statusMapping[key] === quest.status + ); + if (currentStatus !== highestStatus.statusValue) { + Quest.manageQuestObject({ + action: "update", + field: "status", + questID: questId, + oldStatus: currentStatus, + newStatus: highestStatus.statusValue + }); + QuestPageBuilder.updateQuestStatusColor(questId, highestStatus.statusValue); + Utils.sendGMMessage(`Quest "${questId}" has been automatically advanced to status: "${highestStatus.statusName}".`); + } + Object.keys(questStatuses).forEach((status) => { + if (questStatuses[status]) { + Quest.manageQuestObject({ + action: "remove", + field: "autoadvance", + current: quest.autoAdvance[status], + newItem: status + }); + } + }); + }); + }; + return { + getStatusNameByQuestId, + getQuestStatus, + populateQuestsToAutoAdvance, + getValidQuestsForDropdown, + manageRelationship, + addQuest, + removeQuest, + cleanUpLooseEnds, + manageQuestObject, + manageGroups, + autoAdvance + }; + })(); + const Calendar = (() => { + const H = { + generateNewEventId: () => { + const existingEventIds = Object.keys(QUEST_TRACKER_Events); + const highestEventNumber = existingEventIds.reduce((max, id) => { + const match = id.match(/^event_(\d+)$/); + return match ? Math.max(max, parseInt(match[1], 10)) : max; + }, 0); + return `event_${highestEventNumber + 1}`; + }, + checkQuestAutoAdvance: () => { + const autoAdvanceData = {}; + Object.keys(QUEST_TRACKER_globalQuestData).forEach((questId) => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + if (!quest.autoAdvance || Object.keys(quest.autoAdvance).length === 0) { + return; + } + const statusUpdates = {}; + Object.keys(quest.autoAdvance).forEach((status) => { + const dateToAdvance = quest.autoAdvance[status]; + if (!dateToAdvance || !/^\d{4}-\d{2}-\d{2}$/.test(dateToAdvance)) { + return; + } + statusUpdates[status] = QUEST_TRACKER_currentDate >= dateToAdvance; + }); + if (Object.keys(statusUpdates).length > 0) { + autoAdvanceData[questId] = statusUpdates; + } + }); + Quest.autoAdvance(autoAdvanceData); + }, + checkEvent: () => { + if (!QUEST_TRACKER_Events || typeof QUEST_TRACKER_Events !== "object") { + return; + } + const todayEvents = H.findNextEvents(0, true); + todayEvents.forEach(([eventDate, eventName, eventID]) => { + if (eventID) { + const event = QUEST_TRACKER_Events[eventID]; + if (errorCheck(13, 'exists', event, 'event')) return occurrences; + if (event.hidden === false) { + Utils.sendMessage(`${event.name} - ${event.description}`); + } else { + Utils.sendGMMessage(`Event triggered: ${event.name} - ${event.description}`); + } + if (!event.repeatable) { + delete QUEST_TRACKER_Events[eventID]; + Utils.updateHandoutField("event"); + } else { + const frequencyDays = event.frequency || 1; + const [year, month, day] = event.date.split("-").map(Number); + const nextDate = new Date(year, month - 1, day + frequencyDays) + .toISOString() + .split("T")[0]; + event.date = nextDate; + Utils.updateHandoutField("event"); + } + } else { + Utils.sendMessage(`Today is ${eventName}`); + } + }); + }, + evaluateLogic: (logic, year) => { + if (errorCheck(15, 'exists', logic,'logic')) return false; + if (errorCheck(16, 'exists', logic.operation,'logic.operation')) return false; + if (logic.conditions) { + if (logic.operation === "or") { + return logic.conditions.some((condition) => H.evaluateLogic(condition, year)); + } else if (logic.operation === "and") { + return logic.conditions.every((condition) => H.evaluateLogic(condition, year)); + } + errorCheck(17, 'msg', null,`Unsupported logic operation: ${logic.operation}`); + return false; + } + if (logic.operation === "mod") { + const result = (year % logic.operand) === logic.equals; + return logic.negate ? !result : result; + } + errorCheck(18, 'msg', null,`Unsupported condition operation: ${logic.operation}`); + return false; + }, + getDaysInMonth: (monthIndex, year) => { + const month = CALENDARS[QUEST_TRACKER_calenderType].months[monthIndex - 1]; + if (month.leap) { + const isLeapYear = H.evaluateLogic(month.leap.logic, year); + if (isLeapYear) { + return month.leap.days; + } + } + return month.days; + }, + getTotalDaysInYear: (year) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(19, 'exists', calendar,'calendar')) return; + if (errorCheck(20, 'exists', calendar.months,'calendar.monthsn')) return; + return calendar.months.reduce((totalDays, monthObj, index) => { + const daysInMonth = H.getDaysInMonth(index + 1, year); + return totalDays + daysInMonth; + }, 0); + }, + calculateDateDifference: (target, baseYear, baseMonth, baseDay) => { + if (!target) return Infinity; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(21, 'exists', calendar,'calendar')) return Infinity; + const { year: targetYear, month: targetMonth, day: targetDay } = target; + let totalDays = 0; + if (targetYear === baseYear) { + if (targetMonth === baseMonth) { + return targetDay - baseDay; + } + totalDays += H.getDaysInMonth(baseMonth, baseYear) - baseDay; + for (let m = baseMonth + 1; m < targetMonth; m++) { + totalDays += H.getDaysInMonth(m, baseYear); + } + totalDays += targetDay; + return totalDays; + } + totalDays += H.getDaysInMonth(baseMonth, baseYear) - baseDay; + for (let m = baseMonth + 1; m <= calendar.months.length; m++) { + totalDays += H.getDaysInMonth(m, baseYear); + } + for (let y = baseYear + 1; y < targetYear; y++) { + totalDays += H.getTotalDaysInYear(y); + } + for (let m = 1; m < targetMonth; m++) { + totalDays += H.getDaysInMonth(m, targetYear); + } + totalDays += targetDay; + return totalDays; + }, + isEventToday: (event, eventID) => { + let { date, repeatable, frequency, name, weekdayname } = event; + let [eventYear, eventMonth, eventDay] = date.split("-").map(Number); + const [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + if (!repeatable) { + return date === QUEST_TRACKER_currentDate ? [[QUEST_TRACKER_currentDate, name, eventID]] : []; + } + const freqType = frequencyMapping[frequency]; + switch (freqType) { + case "Daily": + return [[QUEST_TRACKER_currentDate, name, eventID]]; + case "Weekly": + if (weekdayname && weekdayname === QUEST_TRACKER_currentWeekdayName) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + case "Monthly": + const daysInMonth = H.getDaysInMonth(currentMonth, currentYear); + if (eventDay <= daysInMonth && eventMonth === currentMonth && eventDay === currentDay) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + case "Yearly": + if (eventMonth === currentMonth && eventDay === currentDay) { + return [[QUEST_TRACKER_currentDate, name, eventID]]; + } + break; + default: + break; + } + return []; + }, + findNextEvents: (limit = 1, isToday = false) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + const daysOfWeek = calendar.daysOfWeek || []; + const specialDays = calendar.significantDays || {}; + const events = QUEST_TRACKER_Events || {}; + const [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + let upcomingEvents = []; + const todayEvents = []; + if (isToday) { + Object.entries(events).forEach(([eventID, event]) => { + const todaysOccurrences = H.isEventToday(event, eventID); + todayEvents.push(...todaysOccurrences); + }); + Object.entries(specialDays).forEach(([key, name]) => { + const [eventMonth, eventDay] = key.split("-").map(Number); + if (eventMonth === currentMonth && eventDay === currentDay) { + todayEvents.push([QUEST_TRACKER_currentDate, name, null]); + } + }); + return todayEvents; + } + const calculateNextOccurrences = (event, eventID, maxOccurrences) => { + let { date, repeatable, frequency, name, weekdayname } = event; + let [startYear, startMonth, startDay] = date.split("-").map(Number); + let [currentYear, currentMonth, currentDay] = QUEST_TRACKER_currentDate.split("-").map(Number); + let [eventYear, eventMonth, eventDay] = [startYear, startMonth, startDay]; + const occurrences = []; + const freqType = repeatable ? frequencyMapping[frequency] : null; + if (repeatable) { + if (`${startYear}-${String(startMonth).padStart(2, "0")}-${String(startDay).padStart(2, "0")}` < QUEST_TRACKER_currentDate) { + [eventYear, eventMonth, eventDay] = [currentYear, currentMonth, currentDay]; + } + switch (freqType) { + case "Daily": + break; + case "Weekly": + if (weekdayname) { + const targetWeekdayIndex = daysOfWeek.indexOf(weekdayname); + const currentWeekdayIndex = daysOfWeek.indexOf(QUEST_TRACKER_currentWeekdayName); + let daysToAdd = (targetWeekdayIndex - currentWeekdayIndex + daysOfWeek.length) % daysOfWeek.length; + if (daysToAdd === 0 && (eventYear === currentYear && eventMonth === currentMonth && eventDay === currentDay)) { + daysToAdd = daysOfWeek.length; + } + eventDay += daysToAdd; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + } + break; + case "Monthly": + while ( + eventYear < currentYear || + (eventYear === currentYear && eventMonth < currentMonth) + ) { + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + eventDay = Math.min(eventDay, H.getDaysInMonth(eventMonth, eventYear)); + break; + case "Yearly": + if (eventYear < currentYear) { + eventYear = currentYear; + } + break; + default: + break; + } + } + let occurrencesCount = 0; + while (occurrencesCount < maxOccurrences) { + const eventDate = `${eventYear}-${String(eventMonth).padStart(2, "0")}-${String(eventDay).padStart(2, "0")}`; + if (eventDate >= date) { + occurrences.push([eventDate, name, eventID]); + occurrencesCount++; + } + switch (freqType) { + case "Daily": + eventDay++; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + break; + case "Weekly": + eventDay += daysOfWeek.length; + if (eventDay > H.getDaysInMonth(eventMonth, eventYear)) { + eventDay -= H.getDaysInMonth(eventMonth, eventYear); + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + } + break; + case "Monthly": + eventMonth++; + if (eventMonth > calendar.months.length) { + eventMonth = 1; + eventYear++; + } + eventDay = Math.min(eventDay, H.getDaysInMonth(eventMonth, eventYear)); + break; + case "Yearly": + eventYear++; + break; + default: + break; + } + if (!repeatable) break; + } + return occurrences; + }; + Object.entries(events).forEach(([eventID, event]) => { + const eventOccurrences = calculateNextOccurrences(event, eventID, 5); + upcomingEvents.push(...eventOccurrences); + }); + Object.entries(specialDays).forEach(([key, name]) => { + const [eventMonth, eventDay] = key.split("-").map(Number); + let eventYear = currentYear; + if (eventMonth < currentMonth || (eventMonth === currentMonth && eventDay < currentDay)) { + eventYear++; + } + if (H.getDaysInMonth(eventMonth, eventYear) >= eventDay) { + const eventDate = `${eventYear}-${String(eventMonth).padStart(2, "0")}-${String(eventDay).padStart(2, "0")}`; + if (isToday) { + if (eventDate === QUEST_TRACKER_currentDate) { + todayEvents.push([eventDate, name, null]); + } + } else { + if (eventDate > QUEST_TRACKER_currentDate) { + upcomingEvents.push([eventDate, name, null]); + } + } + } + }); + upcomingEvents.sort((a, b) => { + const [aYear, aMonth, aDay] = a[0].split("-").map(Number); + const [bYear, bMonth, bDay] = b[0].split("-").map(Number); + return H.calculateDateDifference({ year: aYear, month: aMonth, day: aDay }, currentYear, currentMonth, currentDay) + - H.calculateDateDifference({ year: bYear, month: bMonth, day: bDay }, currentYear, currentMonth, currentDay); + }); + + return upcomingEvents.slice(0, limit); + }, + calculateWeekday: (year, month, day) => { + if (errorCheck(23, 'calendar', CALENDARS[QUEST_TRACKER_calenderType])) return; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(24, 'calendar.daysOfWeek', calendar.daysOfWeek)) return; + if (errorCheck(25, 'calendar.startingWeekday', calendar.startingWeekday)) return; + if (errorCheck(26, 'calendar.startingYear', calendar.startingYear)) return; + const daysOfWeek = calendar.daysOfWeek; + const startingWeekday = calendar.startingWeekday; + const startingYear = calendar.startingYear; + let totalDays = 0; + for (let y = startingYear; y < year; y++) { + totalDays += H.getTotalDaysInYear(y); + } + for (let m = 1; m < month; m++) { + totalDays += typeof calendar.months[m - 1].days === "function" + ? calendar.months[m - 1].days(year) + : calendar.months[m - 1].days; + } + totalDays += day - 1; + return daysOfWeek[(daysOfWeek.indexOf(startingWeekday) + totalDays) % daysOfWeek.length]; + } + }; + const determineWeather = (date) => { + const W = { + getSeasonBoundaries: (year) => { + if (errorCheck(27, 'exists', CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location], `CALENDARS[${QUEST_TRACKER_calenderType}]?.climates[${QUEST_TRACKER_Location}]`)) return; + const climate = CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location]; + const boundaries = []; + const seasonStart = climate.seasonStart || {}; + for (const [seasonName, startMonth] of Object.entries(seasonStart)) { + let startDayOfYear = 0; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + for (let i = 0; i < startMonth - 1; i++) { + const monthObj = calendar.months[i]; + startDayOfYear += typeof monthObj.days === "function" ? monthObj.days(year) : monthObj.days; + } + boundaries.push({ season: seasonName, startDayOfYear }); + } + boundaries.sort((a, b) => a.startDayOfYear - b.startDayOfYear); + const totalDaysInYear = H.getTotalDaysInYear(year); + boundaries.forEach((boundary, i) => { + const nextIndex = (i + 1) % boundaries.length; + boundary.endDayOfYear = + boundaries[nextIndex].startDayOfYear - 1 >= 0 + ? boundaries[nextIndex].startDayOfYear - 1 + : totalDaysInYear - 1; + }); + return boundaries; + }, + getCurrentSeason: (date) => { + const [year, month, day] = date.split("-").map(Number); + const boundaries = W.getSeasonBoundaries(year); + if (!boundaries || boundaries.length === 0) return null; + let dayOfYear = 0; + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + for (let i = 0; i < month - 1; i++) { + const monthObj = calendar.months[i]; + dayOfYear += typeof monthObj.days === "function" ? monthObj.days(year) : monthObj.days; + } + dayOfYear += day; + for (const { season, startDayOfYear, endDayOfYear } of boundaries) { + if (startDayOfYear <= endDayOfYear) { + if (dayOfYear >= startDayOfYear && dayOfYear <= endDayOfYear) { + return { season, dayOfYear }; + } + } else { + if (dayOfYear >= startDayOfYear || dayOfYear <= endDayOfYear) { + return { season, dayOfYear }; + } + } + } + return null; + }, + getSuddenSeasonalChangeProbability: (dayOfYear, boundaries) => { + const buffer = 5; + for (const { startDayOfYear, endDayOfYear } of boundaries) { + if (Math.abs(dayOfYear - startDayOfYear) <= buffer || Math.abs(dayOfYear - endDayOfYear) <= buffer) { + return 0.25; + } + } + return 0.05; + }, + applyForcedTrends: (rolls) => { + const { temperatureRoll, precipitationRoll, windRoll, humidityRoll, visibilityRoll, cloudCoverRoll } = rolls; + return { + temperatureRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.heat + ? Math.min(100, temperatureRoll + 20) + : QUEST_TRACKER_FORCED_WEATHER_TRENDS.cold + ? Math.max(1, temperatureRoll - 20) + : temperatureRoll, + precipitationRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.wet + ? Math.min(100, precipitationRoll + 20) + : QUEST_TRACKER_FORCED_WEATHER_TRENDS.dry + ? Math.max(1, precipitationRoll - 20) + : precipitationRoll, + windRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.wind + ? Math.min(100, windRoll + 20) + : windRoll, + humidityRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.humid + ? Math.min(100, humidityRoll + 20) + : humidityRoll, + visibilityRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.visibility + ? Math.min(100, visibilityRoll + 20) + : visibilityRoll, + cloudCoverRoll: QUEST_TRACKER_FORCED_WEATHER_TRENDS.cloudy + ? Math.min(100, cloudCoverRoll + 20) + : cloudCoverRoll, + }; + }, + applyTrends: (rolls) => { + const { temperatureRoll, precipitationRoll, windRoll, humidityRoll, visibilityRoll, cloudCoverRoll } = rolls; + return { + temperatureRoll: + temperatureRoll + + (QUEST_TRACKER_WEATHER_TRENDS.heat || 0) * 2 - + (QUEST_TRACKER_WEATHER_TRENDS.cold || 0) * 2, + precipitationRoll: + precipitationRoll + + (QUEST_TRACKER_WEATHER_TRENDS.wet || 0) * 2 - + (QUEST_TRACKER_WEATHER_TRENDS.dry || 0) * 2, + windRoll: windRoll + (QUEST_TRACKER_WEATHER_TRENDS.wind || 0) * 2, + humidityRoll: humidityRoll + (QUEST_TRACKER_WEATHER_TRENDS.humid || 0) * 2, + visibilityRoll: visibilityRoll + (QUEST_TRACKER_WEATHER_TRENDS.visibility || 0) * 2, + cloudCoverRoll: cloudCoverRoll + (QUEST_TRACKER_WEATHER_TRENDS.cloudy || 0) * 2, + }; + }, + updateTrends: (rolls) => { + ["heat", "cold", "wet", "dry", "wind", "visibility", "cloudy"].forEach((trendType) => { + const roll = rolls[`${trendType}Roll`]; + if (["wind", "visibility", "cloudy"].includes(trendType) && roll < 75) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = 0; + } else if (roll > 75) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = + (QUEST_TRACKER_WEATHER_TRENDS[trendType] || 0) + 1; + } else if (QUEST_TRACKER_WEATHER_TRENDS[trendType]) { + QUEST_TRACKER_WEATHER_TRENDS[trendType] = 0; + } + }); + if (rolls.precipitationRoll > 75) QUEST_TRACKER_WEATHER_TRENDS.dry = 0; + if (rolls.temperatureRoll > 75) QUEST_TRACKER_WEATHER_TRENDS.cold = 0; + if (rolls.temperatureRoll < 25) QUEST_TRACKER_WEATHER_TRENDS.heat = 0; + }, + generateBellCurveRoll: () => { + const randomGaussian = () => { + let u = 0, v = 0; + while (u === 0) u = Math.random(); // Avoid log(0) + while (v === 0) v = Math.random(); + return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); + }; + let roll = Math.random() * 30 + 35; + let bias = roll <= 50 + ? Math.pow((roll - 35) / (50 - 35), 2) + : Math.pow((65 - roll) / (65 - 50), 2); + if (Math.random() < bias) { + return Math.round(roll * 100) / 100; + } else { + return W.generateBellCurveRoll(); + } + }, + adjustDailyFluctuation: (date, trendAdjustedRolls, suddenChangeProbability, seasonBoundary) => { + const previousWeather = QUEST_TRACKER_HISTORICAL_WEATHER[Object.keys(QUEST_TRACKER_HISTORICAL_WEATHER).reverse().find(d => d < date)]; + if (!previousWeather) return trendAdjustedRolls; + const maxChange = suddenChangeProbability > 0.05 ? 10 : 5; + const maxBoundaryChange = suddenChangeProbability > 0.05 ? 20 : 10; + const adjustedRolls = { ...trendAdjustedRolls }; + Object.keys(adjustedRolls).forEach((key) => { + const prevValue = previousWeather[key]; + if (prevValue !== undefined) { + const boundaryLimit = seasonBoundary ? maxBoundaryChange : maxChange; + const change = adjustedRolls[key] - prevValue; + if (Math.abs(change) > boundaryLimit) { + adjustedRolls[key] = prevValue + Math.sign(change) * boundaryLimit; + } + } + }); + return adjustedRolls; + } + }; + const [year, month, day] = date.split("-").map(Number); + const currentSeasonData = W.getCurrentSeason(date); + if (!currentSeasonData) return; + const { season, dayOfYear } = currentSeasonData; + const boundaries = W.getSeasonBoundaries(year); + const suddenChangeProbability = W.getSuddenSeasonalChangeProbability(dayOfYear, boundaries); + const rolls = { + temperatureRoll: W.generateBellCurveRoll(), + precipitationRoll: W.generateBellCurveRoll(), + windRoll: W.generateBellCurveRoll(), + humidityRoll: W.generateBellCurveRoll(), + visibilityRoll: W.generateBellCurveRoll(), + cloudCoverRoll: W.generateBellCurveRoll(), + }; + const forcedAdjustedRolls = W.applyForcedTrends(rolls); + const trendAdjustedRolls = W.applyTrends(forcedAdjustedRolls); + W.updateTrends(trendAdjustedRolls); + const climateModifiers = CALENDARS[QUEST_TRACKER_calenderType]?.climates[QUEST_TRACKER_Location]?.modifiers; + trendAdjustedRolls.temperatureRoll += climateModifiers?.temperature?.[season] || 0; + trendAdjustedRolls.precipitationRoll += climateModifiers?.precipitation?.[season] || 0; + trendAdjustedRolls.windRoll += climateModifiers?.wind?.[season] || 0; + trendAdjustedRolls.humidityRoll += climateModifiers?.humid?.[season] || 0; + trendAdjustedRolls.visibilityRoll += climateModifiers?.visibility?.[season] || 0; + const nearBoundary = suddenChangeProbability > 0.05; + const isBoundaryDay = boundaries.some(({ startDayOfYear, endDayOfYear }) => + Math.abs(dayOfYear - startDayOfYear) <= 1 || Math.abs(dayOfYear - endDayOfYear) <= 1 + ); + const finalAdjustedRolls = W.adjustDailyFluctuation(date, trendAdjustedRolls, suddenChangeProbability, isBoundaryDay); + Object.keys(finalAdjustedRolls).forEach((key) => { + finalAdjustedRolls[key] = Math.max(1, Math.min(100, finalAdjustedRolls[key])); + }); + const weather = { + date, + season, + ...finalAdjustedRolls, + trends: { ...QUEST_TRACKER_WEATHER_TRENDS }, + forcedTrends: { ...QUEST_TRACKER_FORCED_WEATHER_TRENDS }, + nearBoundary, + }; + QUEST_TRACKER_HISTORICAL_WEATHER[date] = weather; + saveQuestTrackerData(); + Utils.updateHandoutField("weather"); + }; + const modifyDate = ({ type = "day", amount = 1, newDate = null }) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(28, 'exists', calendar,'calendar')) return; + const L = { + formatDate: (year, month, day) => { + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + }, + wrapAround: () => { + while (day > H.getDaysInMonth(month, year)) { + day -= H.getDaysInMonth(month, year); + month++; + if (month > calendar.months.length) { + month = 1; + year++; + } + } + while (day < 1) { + month--; + if (month < 1) { + month = calendar.months.length; + year--; + } + day += H.getDaysInMonth(month, year); + } + }, + generateDateArray: () => { + const dates = []; + let targetDate = null; + if (type === "event") { + const closestEvent = H.findNextEvents(1); + if (!closestEvent || closestEvent.length === 0) { + Utils.sendGMMessage("No upcoming festivals, events, or significant dates found."); + return []; + } + targetDate = closestEvent[0][0]; + } + while (steps >= 0 || targetDate) { + dates.push(L.formatDate(year, month, day)); + if (type === "event" && targetDate) { + const [targetYear, targetMonth, targetDay] = targetDate.split("-").map(Number); + while ( + year !== targetYear || + month !== targetMonth || + day !== targetDay + ) { + day += direction; + L.wrapAround(); + dates.push(L.formatDate(year, month, day)); + } + break; + } + switch (type) { + case "day": + day += direction; + L.wrapAround(); + break; + case "week": + day += direction * calendar.daysOfWeek.length; + L.wrapAround(); + break; + case "month": + month += direction; + if (month > calendar.months.length) { + month -= calendar.months.length; + year++; + } else if (month < 1) { + month += calendar.months.length; + year--; + } + day = Math.min(day, H.getDaysInMonth(month, year)); + break; + case "year": + year += direction; + day = Math.min(day, H.getDaysInMonth(month, year)); + break; + default: + break; + } + steps--; + } + return dates; + }, + generateCompleteDateList: (startDate, endDate) => { + const [startYear, startMonth, startDay] = startDate.split("-").map(Number); + const [endYear, endMonth, endDay] = endDate.split("-").map(Number); + let currentYear = startYear, currentMonth = startMonth, currentDay = startDay; + const dateList = []; + while ( + currentYear < endYear || + (currentYear === endYear && currentMonth < endMonth) || + (currentYear === endYear && currentMonth === endMonth && currentDay <= endDay) + ) { + dateList.push(L.formatDate(currentYear, currentMonth, currentDay)); + currentDay++; + if (currentDay > H.getDaysInMonth(currentMonth, currentYear)) { + currentDay = 1; + currentMonth++; + if (currentMonth > calendar.months.length) { + currentMonth = 1; + currentYear++; + } + } + } + dateList.push(L.formatDate(endYear, endMonth, endDay)); + return dateList; + }, + validateISODate: (date) => { + const [y, m, d] = date.split("-").map(Number); + if (!y || !m || !d || m < 1 || m > calendar.months.length) { + errorCheck(29, 'msg', null,`Invalid ISO date format or date out of range for calendar: ${date}`); + return null; + } + const daysInMonth = H.getDaysInMonth(m, y); + if (d < 1 || d > daysInMonth) { + errorCheck(30, 'msg', null,`Day out of range for the specified month: ${date}`); + return null; + } + return { year: y, month: m, day: d }; + }, + isAfterCurrentDate: (eventYear, eventMonth, eventDay) => { + if (eventYear > year) return true; + if (eventYear === year && eventMonth > month) return true; + if (eventYear === year && eventMonth === month && eventDay > day) return true; + return false; + } + }; + let [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + if (type === "set") { + const { year: newYear, month: newMonth, day: newDay } = L.validateISODate(newDate); + QUEST_TRACKER_currentDate = L.formatDate(newYear, newMonth, newDay); + saveQuestTrackerData(); + return; + } + let steps = Math.abs(amount); + let direction = Math.sign(amount); + const dateArray = L.generateDateArray(); + if (QUEST_TRACKER_WEATHER && dateArray.length > 0) { + dateArray.forEach((date) => { + if (!QUEST_TRACKER_HISTORICAL_WEATHER[date]) { + determineWeather(date); + } + }); + } + const [finalYear, finalMonth, finalDay] = dateArray[dateArray.length - 1].split("-").map(Number); + year = finalYear; + month = finalMonth; + day = finalDay; + QUEST_TRACKER_currentDate = L.formatDate(year, month, day); + QUEST_TRACKER_currentWeekdayName = H.calculateWeekday(year, month, day); + H.checkEvent(); + H.checkQuestAutoAdvance(); + describeWeather(); + saveQuestTrackerData(); + Utils.sendMessage(`Date is now: ${Calendar.formatDateFull()}`) + Utils.sendDescMessage(QUEST_TRACKER_CURRENT_WEATHER['description']); + Menu.buildWeather({ isMenu: false }); + }; + const addEvent = () => { + const newEventId = H.generateNewEventId(); + const defaultEventData = { + name: 'New Event', + description: 'Description', + date: `${QUEST_TRACKER_defaultDate}`, + hidden: true, + repeatable: false, + frequency: null + }; + QUEST_TRACKER_Events[newEventId] = defaultEventData; + Utils.updateHandoutField('event'); + }; + const getNextEvents = (number) => { + return H.findNextEvents(number); + }; + const removeEvent = (eventId) => { + delete QUEST_TRACKER_Events[eventId]; + Utils.updateHandoutField('event'); + }; + const manageEventObject = ({ action, field, current, old = '', newItem, date }) => { + const event = QUEST_TRACKER_Events[current]; + switch (field) { + case 'hidden': + event.hidden = !event.hidden; + break; + case 'repeatable': + event.repeatable = !event.repeatable; + event.frequency = 1; + break; + case 'frequency': + event.frequency = newItem; + if (newItem === "2") { + const [year, month, day] = date.split("-").map(Number); + event.weekdayname = H.calculateWeekday(year, month, day); + } + break; + case 'name': + event.name = newItem; + break; + case 'date': + event.date = newItem; + if (event.frequency === "2" && event.repeatable) { + const [year, month, day] = newItem.split("-").map(Number); + event.weekdayname = H.calculateWeekday(year, month, day); + } + break; + case 'description': + event.description = newItem; + break; + default: + errorCheck(31, 'msg', null,`Unknown field command: ${field}`); + break; + } + Utils.updateHandoutField('event'); + }; + const setCalender = (calender) => { + QUEST_TRACKER_calenderType = calender; + const calendar = CALENDARS[calender]; + QUEST_TRACKER_currentDate = calendar.defaultDate; + QUEST_TRACKER_defaultDate = calendar.defaultDate; + const [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + QUEST_TRACKER_currentWeekdayName = H.calculateWeekday(year, month, day); + const firstClimate = Object.keys(calendar.climates)[0]; + if (firstClimate) { + setClimate(firstClimate); + } + saveQuestTrackerData(); + }; + const setClimate = (climate) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + QUEST_TRACKER_Location = climate; + saveQuestTrackerData(); + }; + const setWeatherTrend = (type, amount) => { + QUEST_TRACKER_WEATHER_TRENDS[type] = parseInt(QUEST_TRACKER_WEATHER_TRENDS[type], 10) || 0; + amount = parseInt(amount, 10); + QUEST_TRACKER_WEATHER_TRENDS[type] += amount; + saveQuestTrackerData(); + }; + const formatDateFull = () => { + const [year, month, day] = QUEST_TRACKER_currentDate.split("-").map(Number); + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + const monthName = calendar.months[month - 1].name; + const format = calendar.dateFormat || "{day}{ordinal} of {month}, {year}"; + const ordinal = (n) => { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return s[(v - 20) % 10] || s[v] || s[0]; + }; + return format + .replace("{day}", day) + .replace("{ordinal}", ordinal(day)) + .replace("{month}", monthName) + .replace("{year}", year); + }; + const forceWeatherTrend = (field) => { + const fieldList = ["dry", "wet", "heat", "cold"]; + const isCurrentlyTrue = QUEST_TRACKER_FORCED_WEATHER_TRENDS[field]; + QUEST_TRACKER_FORCED_WEATHER_TRENDS[field] = !isCurrentlyTrue; + if (QUEST_TRACKER_FORCED_WEATHER_TRENDS[field] === true) { + fieldList + .filter((f) => f !== field) + .forEach((f) => { + QUEST_TRACKER_FORCED_WEATHER_TRENDS[f] = false; + }); + } + saveQuestTrackerData(); + }; + const getLunarPhase = (date) => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (!calendar.lunarCycle) return null; + const lunarCycle = calendar.lunarCycle; + const baselineDate = new Date(lunarCycle.baselineNewMoon); + const currentDate = new Date(date); + const daysSinceBaseline = (currentDate - baselineDate) / (1000 * 60 * 60 * 24); + const phase = (daysSinceBaseline % lunarCycle.cycleLength + lunarCycle.cycleLength) % lunarCycle.cycleLength; + for (const { name, start, end } of lunarCycle.phases) { + if (phase >= start && phase < end) { + return name; + } + } + return "Unknown Phase"; + }; + const describeWeather = () => { + const L = { + meetsCondition: (value, cond) => { + if (cond.gte !== undefined && value < cond.gte) return false; + if (cond.lte !== undefined && value > cond.lte) return false; + return true; + }, + matchesConditions: (rolls, conditions, ignoreKeys = []) => { + for (const [metric, cond] of Object.entries(conditions)) { + if (ignoreKeys.includes(metric)) continue; + const val = rolls[metric]; + if (val === undefined) return false; + if (!L.meetsCondition(val, cond)) return false; + } + return true; + }, + countMatches: (rolls, conditions, ignoreKeys = []) => { + let matchCount = 0; + for (const [metric, cond] of Object.entries(conditions)) { + if (ignoreKeys.includes(metric)) continue; + const val = rolls[metric]; + if (val !== undefined && L.meetsCondition(val, cond)) { + matchCount++; + } + } + return matchCount; + }, + determineWeatherType: (rolls) => { + const WEATHER_TYPES = WEATHER.weather; + let matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions)) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions, ['visibility'])) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + matches = []; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + if (L.matchesConditions(rolls, typeData.conditions, ['visibility', 'cloudCover'])) { + matches.push(typeName); + } + } + if (matches.length > 0) { + const chosenMatch = matches[Math.floor(Math.random() * matches.length)]; + return { type: chosenMatch }; + } + let bestType = null; + let bestCount = -1; + for (const [typeName, typeData] of Object.entries(WEATHER_TYPES)) { + const count = L.countMatches(rolls, typeData.conditions); + if (count > bestCount) { + bestCount = count; + bestType = typeName; + } + } + if (bestType) { + return { type: bestType }; + } + return { type: "unclassified normal weather" }; + }, + getScaleDescription: (metric, value) => { + const scaleEntries = Object.entries(WEATHER.scales[metric]); + const numericKeys = scaleEntries.map(([k]) => parseInt(k,10)).sort((a,b) => a - b); + let chosenKey = numericKeys[0]; + for (let k of numericKeys) { + if (k <= value) { + chosenKey = k; + } else { + break; + } + } + return WEATHER.scales[metric][chosenKey.toString()].description; + } + }; + const todayWeather = QUEST_TRACKER_HISTORICAL_WEATHER[QUEST_TRACKER_currentDate]; + if (!todayWeather) return; + const rolls = { + temperature: todayWeather.temperatureRoll, + precipitation: todayWeather.precipitationRoll, + wind: todayWeather.windRoll, + humidity: todayWeather.humidityRoll, + cloudCover: todayWeather.cloudCoverRoll, + visibility: todayWeather.visibilityRoll + }; + const result = L.determineWeatherType(rolls); + const chosenType = result.type; + let chosenWeatherData; + if (WEATHER.weather[chosenType]) { + chosenWeatherData = WEATHER.weather[chosenType]; + } else { + chosenWeatherData = { + descriptions: { + [QUEST_TRACKER_WeatherLocation]: { + "1": "Unclassified normal weather conditions." + } + } + }; + } + const envDescriptions = chosenWeatherData.descriptions[QUEST_TRACKER_WeatherLocation] || { "1": "No description available." }; + const envDescriptionKeys = Object.keys(envDescriptions); + const randomDescKey = envDescriptionKeys[Math.floor(Math.random() * envDescriptionKeys.length)]; + const chosenDescription = envDescriptions[randomDescKey]; + QUEST_TRACKER_CURRENT_WEATHER = { + weatherType: chosenType, + description: chosenDescription, + environment: WEATHER.enviroments[QUEST_TRACKER_WeatherLocation] ? WEATHER.enviroments[QUEST_TRACKER_WeatherLocation].name : QUEST_TRACKER_WeatherLocation, + rolls: { ...rolls }, + scaleDescriptions: { + temperature: L.getScaleDescription("temperature", rolls.temperature), + humidity: L.getScaleDescription("humidity", rolls.humidity), + wind: L.getScaleDescription("wind", rolls.wind), + precipitation: L.getScaleDescription("precipitation", rolls.precipitation), + cloudCover: L.getScaleDescription("cloudCover", rolls.cloudCover), + visibility: L.getScaleDescription("visibility", rolls.visibility) + } + }; + }; + const adjustLocation = (location) => { + if (WEATHER.enviroments.hasOwnProperty(location)) { + QUEST_TRACKER_WeatherLocation = location; + } else return; + }; + return { + modifyDate, + addEvent, + removeEvent, + manageEventObject, + setCalender, + formatDateFull, + setClimate, + setWeatherTrend, + forceWeatherTrend, + getLunarPhase, + getNextEvents, + adjustLocation + }; + })(); + const QuestPageBuilder = (() => { + const vars = { + DEFAULT_PAGE_UNIT: 70, + AVATAR_SIZE: 70, + TEXT_FONT_SIZE: 10, + PAGE_HEADER_WIDTH: 700, + PAGE_HEADER_HEIGHT: 150, + ROUNDED_RECT_WIDTH: 320, + ROUNDED_RECT_HEIGHT: 80, + ROUNDED_RECT_CORNER_RADIUS: 10, + VERTICAL_SPACING: 100, + HORIZONTAL_SPACING: 160, + DEFAULT_FILL_COLOR: '#CCCCCC', + DEFAULT_STATUS_COLOR: '#000000', + QUESTICON_WIDTH: 305, + GROUP_SPACING: 800, + QUESTICON_HEIGHT: 92 + }; + const H = { + adjustPageSettings: (page) => { + page.set({ + showgrid: false, + snapping_increment: 0, + diagonaltype: 'facing', + scale_number: 1, + }); + }, + adjustPageSizeToFitPositions: (page, questPositions) => { + const positions = Object.values(questPositions); + const minX = Math.min(...positions.map(pos => pos.x)); + const maxX = Math.max(...positions.map(pos => pos.x)); + const minY = Math.min(...positions.map(pos => pos.y)); + const maxY = Math.max(...positions.map(pos => pos.y)); + const requiredWidthInPixels = (maxX - minX) + vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING * 2; + const requiredHeightInPixels = (maxY - minY) + vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING * 2 + vars.PAGE_HEADER_HEIGHT; + const requiredWidthInUnits = Math.ceil(requiredWidthInPixels / vars.DEFAULT_PAGE_UNIT); + const requiredHeightInUnits = Math.ceil(requiredHeightInPixels / vars.DEFAULT_PAGE_UNIT); + page.set({ width: requiredWidthInUnits, height: requiredHeightInUnits }); + }, + clearPageObjects: (pageId, callback) => { + const pageElements = [ + ...findObjs({ _type: 'graphic', _pageid: pageId }), + ...findObjs({ _type: 'path', _pageid: pageId }), + ...findObjs({ _type: 'text', _pageid: pageId }) + ]; + pageElements.forEach(obj => obj.remove()); + if (typeof callback === 'function') callback(); + }, + buildPageHeader: (page) => { + const titleText = 'Quest Tracker Quest Tree'; + const descriptionText = 'A visual representation of all quests.'; + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const titleX = pageWidth / 2; + const titleY = 70; + D.drawText(page.id, titleX, titleY, titleText, '#000000', 'map', 32, 'Contrail One', null, 'center', 'middle'); + const descriptionY = titleY + 40; + D.drawText(page.id, titleX, descriptionY, descriptionText, '#666666', 'map', 18, 'Contrail One', null, 'center', 'middle'); + }, + storeQuestRef: (questId, type, objRef, target = null) => { + if (!QUEST_TRACKER_TreeObjRef[questId]) { + QUEST_TRACKER_TreeObjRef[questId] = { paths: {} }; + } + if (type === 'paths' && target) { + if (!QUEST_TRACKER_TreeObjRef[questId][type][target]) { + QUEST_TRACKER_TreeObjRef[questId][type][target] = []; + } + QUEST_TRACKER_TreeObjRef[questId][type][target].push(objRef); + } else { + QUEST_TRACKER_TreeObjRef[questId][type] = objRef; + } + }, + replaceImageSize: (imgsrc) => { + return imgsrc.replace(/\/(med|original|max|min)\.(gif|jpg|jpeg|bmp|webp|png)(\?.*)?$/i, '/thumb.$2$3'); + }, + trimText: (text, maxLength = 150) => { + if (text.length > maxLength) { + return text.slice(0, maxLength - 3) + '...'; + } + return text; + }, + getStatusColor: (status) => { + switch (status) { + case 'Unknown': + return '#A9A9A9'; + case 'Discovered': + return '#ADD8E6'; + case 'Started': + return '#87CEFA'; + case 'Ongoing': + return '#FFD700'; + case 'Completed': + return '#32CD32'; + case 'Completed By Someone Else': + return '#4682B4'; + case 'Failed': + return '#FF6347'; + case 'Time ran out': + return '#FF8C00'; + case 'Ignored': + return '#D3D3D3'; + default: + return '#CCCCCC'; + } + }, + buildDAG: (questData) => { + const questPositions = {}; + const groupMap = {}; + const mutualExclusivityClusters = []; + const visitedForClusters = new Set(); + function findMutualExclusivityCluster(startQuestId) { + const cluster = new Set(); + const stack = [startQuestId]; + while (stack.length > 0) { + const questId = stack.pop(); + if (!cluster.has(questId)) { + cluster.add(questId); + visitedForClusters.add(questId); + const mutuallyExclusiveQuests = questData[questId]?.relationships?.mutually_exclusive || []; + mutuallyExclusiveQuests.forEach(meQuestId => { + if (!cluster.has(meQuestId)) { + stack.push(meQuestId); + } + }); + } + } + return cluster; + } + Object.keys(questData).forEach(questId => { + if (!visitedForClusters.has(questId)) { + const cluster = findMutualExclusivityCluster(questId); + mutualExclusivityClusters.push(cluster); + } + }); + const questIdToClusterIndex = {}; + mutualExclusivityClusters.forEach((cluster, index) => { + cluster.forEach(questId => { + questIdToClusterIndex[questId] = index; + }); + }); + const calculateInitialLevels = (questId, visited = new Set()) => { + if (visited.has(questId)) return questData[questId].level || 0; + visited.add(questId); + const prereqs = questData[questId]?.relationships?.conditions || []; + if (prereqs.length === 0) { + questData[questId].level = 0; + return 0; + } + const prereqLevels = prereqs.map(prereq => { + let prereqId; + if (typeof prereq === 'string') { + prereqId = prereq; + } else if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; // Simplification + } + return calculateInitialLevels(prereqId, new Set(visited)) + 1; + }); + const level = Math.max(...prereqLevels); + questData[questId].level = level; + return level; + }; + Object.keys(questData).forEach(questId => { + calculateInitialLevels(questId); + }); + mutualExclusivityClusters.forEach(cluster => { + const clusterQuestLevels = Array.from(cluster).map(questId => questData[questId].level || 0); + const maxQuestLevel = Math.max(...clusterQuestLevels); + const prerequisiteLevels = Array.from(cluster).map(questId => { + const prereqs = questData[questId]?.relationships?.conditions || []; + const prereqLevels = prereqs.map(prereq => { + let prereqId; + if (typeof prereq === 'string') { + prereqId = prereq; + } else if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; + } + return questData[prereqId]?.level || 0; + }); + if (prereqLevels.length === 0) return -1; + return Math.max(...prereqLevels); + }); + const maxPrereqLevel = Math.max(...prerequisiteLevels); + const clusterLevel = Math.max(maxPrereqLevel + 1, maxQuestLevel); + cluster.forEach(questId => { + questData[questId].level = clusterLevel; + }); + }); + Object.keys(questData).forEach(questId => { + const group = questData[questId]?.group || 'Default Group'; + if (!groupMap[group]) groupMap[group] = []; + groupMap[group].push(questId); + }); + const groupWidths = {}; + const groupOrder = Object.keys(groupMap); + Object.entries(groupMap).forEach(([groupName, groupQuests]) => { + const levels = {}; + groupQuests.forEach(questId => { + const level = questData[questId].level; + if (!levels[level]) levels[level] = []; + levels[level].push(questId); + }); + const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); + let maxLevelWidth = 0; + sortedLevels.forEach(level => { + let questsAtLevel = levels[level]; + const totalQuests = questsAtLevel.length; + const clustersAtLevel = {}; + questsAtLevel.forEach(questId => { + const clusterIndex = questIdToClusterIndex[questId] || null; + if (clusterIndex !== null) { + if (!clustersAtLevel[clusterIndex]) clustersAtLevel[clusterIndex] = new Set(); + clustersAtLevel[clusterIndex].add(questId); + } else { + if (!clustersAtLevel['no_cluster']) clustersAtLevel['no_cluster'] = new Set(); + clustersAtLevel['no_cluster'].add(questId); + } + }); + const arrangedQuests = []; + Object.values(clustersAtLevel).forEach(cluster => { + arrangedQuests.push(...Array.from(cluster)); + }); + levels[level] = arrangedQuests; + const levelWidth = (arrangedQuests.length * vars.ROUNDED_RECT_WIDTH) + ((arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING); + maxLevelWidth = Math.max(maxLevelWidth, levelWidth); + }); + const groupWidth = maxLevelWidth; + groupWidths[groupName] = groupWidth; + }); + const totalTreeWidth = groupOrder.reduce((sum, groupName, index) => { + return sum + groupWidths[groupName] + (index > 0 ? vars.GROUP_SPACING : 0); + }, 0); + let cumulativeGroupWidth = - totalTreeWidth / 2; + groupOrder.forEach((groupName) => { + const groupQuests = groupMap[groupName]; + const levels = {}; + groupQuests.forEach(questId => { + const level = questData[questId].level; + if (!levels[level]) levels[level] = []; + levels[level].push(questId); + }); + const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); + sortedLevels.forEach(level => { + let questsAtLevel = levels[level]; + const totalQuests = questsAtLevel.length; + const arrangedQuests = levels[level]; + const levelWidth = (arrangedQuests.length * vars.ROUNDED_RECT_WIDTH) + ((arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING); + const levelStartX = cumulativeGroupWidth + (groupWidths[groupName] - levelWidth) / 2; + arrangedQuests.forEach((questId, index) => { + const x = levelStartX + index * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); + const y = level * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); + questPositions[questId] = { + x: x, + y: y, + group: groupName, + }; + }); + }); + cumulativeGroupWidth += groupWidths[groupName] + vars.GROUP_SPACING; + }); + return questPositions; + } + }; + const D = { + drawQuestTreeFromPositions: (page, questPositions, callback) => { + const totalWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(32, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const x = position.x + totalWidth / 2; + const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const isHidden = questData.hidden || false; + D.drawQuestGraphics(questId, questData, page.id, x, y, isHidden); + }); + if (typeof callback === 'function') callback(); + }, + drawQuestGraphics: (questId, questData, pageId, x, y, isHidden) => { + const questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (!questTable) { + errorCheck(33, 'msg', null,`Quests rollable table not found.`); + return; + } + const questTableItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + const questTableItem = questTableItems.find(item => item.get('name').toLowerCase() === questId.toLowerCase()); + if (!questTableItem) { + errorCheck(34, 'msg', null,`Rollable table item for quest "${questId}" not found.`); + return; + } + const statusWeight = questTableItem.get('weight'); + const statusName = statusMapping[statusWeight] || 'Unknown'; + const statusColor = H.getStatusColor(statusName); + let imgsrc = questTableItem.get('avatar'); + if (!imgsrc || !imgsrc.includes('https://')) { + imgsrc = QUEST_TRACKER_BASE_QUEST_ICON_URL; + } else { + imgsrc = H.replaceImageSize(imgsrc); + } + D.drawRoundedRectangle(pageId, x, y, vars.ROUNDED_RECT_WIDTH, vars.ROUNDED_RECT_HEIGHT, vars.ROUNDED_RECT_CORNER_RADIUS, statusColor, isHidden ? 'gmlayer' : 'map', questId); + const avatarSpacing = 10; + const avatarX = x; + const avatarY = y - (vars.ROUNDED_RECT_HEIGHT / 2) - (vars.AVATAR_SIZE / 2) - avatarSpacing; + if (imgsrc !== '') D.placeAvatar(pageId, avatarX, avatarY, vars.AVATAR_SIZE, imgsrc, isHidden ? 'gmlayer' : 'objects', questId); + }, + drawQuestTextAfterGraphics: (page, questPositions) => { + const totalWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(35, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const x = position.x + totalWidth / 2; + const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const isHidden = questData.hidden || false; + const textLayer = isHidden ? 'gmlayer' : 'objects'; + D.drawText( + page.id, + x, + y, + questData.name, + '#000000', + textLayer, + vars.TEXT_FONT_SIZE, + 'Contrail One', + questId, + 'center', + 'middle' + ); + }); + }, + drawQuestConnections: (pageId, questPositions) => { + const page = getObj('page', pageId); + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const offsetX = pageWidth / 2; + const incomingPaths = {}; + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(36, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + (questData.relationships?.conditions || []).forEach(prereq => { + let prereqId = prereq; + if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; + } + if (!incomingPaths[prereqId]) { + incomingPaths[prereqId] = []; + } + incomingPaths[prereqId].push(questId); + }); + }); + Object.entries(questPositions).forEach(([questId, position]) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(37, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } + const startX = position.x + offsetX; + const startY = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const startPos = { + x: startX, + y: startY + }; + (questData.relationships?.conditions || []).forEach(prereq => { + let prereqId = prereq; + if (typeof prereq === 'object' && prereq.conditions) { + prereqId = prereq.conditions[0]; + } + const prereqPosition = questPositions[prereqId]; + if (!prereqPosition) { + errorCheck(38, 'msg', null,`Position data for prerequisite "${prereqId}" is missing.`); + return; + } + const endX = prereqPosition.x + offsetX; + const endY = prereqPosition.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const endPos = { + x: endX, + y: endY + }; + let midY; + if (incomingPaths[prereqId].length > 1) { + midY = endPos.y + vars.VERTICAL_SPACING / 2; + } else { + midY = (startPos.y + endPos.y) / 2; + } + const isHidden = questData.hidden || QUEST_TRACKER_globalQuestData[prereqId]?.hidden; + const connectionColor = isHidden ? '#CCCCCC' : '#000000'; + const connectionLayer = isHidden ? 'gmlayer' : 'map'; + D.drawPath(pageId, startPos, endPos, connectionColor, connectionLayer, questId, prereqId, midY); + }); + }); + }, + drawPath: (pageId, startPos, endPos, color = '#FF0000', layer = 'objects', questId, pathToQuestId, controlY = null, isMutualExclusion = false) => { + let pathData; + let left, top, width, height; + controlY = (controlY === null) ? (startPos.y + endPos.y) / 2 : controlY; + if (isMutualExclusion) { + pathData = [ + ['M', startPos.x, startPos.y], + ['L', endPos.x, endPos.y] + ]; + } else { + pathData = [ + ['M', startPos.x, startPos.y], + ['L', startPos.x, controlY], + ['L', endPos.x, controlY], + ['L', endPos.x, endPos.y] + ]; + } + const minX = Math.min(startPos.x, endPos.x); + const maxX = Math.max(startPos.x, endPos.x); + const minY = Math.min(startPos.y, endPos.y, controlY); + const maxY = Math.max(startPos.y, endPos.y, controlY); + left = (minX + maxX) / 2; + top = (minY + maxY) / 2; + width = maxX - minX; + height = maxY - minY; + const adjustedPathData = pathData.map(command => { + const [cmd, ...coords] = command; + const adjustedCoords = coords.map((coord, index) => { + return coord - (index % 2 === 0 ? left : top); + }); + return [cmd, ...adjustedCoords]; + }); + const pathObj = createObj('path', { + _pageid: pageId, + layer: layer, + stroke: color, + fill: 'transparent', + path: JSON.stringify(adjustedPathData), + stroke_width: 2, + controlledby: '', + left: left, + top: top, + width: width, + height: height + }); + if (pathObj) { + if (isMutualExclusion) { + H.storeQuestRef(questId, 'mutualExclusion', pathObj.id, pathToQuestId); + H.storeQuestRef(pathToQuestId, 'mutualExclusion', pathObj.id, questId); + } else { + H.storeQuestRef(questId, 'paths', pathObj.id, pathToQuestId); + H.storeQuestRef(pathToQuestId, 'paths', pathObj.id, questId); + } + } + }, + drawMutuallyExclusiveConnections: (pageId, questPositions) => { + const page = getObj('page', pageId); + const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; + const offsetX = pageWidth / 2; + const mutualExclusions = []; + Object.entries(QUEST_TRACKER_globalQuestData).forEach(([questId, questData]) => { + const mutuallyExclusiveWith = questData.relationships?.mutually_exclusive || []; + mutuallyExclusiveWith.forEach(otherQuestId => { + if (questId < otherQuestId) { + mutualExclusions.push([questId, otherQuestId]); + } + }); + }); + mutualExclusions.forEach(([questId1, questId2]) => { + const position1 = questPositions[questId1]; + const position2 = questPositions[questId2]; + if (!position1 || !position2) { + errorCheck(39, 'msg', null,`Position data for quests "${questId1}" or "${questId2}" is missing.`); + return; + } + const x1 = position1.x + offsetX; + const y1 = position1.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const x2 = position2.x + offsetX; + const y2 = position2.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; + const startPos = { x: x1, y: y1 }; + const endPos = { x: x2, y: y2 }; + const questData1 = QUEST_TRACKER_globalQuestData[questId1]; + const questData2 = QUEST_TRACKER_globalQuestData[questId2]; + const isHidden = questData1.hidden || questData2.hidden; + const connectionLayer = isHidden ? 'gmlayer' : 'map'; + D.drawPath(pageId, startPos, endPos, '#FF0000', connectionLayer, questId1, questId2, null, true); + }); + }, + drawText: (pageId, x, y, textContent, color = '#000000', layer = 'objects', font_size = vars.TEXT_FONT_SIZE, font_family = 'Arial', questId, text_align = 'center', vertical_align = 'middle') => { + const textObj = createObj('text', { + _pageid: pageId, + left: x, + top: y, + text: textContent, + font_size: font_size, + color: color, + layer: layer, + font_family: font_family, + text_align: text_align + }); + if (textObj) { + if (vertical_align !== 'middle') { + const textHeight = font_size; + let adjustedTop = y; + if (vertical_align === 'top') { + adjustedTop = y - (textHeight / 2); + } else if (vertical_align === 'bottom') { + adjustedTop = y + (textHeight / 2); + } + textObj.set('top', adjustedTop); + } + if (questId) { + H.storeQuestRef(questId, 'text', textObj.id); + } + } + }, + placeAvatar: (pageId, x, y, avatarSize, imgsrc, layer = 'objects', questId) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + let tooltipText = `${questData.description || 'No description available.'}`; + let trimmedText = H.trimText(tooltipText, 150); + const avatarObj = createObj('graphic', { + _pageid: pageId, + left: x, + top: y, + width: avatarSize, + height: avatarSize, + layer: layer, + imgsrc: imgsrc, + tooltip: trimmedText, + controlledby: '' + }); + if (avatarObj) { + H.storeQuestRef(questId, 'avatar', avatarObj.id); + } + }, + drawRoundedRectangle: (pageId, x, y, width, height, radius, statusColor, layer = 'objects', questId) => { + let pathData = []; + const w = width; + const h = height; + pathData = [ + ['M', -w / 2, -h / 2], + ['L', w / 2, -h / 2], + ['L', w / 2, h / 2], + ['L', -w / 2, h / 2], + ['L', -w / 2, -h / 2], + ['Z'] + ]; + const rectObj = createObj('path', { + _pageid: pageId, + layer: layer, + stroke: statusColor, + fill: "#FAFAD2", + path: JSON.stringify(pathData), + stroke_width: 4, + controlledby: '', + left: x, + top: y, + width: width, + height: height + }); + if (rectObj) { + H.storeQuestRef(questId, 'rectangle', rectObj.id); + } + }, + redrawQuestText: (questId) => { + let pageObj = findObjs({ _type: 'page', name: QUEST_TRACKER_pageName })[0]; + if (!pageObj) return; + const pageId = pageObj.id; + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].text) return; + const textObjId = QUEST_TRACKER_TreeObjRef[questId].text; + const textObj = getObj('text', textObjId); + if (textObj) { + const questData = QUEST_TRACKER_globalQuestData[questId]; + const isHidden = questData.hidden || false; + const textLayer = isHidden ? 'gmlayer' : 'objects'; + const x = textObj.get('left'); + const y = textObj.get('top'); + textObj.remove(); + D.drawText(pageId, x, y, questData.name, '#000000', textLayer, vars.TEXT_FONT_SIZE, 'Contrail One', questId); + } + } + }; + const buildQuestTreeOnPage = () => { + let questTreePage = findObjs({ _type: 'page', name: QUEST_TRACKER_pageName })[0]; + if (!questTreePage) { + errorCheck(40, 'msg', null,`Page "${QUEST_TRACKER_pageName}" not found. Please create the page manually.`); + return; + } + H.adjustPageSettings(questTreePage); + H.clearPageObjects(questTreePage.id, () => { + const questPositions = H.buildDAG(QUEST_TRACKER_globalQuestData); + H.adjustPageSizeToFitPositions(questTreePage, questPositions); + H.buildPageHeader(questTreePage); + QUEST_TRACKER_TreeObjRef = {}; + D.drawQuestConnections(questTreePage.id, questPositions); + D.drawMutuallyExclusiveConnections(questTreePage.id, questPositions); + D.drawQuestTreeFromPositions(questTreePage, questPositions, () => { + D.drawQuestTextAfterGraphics(questTreePage, questPositions); + saveQuestTrackerData(); + }); + }); + }; + const updateQuestText = (questId, newText) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].text) return; + const textObjId = QUEST_TRACKER_TreeObjRef[questId].text; + const textObj = getObj('text', textObjId); + if (!textObj) return; + textObj.set('text', newText); + saveQuestTrackerData(); + }; + const updateQuestTooltip = (questId, newTooltip) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].avatar) return; + const avatarObjId = QUEST_TRACKER_TreeObjRef[questId].avatar; + const avatarObj = getObj('graphic', avatarObjId); + if (!avatarObj) return; + const trimmedTooltip = H.trimText(newTooltip, 150); + avatarObj.set('tooltip', trimmedTooltip); + saveQuestTrackerData(); + }; + const updateQuestStatusColor = (questId, statusNumber) => { + if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].rectangle) return; + const rectangleObjId = QUEST_TRACKER_TreeObjRef[questId].rectangle; + const rectangleObj = getObj('path', rectangleObjId); + if (!rectangleObj) return; + const statusName = statusMapping[statusNumber] || 'Unknown'; + const statusColor = H.getStatusColor(statusName); + rectangleObj.set('stroke', statusColor); + D.redrawQuestText(questId); + saveQuestTrackerData(); + }; + const updateQuestVisibility = (questId, makeHidden) => { + if (!QUEST_TRACKER_TreeObjRef[questId]) return; + const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) return; + const pageId = findObjs({ type: 'page', name: QUEST_TRACKER_pageName })[0].id; + if (typeof makeHidden === 'string') makeHidden = makeHidden.toLowerCase() === 'true'; + const targetLayer = makeHidden ? 'gmlayer' : 'map'; + const avatarLayer = makeHidden ? 'gmlayer' : 'objects'; + for (const sourceQuestId in QUEST_TRACKER_TreeObjRef) { + const pathsToQuest = QUEST_TRACKER_TreeObjRef[sourceQuestId]?.paths?.[questId]; + if (pathsToQuest) { + pathsToQuest.forEach(segmentId => { + const pathObj = getObj('path', segmentId); + if (pathObj) { + pathObj.set({ + layer: targetLayer, + stroke: makeHidden ? '#CCCCCC' : '#000000' + }); + } + }); + } + } + const elements = ['rectangle', 'avatar']; + elements.forEach(element => { + const objId = QUEST_TRACKER_TreeObjRef[questId][element]; + const obj = getObj(element === 'rectangle' ? 'path' : 'graphic', objId); + if (obj) { + const layer = element === 'avatar' ? avatarLayer : targetLayer; + obj.set('layer', layer); + } + }); + D.redrawQuestText(questId); + if (!makeHidden) { + saveQuestTrackerData(); + } + }; + return { + buildQuestTreeOnPage, + updateQuestText, + updateQuestTooltip, + updateQuestStatusColor, + updateQuestVisibility + }; + })(); + const Rumours = (() => { + const H = { + getNewRumourId: () => { + const existingRumourIds = []; + Object.values(QUEST_TRACKER_globalRumours).forEach(quest => { + Object.values(quest).forEach(category => { + Object.values(category).forEach(location => { + Object.keys(location).forEach(rumourId => { + const match = rumourId.match(/^rumour_(\d+)$/); + if (match) { + existingRumourIds.push(parseInt(match[1], 10)); + } + }); + }); + }); + }); + const highestRumourNumber = existingRumourIds.length > 0 ? Math.max(...existingRumourIds) : 0; + const newRumourId = `rumour_${highestRumourNumber + 1}`; + return newRumourId; + }, + getNewLocationId: (locationTable) => { + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + let maxWeight = locationItems.reduce((max, item) => { + return Math.max(max, item.get('weight')); + }, 0); + let newWeight = maxWeight + 1; + return newWeight; + }, + removeRumours: (locationTable, locationid) => { + const locationObject = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + if (!locationObject) return; + const cleanData = Utils.sanitizeString(locationObject.get('name')).toLowerCase(); + Object.keys(QUEST_TRACKER_globalRumours).forEach(questId => { + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + Object.keys(questRumours).forEach(status => { + const statusRumours = questRumours[status] || {}; + if (statusRumours[cleanData]) { + Object.keys(statusRumours[cleanData]).forEach(rumourKey => { + }); + delete statusRumours[cleanData]; + } + }); + }); + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + } + }; + const calculateRumoursByLocation = () => { + let rumoursByLocation = {}; + let questTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]; + if (errorCheck(146, 'exists', questTable, `questTable`)) return; + let questItems = findObjs({ type: 'tableitem', rollabletableid: questTable.id }); + if (errorCheck(147, 'exists', questItems, `questItems`)) return; + Object.keys(QUEST_TRACKER_globalRumours).forEach(questId => { + let relevantItem = questItems.find(item => item.get('name').toLowerCase() === questId.toLowerCase()); + if (errorCheck(148, 'exists', relevantItem, `relevantItem for questId: ${questId}`)) return; + let relevantStatus = statusMapping[relevantItem.get('weight').toString()].toLowerCase(); + let questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + if (questRumours[relevantStatus]) { + Object.keys(questRumours[relevantStatus] || {}).forEach(location => { + let locationRumours = questRumours[relevantStatus][location] || {}; + if (!rumoursByLocation[location]) rumoursByLocation[location] = []; + Object.keys(locationRumours).forEach(rumourKey => { + const rumourText = locationRumours[rumourKey]; + rumoursByLocation[location].push(rumourText); + }); + }); + } + }); + QUEST_TRACKER_rumoursByLocation = rumoursByLocation; + saveQuestTrackerData(); + }; + const sendRumours = (locationId, numberOfRumours) => { + let allRumours = []; + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(41, 'exists', locationTable,`locationTable`)) return; + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + let location = locationItems.find(loc => loc.get('weight').toString() === locationId.toString()); + if (errorCheck(42, 'exists', location,`location`)) return; + const normalizedLocationId = Utils.sanitizeString(location.get('name')).toLowerCase(); + if (normalizedLocationId === 'everywhere') { + allRumours = Object.values(QUEST_TRACKER_rumoursByLocation['everywhere'] || {}).map((rumour, index) => `${index}|${Utils.sanitizeInput(rumour, 'STRING')}`); + } else { + const locationRumoursObj = QUEST_TRACKER_rumoursByLocation[normalizedLocationId] || {}; + const everywhereRumoursObj = QUEST_TRACKER_rumoursByLocation['everywhere'] || {}; + const locationRumours = Object.values(locationRumoursObj).map(rumour => Utils.sanitizeInput(rumour, 'STRING')); + const everywhereRumours = Object.values(everywhereRumoursObj).map(rumour => Utils.sanitizeInput(rumour, 'STRING')); + locationRumours.forEach((rumour, index) => { + for (let i = 0; i < 3; i++) { + allRumours.push(`${index}|${rumour}`); + } + }); + everywhereRumours.forEach((rumour, index) => { + allRumours.push(`${locationRumours.length + index}|${rumour}`); + }); + } + if (allRumours.length === 0) { + Utils.sendGMMessage(`No rumours available for this location.`); + return; + } + let selectedRumours = []; + while (selectedRumours.length < numberOfRumours && allRumours.length > 0) { + let randomIndex = Math.floor(Math.random() * allRumours.length); + let selectedRumour = allRumours[randomIndex]; + let [rumourKey, rumourText] = selectedRumour.split('|', 2); + selectedRumours.push(rumourText); + allRumours = allRumours.filter(rumour => !rumour.startsWith(`${rumourKey}|`)); + } + selectedRumours.forEach(rumour => { + Utils.sendDescMessage(rumour); + }); + }; + const getLocationNameById = (locationId) => { + const locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(43, 'exists', locationTable,`locationTable`)) return null; + const locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + const locationItem = locationItems.find(item => item.get('weight').toString() === locationId.toString()); + if (errorCheck(44, 'exists', locationItem,`locationItem`)) return null; + return locationItem.get('name'); + }; + const removeAllRumoursForQuest = (questId) => { + if (!QUEST_TRACKER_globalRumours[questId]) return; + Object.keys(QUEST_TRACKER_globalRumours[questId]).forEach(status => { + const statusRumours = QUEST_TRACKER_globalRumours[questId][status] || {}; + Object.keys(statusRumours).forEach(location => { + delete statusRumours[location]; + }); + delete QUEST_TRACKER_globalRumours[questId][status]; + }); + delete QUEST_TRACKER_globalRumours[questId]; + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + }; + const getAllLocations = () => { + let rollableTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(45, 'exists', rollableTable,`rollableTable`)) return []; + const tableItems = findObjs({ _type: 'tableitem', _rollabletableid: rollableTable.id }); + const locations = tableItems.map(item => item.get('name')); + return locations; + }; + const manageRumourLocation = (action, newItem = null, locationid = null) => { + const allLocations = Rumours.getAllLocations(); + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (errorCheck(46, 'exists', locationTable,`locationTable`)) return; + switch (action) { + case 'add': + if (!newItem) return; + if (allLocations.some(loc => Utils.sanitizeString(loc.toLowerCase()) === Utils.sanitizeString(newItem.toLowerCase()))) return; + const newWeight = H.getNewLocationId(locationTable); + if (newWeight === undefined || newWeight === null) return; + let newLocation = createObj('tableitem', { + rollabletableid: locationTable.id, + name: newItem, + weight: newWeight + }); + break; + case 'remove': + if (!locationid || locationid === 1) return; + let locationR = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + H.removeRumours(locationTable,locationid) + locationR.remove(); + break; + case 'update': + if (allLocations.some(loc => Utils.sanitizeString(loc.toLowerCase()) === Utils.sanitizeString(newItem.toLowerCase())) || Utils.sanitizeString(newItem.toLowerCase()) === 'everywhere') return; + let locationU = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }).find(item => item.get('weight') == locationid); + locationU.set('name', newItem); + break; + } + }; + const manageRumourObject = ({ action, questId, newItem = '', status, location, rumourId = ''}) => { + let locationString = getLocationNameById(location) + const sanitizedLocation = locationString ? Utils.sanitizeString(locationString.toLowerCase()) : ''; + if (!QUEST_TRACKER_globalRumours[questId]) QUEST_TRACKER_globalRumours[questId] = {}; + if (!QUEST_TRACKER_globalRumours[questId][status]) QUEST_TRACKER_globalRumours[questId][status] = {}; + const questRumours = QUEST_TRACKER_globalRumours[questId]; + const statusRumours = questRumours[status]; + switch (action) { + case 'add': + if (!statusRumours[sanitizedLocation]) { + statusRumours[sanitizedLocation] = {}; + } + const newRumourKey = rumourId === '' ? H.getNewRumourId() : rumourId; + statusRumours[sanitizedLocation][newRumourKey] = newItem; + break; + case 'remove': + if (statusRumours[sanitizedLocation] && statusRumours[sanitizedLocation][rumourId]) { + delete statusRumours[sanitizedLocation][rumourId]; + if (Object.keys(statusRumours[sanitizedLocation]).length === 0) { + delete statusRumours[sanitizedLocation]; + } + } + break; + default: + break; + } + Utils.updateHandoutField('rumour'); + calculateRumoursByLocation(); + }; + return { + calculateRumoursByLocation, + sendRumours, + manageRumourLocation, + getLocationNameById, + removeAllRumoursForQuest, + getAllLocations, + manageRumourObject + }; + })(); + const Menu = (() => { + const styles = { + menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px; overflow: hidden;', + button: 'background-color: #000; border: 1px solid #292929 ; border-radius: 3px; padding: 2px; color: #fff; text-align: center;', + buttonDisabled: 'pointer-events: none; background-color: #666; border: 1px solid #292929; border-radius: 3px; padding: 2px; text-align: center; color: #000000;', + smallButton: 'display: inline-block; width: 12px; height:16px;', + smallButtonMagnifier: 'display: inline-block; width: 16px; height:16px; background-color:#fff;', + smallButtonContainer: 'text-align:center; width: 20px; padding:1px', + smallButtonAdd: 'text-align:right; width: 20px; padding:1px margin-right:1px', + smallerText: 'font-size: smaller', + list: 'list-style none; padding: 0; margin: 0; overflow: hidden;', + label: 'float: left; font-weight: bold;', + topBorder: 'border-top: 1px solid #ddd;', + bottomBorder: 'border-bottom: 1px solid #ddd;', + topMargin: 'margin-top: 20px;', + column: 'overflow: hidden; padding: 5px 0;', + marginRight: 'margin-right: 2px', + floatLeft: 'float: left;', + floatRight: 'float: right;', + floatClearRight: 'float: right; clear: right;', + overflow: 'overflow: hidden; margin:1px', + rumour: 'text-overflow: ellipsis;overflow: hidden;width: 165px;display: block;word-break: break-all;white-space: nowrap;', + link: 'color: #007bff; text-decoration: underline; cursor: pointer;', + questlink: 'color: #000000; text-decoration: none; cursor: pointer; background-color: #FFFFFF;', + treeStyle: 'display: inline-block; position: relative; text-align: center; margin-top: 0px;', + questBox50: 'display: inline-block; width: 15px; height: 6px; padding: 5px; border: 1px solid #000; border-radius: 5px; background-color: #f5f5f5; text-align: center; position: relative; margin-right: 20px;', + verticalLineStyle: 'position: absolute; width: 2px; background-color: black;', + lineHorizontalRed: 'position: absolute; width: 24px; height: 2px; background-color: red; left: 57%;', + lineHorizontal: 'position: absolute; height: 2px; background-color: black;', + treeContainerStyle: 'position: relative; width: 100%; height: 100%; text-align: center; margin-top: 20px;', + ulStyle: 'list-style: none; position: relative; padding: 0; margin: 0; display: block; text-align: center;', + liStyle: 'display: inline-block; text-align: center; position: relative;', + spanText: 'bottom: -1px; position: absolute; left: -1px; right: 0px;' + }; + const H = { + showActiveQuests: () => { + let AQMenu = ""; + const activeStatuses = [2, 3, 4]; + const activeQuests = QUEST_TRACKER_globalQuestArray + .filter(quest => { + const status = parseInt(Quest.getQuestStatus(quest.id), 10); + return activeStatuses.includes(status); + }) + .map(quest => quest.id); + if (activeQuests.length === 0) { + AQMenu += `
      +
    • + No Active Quests +
    • +
    `; + } else { + AQMenu += `
      `; + activeQuests.forEach(quest => { + let questData = QUEST_TRACKER_globalQuestData[quest]; + AQMenu += ` +
    • + ${questData.name} + + Inspect + +
    • `; + }); + AQMenu += `
    `; + } + return AQMenu; + }, + showActiveRumours: () => { + let menu = `
      `; + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (locationTable) { + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + locationItems.sort((a, b) => a.get('weight') - b.get('weight')).forEach(location => { + let locationName = location.get('name'); + let locationKey = Utils.sanitizeString(locationName).toLowerCase(); + let locationWeight = location.get('weight'); + let rumourCount = QUEST_TRACKER_rumoursByLocation[locationKey] ? Object.keys(QUEST_TRACKER_rumoursByLocation[locationKey]).length : 0; + let everywhereRumourCount = QUEST_TRACKER_rumoursByLocation['everywhere'] ? Object.keys(QUEST_TRACKER_rumoursByLocation['everywhere']).length : 0; + let displayRumourCount = locationKey !== 'everywhere' && everywhereRumourCount > 0 + ? `${rumourCount} (+${everywhereRumourCount})` + : `${rumourCount}`; + let totalRumourCount = locationKey === 'everywhere' ? rumourCount : rumourCount + everywhereRumourCount; + if (rumourCount > 0 || locationKey === 'everywhere') { + menu += ` +
    • + ${locationName}
      ${displayRumourCount} Rumours
      + + Show + +
    • `; + } + }); + } + menu += `
    `; + return menu; + }, + generateQuestList: (groupName, quests) => { + let menu = `

    ${groupName} Quests

    `; + Object.keys(quests).sort((a, b) => a - b).forEach(weight => { + menu += `
    ${statusMapping[weight]}
      `; + quests[weight].forEach(quest => { + let questData = QUEST_TRACKER_globalQuestData[quest.id]; + if (questData) { + questData = Object.keys(questData).reduce((acc, key) => { + acc[key.toLowerCase()] = questData[key]; + return acc; + }, {}); + if (questData.name) { + menu += ` +
    • + ${questData.name} + + Inspect + - + +
    • `; + } else { + errorCheck(149, 'msg', handout,'Quest data for "${quest.id}" is missing or incomplete.') + } + } + }); + menu += `
    `; + }); + return menu; + }, + formatAutocompleteListWithDates: (fieldName, questId, statusMapping) => { + let questData = QUEST_TRACKER_globalQuestData[questId]; + let fieldData = questData[fieldName] || {}; + let isDropdownDisabled = Object.keys(statusMapping).length === 0; + let buttonStyle = isDropdownDisabled ? `${styles.buttonDisabled}` : `${styles.button}`; + let spanOrAnchor = isDropdownDisabled ? `span` : `a`; + let fieldDataLowercaseKeys = Object.keys(fieldData).reduce((acc, key) => { + acc[key.toLowerCase()] = fieldData[key]; + return acc; + }, {}); + let tableRows = Object.keys(statusMapping).map(statusKey => { + let statusName = statusMapping[statusKey]; + let dateValue = fieldDataLowercaseKeys[statusName.toLowerCase()] || "No Date"; + let changeDateContent = `?{Change Date for ${statusName}|${dateValue}}`; + if (fieldDataLowercaseKeys[statusName.toLowerCase()]) { + return ` + + ${statusName}
    ${dateValue} + + <${spanOrAnchor} style="${buttonStyle} ${styles.smallButton}" href="!qt-quest action=update|field=${fieldName}|current=${questId}|old=${statusName}|new=${changeDateContent}">c + + + - + + `; + } else { + return ` + + ${statusName}
    ${dateValue} + + <${spanOrAnchor} style="${buttonStyle} ${styles.smallButton}" href="!qt-quest action=add|field=${fieldName}|current=${questId}|old=${statusName}|new=?{Add Date for ${statusName}}">+ + + `; + } + }).join(''); + return ` +

    ${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}


    + + ${tableRows} +
    `; + }, + calculateStartingGroupNum: (conditions, isInLogicGroup = false) => { + let count = 0; + if (isInLogicGroup) return count; + for (let i = 0; i < conditions.length; i++) { + if (typeof conditions[i] === 'object' && conditions[i].logic) { + break; + } + if (typeof conditions[i] === 'string') { + count++; + } + } + return count; + }, + calculateGroupNum: (condition, conditions, groupnum) => { + let count = 0; + for (let i = 0; i < conditions.length; i++) { + if (conditions[i] === condition) { + break; + } + if (typeof conditions[i] === 'object' && conditions[i].logic) { + count++; + } + } + return groupnum + count; + }, + formatConditions: (questId, conditions, parentLogic = 'AND', indent = false, groupnum = 0, isInLogicGroup = false) => { + if (!Array.isArray(conditions)) return ''; + let spanOrAnchor = `${H.buildDropdownString(questId) === '' ? 'span' : 'a'}`; + let renderButtonStyle = `${H.buildDropdownString(questId) === '' ? styles.buttonDisabled : styles.button}`; + groupnum += H.calculateStartingGroupNum(conditions, isInLogicGroup); + return conditions.map((condition, index) => { + const currentGroupNum = H.calculateGroupNum(condition, conditions, groupnum); + const displayIndex = index + 1; + const isLastCondition = displayIndex === conditions.length; + const isLastnonGroupCondition = (index + 1 < conditions.length && typeof conditions[index + 1] === 'object') || index === conditions.length - 1; + const isOnlyGroupCondition = conditions.length === 1 && typeof conditions[0] === 'object'; + if (typeof condition === 'string') { + return ` + + ${indent ? ` ` : ``} + ${H.getQuestName(condition)} + + + c + + + - + + + ${indent && isLastCondition ? ` + +   + + Add Relationship + + + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=group|groupnum=${currentGroupNum}|quest=?{Choose Quest|${H.buildDropdownString(questId)}}" style="${renderButtonStyle} ${styles.smallButton}">+ + + + ` : ''} + ${!indent && isLastnonGroupCondition ? ` + + + Add Relationship + + + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=?{Choose Quest|${H.buildDropdownString(questId)}}" style="${renderButtonStyle} ${styles.smallButton}">+ + + + ` : ''} + `; + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + const subLogic = H.formatConditions(questId, condition.conditions, condition.logic, true, currentGroupNum, true); + const reverseLogic = condition.logic === 'AND' ? 'OR' : 'AND'; + let addRelasionshipRow = '' + if (currentGroupNum === 0) { + addRelasionshipRow += ` + + + Add Relationship + + + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=?{Choose Quest|${H.buildDropdownString(questId)}}" style="${renderButtonStyle} ${styles.smallButton}">+ + + `; + } + return ` + ${addRelasionshipRow} + +   + ${condition.logic} + + + c + + + - + + + ${subLogic} + `; + } + }).join(''); + }, + buildDropdownString: (questId) => { + if (!Quest.getValidQuestsForDropdown(questId)) return ''; + else { + const validQuests = Quest.getValidQuestsForDropdown(questId); + if (validQuests.length === 1) return validQuests[0]; + validQuests.sort((a, b) => H.getQuestName(a).localeCompare(H.getQuestName(b))); + const dropdownString = validQuests.map(questId => { + return `${H.getQuestName(questId)},${questId}`; + }).join('|'); + return `?{Choose Quest|${dropdownString}}`; + } + }, + getQuestName: (questId) => { + return QUEST_TRACKER_globalQuestData[questId]?.name || 'Unnamed Quest'; + }, + relationshipMenu: (questId) => { + const quest = QUEST_TRACKER_globalQuestData[questId]; + let htmlOutput = ""; + let spanOrAnchor = `${H.buildDropdownString(questId) === '' ? 'span' : 'a'}`; + let renderButtonStyle = `${H.buildDropdownString(questId) === '' ? styles.buttonDisabled : styles.button}`; + if (!quest || !quest.relationships || !Array.isArray(quest.relationships.conditions) || quest.relationships.conditions.length === 0) { + htmlOutput += `
    + + + + + + + + +
    + Add Relationship + + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=single|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ +
    Add Relationship Group + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=addgroup|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ +
    `; + } else { + const conditionsHtml = H.formatConditions(questId, quest.relationships.conditions, quest.relationships.logic || 'AND'); + htmlOutput += ` + + ${quest.relationships.conditions.length > 1 ? ` + + + ` : ''} + ${conditionsHtml} + + + + +
    + ${quest.relationships.logic || 'AND'} + + c +
    + Add Relationship Group + + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=addgroup|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ +
    `; + } + let mutuallyExclusiveHtml = ""; + if (Array.isArray(quest.relationships.mutually_exclusive) && quest.relationships.mutually_exclusive.length > 0) { + mutuallyExclusiveHtml += quest.relationships.mutually_exclusive.map(exclusive => ` + + + ${H.getQuestName(exclusive)} + + + c + + + - + + + `).join(''); + } else { + mutuallyExclusiveHtml += `No mutually exclusive quests available.`; + } + htmlOutput += ` +
    +

    Mutually Exclusive Quests

    + + ${mutuallyExclusiveHtml} + + + + +
    + <${spanOrAnchor} href="!qt-questrelationship currentquest=${questId}|action=add|type=mutuallyexclusive|quest=${H.buildDropdownString(questId)}" style="${renderButtonStyle} ${styles.smallButton}">+ +
    `; + return htmlOutput; + }, + getValidQuestGroups: (questId) => { + let result = ''; + const quest = QUEST_TRACKER_globalQuestData[questId]; + const questGroupsTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!questGroupsTable) return result; + const questGroups = findObjs({ type: 'tableitem', rollabletableid: questGroupsTable.id }); + if (quest && quest.group) { + if (questGroups.length === 1) { + return "remove"; + } + else { + result += 'Remove from Group,remove|'; + } + } + result += questGroups + .filter(group => parseInt(quest.group) !== parseInt(group.get('weight'))) + .map(group => `${group.get('name')},${group.get('weight')}`) + .join('|'); + if (result.includes('|')) return "?{Change Quest Grouping|" + result + "}"; + else { + const [f,s] = result.split(','); + return s; + } + }, + getQuestGroupNameByWeight: (weight) => { + if (!weight) return 'No Assigned Group'; + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) { + Utils.sendGMMessage('Error: Quest Groups table not found. Please check if the table exists in the game.'); + return null; + } + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + let group = groupItems.find(item => item.get('weight') == weight); + return group.get('name'); + }, + showUpcomingEvents: () => { + const upcomingEvents = Calendar.getNextEvents(5); + let menu = ""; + if (upcomingEvents.length === 0) { + menu += `
      +
    • + + No Upcoming Events + +
    • +
    `; + } else { + menu += `
      `; + upcomingEvents.forEach((event, index) => { + const [date, name] = event; + const eventId = `event-${index}`; + menu += `
    • + + ${name} +
      + ${date} +
      `; + if (index === 0) { + menu += ` + + Advance + `; + } + menu += `
    • `; + }); + menu += `
    `; + } + + return menu; + }, + buildFrequencyDropdown: () => { + const dropdownString = Object.entries(frequencyMapping) + .map(([key, value]) => `|${value},${key}`) + .join(''); + return dropdownString; + }, + buildLocationDropdown: () => { + const dropdownString = Object.entries(WEATHER.enviroments) + .map(([key, value]) => `|${value.name},${key}`) + .join(''); + return dropdownString; + }, + buildCalenderDropdown: () => { + const dropdownString = Object.entries(CALENDARS) + .map(([key, value]) => `|${value.name},${key}`) + .join(''); + return dropdownString; + }, + buildClimateDropdown: () => { + const currentCalendar = CALENDARS[QUEST_TRACKER_calenderType]; + const dropdownString = Object.keys(currentCalendar.climates) + .map((climate) => `|${climate.charAt(0).toUpperCase() + climate.slice(1)},${climate}`) + .join(""); + return dropdownString; + } + }; + const buildWeather = (isMenu = false, isHome = false) => { + const FromValue = { + temperature: (x) => { + const celsius = ((-0.0113 * x * x) + (2.589 * x) - 89.2).toFixed(1); + const fahrenheit = ((celsius * 9 / 5) + 32).toFixed(1); + return { celsius: parseFloat(celsius), fahrenheit: parseFloat(fahrenheit) }; + }, + humidity: (x) => { + const k = 0.1; + const c = 50; + const humidity = 100 / (1 + Math.exp(-k * (x - c))); + return parseFloat(Math.max(humidity, 0).toFixed(1)); + }, + precipitation: (x) => { + const k = 0.04; + const maxPrecipitation = 500; + const center = 50; + const precipitationMm = maxPrecipitation * (Math.exp(k * (x - center)) - 1) / (Math.exp(k * (100 - center)) - 1); + const precipitationInches = precipitationMm * 0.0393701; + return { + mm: parseFloat(Math.max(precipitationMm, 0).toFixed(1)), + inches: parseFloat(Math.max(precipitationInches, 0).toFixed(1)) + }; + }, + windSpeed: (x) => { + const maxSpeed = 400; + const a = 5; + const c = 400; + const windSpeedKmh = (c / (1 + Math.exp(-0.2 * (x - 70)))) + (a * Math.pow(Math.max(x - 40, 0), 1.5)) / 50; + const windSpeedMph = windSpeedKmh * 0.621371; + return { + kmh: parseFloat(windSpeedKmh.toFixed(1)), + mph: parseFloat(windSpeedMph.toFixed(1)) + }; + }, + visibility: (x) => { + const maxDistanceMeters = 50000; + const visibilityMeters = maxDistanceMeters * (x / 100); + let result = { + imperial: {}, + metric: {} + }; + if (visibilityMeters <= 100) { + result.metric.distance = parseFloat(visibilityMeters.toFixed(1)); + result.metric.unit = "m"; + } else { + result.metric.distance = parseFloat((visibilityMeters / 1000).toFixed(1)); + result.metric.unit = "km"; + } + const visibilityFeet = visibilityMeters * 3.28084; + if (visibilityFeet <= 100) { + result.imperial.distance = parseFloat(visibilityFeet.toFixed(1)); + result.imperial.unit = "\""; + } else if (visibilityFeet <= 300) { + result.imperial.distance = parseFloat(visibilityFeet.toFixed(1)); + result.imperial.unit = "\""; + } else { + result.imperial.distance = parseFloat((visibilityFeet / 5280).toFixed(1)); + result.imperial.unit = "mi"; + } + return result; + } + }; + const temperatureValue = FromValue.temperature(QUEST_TRACKER_CURRENT_WEATHER['rolls']['temperature']); + const windSpeedValue = FromValue.windSpeed(QUEST_TRACKER_CURRENT_WEATHER['rolls']['wind']); + const precipitationValue = FromValue.precipitation(QUEST_TRACKER_CURRENT_WEATHER['rolls']['precipitation']); + const visibilityValue = FromValue.visibility(QUEST_TRACKER_CURRENT_WEATHER['rolls']['visibility']); + const humidityDisplay = FromValue.humidity(QUEST_TRACKER_CURRENT_WEATHER['rolls']['humidity']); + const temperatureDisplay = QUEST_TRACKER_imperialMeasurements['temperature'] ? temperatureValue['fahrenheit'] + "°F" : temperatureValue['celsius'] + "°C"; + const windSpeedDisplay = QUEST_TRACKER_imperialMeasurements['wind'] ? windSpeedValue['mph'] + "mph" : windSpeedValue['kmh'] + "kmh"; + const precipitationDisplay = QUEST_TRACKER_imperialMeasurements['precipitation'] ? precipitationValue['inches'] + "'" : precipitationValue['mm'] + "mm"; + const cloudCoverDisplay = QUEST_TRACKER_CURRENT_WEATHER['rolls']['cloudCover']; + const visibilityDisplay = QUEST_TRACKER_imperialMeasurements['wind'] ? visibilityValue['imperial']['distance'] + visibilityValue['metric']['unit'] : visibilityValue['metric']['unit'] + visibilityValue['imperial']['unit']; + const locationDropdown = H.buildLocationDropdown(); + const returnto = isMenu ? "menu=true|" : isHome ? "home=true|" : ""; + let menu = ` + + + + + + + + + + + + + + + + + + +
      
    Weather
    ${QUEST_TRACKER_CURRENT_WEATHER['weatherType']}
    Lunar Phase
    ${Calendar.getLunarPhase(QUEST_TRACKER_currentDate)}
    Location
    ${QUEST_TRACKER_WeatherLocation}Change
    Temperature${temperatureDisplay}
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['temperature']}
    Precipitation${precipitationDisplay}
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['precipitation']}
    Wind${windSpeedDisplay}
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['wind']}
    Humidity${humidityDisplay}%
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['humidity']}
    Cloud Cover${cloudCoverDisplay}%
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['cloudCover']}
    Visibility${visibilityDisplay}
    ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['visibility']}
    `; + if (!isMenu) { + let newMenu = `

    Weather

    `; + newMenu += menu; + newMenu += `
    `; + newMenu = newMenu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(newMenu); + } + else { + return menu; + } + }; + const displayQuestRelationships = (questId) => { + const d = { + drawLine: (type, depth, half = false, flip = false) => { + let style = ""; + switch (type) { + case 'r': + style = `${styles.lineHorizontalRed} top: ${26 + (depth * 26)}px`; + return `
    `; + case 'v': + style = `${styles.verticalLineStyle} height: 16px; left:${half ? 38 : 13}px; top:${38 + (depth * 16)}px`; + return `
    `; + case 'h': + style = `${styles.lineHorizontal} top: ${52 + (depth * 16)}px; width:${half ? 26 : 52}px; left:${flip ? 39 : 13}px`; + return `
    `; + } + }, + drawQuestBox: (content, columnInstructions = [], depth = false) => { + const renderInstructions = columnInstructions.map(instruction => { + const { type, depth, center, flip } = instruction; + return d.drawLine(type, depth, center, flip); + }).join(''); + return ` +
  • +
    + ${content} +
    + ${renderInstructions} +
  • `; + } + }; + const l = { + checkMutualExclusivity: (questIds) => { + const questData = QUEST_TRACKER_globalQuestData[questIds[0].toLowerCase()]; + if (!questData || !questData.relationships || !Array.isArray(questData.relationships.mutually_exclusive)) { + return false; + } + const mutuallyExclusiveList = questData.relationships.mutually_exclusive; + return mutuallyExclusiveList.includes(questIds[1]); + }, + processConditions: (conditions, parentLogic = 'AND') => { + const flattenedArray = []; + if (!Array.isArray(conditions)) return flattenedArray; + + conditions.forEach((condition, index) => { + if (typeof condition === 'string') { + flattenedArray.push(condition); + if (index < conditions.length - 1) { + flattenedArray.push(parentLogic); + } + } else if (typeof condition === 'object' && condition.logic && Array.isArray(condition.conditions)) { + condition.conditions.forEach((subCondition, subIndex) => { + if (typeof subCondition === 'string') { + flattenedArray.push(subCondition); + if (subIndex < condition.conditions.length - 1) { + flattenedArray.push(condition.logic); + } + } + }); + if (index < conditions.length - 1) { + flattenedArray.push(parentLogic); + } + } + }); + return flattenedArray; + }, + traverseLogicTree: (conditions, depth = 0, columnOffset = 0, depthMap = {}, parentLogic = 'AND') => { + if (!depthMap[depth]) depthMap[depth] = []; + let column = columnOffset; + conditions.forEach((condition) => { + if (typeof condition === 'string') { + depthMap[depth].push({ type: 'quest', value: condition, logic: parentLogic, depth, column }); + column++; + } else if (typeof condition === 'object' && condition.logic) { + const subColumnsStart = column; + const subColumnsEnd = column + condition.conditions.length - 1; + const nextDepth = depth + 1; + l.traverseLogicTree(condition.conditions, nextDepth, column, depthMap, condition.logic); + depthMap[depth].push({ type: 'logic', logic: condition.logic, conditions: condition.conditions.map(cond => (typeof cond === 'string' ? cond : cond.conditions)), depth, column: subColumnsStart, endColumn: subColumnsEnd, }); + column = subColumnsEnd + 1; + } + }); + questLayers = depthMap; + return { depthMap }; + }, + connectHorizontalLines: (depthMap, instructionsPerColumn) => { + const depth0Elements = depthMap['0'] ? depthMap['0'] : []; + if (depth0Elements.length + (depthMap['1'] ? depthMap['1'].length : 0) <= 1) return; + const depth0Groups = depth0Elements.filter(el => el.type === 'logic') + .map(el => ({ column: el.column, endColumn: el.endColumn, logic: el.logic, conditions: el.conditions })); + depth0Groups.forEach(group => { + for (let col = group.column; col < group.endColumn; col++) { + if (!instructionsPerColumn[col]) instructionsPerColumn[col] = []; + instructionsPerColumn[col].push({ type: 'h', depth: 0, center: false }); + } + }); + if (!depthMap['1']) { + const allColumns = depth0Elements.flatMap(el => el.type === 'logic' ? [el.column, el.endColumn] : [el.column]); + const startColumn = Math.min(...allColumns); + const endColumn = Math.max(...allColumns); + for (let col = startColumn; col < endColumn; col++) { + if (!instructionsPerColumn[col]) instructionsPerColumn[col] = []; + instructionsPerColumn[col].push({ type: 'h', depth: 0, center: false }); + } + const baseLogic = depthMap['0'].length && depthMap['0'][0].logic; + return; + } + const allColumns = [ + ...depth0Elements.flatMap(el => el.type === 'logic' ? [el.column, el.endColumn] : [el.column]), + ...depthMap['1'].map(el => el.column) + ]; + const lastDepth0LogicGroup = depth0Groups.reduce((lastGroup, group) => { + return group.endColumn > lastGroup.endColumn ? group : lastGroup; + }, { endColumn: -1, conditions: [] }); + const groupSize = lastDepth0LogicGroup.conditions.length; + if (allColumns.length > 1) { + const startColumn = Math.min(...allColumns); + const endColumn = Math.max(...allColumns); + for (let col = startColumn; col < endColumn; col++) { + if (!instructionsPerColumn[col]) instructionsPerColumn[col] = []; + let lineInstruction; + if (col < endColumn - 1) { + lineInstruction = { type: 'h', depth: 1, center: false }; + } else if (col === endColumn - 1) { + lineInstruction = { type: 'h', depth: 1, center: groupSize % 2 === 0 }; + } else { + continue; + } + + instructionsPerColumn[col].push(lineInstruction); + } + } + }, + addOrIndicators: (elements, instructionsPerColumn, depth) => { + elements.forEach((element) => { + if (element.type === 'logic' && element.logic === 'OR' && l.checkMutualExclusivity(element.conditions)) { + for (let col = element.column; col < element.endColumn; col++) { + if (!instructionsPerColumn[col]) instructionsPerColumn[col] = []; + instructionsPerColumn[col].push({ type: 'r', depth, center: false }); + } + } + }); + }, + addCenterVerticalLine: (totalColumns, depth, instructionsPerColumn, startColumn = 0) => { + const centerColumn = (totalColumns % 2 === 0) + ? startColumn + Math.floor((totalColumns - 1) / 2) + : startColumn + Math.floor(totalColumns / 2); + if (!instructionsPerColumn[centerColumn]) instructionsPerColumn[centerColumn] = []; + instructionsPerColumn[centerColumn].push({ type: 'v', depth, center: totalColumns % 2 === 0 }); + }, + buildVerticalLines: (depthMap, instructionsPerColumn) => { + if (Array.isArray(depthMap['0'])) { + const totalColumns = depthMap['0'].reduce((count, element) => { + if (element.type === 'quest') { + return count + 1; + } else if (element.type === 'logic' && Array.isArray(element.conditions)) { + return count + element.conditions.length; + } + return count; + }, 0); + for (let column = 0; column < totalColumns; column++) { + if (!instructionsPerColumn[column]) instructionsPerColumn[column] = []; + instructionsPerColumn[column].push({ type: 'v', depth: 0, center: false }); + } + if (!depthMap['1']) { + l.addCenterVerticalLine(totalColumns, 1, instructionsPerColumn); + } + } + if (Array.isArray(depthMap['1']) && Array.isArray(depthMap['0'])) { + depthMap['0'].forEach((element) => { + if (element.type === 'logic') { + const startColumn = element.column; + l.addCenterVerticalLine(element.conditions.length, 1, instructionsPerColumn, startColumn); + } else if (element.type === 'quest') { + const column = element.column; + if (!instructionsPerColumn[column]) instructionsPerColumn[column] = []; + instructionsPerColumn[column].push({type: 'v', depth: 1, center: false}); + } + }); + const totalQuestCount = depthMap['0'].reduce((count, element) => { + return count + (element.type === 'quest' ? 1 : element.conditions.length); + }, 0); + l.addCenterVerticalLine(totalQuestCount, 2, instructionsPerColumn); + } + }, + buildQuestTreeBottomUp: (relationships, currentDepth = 0) => { + const { depthMap } = l.traverseLogicTree(relationships.conditions, currentDepth, 0, {}, relationships.logic || 'AND'); + const instructionsPerColumn = []; + l.buildVerticalLines(depthMap, instructionsPerColumn); + const depths = Object.keys(depthMap).sort((a, b) => b - a); + depths.forEach((depth) => { + const elements = depthMap[depth]; + l.addOrIndicators(elements, instructionsPerColumn, parseInt(depth)); + }); + l.connectHorizontalLines(depthMap, instructionsPerColumn); + return instructionsPerColumn; + }, + buildQuestListHTML: (flattenedLogic, columnInstructionsMap, depth = 0) => { + let questListHTML = `
      `; + let questIndex = 0; + flattenedLogic.forEach((item, index) => { + const instructions = columnInstructionsMap[questIndex] || []; + if (item !== 'AND' && item !== 'OR') { + questListHTML += d.drawQuestBox('P', instructions, depth); + questIndex++; + } + }); + questListHTML += '
    '; + return questListHTML; + } + }; + const quest = QUEST_TRACKER_globalQuestData[questId]; + let questLayers = {}; + if (!quest || !quest.relationships || !Array.isArray(quest.relationships.conditions) || quest.relationships.conditions.length === 0) { + return `
      ${d.drawQuestBox("Q", [])}
    `; + } + else { + const flattenedLogic = l.processConditions(quest.relationships.conditions, quest.relationships.logic || 'AND'); + const columnInstructionsMap = l.buildQuestTreeBottomUp(quest.relationships); + let html = `
    `; + html += l.buildQuestListHTML(flattenedLogic, columnInstructionsMap, 0); + html += ` +
      + ${d.drawQuestBox("Q", [], questLayers['1'] ? true : false)} +
    + `; + html += '
    '; + return html; + } + }; + const generateGMMenu = () => { + let menu = `

    Calendar

    `; + menu += `
    ${Calendar.formatDateFull()}
    ( ${QUEST_TRACKER_currentDate} )`; + if (QUEST_TRACKER_WEATHER && QUEST_TRACKER_CURRENT_WEATHER !== null) { + menu += buildWeather({ isMenu: true }); + } + menu += `

    Adjust Date`; + menu += `

    Active Quests

    `; + menu += H.showActiveQuests(); + menu += `
    Show All Quests`; + menu += `

    Active Rumours

    `; + menu += H.showActiveRumours(); + menu += `
    Show All Rumours`; + menu += `

    Upcoming Events

    `; + menu += H.showUpcomingEvents(); + menu += `
    Show All Events`; + menu += `

    Configuration`; + menu += `
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showAllQuests = () => { + let menu = `

    All Quests

    `; + if (Object.keys(QUEST_TRACKER_globalQuestData).length === 0) { + menu += ` +

    There doesn't seem to be any Quests, you need to create a quest or Import from the Handouts.

    + `; + } else { + let groupedQuestsByGroup = {}; + QUEST_TRACKER_globalQuestArray.forEach(quest => { + let questData = QUEST_TRACKER_globalQuestData[quest.id]; + if (questData) { + questData = Object.keys(questData).reduce((acc, key) => { + acc[key.toLowerCase()] = questData[key]; + return acc; + }, {}); + const group = H.getQuestGroupNameByWeight(questData.group) || 'Ungrouped'; + const visibilityGroup = questData.hidden ? 'hidden' : 'visible'; + if (!groupedQuestsByGroup[group]) { + groupedQuestsByGroup[group] = { + visible: {}, + hidden: {} + }; + } + if (!groupedQuestsByGroup[group][visibilityGroup][quest.weight]) { + groupedQuestsByGroup[group][visibilityGroup][quest.weight] = []; + } + groupedQuestsByGroup[group][visibilityGroup][quest.weight].push(quest); + } + }); + Object.keys(groupedQuestsByGroup).forEach(group => { + menu += `

    ${group}

    `; + menu += H.generateQuestList('Visible', groupedQuestsByGroup[group].visible); + menu += H.generateQuestList('Hidden', groupedQuestsByGroup[group].hidden); + }); + } + menu += ` +

    + + Quest Groups +   + Add New Quest + +

    + Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showAllRumours = () => { + let menu = `

    All Rumours

    `; + menu += `

    This menu displays all the rumours currently associated with quests in the game. Use the options below to navigate through the locations and statuses to add new rumours or modify existing ones.

    `; + if (Object.keys(QUEST_TRACKER_globalQuestData).length === 0) { + menu += ` +

    There are no quests available. You need to create a quest or import quests from the handouts.

    + `; + } else { + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + let rumourCount = 0; + let questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + Object.keys(questRumours).forEach(status => { + let locationRumours = questRumours[status] || {}; + Object.keys(locationRumours).forEach(location => { + rumourCount += Object.keys(locationRumours[location] || {}).length; + }); + }); + let questData = QUEST_TRACKER_globalQuestData[questId] || {}; + let questName = questData.name || `Quest: ${questId}`; + menu += `
    + ${questName}
    ${rumourCount} rumours
    + + Show + +
    `; + }); + } + menu += ` +

    + Rumour Locations +   + Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showQuestRumourByStatus = (questId) => { + let questData = QUEST_TRACKER_globalQuestData[questId]; + const questDisplayName = questData && questData.name ? questData.name : `Quest: ${questId}`; + let menu = `

    Rumours for ${questDisplayName}

    `; + menu += `

    ${questData.description}

    `; + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + const allStatuses = Object.values(statusMapping); + if (allStatuses.length > 0) { + menu += `

    `; + allStatuses.forEach(status => { + const rumoursByLocation = questRumours[status.toLowerCase()] || {}; + const rumourCount = Object.values(rumoursByLocation).reduce((count, locationRumours) => { + return count + Object.keys(locationRumours).length; + }, 0); + menu += ` + + + + `; + }); + menu += `
    ${status}
    ${rumourCount} rumours
    + Show +

    `; + } else { + menu += ` +

    There are no rumours available; either refresh the data, or start adding manually.

    +

    + Location Management +

    + Import Quest and Rumour Data + `; + } + menu += ` +

    + + All Rumours +   + Main Menu + +

    +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showRumourDetails = (questId, statusId) => { + const questData = QUEST_TRACKER_globalQuestData[questId]; + const questDisplayName = questData && questData.name ? questData.name : `Quest: ${questId}`; + const statusName = statusMapping[statusId] || statusId; + let menu = `

    Rumours for ${questDisplayName}

    Status: ${statusName}

    `; + menu += `

    This menu displays all the rumours currently associated with ${questDisplayName} under the status "${statusName}". Use the options below to update, add, or remove rumours.

    To add new lines into the rumours use %NEWLINE%. To add in quotation marks you need to use &quot;.



    `; + const locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (!locationTable) { + menu += ` +

    Error: Locations table not found. Please check if the table exists in the game.

    +

    + Location Management +

    + Import Quest and Rumour Data +
    `; + Utils.sendGMMessage(menu.replace(/[\r\n]/g, '')); + return; + } + const locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + const locationMapping = {}; + locationItems.forEach(location => { + const locationName = location.get('name'); + const sanitizedLocationName = Utils.sanitizeString(locationName.toLowerCase()); + locationMapping[sanitizedLocationName] = { + originalName: locationName, + sanitizedName: sanitizedLocationName, + weight: location.get('weight') + }; + }); + const questRumours = QUEST_TRACKER_globalRumours[questId] || {}; + const rumoursByStatus = questRumours[statusId.toLowerCase()] || {}; + Object.keys(locationMapping).forEach(sanitizedLocationName => { + const { originalName, weight } = locationMapping[sanitizedLocationName]; + const locationRumours = rumoursByStatus[sanitizedLocationName] || {}; + menu += `

    ${originalName}

    `; + if (Object.keys(locationRumours).length > 0) { + Object.keys(locationRumours).forEach(rumourId => { + const rumourText = locationRumours[rumourId]; + let trimmedRumourText = String(rumourText).substring(0, 50); + let rumourTextSanitized = rumourText + .replace(/"/g, '"') + .replace(/%NEWLINE%|
    /g, ' | '); + let rumourInputSanitized = rumourText + .replace(/"/g, '"') + .replace(/
    /g, '%NEWLINE%'); + menu += ` + + + + + + `; + }); + } else { + menu += ` + + + `; + } + menu += ` + + + + +
    ${trimmedRumourText} + + + c + + - +
    No rumours
    + + +
    `; + }); + menu += ` +

    + + By Status +   + All Rumours +   + Main Menu + +

    + `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showQuestDetails = (questId) => { + let quest = QUEST_TRACKER_globalQuestData[questId]; + if (!quest) { + Utils.sendGMMessage(`Error: Quest "${questId}" not found.`); + return; + } + let statusName = Quest.getStatusNameByQuestId(questId, QUEST_TRACKER_globalQuestArray); + quest = Utils.normalizeKeys(quest); + let hiddenStatus = quest.hidden ? 'Yes' : 'No'; + let questGroup = H.getQuestGroupNameByWeight(quest.group); + let hiddenStatusTorF = quest.hidden ? 'true' : 'false'; + let hiddenStatusTorF_reverse = quest.hidden ? 'false' : 'true'; + let relationshipsHtml = displayQuestRelationships(questId); + let relationshipMenuHtml = H.relationshipMenu(questId); + let validQuestGrouping = H.getValidQuestGroups(questId); + let menu = ` +
    +

    ${quest.name || 'Unnamed Quest'}

    +

    ${quest.description || 'No description available.'}

    + + Edit Title +   + Edit Description + +
    +

    Relationships

    + ${relationshipsHtml} + ${relationshipMenuHtml} +

    Status


    + ${statusName} + + Change + +

    Hidden


    + ${hiddenStatus} + + Change + +

    Quest Group


    + ${questGroup} + + Adjust + + ${H.formatAutocompleteListWithDates('autoadvance', questId, statusMapping)} +

    + Show All Quests Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const manageRumourLocations = () => { + let menu = `

    Manage Rumour Locations

    `; + let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; + if (!locationTable) { + menu += `

    Error: Locations table not found. Please check if the table exists in the game.

    `; + Utils.sendGMMessage(menu.replace(/[\r\n]/g, '')); + return; + } + let locationItems = findObjs({ type: 'tableitem', rollabletableid: locationTable.id }); + let uniqueLocations = new Set(); + locationItems.sort((a, b) => a.get('weight') - b.get('weight')).forEach(location => { + let locationName = location.get('name'); + let locationKey = locationName.toLowerCase(); + let locationId = location.get('weight'); + if (!uniqueLocations.has(locationKey)) { + uniqueLocations.add(locationKey); + let rumourCount = QUEST_TRACKER_rumoursByLocation[locationKey] ? Object.keys(QUEST_TRACKER_rumoursByLocation[locationKey]).length : 0; + let showButtons = !(locationId === 1 || locationName.toLowerCase() === 'everywhere'); + menu += `
  • + ${locationName}
    ${rumourCount} Rumours
    + `; + if (showButtons) { + menu += `c + -`; + } + menu += `
  • `; + } + }); + menu += `
    Add New Location`; + menu += `

    Back to Rumours`; + Utils.sendGMMessage(menu.replace(/[\r\n]/g, '')); + }; + const manageQuestGroups = () => { + let menu = `

    Manage Quest Groups

    `; + let groupTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]; + if (!groupTable) { + menu += `

    Error: Quest Groups table not found. Please check if the table exists in the game.

    `; + } + else { + let groupItems = findObjs({ type: 'tableitem', rollabletableid: groupTable.id }); + let uniqueGroups = new Set(); + groupItems.sort((a, b) => a.get('weight') - b.get('weight')).forEach(group => { + let groupName = group.get('name'); + let groupKey = groupName.toLowerCase(); + let groupId = group.get('weight'); + if (!uniqueGroups.has(groupKey)) { + uniqueGroups.add(groupKey); + let questCount = 0; + Object.keys(QUEST_TRACKER_globalQuestData).forEach(questId => { + let questData = QUEST_TRACKER_globalQuestData[questId]; + if (questData.group && parseInt(questData.group) === parseInt(groupId)) { + questCount++; + } + }); + let plural = (questCount === 1) ? '' : 's'; + menu += `
  • + ${groupName}
    ${questCount} Quest${plural}
    + `; + menu += `c + -`; + menu += `
  • `; + } + }); + } + menu += `
    Add New Group`; + menu += `

    Back to Quests`; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const adminMenu = () => { + let menu = `

    Quest Tracker Configuration

    `; + let RefreshImport = "Import"; + if (Object.keys(QUEST_TRACKER_globalQuestData).length !== 0) { + RefreshImport = "Refresh"; + } + const calenderDropdown = H.buildCalenderDropdown(); + const climateDropdown = H.buildClimateDropdown(); + menu += `

    Settings

    Toggle Readable JSON (${QUEST_TRACKER_readableJSON === true ? 'on' : 'off'})`; + // menu += `
    Toggle JumpGate (${QUEST_TRACKER_jumpGate === true ? 'on' : 'off'})`; + menu += `
    Toggle Verbose Errors (${QUEST_TRACKER_verboseErrorLogging === true ? 'on' : 'off'})`; + menu += `

    Data

    ${RefreshImport} JSON Data`; + menu += `
    Reset to Defaults`; + menu += `

    Quest Tree

    Build Quest Tree Page`; + menu += `

    Calander

    Calendar: ${CALENDARS[QUEST_TRACKER_calenderType]?.name || "Unknown Calendar"}`; + menu += `

    Weather


    Toggle Weather (${QUEST_TRACKER_WEATHER === true ? 'on' : 'off'})`; + if (QUEST_TRACKER_WEATHER) { + menu += `
    Climate: ${QUEST_TRACKER_Location}`; + menu += `

    Weather Trends


    Dry: ${QUEST_TRACKER_WEATHER_TRENDS['dry'] || 0}`; + menu += `
    Wet: ${QUEST_TRACKER_WEATHER_TRENDS['wet'] || 0}`; + menu += `
    Heat: ${QUEST_TRACKER_WEATHER_TRENDS['heat'] || 0}`; + menu += `
    Cold: ${QUEST_TRACKER_WEATHER_TRENDS['cold'] || 0}`; + menu += `
    Wind: ${QUEST_TRACKER_WEATHER_TRENDS['wind'] || 0}`; + menu += `
    Humidity: ${QUEST_TRACKER_WEATHER_TRENDS['humid'] || 0}`; + menu += `
    Fog: ${QUEST_TRACKER_WEATHER_TRENDS['visibility'] || 0}`; + menu += `
    Cloud Cover: ${QUEST_TRACKER_WEATHER_TRENDS['cloudy'] || 0}`; + menu += `

    Forced Weather Trends


    Dry: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['dry'] || 'False'}`; + menu += `
    Wet: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['wet'] || 'False'}`; + menu += `
    Heat: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['heat'] || 'False'}`; + menu += `
    Cold: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['cold'] || 'False'}`; + menu += `
    Wind: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['wind'] || 'False'}`; + menu += `
    Humidity: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['humid'] || 'False'}`; + menu += `
    Visibility: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['visibility'] || 'False'}`; + menu += `
    Cloud Cover: ${QUEST_TRACKER_FORCED_WEATHER_TRENDS['cloudy'] || 'False'}`; + menu += `

    Imperial Measurements


    Temperature: ${QUEST_TRACKER_imperialMeasurements['temperature'] || 'False'}`; + menu += `
    Precipitation: ${QUEST_TRACKER_imperialMeasurements['precipitation'] || 'False'}`; + menu += `
    Wind: ${QUEST_TRACKER_imperialMeasurements['wind'] || 'False'}`; + menu += `
    Visibility: ${QUEST_TRACKER_imperialMeasurements['visibility'] || 'False'}`; + } + menu += `

    Back to Main Menu`; + menu += `
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showAllEvents = () => { + let menu = `

    All Events

    `; + if (Object.keys(QUEST_TRACKER_Events).length === 0) { + menu += ` +

    There doesn't seem to be any Events, you need to create a quest or Import from the Handouts.

    + `; + } else { + menu += `
      `; + Object.keys(QUEST_TRACKER_Events).forEach(eventId => { + const event = QUEST_TRACKER_Events[eventId]; + const name = event.name; + const date = event.date; + menu += ` +
    • + + ${name} +
      + ${date} +
      + + Inspect + x + +
    • + `; + }); + menu += `
    `; + } + menu += ` +

    + + Add New Event + +

    + Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const showEventDetails = (eventid) => { + let event = QUEST_TRACKER_Events[eventid]; + if (!event) { + Utils.sendGMMessage(`Error: Event "${eventid}" not found.`); + return; + } + let hiddenStatus = event.hidden ? 'Yes' : 'No'; + let hiddenStatusTorF = event.hidden ? 'true' : 'false'; + let hiddenStatusTorF_reverse = event.hidden ? 'false' : 'true'; + let repeatStatus = event.repeatable ? 'Yes' : 'No'; + let repeatStatusTorF = event.repeatable ? 'true' : 'false'; + let repeatStatusTorF_reverse = event.repeatable ? 'false' : 'true'; + const frequencyDropdown = H.buildFrequencyDropdown(); + const showFrequency = event.repeatable ? `

    Frequency: ${frequencyMapping[event.frequency]}Adjust` : ''; + let menu = ` +
    +

    ${event.name || 'Unnamed Event'}

    +

    ${event.description || 'No description available.'}

    + + Edit Event Name +   + Edit Description + +
    +

    ${event.repeatable ? 'Starting ' : ''}Date


    + ${event.date} + + Change + +
    +

    Hidden


    + ${hiddenStatus} + + Change + +
    +

    Repeatable


    + ${repeatStatus} + + Change + + ${showFrequency}`; + if (event.repeatable && event.frequency === "2") { + menu += `
    Occurs every ${event.weekdayname || 'Unknown'}`; + } + menu += `

    + All Events +   + Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + }; + const adjustDate = () => { + let menu = ` +
    +

    Adjust Date

    +
    ${Calendar.formatDateFull()}
    ( ${QUEST_TRACKER_currentDate} )`; + if (QUEST_TRACKER_WEATHER && QUEST_TRACKER_CURRENT_WEATHER !== null) { + menu += buildWeather({ isHome: true }); + } + menu += `

    Set Date +

    Advance Date

    `; + if (QUEST_TRACKER_WEATHER) { + menu += `Advancing Dates calculates weather so there are hard limits imposed.`; + } + menu += `
    Day +  Week +  Month +  Year +
    Custom`; + if (QUEST_TRACKER_WEATHER) { + menu += `
    Day +  Week +  Month`; + } + else { + menu += `
    Day +  Week +  Month +  Year`; + } + menu += `

    Retreat Date

    `; + if (QUEST_TRACKER_WEATHER) { + menu += `Retreating Dates does not calculate weather, so there are no limits imposed.`; + } + menu += `
    Day +  Week +  Month +  Year +
    Custom +
    Day +  Week +  Month +  Year +

    Special Advance

    + Nothing will happen if there are no Festivals, Significant Dates or Events set in your Calendar. +
    Next Date of Significance +

    Back to Main Menu +
    `; + menu = menu.replace(/[\r\n]/g, ''); + Utils.sendGMMessage(menu); + } + return { + generateGMMenu, + showQuestDetails, + showAllQuests, + showAllRumours, + showRumourDetails, + showQuestDetails, + showQuestRumourByStatus, + showAllEvents, + showEventDetails, + manageRumourLocations, + manageQuestGroups, + adminMenu, + adjustDate, + buildWeather + }; + })(); + const handleInput = (msg) => { + if (msg.type !== 'api' || !playerIsGM(msg.playerid) || !msg.content.startsWith('!qt')) { + return; + } + msg.content = Utils.inputAlias(msg.content); + const args = msg.content.split(' '); + const command = args.shift(); + const params = args.join(' ').split('|').reduce((acc, param) => { + const [key, value] = param.split('='); + if (key && value) { + acc[key.trim()] = value.trim(); + } + return acc; + }, {}); + loadQuestTrackerData(); + if (errorCheck(47, 'exists', command,'command')) return; + if (command === '!qt-quest') { + const { action, field, current, old = '', new: newItem = '', id, confirmation } = params; + if (errorCheck(48, 'exists', action,'action')) return; + switch (action) { + case 'removequest': + if (!errorCheck(49, 'confirmation', confirmation, 'DELETE')) return; + if (errorCheck(50, 'exists', id,'id')) return; + if (errorCheck(51, 'exists', QUEST_TRACKER_globalQuestData[id],`QUEST_TRACKER_globalQuestData[${id}]`)) return; + Quest.removeQuest(id); + setTimeout(() => { + Menu.showAllQuests(); + }, 500); + break; + case 'addquest': + Quest.addQuest(); + setTimeout(() => { + Menu.showAllQuests(); + }, 500); + break; + case 'add': + case 'remove': + case 'update': + if (errorCheck(52, 'exists', field,'field')) return; + if (errorCheck(53, 'exists', newItem,'newItem')) return; + if (errorCheck(54, 'exists', QUEST_TRACKER_globalQuestData[current],`QUEST_TRACKER_globalQuestData[${current}]`)) return; + switch (field) { + case 'status': + Quest.manageQuestObject({ action, field, current, old, newItem }); + QuestPageBuilder.updateQuestStatusColor(current, newItem); + break; + case 'name': + if (action === 'add') { + Quest.manageQuestObject({ action, field, current, old, newItem }); + QuestPageBuilder.updateQuestText(current, newItem); + } else if (action === 'update') { + Quest.manageQuestObject({ action: 'remove', field, current, old }); + Quest.manageQuestObject({ action: 'add', field, current, old, newItem }); + } + break; + case 'description': + if (action === 'add') { + Quest.manageQuestObject({ action, field, current, old, newItem }); + QuestPageBuilder.updateQuestTooltip(current, newItem); + } else if (action === 'update') { + Quest.manageQuestObject({ action: 'remove', field, current, old }); + Quest.manageQuestObject({ action: 'add', field, current, old, newItem }); + } + break; + case 'hidden': + if (action === 'update') { + Quest.manageQuestObject({ action, field, current }); + QuestPageBuilder.updateQuestVisibility(current, newItem); + } + break; + case 'group': + if (action === 'update') { + Quest.manageQuestObject({ action: 'remove', field, current, old }); + if (newItem !== 'remove') { + Quest.manageQuestObject({ action: 'add', field, current, old, newItem }); + } + } + break; + case 'autoadvance': + if (errorCheck(55, 'exists', old,'old')) return; + switch (action) { + case 'add': + if (errorCheck(56, 'date', newItem)) return; + Quest.manageQuestObject({ action, field, current, old, newItem }); + break; + case 'remove': + Quest.manageQuestObject({ action, field, current, old }); + break; + case 'update': + if (errorCheck(57, 'date', newItem)) return; + Quest.manageQuestObject({ action: 'remove', field, current, old }); + Quest.manageQuestObject({ action: 'add', field, current, old, newItem }); + break; + default: + errorCheck(58, 'msg', null,`Unsupported action for autoadvance ( ${action} )`); + break; + } + break; + default: + errorCheck(59, 'msg', null,`Unsupported action for field ( ${field} )`); + break; + } + setTimeout(() => { + Menu.showQuestDetails(current); + }, 500); + break; + default: + errorCheck(60, 'msg', null,`Unsupported action for action ( ${action} )`); + break; + } + } else if (command === '!qt-questrelationship') { + const { action, type, currentquest, quest, groupConditions, groupnum, oldquest, confirmation } = params; + if (errorCheck(61, 'exists', action,'action')) return; + if (errorCheck(62, 'exists', type,'type')) return; + if (errorCheck(63, 'exists', currentquest,'currentquest')) return; + if (errorCheck(64, 'exists', QUEST_TRACKER_globalQuestData[currentquest],`QUEST_TRACKER_globalQuestData[${currentquest}]`)) return; + switch (action) { + case 'add': + if (errorCheck(65, 'exists', quest,'quest')) return; + if (errorCheck(66, 'exists', QUEST_TRACKER_globalQuestData[quest],`QUEST_TRACKER_globalQuestData[${quest}]`)) return; + switch (type) { + case 'mutuallyexclusive': + Quest.manageRelationship(currentquest, 'add', 'mutuallyExclusive', quest); + Quest.manageRelationship(quest, 'add', 'mutuallyExclusive', currentquest); + break; + case 'single': + Quest.manageRelationship(currentquest, 'add', 'single', quest); + break; + case 'group': + if (errorCheck(67, 'exists', groupnum,'groupnum')) return; + Quest.manageRelationship(currentquest, 'add', 'group', quest, groupnum); + break; + case 'addgroup': + Quest.manageRelationship(currentquest, 'add', 'addgroup', quest); + default: + errorCheck(68, 'msg', null,`Unsupported action for type ( ${type} )`); + break; + } + break; + case 'remove': + switch (type) { + case 'mutuallyexclusive': + if (errorCheck(69, 'exists', quest,'quest')) return; + if (errorCheck(70, 'exists', QUEST_TRACKER_globalQuestData[quest],`QUEST_TRACKER_globalQuestData[${quest}]`)) return; + if (errorCheck(71, 'exists', quest,'quest')) return; + Quest.manageRelationship(currentquest, 'remove', 'mutuallyExclusive', quest); + Quest.manageRelationship(quest, 'remove', 'mutuallyExclusive', currentquest); + break; + case 'single': + if (errorCheck(72, 'exists', quest,'quest')) return; + if (errorCheck(73, 'exists', QUEST_TRACKER_globalQuestData[quest],`QUEST_TRACKER_globalQuestData[${quest}]`)) return; + if (errorCheck(74, 'exists', quest,'quest')) return; + Quest.manageRelationship(currentquest, 'remove', 'single', quest); + break; + case 'group': + if (errorCheck(75, 'exists', quest,'quest')) return; + if (errorCheck(76, 'exists', QUEST_TRACKER_globalQuestData[quest],`QUEST_TRACKER_globalQuestData[${quest}]`)) return; + if (errorCheck(77, 'exists', groupnum,'groupnum')) return; + Quest.manageRelationship(currentquest, 'remove', 'group', quest, groupnum); + break; + case 'removegroup': + if (errorCheck(78, 'exists', groupnum,'groupnum')) return; + if (!errorCheck(79, 'confirmation', confirmation, 'DELETE')) return; + Quest.manageRelationship(currentquest, 'remove', 'removegroup', null, groupnum); + break; + default: + errorCheck(80, 'msg', null,`Unsupported action for type ( ${type} )`); + break; + } + break; + case 'update': + switch (type) { + case 'mutuallyexclusive': + if (errorCheck(81, 'exists', quest,'quest')) return; + if (errorCheck(82, 'exists', oldquest,'oldquest')) return; + Quest.manageRelationship(currentquest, 'remove', 'mutuallyExclusive', oldquest); + Quest.manageRelationship(oldquest, 'remove', 'mutuallyExclusive', currentquest); + Quest.manageRelationship(currentquest, 'add', 'mutuallyExclusive', quest); + Quest.manageRelationship(quest, 'add', 'mutuallyExclusive', currentquest); + break; + case 'single': + if (errorCheck(83, 'exists', quest,'quest')) return; + Quest.manageRelationship(currentquest, 'add', 'single', quest); + Quest.manageRelationship(currentquest, 'remove', 'single', oldquest); + break; + case 'group': + if (errorCheck(84, 'exists', quest,'quest')) return; + if (errorCheck(85, 'exists', oldquest,'oldquest')) return; + Quest.manageRelationship(currentquest, 'add', 'group', quest, groupnum); + Quest.manageRelationship(currentquest, 'remove', 'group', oldquest, groupnum); + break; + case 'grouplogic': + Quest.manageRelationship(currentquest, 'update', 'grouplogic', null, groupnum); + break; + case 'logic': + Quest.manageRelationship(currentquest, 'update', 'logic', null); + break; + default: + errorCheck(86, 'msg', null,`Unsupported action for type ( ${type} )`); + break; + } + break; + default: + errorCheck(87, 'msg', null,`Unsupported action for action ( ${action} )`); + break; + } + setTimeout(() => { + Menu.showQuestDetails(currentquest); + }, 500); + } else if (command === '!qt-rumours') { + const { action, questid, status, location, rumourid, new: newItem, number, locationId, old, confirmation } = params; + if (errorCheck(88, 'exists', action, 'action')) return; + switch (action) { + case 'send': + if (errorCheck(89, 'exists', number, 'number')) return; + if (errorCheck(90, 'number', number, 'number')) return; + if (errorCheck(91, 'exists', location, 'location')) return; + Rumours.sendRumours(location, number); + break; + case 'add': + case 'update': + case 'remove': + if (errorCheck(92, 'exists', location, 'location')) return; + if (errorCheck(93, 'exists', status, 'status')) return; + if (errorCheck(94, 'exists', questid, 'questid')) return; + if (action === 'add') { + if (errorCheck(95, 'exists', newItem, 'newItem')) return; + Rumours.manageRumourObject({ action: 'add', questId: questid, newItem, status, location }); + setTimeout(() => { + Menu.showRumourDetails(questid, status); + }, 500); + } else if (action === 'update') { + if (errorCheck(96, 'exists', newItem, 'newItem')) return; + if (errorCheck(97, 'exists', rumourid, 'rumourid')) return; + if (errorCheck(98, 'exists', QUEST_TRACKER_globalRumours[questid], `QUEST_TRACKER_globalRumours[${questid}]`)) return; + Rumours.manageRumourObject({ action: 'remove', questId: questid, newItem: '', status, location, rumourId: rumourid }); + Rumours.manageRumourObject({ action: 'add', questId: questid, newItem, status, location, rumourId: rumourid }); + setTimeout(() => { + Menu.showRumourDetails(questid, status); + }, 500); + } else if (action === 'remove') { + if (errorCheck(99, 'exists', QUEST_TRACKER_globalRumours[questid], `QUEST_TRACKER_globalRumours[${questid}]`)) return; + if (errorCheck(100, 'exists', QUEST_TRACKER_globalRumours[questid][status], `QUEST_TRACKER_globalRumours[${questid}][${status}]`)) return; + if (errorCheck(101, 'exists', Rumours.getLocationNameById(location), `getLocationNameById(${location})`)) return; + if (errorCheck(102, 'exists', QUEST_TRACKER_globalRumours[questid][status][Rumours.getLocationNameById(location).toLowerCase()], `QUEST_TRACKER_globalRumours[${questid}][${status}][getLocationNameById(${location}).toLowerCase()]`)) return; + Rumours.manageRumourObject({ action: 'remove', questId: questid, newItem: '', status, location, rumourId: rumourid }); + setTimeout(() => { + Menu.showRumourDetails(questid, status); + }, 500); + } + break; + case 'addLocation': + if (errorCheck(103, 'exists', newItem, 'newItem')) return; + Rumours.manageRumourLocation('add', newItem, null); + setTimeout(() => { + Menu.manageRumourLocations(); + }, 500); + break; + case 'editGroupName': + if (errorCheck(104, 'exists', newItem, 'newItem')) return; + if (errorCheck(105, 'exists', locationId, 'locationId')) return; + Rumours.manageRumourLocation('update', newItem, locationId); + setTimeout(() => { + Menu.manageRumourLocations(); + }, 500); + break; + case 'removeLocation': + if (errorCheck(106, 'exists', locationId, 'locationId')) return; + if (!errorCheck(107, 'confirmation', confirmation, 'DELETE')) return; + Rumours.manageRumourLocation('remove', null, locationId); + setTimeout(() => { + Menu.manageRumourLocations(); + }, 500); + break; + default: + errorCheck(108, 'msg', null,`Unsupported action for type ( ${action} )`); + break; + } + } else if (command === '!qt-questgroup') { + const { action, groupid, new: newItem, confirmation } = params; + if (!action) return; + switch (action) { + case 'add': + if (errorCheck(109, 'exists', newItem,'newItem')) return; + Quest.manageGroups('add', newItem, null); + setTimeout(() => { + Menu.manageQuestGroups(); + }, 500); + break; + case 'update': + if (errorCheck(110, 'exists', newItem,'newItem')) return; + if (errorCheck(111, 'exists', groupid,'groupid')) return; + Quest.manageGroups('update', newItem, groupid); + setTimeout(() => { + Menu.manageQuestGroups(); + }, 500); + break; + case 'remove': + if (errorCheck(112, 'exists', groupid,'groupid')) return; + if (!errorCheck(113, 'confirmation', confirmation, 'CONFIRM')) return; + Quest.manageGroups('remove', null, groupid); + setTimeout(() => { + Menu.manageQuestGroups(); + }, 500); + break; + default: + errorCheck(114, 'msg', null,`Unsupported action for type ( ${action} )`); + break; + } + } else if (command === '!qt-menu') { + const { action, id, questId, locationId, status, eventid, menu} = params; + if (!action || action === 'main') { + Menu.generateGMMenu(); + } else if (action === 'config') { + Menu.adminMenu(); + } else if (action === 'quest') { + if (errorCheck(115, 'exists', id,'id')) return; + Menu.showQuestDetails(id); + } else if (action === 'allquests') { + Menu.showAllQuests(); + } else if (action === 'allrumours') { + Menu.showAllRumours(); + } else if (action === 'showQuestRumours') { + if (errorCheck(116, 'exists', questId,'questId')) return; + Menu.showQuestRumourByStatus(questId); + } else if (action === 'showRumourDetails') { + if (errorCheck(117, 'exists', questId,'questId')) return; + if (errorCheck(118, 'exists', status,'status')) return; + Menu.showRumourDetails(questId, status); + } else if (action === 'manageRumourLocations') { + Menu.manageRumourLocations(); + } else if (action === 'manageQuestGroups') { + Menu.manageQuestGroups(); + } else if (action === 'allevents') { + Menu.showAllEvents(); + } else if (action === 'showevent') { + if (errorCheck(119, 'exists', eventid,'eventid')) return; + Menu.showEventDetails(eventid); + } else if (action === 'adjustdate') { + Menu.adjustDate(); + } else errorCheck(120, 'msg', null,`Unknown menu action: ${action}`); + } else if (command === '!qt-date') { + const { action, field, current, old, new: newItem, unit = 'day', date, eventid, menu = false, home = false} = params; + if (errorCheck(121, 'exists', action,'action')) return; + switch (action) { + case 'set': + if (errorCheck(122, 'exists', newItem)) return; + if (errorCheck(145, 'date', newItem)) return; + Calendar.modifyDate({type: 'set', newDate: newItem}); + if (menu) { + setTimeout(() => { + Menu.adjustDate(); + }, 500); + } + break; + case 'addevent': + Calendar.addEvent(); + setTimeout(() => { + Menu.showAllEvents(); + }, 500); + break; + case 'removeevent': + if (errorCheck(123, 'exists', eventid, 'eventid')) return; + Calendar.removeEvent(eventid); + setTimeout(() => { + Menu.showAllEvents(); + }, 500); + break; + case 'update': + if (field === 'date') { + if (errorCheck(125, 'date', newItem)) return; + } + Calendar.manageEventObject({ action, field, current, old, newItem, date}); + setTimeout(() => { + Menu.showEventDetails(current); + }, 500); + break; + case 'setcalender': + if (errorCheck(126, 'exists', newItem, 'newItem')) return; + Calendar.setCalender(newItem); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + break; + case 'setclimate': + if (errorCheck(127, 'exists', newItem, 'newItem')) return; + Calendar.setClimate(newItem); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + break; + case 'adjustlocation': + if (errorCheck(128, 'exists', newItem, 'newItem')) return; + Calendar.adjustLocation(newItem); + if (menu) { + setTimeout(() => { + Menu.adjustDate(); + }, 500); + } + else if (home) { + setTimeout(() => { + Menu.generateGMMenu(); + }, 500); + } + break; + case 'settrend': + if (errorCheck(129, 'exists', newItem, 'newItem')) return; + if (errorCheck(130, 'number', newItem, 'newItem')) return; + const num = Math.trunc(newItem); + if (num <= 0) return; + Calendar.setWeatherTrend(field, num); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + break; + case 'forcetrend': + if (errorCheck(131, 'exists', field, 'field')) return; + Calendar.forceWeatherTrend(field); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + break; + case 'modify': + if (errorCheck(132, 'exists', newItem, 'newItem')) return; + if (errorCheck(133, 'number', newItem, 'newItem')) return; + if (errorCheck(134, 'exists', unit, 'unit')) return; + const number = Math.trunc(newItem); + if (QUEST_TRACKER_WEATHER) { + switch (unit.toLowerCase()) { + case "years": + if (number > 1) number = 1; + break; + case "days": + if (number > 500) number = 500; + break; + case "weeks": + if (number > 60) number = 60; + break; + case "months": + if (number > 15) number = 15; + break; + default: + break; + } + } + Calendar.modifyDate({type: unit, amount: number}); + if (menu) { + setTimeout(() => { + Menu.adjustDate(); + }, 500); + } + else if (home) { + setTimeout(() => { + Menu.generateGMMenu(); + }, 500); + } + else { + setTimeout(() => { + Menu.buildWeather(); + }, 500); + } + break; + default: + errorCheck(136, 'msg', null,`Unknown date command: ${params.action}`); + break; + } + } else if (command === '!qt-import') { + Import.fullImportProcess(); + } else if (command === '!qt-config') { + const { action, value, confirmation, type } = params; + if (action === 'togglereadableJSON'){ + if (errorCheck(137, 'exists', value, 'value')) return; + Utils.togglereadableJSON(value); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } else if (action === 'toggleWeather'){ + if (errorCheck(138, 'exists', value, 'value')) return; + Utils.toggleWeather(value); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } else if (action === 'togglejumpgate'){ + if (errorCheck(139, 'exists', value, 'value')) return; + Utils.toggleJumpGate(value); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } else if (action === 'toggleVerboseErrors'){ + if (errorCheck(140, 'exists', value, 'value')) return; + Utils.toggleVerboseError(value); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } else if (action === 'toggleimperial'){ + if (errorCheck(150, 'exists', value, 'value')) return; + if (errorCheck(151, 'exists', type, 'type')) return; + Utils.toggleImperial(type,value); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } else if (action === 'reset') { + if (!errorCheck(141, 'confirmation', confirmation, 'CONFIRM')) return; + state.QUEST_TRACKER = {}; + initializeQuestTrackerState(true); + loadQuestTrackerData(); + QUEST_TRACKER_HISTORICAL_WEATHER = {}; + Utils.updateHandoutField("weather"); + saveQuestTrackerData(); + setTimeout(() => { + Menu.adminMenu(); + }, 500); + } + } else if (command === '!qt-questtree') { + const { action, value } = params; + if (errorCheck(142, 'exists', action, 'action')) return; + switch (action) { + case 'build': + QuestPageBuilder.buildQuestTreeOnPage(); + break; + default: + errorCheck(143, 'msg', null,`Unknown action: ${action}`); + break; + } + } + else { + errorCheck(144, 'msg', null,`Unknown command: ${command}`); + } + }; + const errorCheck = (id = 0, type = null, data = null, check = null) => { + switch (type) { + case 'confirmation': + if (data === check) return true; + else { + switch (check) { + case 'CONFIRM': + Utils.sendGMMessage(`Error ${id}: Confirmation required to reset all data. Please type CONFIRM when prompted.`); + break; + case 'DELETE': + Utils.sendGMMessage(`Error ${id}: Confirmation required to delete location. Please type DELETE to confirm.`); + break; + } + } + break; + case 'date': + if (!/^\d+-\d+-\d+$/.test(data)) { + Utils.sendGMMessage(`Error ${id}: Invalid date format: ${data}. Must be digits separated by dashes (e.g., YYYY-MM-DD or similar).`); + return true + } + break; + case 'exists': + if (data === null) { + if (QUEST_TRACKER_verboseErrorLogging) Utils.sendGMMessage(`Error ${id}: The variable ${check} does not exist.`); + return true; + } + break; + case 'msg': + Utils.sendGMMessage(`Error ${id}: ${check}`); + break; + case 'number': + if (isNaN(data)) { + if (QUEST_TRACKER_verboseErrorLogging) Utils.sendGMMessage(`Error ${id}: ${check} is not a number.`); + return true; + } + break; + } + return false; + }; + return { + CALENDARS, + WEATHER, + loadQuestTrackerData, + saveQuestTrackerData, + handleInput, + Import, + Calendar, + Quest, + Rumours, + QuestPageBuilder, + Menu, + errorCheck, + initializeQuestTrackerState, + getCalendarAndWeatherData + }; +})(); +on('ready', function () { + 'use strict'; + const { CALENDARS, WEATHER } = QuestTracker.getCalendarAndWeatherData(); + if (!CALENDARS || !WEATHER) return; + QuestTracker.initializeQuestTrackerState(); + QuestTracker.loadQuestTrackerData(); + on('chat:message', function(msg) { + QuestTracker.handleInput(msg); + }); +}); \ No newline at end of file diff --git a/QuestTracker/README.md b/QuestTracker/README.md new file mode 100644 index 0000000000..ce3c1e1018 --- /dev/null +++ b/QuestTracker/README.md @@ -0,0 +1,519 @@ +# Quest Tracker +Quest Tracker is a comprehensive tool for managing quests, rumors, and events in a tabletop RPG setting. It integrates seamlessly with Roll20 to provide detailed tracking and visualization of game elements, making it ideal for GMs and players who want to streamline their campaigns. + +### Features + +- **Quest Management:** + - Create, edit, and remove quests. + - Track quest statuses (e.g., "Started", "Completed", "Failed"). + - Group quests into logical categories. + +- **Rumor Handling:** + - Add and manage rumors by location or quest. + - Generate rumors dynamically. + - Associate rumors with quest progression. + +- **Event Scheduling:** + - Schedule events with repeatable options. + - Adjust events based on in-game calendars. + +- **Weather and Climate Integration:** + - Dynamic weather generation based on in-game conditions. + - Detailed descriptions of current weather conditions. + +- **Calender Integration:** + - Track Leap years + - Different Calander types, e.g. Harpto, Gregorian etc. + +- **Visual Quest Tree:** + - Display quests and relationships as a tree diagram. + - Automatically handle mutually exclusive relationships. + +### Getting Started + +1. **Installation:** + Install CalanderData first, once you see this in the log: "CalenderData initialized in state.CalenderData.CALENDARS & state.CalenderData.WEATHER" you know QuestTracker is ready to be installed it and load into the game it will initialise. + +3. **Usage:** + - Access all features through an intuitive graphical user interface. simply type **!qt** into chat. + - Navigate through menus to manage quests, rumors, and events seamlessly. + +## Rumours Module +The rumours module provides a flexible framework for dynamically integrating narrative elements into your campaign. It connects directly to quests, locations, and events, allowing for automated storytelling and background interactions. + +### Module Architecture + +#### Data Structures + +Rumours are structured hierarchically by quest, status, and location. Example structure: +``` +{ + "quest_1": { + "unknown": {}, + "discovered": { + "everywhere": { + "rumour_1": "This is a rumour text" + }, + "general_store": { + "rumour_2": "Wonderings of goings on", + "rumour_4": "Gossip" + } + } + }, + "quest_2": { + "unknown": { + "the_boathouse": { + "rumour_3": "Dave is skimming from the books" + } + } + } +} +``` +#### Hierarchy: + +* quest: The ID of the associated quest. +* status: The state of the rumour (e.g., unknown, discovered). +* location: The in-game location where the rumour is tied. +* rumour_id: Unique identifier for the specific rumour. +* description: The text of the rumour. + +#### Storage: + +Rumours are stored in handout files within Roll20. + +These files can be imported using Configuration > Refresh/Import JSON data from the module's graphical interface. + +The JSON data structure must follow the hierarchical format described above. + +#### Locations: + +Rumours tied to specific locations are triggered when players interact with those areas. + +### Core Functionalities + +#### Adding and Editing Rumours + +Rumours are managed directly through the graphical interface, providing an intuitive way to organize and modify them: + +#### Show All Rumours + +Navigate to the "Show All Rumours" panel to view rumours linked to a specific quest. Select the relevant quest and choose the status (unknown, discovered, etc.) to which you want to add or edit rumours. + +#### Location-Specific Actions + +![Rumour Screen](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/rumours.png) + +Under each location, buttons allow for streamlined rumour management: + +* "+" Add a Rumour: Add a new rumour to the specified location and status. +* "c" Change: Edit the existing rumour text. +* "-" Remove: Delete the rumour from the selected location. + +##### Viewing Full Rumour Text +* Hover over the magnifying glass icon to see the full rumour text. The displayed text will truncate if it exceeds the visible area. + +#### Formatting Tips +* Use %NEWLINE% to insert line breaks within rumour text. +* Use " to include quotation marks in rumour descriptions. + +### Rumour Locations Management + +Navigate to "All Rumours > Rumour Locations" to manage locations associated with rumours. + +![Rumour Management Screen](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/rumourManagement.png) + +Buttons provide streamlined location management: +* "+" Add a Location: Create a new location to associate with rumours. +* "c" Change: Edit the name or properties of an existing location. +* "-" Remove: Delete a location, with a confirmation prompt to ensure that all rumours under the location are not removed unintentionally. + +#### Quest Status Changes + +Different quest statuses trigger distinct sets of rumours. + +*Example: A quest in the discovered status may have rumours tied to general_store, while the same quest in the completed status has no active rumours.* + +#### Location-Based Differentiation + +The same quest and status can yield different rumours depending on the location. + +*Example: In everywhere, a rumour might say "A strange light in the forest," while in general_store, it could suggest "A missing person was last seen here."* + +### How to Show? + +This is stright-forward; simply choose the location the players are in, and select how many (random) rumours will be shown in chat. 'Eeverywhere' is a global location and rumours will be chosen from either the selected location OR everywhere. + +![Show Button](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/show_rumours.png) + +![Rumour Display](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/rumour_display.png) + +### Error Handling: + +Ensure rumour_id fields are unique to prevent overwrites. +Validate linked quest and location IDs to maintain data integrity. + +## QUEST Module + +The QUEST Module is a core component of the Quest Tracker system for Roll20. It provides robust quest management features, allowing game masters to dynamically control quest progression, relationships, and status changes, all integrated seamlessly into sandbox-style RPG campaigns. + +### Quest Management +- Create, update, and delete quests using an intuitive graphical interface. +- Track quest statuses: + - **Unknown:** The Quest is unknown at this point. it is worth noting rumours can exists for quests within this state. + - **Discovered:** The quest has been discovered but not necessarily active or accepted. + - **Started:** The quest is currently in progress, and has been accepted + - **Ongoing:** The quest is currently ongoing. + - **Completed:** The quest has been completed. + - **Completed by Someone Else:** Another band of adventurers perhaps? + - **Failed:** The quest was not completed as intended. + - **Time ran out:** The arbitary time has run out on the quest + - **Ignored:** The clues have not been followed or the PCs have clearly ignored this quest + +As you can see whilst these statuses are at this time hard coded ine, there is a lot of room to wiggle in how you define a quest's status. +Depending on feedback I may allow these statuses to be user specific, although it is a fair amount of work. + +### Prerequisites and Dependencies +- Define quest prerequisites to unlock quests based on player actions or story progression. +- Establish mutually exclusive relationships between quests to enforce narrative constraints. +- Auto-advance quests based on time-sensitive conditions. + +None of these quest relasionships rules are rigidly enforced within the code; but will allow you as the DM to follow a basic story logic, as well as offer a more visual way for the players to understand what is happening if they try to support one faction over another. + +### Data Structure + +Quests are stored in a hierarchical JSON format, supporting complex relationships. Example: + +```json +"quest_1": { + "name": "Primary Quest", + "description": "This is a Primary Quest", + "relationships": { + "logic": "AND", + "conditions": [ + "quest_4", + { + "logic": "OR", + "conditions": [ + "quest_2", + "quest_3" + ] + }, + { + "logic": "OR", + "conditions": [ + "quest_7", + "quest_9" + ] + } + ], + "mutually_exclusive": [] + }, + "hidden": false, + "autoadvance": { + "unknown": "1970-01-01" + }, + "group": "6", + "level": 3 + }, + "quest_8": { + "name": "Secondary Quest", + "description": "There are more quests here?", + "relationships": { + "logic": "AND", + "conditions": [ + "quest_12" + ] + }, + "hidden": false, + "autoadvance": {}, + "level": 1, + "group": "6" + }, + "quest_11": { + "name": "Another Quest?", + "description": "Clearly this game has a lot of quests.", + "relationships": { + "logic": "AND", + "conditions": [ + "quest_4" + ], + "mutually_exclusive": [ + "quest_12" + ] + }, + "hidden": false, + "autoadvance": {}, + "group": "6", + "level": 2 + } +``` + +### Quest Features + +* **Name:** The name of the quest, defaults to 'New Quest' and it is does not need to be unique. +* **Description:** A short description of the quest; it will be the tooltip on the Quest Tree page. +* **Status:** The status of the quest, this is stored in a rollable table as a 'weight'. +* **Hidden:** This quest is completely hidden from the Players when displayed on the page, the relasionships of this quest are also hidden. If you do not use the Quest Tree page there no difference betwene a hidden quest or a visible one (e.g. rumours from hidden quests are still shown), by default quests start out as hidden. +* **Quest Group:** This is to help you organise your quests better, relasionships can only be formed by quests within their own quest group. +* **AutoAdvance:** Simply add a Date (YYYY-MM-DD) into one of the status fields and when that date occurs the quest will autoadvance to that specific status; it will then clear this field. There are no checks to make sure things go in the correct order it is up to you to maintain your own quests. +* **Icon:** (potentially a future UI implimentation) This is actually a hidden field as I have currently not built a UI for it, but on the rollable table you can upload an icon for the quest which will appear as a token on the Quest Tree Page. This is important as it will allow you to use tokenmod commands to trigger a quest change in state using the questID in the GM Notes field of said token. + +### Navigating the UI + +![A standard quest page](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/quest_page.png) ![An Extensive Quest](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/extensive_quest.png) + +* If the + buttons are shaded grey and cannot be selected it means there are no valid quests to add, quests need to be in the same quest group and not already be selected in a relasionship +* Quest Relationships work on a AND and OR functionality and you can put them within relasionship groups in order to visualise the prerequisities; as you can see in the more complex quest tree; five quests are its prerequisites (under AND) but 4 of them are separated into two groups of OR functionality, the grouped quests are also mutually exclusive with each other (note the red line). +* The Quest diagram is generated automatically and only works on a quest by quest basis (so no futher back); if you come across any failures within rendering please make sure you raise this as an issue. note: before you do this drag the chat window wider, as most issues are resolved with this. + +## Weather Module + +### Overview + +The Weather Module is designed to simulate dynamic weather conditions, taking into account environmental trends, forced modifiers, and randomness. This module supports graphical visualizations for key weather parameters and provides an intuitive interface for adjusting and interpreting weather trends. + +### Key Weather Parameters + +Weather is determined by six key values; each value is rated on a scale of 0 to 100, with 50 representing the average condition. A random number is generated daily to define the weather, following a restricted bell curve distribution centered at 50 (see Bell Curve Graph). + +|||| +|:-------------------------:|:-------------------------:|:-------------------------:| +|![Temperature Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/temp.png) **Temperature Distribution**|![Precipitation Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/rain.png) **Precipitation Distribution**|![Wind Speed Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/wind.png) **Wind Speed Distribution**| +|![Humidity Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/humid.png) **Humidity Distribution**|![Cloud Cover Graph ](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/cloud.png) **Cloud Cover Distribution**|![Visibility Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/visibility.png) **Visibility Distribution**| +||![Bellcurve Graph](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/bellcurve.png) **Bell Curve Graph**|| + + +### Climate Configuration + +Users can select a specific climate configuration, which adjusts weather values based on predefined modifiers. These modifiers vary between -20 to +20 and are season-dependent, linked to the Calendar Module. For instance: + +*Icewind Dale (Forgotten Realms): During the "Long Winter" season, the temperature adjustment is -20, the maximum possible adjustment.* + +### Daily Adjustments + +Weather values change by a maximum of +/- 5 points per day. Near seasonal boundaries (e.g., winter transitioning to spring), this limit increases to +/- 10 points. There is a small chance, especially near seasonal boundaries, that these adjustments can double to +/- 10 or +/- 20 points, resulting in massive weather shifts. + +### Forced Trends + +A Forced Trend feature can be toggled on or off. When activated, this applies a fixed +/- 20-point difference to simulate significant weather deviations, overriding standard adjustments, this option can be can be found underneath configutation + +### How to Use the Module + +![Weather GUI](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/weather.png) + +#### Initialize the Weather System: + +The weather system can be enabled or disabled in the configuration menu. It is enabled by default. + +#### Select a Climate Zone: +In the configuration menu, choose the climate zone your players are in (e.g., Northern Temperate or Equatorial). Climate zones are specific to the selected calendar. For instance, the Harptos climates are based on common adventure areas rather than a general climate type like temperate. + +#### Change Player Location: +You can change the location the players are in under the 'Adjust Date' menu. Examples include selecting Plains, Swamp, or other terrain types. + +### Weather Descriptions + +The module provides detailed descriptions of weather conditions based on the player's location and daily weather parameters. When weather is enabled, the description is shown to players as a /desc message whenever the day advances. + +![Weather Description](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/weatherdescription.png) + +#### Weather Condition Matching + +Each day, weather conditions are matched to predefined scenarios. For example: + +``` +"Persistent Downpour": { + "conditions": { + "temperature": { "gte": 50, "lte": 70 }, + "precipitation": { "gte": 60 }, + "wind": { "lte": 50 }, + "humidity": { "gte": 60 }, + "cloudCover": { "gte": 60 }, + "visibility": { "lte": 50 } + } +} +``` + +Each scenario is linked to over **11,000 unique descriptions** for potential player locations, such as plains, farms, or forests. These descriptions create immersive and varied environmental narratives. + +## Calendar Module + +### Overview + +The Calendar Module is a comprehensive system designed to manage and integrate various calendar types into your campaign. It supports custom events, lunar cycles, leap years, and dynamic seasonal markers. + +### Supported Calendars + +The module includes several pre-configured calendars: + +* **Gregorian** +* **Harptos (Forgotten Realms)** +* **Barovian (Curse of Strahd)** +* **Golarion (Pathfinder)** - A Pathfinder-specific calendar with varying month lengths and leap year logic. +* **Greyhawk (Original and 2024 Default setting)** +* **Exandria (Critical Role)** + +I can add additional Calendars into the module if you were to provide the details and JSON object. + +#### Lunar Cycles + +Each calendar includes options for tracking lunar phases. The lunar cycle will display key phases, such as the below and are setting specific. There are no current plans to incorporate multiple moons at this time for settings such as Dragonlance. + +* New Moon +* Waxing Crescent +* First Quarter +* Full Moon +* Waning Crescent + +#### Leap Years + +Calendars with leap year logic will account for additional days. for example: + +* Gregorian: Leap years occur every 4 years, except for years divisible by 100 but not by 400. +* Harptos: Leap days occur every 4 years as "Shieldmeet." + +#### Seasonal Events + +The module supports predefined seasonal and celestial events, such as: + +* Spring Equinox +* Summer Solstice +* Autumn Equinox +* Winter Solstice + +These events are always shown to your players. + +### Custom Events + +Users can add custom events tied to specific dates. These can include festivals, holidays, or recurring milestones within your campaign world. + +### Initializing the Calendar + +Select a calendar type from the configuration menu. The system will automatically set the date to the default starting date for the selected calendar. + +***Warning: Changing the calendar type resets the current date to the default date for the chosen calendar.*** + +### Setting Events + +Navigate to the "Events" section in the configuration menu. Add, edit, or remove events as needed. Assign dates for recurring or one-time events. + +| | | | +|:-------------------------:|:-------------------------:|:-------------------------:| +|![Upcoming Events](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/upcoming_events.png)|![All Events](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/allevents.png)|![Modify Events](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/event_modify.png)| + + +### Adjusting Dates + +You can set or move the date using the "Adjust Date" menu. + +![Adjust Date Menu](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/datechangesUI.png) + +There are also quick commands available for date adjustments. These commands provide a streamlined interface, bypassing the full GUI, and include a cut-down weather display with key information for players: + +* **!qt-date advance** Increases the date by 1. +* **!qt-date retreat** Decreases the date by 1. + +![Cut Down Weather](https://raw.githubusercontent.com/boli32/QuestTracker/refs/heads/main/img/calander_cut.png) + +## Quest Tree Page Module + +### Overview + +The Quest Tree Page provides a visual representation of quests and their relationships. + +### How to Create the Quest Tree Page: + +Make sure the page **Quest Tree Page** is created, this script will not create this page automatically. Once this is done, navigate to the configuration menu. Press the "Generate Quest Tree Page" button + +### Automated Updates + +The Quest Tree Page will be updated sporadically but often you may need to refresh the page by pressing the button again. + +#### When it is updated automatically + +* Quest Visibility +* Quest Status +* Quest Name: +* Quest Description: + +#### When it is *not* updated automatically + +* Changes to relasionship data +* adding or deleting new quests + +This is intentional as the calculations to create such a page can get increasingly complex, and recalculating new quests and relationship is something which needs to be done irregually. Also, I cannot see a need for this to be updated on the fly. + +### Quest Visibility + +Quests that are marked as hidden are not shown on the quest tree, although it can become obvious if entire sections are missing or ther is a 'quest shaped gap' in the viible tree. When a quest is hidden all of their connections are also hidden, this can result in edge cases where there is no obvious connection between quests as the interveneing quests are hidden. You need to keep this in mind when designing Quest Trees. + +### Quest Groups + +Quests are organised into quest groups on the Quest Tree page, they are ordered in their creation order, this order is seen in the UI. + +### Quest Images + +Quest can have images which are tokens. These are set manually using the rollable table, they have the questID in the GM Notes so can be used with tokenMod in order to trigger quest action, such as changing it's status. + +I will develop a macro for this use and include it soon. + +## FAQ + +### How do I access the quest tracker interface? +Use the in-game interface provided by the tool. Simply open the menu to begin navigating quests, rumors, and events by typing !qt into chat + +### Can I customize the weather settings? +Yes, the graphical interface allows you to adjust weather trends, add forced conditions. + +### How are mutually exclusive quests displayed? +Mutually exclusive quests are visually highlighted and organized in the quest tree to prevent conflicts. + +### Can I adjust weather effects manually +Technically, yes you can, by editing the JSON files direct and setting the date to before these changes took place. It is advisable to use the inbuilt tools and make sure the weather runs for a couple of months bfore you start the campaign to even out any extreme fluctuations. Look to forcing weather trends if you wanted to have a drought or cold snap effect the world outside of the seasonal changes. + +### Can I change the order of Quest Groups to have them display in a different order on the Quest Tree Page +Yes, you can carefully edit the qt-quest-groups rollable table, although this is not an ideal solution and I may add a reordering functionality later should there be call for it. + +### I've noticed you can create quest relationships and then move them into separate quest groups, this results in weirdness on the Quest Tree Page +Yes, that is a workaround to having relationships between quest groups and it *can* result in a very pretty Quest Tree Page, but without a lot of trial and error the Quest Tree Page is not designed to work with this in mind. I left this in as the only other option would be to wipe all relasionships when you add a quest to a quest group which would cause more frustration. + +## Updates + +#### 2026-01-09 +* **v1.0** Official Release +#### 2025-01-08 +* **v0.9.2** Fixed quest dropdown to deal with single quests without a dropdown. Also an ungrouped quest which get assigned a relasionship to a quest within a quest group automatically now gets assigned to that quest group. +* **v0.9.1.7.2** Adjusted font and rectangle size on quest tree page +#### 2025-01-07 +* **v0.9.1.7.1** Swapped DELETE for CONFIRM in a popup +* **v0.9.1.7** Removed JumpGate Toggle and where it was used (paths fixed now), kept variable in case it is needed later. +* **v0.9.1.6** If a rollable table needs to be created, it is now hidden from players. +* **v0.9.1.5** Added Switches to toggle between Imperial or Metric Weather Measurements. +* **v0.9.1.4** Fixed Allias Date Advance to show cut down description. +#### 2025-01-06 +* **v0.9.1.3** Fixed Climate modifiers and adjusted bellcurve +#### 2025-01-03 +* **v0.9.1.2** Disabled Quest Relationship buttons when no quests available. +* **v0.9.1.1** Fixed Quest Group Dropdown Menu +* **v0.9.1** Adjusted climate values and streamlined climate values +#### 2025-01-02 +* **v0.9.0.1** Fixed rumour filtering issue +#### 2024-12-19 +* **v0.9** Initial Upload + +## Contributing + +Contributions are welcome! Please submit pull requests or report issues on the GitHub repository: + +[GitHub Repository](https://github.com/boli32/QuestTracker) + +## Credits + +- **Author:** Steven Wrighton (Boli) +- **Contact:** [Roll20 Profile](https://app.roll20.net/users/3714078/boli) +- **License:** MIT + +--- + +Thank you for using Quest Tracker. Happy gaming! + diff --git a/QuestTracker/script.json b/QuestTracker/script.json new file mode 100644 index 0000000000..25bb593685 --- /dev/null +++ b/QuestTracker/script.json @@ -0,0 +1,21 @@ +{ + "name": "QuestTracker", + "script": "QuestTracker.js", + "version": "1.0", + "previousversions": [], + "description": "# Quest Tracker Quest Tracker is a comprehensive tool for managing quests, rumors, and events in a tabletop RPG setting. It integrates seamlessly with Roll20 to provide detailed tracking and visualization of game elements, making it ideal for GMs and players who want to streamline their campaigns. ### Features - **Quest Management:** - Create, edit, and remove quests. - Track quest statuses (e.g., \"Started\", \"Completed\", \"Failed\"). - Group quests into logical categories. - **Rumor Handling:** - Add and manage rumors by location or quest. - Generate rumors dynamically. - Associate rumors with quest progression. - **Event Scheduling:** - Schedule events with repeatable options. - Adjust events based on in-game calendars. - **Weather and Climate Integration:** - Dynamic weather generation based on in-game conditions. - Detailed descriptions of current weather conditions. - **Calender Integration:** - Track Leap years - Different Calander types, e.g. Harpto, Gregorian etc. - **Visual Quest Tree:** - Display quests and relationships as a tree diagram. - Automatically handle mutually exclusive relationships. ### Getting Started - Access all features through an intuitive graphical user interface. simply type **!qt** into chat. - Navigate through menus to manage quests, rumors, and events seamlessly. ## More Information? [See the README](https://github.com/Roll20/roll20-api-scripts/blob/master/QuestTracker/README.md) ## Contributing Contributions are welcome! Please submit pull requests or report issues on the GitHub repository: [GitHub Repository](https://github.com/boli32/QuestTracker) ## Credits - **Author:** Steven Wrighton (Boli) - **Contact:** [Roll20 Profile](https://app.roll20.net/users/3714078/boli) - **License:** MIT", + "authors": "Boli", + "roll20userid": "3714078", + "useroptions": [], + "dependencies": ["CalenderData"], + "modifies": { + "chat": "read, write", + "layer": "read, write", + "paths": "read, write", + "graphic": "read, write", + "text": "read, write", + "token": "read, write", + "state.QUEST_TRACKER": "read, write" + }, + "conflicts": [] +} \ No newline at end of file From 0398ff1d2877c7421c0204957b64c357b76568ce Mon Sep 17 00:00:00 2001 From: boli32 Date: Thu, 9 Jan 2025 16:00:16 +0000 Subject: [PATCH 30/42] Fixed Location Saving --- QuestTracker/1.0/QuestTracker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index db3df54080..5375ceaa0a 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -2055,6 +2055,7 @@ var QuestTracker = QuestTracker || (function () { const adjustLocation = (location) => { if (WEATHER.enviroments.hasOwnProperty(location)) { QUEST_TRACKER_WeatherLocation = location; + saveQuestTrackerData(); } else return; }; return { From 575e8ea2648058827f654c643244a4ea84c8d78a Mon Sep 17 00:00:00 2001 From: boli32 Date: Thu, 9 Jan 2025 16:08:10 +0000 Subject: [PATCH 31/42] Added return to menu for button. --- QuestTracker/1.0/QuestTracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 5375ceaa0a..4f3d3f18ee 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -4269,7 +4269,7 @@ var QuestTracker = QuestTracker || (function () { menu += `
    Day  Week  Month -  Year +  Year
    Custom
    Day  Week From 5706e78e4e45b712d45545228283872950b4d3fb Mon Sep 17 00:00:00 2001 From: boli32 Date: Thu, 9 Jan 2025 16:19:43 +0000 Subject: [PATCH 32/42] Update QuestTracker.js --- QuestTracker/1.0/QuestTracker.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 4f3d3f18ee..9e034f53d8 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -3392,6 +3392,11 @@ var QuestTracker = QuestTracker || (function () { .join(''); return dropdownString; }, + returnCurrentLocation: (key) => { + const { WEATHER } = getCalendarAndWeatherData(); + if (WEATHER.enviroments && WEATHER.enviroments[key]) return WEATHER.enviroments[key].name; + else return "Unknown Location"; + }, buildCalenderDropdown: () => { const dropdownString = Object.entries(CALENDARS) .map(([key, value]) => `|${value.name},${key}`) @@ -3489,7 +3494,7 @@ var QuestTracker = QuestTracker || (function () { Lunar Phase ${Calendar.getLunarPhase(QUEST_TRACKER_currentDate)} Location - ${QUEST_TRACKER_WeatherLocation}Change + ${H.returnCurrentLocation(QUEST_TRACKER_WeatherLocation)}Change Temperature${temperatureDisplay} ${QUEST_TRACKER_CURRENT_WEATHER['scaleDescriptions']['temperature']} Precipitation${precipitationDisplay} From d42f809ad5ba18c82c112d27c3ef85b3f128f2a5 Mon Sep 17 00:00:00 2001 From: boli32 Date: Fri, 10 Jan 2025 11:03:00 +0000 Subject: [PATCH 33/42] Tweaked Quest Builder Sizes and added gmnotes field --- QuestTracker/1.0/QuestTracker.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 9e034f53d8..14d72c5e26 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -2077,11 +2077,11 @@ var QuestTracker = QuestTracker || (function () { const vars = { DEFAULT_PAGE_UNIT: 70, AVATAR_SIZE: 70, - TEXT_FONT_SIZE: 10, + TEXT_FONT_SIZE: 14, PAGE_HEADER_WIDTH: 700, PAGE_HEADER_HEIGHT: 150, - ROUNDED_RECT_WIDTH: 320, - ROUNDED_RECT_HEIGHT: 80, + ROUNDED_RECT_WIDTH: 280, + ROUNDED_RECT_HEIGHT: 60, ROUNDED_RECT_CORNER_RADIUS: 10, VERTICAL_SPACING: 100, HORIZONTAL_SPACING: 160, @@ -2596,7 +2596,8 @@ var QuestTracker = QuestTracker || (function () { layer: layer, imgsrc: imgsrc, tooltip: trimmedText, - controlledby: '' + controlledby: '', + gmnotes: questId }); if (avatarObj) { H.storeQuestRef(questId, 'avatar', avatarObj.id); From 798e7a343e9b3e66b5cf4b67e6006fddcd188a70 Mon Sep 17 00:00:00 2001 From: boli32 Date: Fri, 10 Jan 2025 12:53:52 +0000 Subject: [PATCH 34/42] Adjusted and fixed QuestBuilder Page to work with Supernotes --- QuestTracker/1.0/QuestTracker.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 14d72c5e26..51ccf38102 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -162,7 +162,8 @@ var QuestTracker = QuestTracker || (function () { state.QUEST_TRACKER.historicalWeather = QUEST_TRACKER_HISTORICAL_WEATHER; state.QUEST_TRACKER.weatherDescription = QUEST_TRACKER_WEATHER_DESCRIPTION; state.QUEST_TRACKER.weather = QUEST_TRACKER_WEATHER; - state.QUEST_TRACKER.imperialMeasurements = QUEST_TRACKER_imperialMeasurements + state.QUEST_TRACKER.imperialMeasurements = QUEST_TRACKER_imperialMeasurements; + state.QUEST_TRACKER.TreeObjRef = QUEST_TRACKER_TreeObjRef; }; const initializeQuestTrackerState = (forced = false) => { if (!state.QUEST_TRACKER || Object.keys(state.QUEST_TRACKER).length === 0 || forced) { @@ -175,7 +176,7 @@ var QuestTracker = QuestTracker || (function () { rumoursByLocation: {}, generations: {}, readableJSON: true, - QUEST_TRACKER_TreeObjRef: {}, + TreeObjRef: {}, jumpGate: true, events: {}, calenderType: 'gregorian', @@ -2143,6 +2144,7 @@ var QuestTracker = QuestTracker || (function () { } else { QUEST_TRACKER_TreeObjRef[questId][type] = objRef; } + saveQuestTrackerData(); }, replaceImageSize: (imgsrc) => { return imgsrc.replace(/\/(med|original|max|min)\.(gif|jpg|jpeg|bmp|webp|png)(\?.*)?$/i, '/thumb.$2$3'); @@ -2597,7 +2599,12 @@ var QuestTracker = QuestTracker || (function () { imgsrc: imgsrc, tooltip: trimmedText, controlledby: '', - gmnotes: questId + gmnotes: ` + [Open Quest](!qt-menu action=quest|id=${questId}) + [Toggle Visibilty](!qt-quest action=update|field=hidden|current=${questId}|old=${questData.hidden}|new=${questData.hidden ? 'false ' : 'true'}) + [Change Status](!qt-quest action=update|field=status|current=${questId}|new=?{Change Status|Unknown,1|Discovered,2|Started,3|Ongoing,4|Completed,5|Completed By Someone Else,6|Failed,7|Time ran out,8|Ignored,9}) + `, + name: `${questData.name || 'No description available.'}` }); if (avatarObj) { H.storeQuestRef(questId, 'avatar', avatarObj.id); @@ -2638,9 +2645,13 @@ var QuestTracker = QuestTracker || (function () { const pageId = pageObj.id; if (!QUEST_TRACKER_TreeObjRef[questId] || !QUEST_TRACKER_TreeObjRef[questId].text) return; const textObjId = QUEST_TRACKER_TreeObjRef[questId].text; - const textObj = getObj('text', textObjId); + const textObj = getObj('text', textObjId); if (textObj) { const questData = QUEST_TRACKER_globalQuestData[questId]; + if (!questData) { + errorCheck(152, 'msg', null,`Quest data for "${questId}" is missing.`); + return; + } const isHidden = questData.hidden || false; const textLayer = isHidden ? 'gmlayer' : 'objects'; const x = textObj.get('left'); @@ -2700,8 +2711,6 @@ var QuestTracker = QuestTracker || (function () { }; const updateQuestVisibility = (questId, makeHidden) => { if (!QUEST_TRACKER_TreeObjRef[questId]) return; - const questData = QUEST_TRACKER_globalQuestData[questId]; - if (!questData) return; const pageId = findObjs({ type: 'page', name: QUEST_TRACKER_pageName })[0].id; if (typeof makeHidden === 'string') makeHidden = makeHidden.toLowerCase() === 'true'; const targetLayer = makeHidden ? 'gmlayer' : 'map'; From 3c5e9df94557a29453d1c469ea077f3f3a7ac8bd Mon Sep 17 00:00:00 2001 From: boli32 Date: Fri, 10 Jan 2025 13:02:32 +0000 Subject: [PATCH 35/42] Updated Questracker to v1.0.1 --- QuestTracker/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/QuestTracker/README.md b/QuestTracker/README.md index ce3c1e1018..c842b13c95 100644 --- a/QuestTracker/README.md +++ b/QuestTracker/README.md @@ -453,9 +453,11 @@ Quests are organised into quest groups on the Quest Tree page, they are ordered ### Quest Images -Quest can have images which are tokens. These are set manually using the rollable table, they have the questID in the GM Notes so can be used with tokenMod in order to trigger quest action, such as changing it's status. +Quest can have images which are tokens. These are set manually using the rollable table; they will not how unles it is directly pulled from your own roll20 library (no external or using premium assets) -I will develop a macro for this use and include it soon. +### Compatability with Supernotes + +Simply use the command **!gmnote** (create it as a token macro) when selecting a quest token and it will open up an small menu with quick functionality with the main interface. actioning any of these commands will open up the full quest interface afterwards. I highly recomend using **!gmnote --config** to toggle off the footer buttons when you set it up. ## FAQ @@ -479,7 +481,9 @@ Yes, that is a workaround to having relationships between quest groups and it *c ## Updates -#### 2026-01-09 +#### 2025-01-10 +* **v1.0.1** Various small fixes. Made Compatable with Supernotes Mod +#### 2025-01-09 * **v1.0** Official Release #### 2025-01-08 * **v0.9.2** Fixed quest dropdown to deal with single quests without a dropdown. Also an ungrouped quest which get assigned a relasionship to a quest within a quest group automatically now gets assigned to that quest group. From c6b90150c427d3620818f96ad733546f3e9e9aa4 Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 13 Jan 2025 11:41:26 +0000 Subject: [PATCH 36/42] Added multiple moon functionality Added the ability to have multiple moons, the names of moons and added 2 additional calendars into the calendarData (for Dragonlance and Eberon Settings) --- CalenderData/1.0/CalenderData.js | 686 ++++++++++++++++++++++++++++--- QuestTracker/1.0/QuestTracker.js | 42 +- QuestTracker/README.md | 4 +- 3 files changed, 659 insertions(+), 73 deletions(-) diff --git a/CalenderData/1.0/CalenderData.js b/CalenderData/1.0/CalenderData.js index 29c8a404fc..bd0bfc969e 100644 --- a/CalenderData/1.0/CalenderData.js +++ b/CalenderData/1.0/CalenderData.js @@ -51,16 +51,19 @@ on('ready', () => { "startingWeekday": "Thursday", "dateFormat": "{day}{ordinal} of {month}, {year}", "lunarCycle": { - "baselineNewMoon": "1970-01-07", - "cycleLength": 29.53059, - "phases": [ - { "name": "New Moon", "start": 0, "end": 1 }, - { "name": "Waxing Crescent", "start": 1, "end": 7.4 }, - { "name": "First Quarter", "start": 7.4, "end": 14.8 }, - { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, - { "name": "Full Moon", "start": 22.1, "end": 29.5 }, - { "name": "Waning Crescent", "start": 29.5, "end": 29.53059 } - ] + "moon": { + "name": "Moon", + "baselineNewMoon": "1970-01-07", + "cycleLength": 29.53059, + "phases": [ + { "name": "New Moon", "start": 0, "end": 1 }, + { "name": "Waxing Crescent", "start": 1, "end": 7.4 }, + { "name": "First Quarter", "start": 7.4, "end": 14.8 }, + { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, + { "name": "Full Moon", "start": 22.1, "end": 29.5 }, + { "name": "Waning Crescent", "start": 29.5, "end": 29.53059 } + ] + } }, "climates": { "northern temperate": { @@ -322,19 +325,22 @@ on('ready', () => { "startingWeekday": "First Day", "dateFormat": "{day}{ordinal} of {month}, {year}", "lunarCycle": { - "baselineNewMoon": "1372-01-01", - "cycleLength": 30.4375, - "phases": [ - { "name": "New Moon", "start": 0, "end": 3.8 }, - { "name": "Young", "start": 3.8, "end": 7.6 }, - { "name": "Waxing Crescent", "start": 7.6, "end": 11.4 }, - { "name": "Waxing Quarter", "start": 11.4, "end": 15.2 }, - { "name": "Waxing Gibbous", "start": 15.2, "end": 19.0 }, - { "name": "Full Moon", "start": 19.0, "end": 22.8 }, - { "name": "Waning Gibbous", "start": 22.8, "end": 26.6 }, - { "name": "Waning Quarter", "start": 26.6, "end": 29.0 }, - { "name": "Waning Crescent", "start": 29.0, "end": 30.4375 } - ] + "selune": { + "name": "Selûne", + "baselineNewMoon": "1372-01-01", + "cycleLength": 30.4375, + "phases": [ + { "name": "New Moon", "start": 0, "end": 3.8 }, + { "name": "Young", "start": 3.8, "end": 7.6 }, + { "name": "Waxing Crescent", "start": 7.6, "end": 11.4 }, + { "name": "Waxing Quarter", "start": 11.4, "end": 15.2 }, + { "name": "Waxing Gibbous", "start": 15.2, "end": 19.0 }, + { "name": "Full Moon", "start": 19.0, "end": 22.8 }, + { "name": "Waning Gibbous", "start": 22.8, "end": 26.6 }, + { "name": "Waning Quarter", "start": 26.6, "end": 29.0 }, + { "name": "Waning Crescent", "start": 29.0, "end": 30.4375 } + ] + } }, "climates": { "Icewind Dale": { @@ -584,18 +590,21 @@ on('ready', () => { "startingWeekday": "Vasárnap", "dateFormat": "{day}{ordinal} of {month}, {year}", "lunarCycle": { - "baselineNewMoon": "735-01-15", - "cycleLength": 28, - "phases": [ - { "name": "Full Moon", "start": 0, "end": 1 }, - { "name": "Waning Gibbous", "start": 2, "end": 7 }, - { "name": "Left Half", "start": 8, "end": 8 }, - { "name": "Waning Crescent", "start": 9, "end": 14 }, - { "name": "New Moon", "start": 15, "end": 15 }, - { "name": "Waxing Crescent", "start": 16, "end": 21 }, - { "name": "Right Half", "start": 22, "end": 22 }, - { "name": "Waxing Gibbous", "start": 23, "end": 28 } - ] + "ghostmoon": { + "name": "The Ghost Moon", + "baselineNewMoon": "735-01-15", + "cycleLength": 28, + "phases": [ + { "name": "Full Moon", "start": 0, "end": 1 }, + { "name": "Waning Gibbous", "start": 2, "end": 7 }, + { "name": "Left Half", "start": 8, "end": 8 }, + { "name": "Waning Crescent", "start": 9, "end": 14 }, + { "name": "New Moon", "start": 15, "end": 15 }, + { "name": "Waxing Crescent", "start": 16, "end": 21 }, + { "name": "Right Half", "start": 22, "end": 22 }, + { "name": "Waxing Gibbous", "start": 23, "end": 28 } + ] + } }, "climates": { "barovian standard": { @@ -641,14 +650,17 @@ on('ready', () => { "startingWeekday": "Moonday", "dateFormat": "{day}{ordinal} of {month}, {year}", "lunarCycle": { - "baselineNewMoon": "4712-01-12", - "cycleLength": 29.5, - "phases": [ - { "name": "New Moon", "start": 0, "end": 7.375 }, - { "name": "First Quarter", "start": 7.375, "end": 14.75 }, - { "name": "Full Moon", "start": 14.75, "end": 22.125 }, - { "name": "Last Quarter", "start": 22.125, "end": 29.5 } - ] + "somal": { + "name": "Somal", + "baselineNewMoon": "4712-01-12", + "cycleLength": 29.5, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7.375 }, + { "name": "First Quarter", "start": 7.375, "end": 14.75 }, + { "name": "Full Moon", "start": 14.75, "end": 22.125 }, + { "name": "Last Quarter", "start": 22.125, "end": 29.5 } + ] + } }, "climates": { "northern temperate": { @@ -891,14 +903,28 @@ on('ready', () => { "startingWeekday": "Starday", "dateFormat": "{day}{ordinal} of {month}, {year}", "lunarCycle": { - "baselineNewMoon": "591-01-01", - "cycleLength": 28, - "phases": [ - { "name": "New Moon", "start": 0, "end": 7 }, - { "name": "First Quarter", "start": 7, "end": 14 }, - { "name": "Full Moon", "start": 14, "end": 21 }, - { "name": "Last Quarter", "start": 21, "end": 28 } - ] + "luna": { + "name": "Luna", + "baselineNewMoon": "591-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "First Quarter", "start": 7, "end": 14 }, + { "name": "Full Moon", "start": 14, "end": 21 }, + { "name": "Last Quarter", "start": 21, "end": 28 } + ] + }, + "celene": { + "name": "Celene", + "baselineNewMoon": "591-02-15", + "cycleLength": 91, + "phases": [ + { "name": "New Moon", "start": 0, "end": 22.75 }, + { "name": "First Quarter", "start": 22.75, "end": 45.5 }, + { "name": "Full Moon", "start": 45.5, "end": 68.25 }, + { "name": "Last Quarter", "start": 68.25, "end": 91 } + ] + } }, "climates": { "northern temperate": { @@ -1135,15 +1161,31 @@ on('ready', () => { "daysOfWeek": ["Miresen", "Grissen", "Whelsen", "Conthsen", "Folsen", "Yulisen"], "defaultDate": "835-01-01", "lunarCycle": { - "baselineNewMoon": "835-01-01", - "cycleLength": 29.5, - "phases": [ - { "name": "New Moon", "start": 0, "end": 3.6 }, - { "name": "Waxing Crescent", "start": 3.6, "end": 7.4 }, - { "name": "First Quarter", "start": 7.4, "end": 14.8 }, - { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, - { "name": "Full Moon", "start": 22.1, "end": 29.5 } - ] + "catha": { + "name": "Catha", + "baselineNewMoon": "835-01-01", + "cycleLength": 29.5, + "phases": [ + { "name": "New Moon", "start": 0, "end": 3.6 }, + { "name": "Waxing Crescent", "start": 3.6, "end": 7.4 }, + { "name": "First Quarter", "start": 7.4, "end": 14.8 }, + { "name": "Waxing Gibbous", "start": 14.8, "end": 22.1 }, + { "name": "Full Moon", "start": 22.1, "end": 29.5 } + ] + }, + "ruidus": { + "name": "Ruidus", + "baselineNewMoon": "835-01-01", + "cycleLength": 327, + "phases": [ + { "name": "Dormant Glow", "start": 0, "end": 60 }, + { "name": "Rising Flare", "start": 60, "end": 120 }, + { "name": "Subtle Radiance", "start": 120, "end": 163.5 }, + { "name": "Burning Apex", "start": 163.5, "end": 200 }, + { "name": "Fading Glimmer", "start": 200, "end": 260 }, + { "name": "Vanishing Ember", "start": 260, "end": 327 } + ] + } }, "climates": { "northern temperate": { @@ -1359,6 +1401,530 @@ on('ready', () => { "9-5": "Day of the Harvest", "11-1": "Duscar's End" } + }, + "galifar": { + "name": "Galifar", + "months": [ + { "id": 1, "name": "Zarantyr", "days": 28 }, + { "id": 2, "name": "Olarune", "days": 28 }, + { "id": 3, "name": "Therendor", "days": 28 }, + { "id": 4, "name": "Eyre", "days": 28 }, + { "id": 5, "name": "Dravago", "days": 28 }, + { "id": 6, "name": "Nymm", "days": 28 }, + { "id": 7, "name": "Lharvion", "days": 28 }, + { "id": 8, "name": "Barrakas", "days": 28 }, + { "id": 9, "name": "Rhaan", "days": 28 }, + { "id": 10, "name": "Sypheros", "days": 28 }, + { "id": 11, "name": "Aryth", "days": 28 }, + { "id": 12, "name": "Vult", "days": 28 } + ], + "daysOfWeek": ["Sul", "Mol", "Zol", "Wir", "Zor", "Far", "Sar"], + "defaultDate": "998-01-01", + "lunarCycle": { + "eyre": { + "name": "Eyre", + "baselineNewMoon": "998-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "First Quarter", "start": 7, "end": 14 }, + { "name": "Full Moon", "start": 14, "end": 21 }, + { "name": "Last Quarter", "start": 21, "end": 28 } + ] + }, + "lurthir": { + "name": "Lurthir", + "baselineNewMoon": "998-01-01", + "cycleLength": 91, + "phases": [ + { "name": "New Moon", "start": 0, "end": 15 }, + { "name": "First Quarter", "start": 15, "end": 45 }, + { "name": "Full Moon", "start": 45, "end": 75 }, + { "name": "Last Quarter", "start": 75, "end": 91 } + ] + } + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "Day of Renewal", + "2-14": "Feast of Olarune", + "4-15": "Therendor’s Bloom", + "6-28": "Day of Nymm's Zenith", + "8-15": "Lharvion’s Light", + "10-31": "Night of Sypheros", + "12-28": "Vult's Last Light" + } + }, + "krynn": { + "name": "Krynn", + "months": [ + { "id": 1, "name": "Aelmont", "days": 30 }, + { "id": 2, "name": "Rannmont", "days": 30 }, + { "id": 3, "name": "Mishamont", "days": 30 }, + { "id": 4, "name": "Chislmont", "days": 30 }, + { "id": 5, "name": "Bran", "days": 30 }, + { "id": 6, "name": "Corij", "days": 30 }, + { "id": 7, "name": "Argon", "days": 30 }, + { "id": 8, "name": "Sirrimont", "days": 30 }, + { "id": 9, "name": "Reorxmont", "days": 30 }, + { "id": 10, "name": "Hiddumont", "days": 30 }, + { "id": 11, "name": "Brammermont", "days": 30 }, + { "id": 12, "name": "Phoenix", "days": 30 } + ], + "daysOfWeek": ["Borelsday", "Argensday", "Paltorsday", "Urday", "Nuitarsday"], + "defaultDate": "351-01-01", + "lunarCycle": { + "solinari": { + "name": "Solinari", + "baselineNewMoon": "351-01-01", + "cycleLength": 36, + "phases": [ + { "name": "New Moon", "start": 0, "end": 9 }, + { "name": "First Quarter", "start": 9, "end": 18 }, + { "name": "Full Moon", "start": 18, "end": 27 }, + { "name": "Last Quarter", "start": 27, "end": 36 } + ] + }, + "lunitari": { + "name": "Lunitari", + "baselineNewMoon": "351-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "First Quarter", "start": 7, "end": 14 }, + { "name": "Full Moon", "start": 14, "end": 21 }, + { "name": "Last Quarter", "start": 21, "end": 28 } + ] + }, + "nuitari": { + "name": "Nuitari", + "baselineNewMoon": "351-01-01", + "cycleLength": 8, + "phases": [ + { "name": "New Moon", "start": 0, "end": 2 }, + { "name": "First Quarter", "start": 2, "end": 4 }, + { "name": "Full Moon", "start": 4, "end": 6 }, + { "name": "Last Quarter", "start": 6, "end": 8 } + ] + } + }, + "climates": { + "northern temperate": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -10, "Spring": 5, "Summer": 7.5, "Autumn": 2.5 }, + "precipitation": { "Winter": 5, "Spring": 5, "Summer": -2.5, "Autumn": 2.5 }, + "wind": { "Winter": 5, "Spring": 3, "Summer": 2, "Autumn": 3 }, + "humid": { "Winter": 7.5, "Spring": 10, "Summer": 5, "Autumn": 7.5 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 5, "Spring": 7, "Summer": -2, "Autumn": 0 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "southern temperate": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 7.5, "Autumn": 2.5, "Winter": -10, "Spring": 5 }, + "precipitation": { "Summer": 2.5, "Autumn": 7.5, "Winter": 2.5, "Spring": 7.5 }, + "wind": { "Summer": 3, "Autumn": 5, "Winter": 7, "Spring": 5 }, + "humid": { "Summer": 5, "Autumn": 7.5, "Winter": 7.5, "Spring": 10 }, + "visibility": { "Summer": 5, "Autumn": 0, "Winter": -5, "Spring": 0 }, + "cloudy": { "Summer": -2.5, "Autumn": 0, "Winter": 5, "Spring": 2.5 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "northern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 2.5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 5, "Dry": 11 } + }, + "southern tropical": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 2.5, "Dry": 5 }, + "precipitation": { "Wet": 10, "Dry": -10 }, + "wind": { "Wet": 5, "Dry": 3 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": 5, "Dry": 10 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 11, "Dry": 5 } + }, + "northern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 5, "Polar Night": 11 } + }, + "southern polar": { + "seasons": ["Polar Day", "Polar Night"], + "modifiers": { + "temperature": { "Polar Day": -15, "Polar Night": -20 }, + "precipitation": { "Polar Day": -2.5, "Polar Night": 0 }, + "wind": { "Polar Day": 10, "Polar Night": 15 }, + "humid": { "Polar Day": 5, "Polar Night": 10 }, + "visibility": { "Polar Day": 0, "Polar Night": 5 }, + "cloudy": { "Polar Day": 5, "Polar Night": 10 } + }, + "seasonStart": { "Polar Day": 11, "Polar Night": 5 } + }, + "northern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 4, "Cool": 10 } + }, + "northern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 3, "Dry": 9 } + }, + "northern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 6, "Winter": 12 } + }, + "northern mountain": { + "seasons": ["Winter", "Spring", "Summer", "Autumn"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Winter": 12, "Spring": 3, "Summer": 6, "Autumn": 9 } + }, + "equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 7.5, "Dry": 5 }, + "precipitation": { "Wet": 15, "Dry": -5 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 15, "Dry": 10 }, + "visibility": { "Wet": -5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 1, "Dry": 7 } + }, + "southern continental": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 0, "Winter": -10, "Spring": 0 }, + "precipitation": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 0 }, + "wind": { "Summer": 10, "Autumn": 15, "Winter": 20, "Spring": 15 }, + "humid": { "Summer": 10, "Autumn": 15, "Winter": 10, "Spring": 15 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 0, "Spring": 5 }, + "cloudy": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern mediterranean": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Summer": 10, "Autumn": 5, "Winter": -5, "Spring": 5 }, + "precipitation": { "Summer": -5, "Autumn": 5, "Winter": 7.5, "Spring": 5 }, + "wind": { "Summer": 5, "Autumn": 10, "Winter": 15, "Spring": 10 }, + "humid": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 }, + "visibility": { "Summer": 5, "Autumn": 10, "Winter": 10, "Spring": 10 }, + "cloudy": { "Summer": 10, "Autumn": 20, "Winter": 25, "Spring": 20 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + }, + "southern desert": { + "seasons": ["Hot", "Cool"], + "modifiers": { + "temperature": { "Hot": 20, "Cool": 10 }, + "precipitation": { "Hot": -20, "Cool": -15 }, + "wind": { "Hot": 10, "Cool": 15 }, + "humid": { "Hot": -5, "Cool": 0 }, + "visibility": { "Hot": 0, "Cool": 5 }, + "cloudy": { "Hot": -2.5, "Cool": 2.5 } + }, + "seasonStart": { "Hot": 10, "Cool": 4 } + }, + "southern equatorial": { + "seasons": ["Wet", "Dry"], + "modifiers": { + "temperature": { "Wet": 10, "Dry": 17.5 }, + "precipitation": { "Wet": 15, "Dry": 10 }, + "wind": { "Wet": 5, "Dry": 5 }, + "humid": { "Wet": 10, "Dry": 5 }, + "visibility": { "Wet": -2.5, "Dry": 5 }, + "cloudy": { "Wet": 10, "Dry": 5 } + }, + "seasonStart": { "Wet": 9, "Dry": 3 } + }, + "southern tundra": { + "seasons": ["Summer", "Winter"], + "modifiers": { + "temperature": { "Summer": -5, "Winter": -15 }, + "precipitation": { "Summer": -5, "Winter": 5 }, + "wind": { "Summer": 10, "Winter": 15 }, + "humid": { "Summer": 10, "Winter": 5 }, + "visibility": { "Summer": 5, "Winter": -2.5 }, + "cloudy": { "Summer": 5, "Winter": 10 } + }, + "seasonStart": { "Summer": 12, "Winter": 6 } + }, + "southern mountain": { + "seasons": ["Summer", "Autumn", "Winter", "Spring"], + "modifiers": { + "temperature": { "Winter": -15, "Spring": -5, "Summer": 5, "Autumn": 0 }, + "precipitation": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 5 }, + "wind": { "Winter": 20, "Spring": 15, "Summer": 10, "Autumn": 15 }, + "humid": { "Winter": 10, "Spring": 15, "Summer": 10, "Autumn": 10 }, + "visibility": { "Winter": -5, "Spring": 0, "Summer": 5, "Autumn": 0 }, + "cloudy": { "Winter": 10, "Spring": 15, "Summer": 5, "Autumn": 10 } + }, + "seasonStart": { "Summer": 12, "Autumn": 3, "Winter": 6, "Spring": 9 } + } + }, + "significantDays": { + "1-1": "New Year's Day", + "4-15": "Festival of Mishakal", + "8-25": "Night of the Eye", + "12-30": "End of the Year Feast" + } } }; state.CalenderData.WEATHER = { diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 51ccf38102..18d3d67c76 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -1908,20 +1908,21 @@ var QuestTracker = QuestTracker || (function () { } saveQuestTrackerData(); }; - const getLunarPhase = (date) => { + const getLunarPhase = (date, moonId) => { const calendar = CALENDARS[QUEST_TRACKER_calenderType]; - if (!calendar.lunarCycle) return null; - const lunarCycle = calendar.lunarCycle; - const baselineDate = new Date(lunarCycle.baselineNewMoon); + if (errorCheck(153, 'exists', calendar.lunarCycle, `calendar.lunarCycle`)) return; + if (errorCheck(154, 'exists', calendar.lunarCycle[moonId], `calendar.lunarCycle[${moonId}]`)) return; + const { baselineNewMoon, cycleLength, phases, name } = calendar.lunarCycle[moonId]; + const baselineDate = new Date(baselineNewMoon); const currentDate = new Date(date); const daysSinceBaseline = (currentDate - baselineDate) / (1000 * 60 * 60 * 24); - const phase = (daysSinceBaseline % lunarCycle.cycleLength + lunarCycle.cycleLength) % lunarCycle.cycleLength; - for (const { name, start, end } of lunarCycle.phases) { + const phase = (daysSinceBaseline % cycleLength + cycleLength) % cycleLength; + for (const { name: phaseName, start, end } of phases) { if (phase >= start && phase < end) { - return name; + return `${name}: ${phaseName}`; } } - return "Unknown Phase"; + return `${name}: Unknown Phase`; }; const describeWeather = () => { const L = { @@ -3419,7 +3420,24 @@ var QuestTracker = QuestTracker || (function () { .map((climate) => `|${climate.charAt(0).toUpperCase() + climate.slice(1)},${climate}`) .join(""); return dropdownString; - } + }, + hasMultipleMoons: (l) => { + if (Object.keys(l).length > 1) return true; + else return false; + }, + lunarPhases: () => { + const calendar = CALENDARS[QUEST_TRACKER_calenderType]; + if (errorCheck(155, 'exists', calendar.lunarCycle, `calendar.lunarCycle`)) return; + const currentDate = QUEST_TRACKER_currentDate; + let output = `Lunar Phase${H.hasMultipleMoons(calendar.lunarCycle) ? 's' : ''}`; + for (const moonId in calendar.lunarCycle) { + if (calendar.lunarCycle.hasOwnProperty(moonId)) { + const phase = Calendar.getLunarPhase(currentDate, moonId); + output += `${phase}`; + } + } + return output; + } }; const buildWeather = (isMenu = false, isHome = false) => { const FromValue = { @@ -3495,14 +3513,14 @@ var QuestTracker = QuestTracker || (function () { const cloudCoverDisplay = QUEST_TRACKER_CURRENT_WEATHER['rolls']['cloudCover']; const visibilityDisplay = QUEST_TRACKER_imperialMeasurements['wind'] ? visibilityValue['imperial']['distance'] + visibilityValue['metric']['unit'] : visibilityValue['metric']['unit'] + visibilityValue['imperial']['unit']; const locationDropdown = H.buildLocationDropdown(); + const LunarPhaseDisplay = H.lunarPhases(); const returnto = isMenu ? "menu=true|" : isHome ? "home=true|" : ""; let menu = ` + ${LunarPhaseDisplay} - - @@ -3757,7 +3775,7 @@ var QuestTracker = QuestTracker || (function () { } }; const generateGMMenu = () => { - let menu = `

    Calendar

    `; + let menu = `

    Calendarr

    `; menu += `
    ${Calendar.formatDateFull()}
    ( ${QUEST_TRACKER_currentDate} )`; if (QUEST_TRACKER_WEATHER && QUEST_TRACKER_CURRENT_WEATHER !== null) { menu += buildWeather({ isMenu: true }); diff --git a/QuestTracker/README.md b/QuestTracker/README.md index c842b13c95..6b32ab8106 100644 --- a/QuestTracker/README.md +++ b/QuestTracker/README.md @@ -357,7 +357,7 @@ I can add additional Calendars into the module if you were to provide the detail #### Lunar Cycles -Each calendar includes options for tracking lunar phases. The lunar cycle will display key phases, such as the below and are setting specific. There are no current plans to incorporate multiple moons at this time for settings such as Dragonlance. +Each calendar includes options for tracking lunar phases. The lunar cycle will display key phases, such as the below and are setting specific. Each Calander can have multiple moons and their lunar cycle including their custom phases is displayed along with the weather. * New Moon * Waxing Crescent @@ -481,6 +481,8 @@ Yes, that is a workaround to having relationships between quest groups and it *c ## Updates +#### 2025-01-10 +* **v1.0.2** Added Krynn (Dragonlance) and Galifar (Eberon) Calanders. also expanded to allow for multiple moons and different cycles. Added the smaller and secondary moons to both Exandria and Grekhawk calander. #### 2025-01-10 * **v1.0.1** Various small fixes. Made Compatable with Supernotes Mod #### 2025-01-09 From 1db1bda6258bbdcff98d5c5029fee91b0d9ca553 Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 13 Jan 2025 11:43:34 +0000 Subject: [PATCH 37/42] small error in readme --- QuestTracker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestTracker/README.md b/QuestTracker/README.md index 6b32ab8106..f3206ec255 100644 --- a/QuestTracker/README.md +++ b/QuestTracker/README.md @@ -481,7 +481,7 @@ Yes, that is a workaround to having relationships between quest groups and it *c ## Updates -#### 2025-01-10 +#### 2025-01-13 * **v1.0.2** Added Krynn (Dragonlance) and Galifar (Eberon) Calanders. also expanded to allow for multiple moons and different cycles. Added the smaller and secondary moons to both Exandria and Grekhawk calander. #### 2025-01-10 * **v1.0.1** Various small fixes. Made Compatable with Supernotes Mod From 4e17af2dbfe5621b17df7205a69741cc3cb0f7a0 Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 13 Jan 2025 12:50:47 +0000 Subject: [PATCH 38/42] Suggested fix to Wildshape Chat message on startup only to show in debug mode --- WildShape/1.4.3/WildShape.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WildShape/1.4.3/WildShape.js b/WildShape/1.4.3/WildShape.js index 2822fe64bd..cd1fb8e6f7 100644 --- a/WildShape/1.4.3/WildShape.js +++ b/WildShape/1.4.3/WildShape.js @@ -2364,7 +2364,9 @@ var WildShape = WildShape || (function() { }); log(WS_API.NAME + ' v' + WS_API.VERSION + " Ready! WildUtils v" + UTILS.VERSION); - UTILS.chat("API v" + WS_API.VERSION + " Ready! command: " + WS_API.CMD.ROOT); + + // Chat message only appears on debug mode. + if(state[WS_API.STATENAME][WS_API.DATA_CONFIG].ENABLE_DEBUG) UTILS.chat("API v" + WS_API.VERSION + " Ready! command: " + WS_API.CMD.ROOT); } return { From 6b82288a3aad1078fbad75a43df032f6f9dc7ba3 Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 13 Jan 2025 20:31:37 +0000 Subject: [PATCH 39/42] same result, cleaner code discussed with original script author who approves (and suggested the cleaner code fix). --- WildShape/1.4.3/WildShape.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WildShape/1.4.3/WildShape.js b/WildShape/1.4.3/WildShape.js index cd1fb8e6f7..d2a6008f8a 100644 --- a/WildShape/1.4.3/WildShape.js +++ b/WildShape/1.4.3/WildShape.js @@ -2366,7 +2366,7 @@ var WildShape = WildShape || (function() { log(WS_API.NAME + ' v' + WS_API.VERSION + " Ready! WildUtils v" + UTILS.VERSION); // Chat message only appears on debug mode. - if(state[WS_API.STATENAME][WS_API.DATA_CONFIG].ENABLE_DEBUG) UTILS.chat("API v" + WS_API.VERSION + " Ready! command: " + WS_API.CMD.ROOT); + UTILS.debugChat("API v" + WS_API.VERSION + " Ready! command: " + WS_API.CMD.ROOT } return { From b17c9c759cc5131ec49eb6583d8621a991c15088 Mon Sep 17 00:00:00 2001 From: ChrisDDickey Date: Tue, 14 Jan 2025 07:29:25 +0800 Subject: [PATCH 40/42] 03.400 --- .../03.400/Earthdawn.js | 13135 ++++++++++++++++ .../script.json | 4 +- 2 files changed, 13137 insertions(+), 2 deletions(-) create mode 100644 Earthdawn (FASA Official) character sheet companion/03.400/Earthdawn.js diff --git a/Earthdawn (FASA Official) character sheet companion/03.400/Earthdawn.js b/Earthdawn (FASA Official) character sheet companion/03.400/Earthdawn.js new file mode 100644 index 0000000000..af56df25b1 --- /dev/null +++ b/Earthdawn (FASA Official) character sheet companion/03.400/Earthdawn.js @@ -0,0 +1,13135 @@ + +// +// Earthdawn Step Dice Roller +// Plus Earthdawn 4th edition character sheet helper class, which also serves as helper for the 1879 (FASA Official) character sheet. +// +// By Chris Dickey +// Version: See line two of code below. +// Last updated: 2024 June. +// +// Earthdawn (FASA Official) Character sheet and associated API Copyright 2015-2024 by Christopher D. Dickey. +// +// The Earthdawn step dice roller will take an Earthdawn 4th edition step number and roll the appropriate dice. +// For example: if a macro such as !edsdr~ ?{Step|0}~ ?{Karma Step|0}~ for ?{reason| no reason} +// results in a string such as !edsdr~ 10~ 0~ for melee attack +// this module will roll the step 10 dice (2d8!) and display the results. + +// Commands that will call this section of the script are as follows. +// !edsdr~ ?{Step|0}~ ?{Karma Step|0}~ for ?{reason| no reason} These results will be public to everybody +// !edsdrGM~ ?{Step|0}~ ?{Karma Step|0}~ for ?{reason| no reason} These results will display only to the GM and the person who ordered the dice roll. +// !edsdrHidden~ ?{Step|0}~ ?{Karma Step|0}~ for ?{reason| no reason} These results will display to the GM only, and NOT to the person (other than the GM) who ordered the dice roll. +// !edInit~ ?{Initiative Step}~ ?{Karma Step | 0}~ for Initiative The results will be added to the Initiative tracker. +// +// +// This module also contains a great deal of code that works with the Earthdawn and 1879 character sheets authored by Chris Dickey and Jean-Baptiste Faure (Discord/Facebook Jiboux Faure). +// This is all within the ParseObj class. If that class is removed, the stepdice roller will still work, but not the character sheet buttons. +// +// All commands that invoke this section of the code start with !Earthdawn~ +// See the comments in the ParseObj.Parse() routine and the individual routines for additional information on each of them. +// +// Programming Note: Nested Roll20 chat menu queries are confusing. for a sample of code that demonstrates best practices, search for "arrayAdd" in the chatMenu: stateEdit routine. +// Programming Note: HTML Builder call ( @param {string} [tag='div'], @param {(HtmlBuilder|string)} [content=''], @param {object} [attrs={}] ) +// Examples: return new HtmlBuilder( "", "", { "sheet-rolltemplate-sectSmall" }); +/* + return new HtmlBuilder( "a", buttonDisplayTxt, Object.assign( {}, { + href: noColonFix ? linkText : Earthdawn.encode( Earthdawn.colonFix( linkText )), + class: "sheet-chatbutton" }, + ( buttonColor || txtColor ) ? { // Optional style section if have a color + style: Object.assign( buttonColor ? { "background-color": buttonColor } : {}, + txtColor ? { "color": txtColor } : {} )} : {}, + tipText ? { // Optional tipText section + title: Earthdawn.encode( Earthdawn.encode( tipText )) } : {} + )) + " "; + Note that this one is a link (a), with a display text, and all of an href, a class, a style, and a title in the third parameter. + return new HtmlBuilder( "span", txt, { + style: { "border": "solid 1px yellow" }, + title: Earthdawn.encode( Earthdawn.encode( tip )) }) + And not this one has a span, some text, and an object that contains a style and a title. + return new HtmlBuilder( "span", txt, { class: "sheet-rolltemplate-texttip" }); +*/ + + +// +// Define a Name-space +var Earthdawn = Earthdawn || {}; + // define any name-space constants +Earthdawn.Version = "3.4"; // This is the version number of this API file. + // state.Earthdawn.sheetVersion is the version number of the html file being used and might or might not be the same as the API version. + // Each individual sheets edition_max is the sheetVersion that sheet has been updated to. + // So if a getAttrBN( "edition_max" ) is < sheetVersion, then the sheet is in the process of updating. + +Earthdawn.whoFrom = { + player: 0x08, + character: 0x10, + api: 0x20, + apiError: 0x40, + apiWarning: 0x80, + noArchive: 0x0100, + mask: 0x01F8 }; // This can be &'ed to get only the whoFrom part. +Earthdawn.whoTo = { // Note: whoTo: 0 is broadcast to everybody. WhoTo 3 is both player and GM. + public: 0x00, + player: 0x01, + gm: 0x02, + playerList: 0x04, // This is all players who can control the token. It modifies player and only has effect if player is also present. + mask: 0x03 }; // This can be &'ed to get only the whoTo part + +Earthdawn.flagsArmor = { // Note: This describes the contents of the edParse bFlags + na: 0x0001, + PA: 0x0002, + MA: 0x0004, + None: 0x0008, + Unknown: 0x0010, + Natural: 0x01000000, + Mask: 0x0100001F }; // This can be &'ed to get only the flagsArmor part. +Earthdawn.flagsTarget = { // Note: This describes the contents of the edParse bFlags + PD: 0x0020, + MD: 0x0040, + SD: 0x0080, + Highest: 0x0100, // Modifies above, such as Highest MD of all targets. + P1pt: 0x0200, // Plus one per target. + Each: 0x04000000, // Roll one dice, but compare the result to multiple target numbers. + Riposte: 0x0400, + Ask: 0x0800, + Set: 0x1000, // Set means attach the targetList to the token. + Natural: 0x02000000, + Mask: 0x06001FE0 }; // This can be &'ed to get only the flagsTarget part. +Earthdawn.flags = { // Note: This describes the contents of the edParse bFlags + HitsFound: 0x2000, // At least one of the selected tokens has recorded a hit that has not been cleared. + HitsNot: 0x4000, + WillEffect: 0x8000, // This roll is for a will effect. + NoOppMnvr: 0x010000, // This to-hit does not do extra damage on successes. + VerboseRoll: 0x020000, // Don't keep this roll information as secret as most rolls. + Recovery: 0x040000, // This is a recovery test. Add result to health. + RecoveryStun: 0x080000 }; // This is a recovery test. Stun only. +Earthdawn.flagsCreature = { // Note, if you ever change this, it also needs changing in sheetworkers updateCreatureFlags(). + Fury: 0x0001, + ResistPain: 0x0002, + HardenedArmor: 0x0004, + GrabAndBite: 0x0100, + Hamstring: 0x0200, + Overrun: 0x0400, + Pounce: 0x0800, + SqueezeTheLife: 0x1000, + CreatureMask: 0xffff, + ClipTheWing: 0x00100000, + CrackTheShell: 0x00200000, + Defang: 0x00400000, + Enrage: 0x00800000, + Provoke: 0x01000000, + PryLoose: 0x02000000, + OpponentMask: 0x1ff00000 }; +Earthdawn.style = { + Full: 0x00, // Give all information about the roll and target number. IE: Target number 12, Result 18, succeeded by 6 with 1 extra success. + VagueSuccess: 0x01, // Give full result of roll, but don't give detail upon target number or exactly how close to success roll was. IE: Result: 18. 1 extra success. + VagueRoll: 0x02 }; // Default. Don't give detail on the roll or the target number, just say how much succeeded or failed by. IE: Succeeded by 6 with 1 extra success. +Earthdawn.charType = { + object: "-1", // The token is not really a character at all, but is an object such as a campfire or a torch. + pc: "0", + npc: "1", + mook: "2" }; + + + + // define namespace variables. +Earthdawn.StatusMarkerCollection = undefined; + + + // These are namespace utility functions, and as such have no direct access to any object of ether EDclass or ParseObj. + + + + // recreate named ability. +Earthdawn.abilityAdd = function ( cID, ability, actionStr ) { + 'use strict'; + try { + Earthdawn.abilityRemove( cID, ability ); + createObj( "ability", { characterid: cID, name: ability, action: actionStr, istokenaction: true }); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:abilityAdd() error caught: " + err ); } +} // End abilityAdd + + + // If a named ability exists for this character, remove it. +Earthdawn.abilityRemove = function ( cID, ability ) { + 'use strict'; + try { + let aname = Earthdawn.matchString( ability ); // get the ability name we are looking for with all the non-latin alphabetic characters stripped out. + _.each( findObjs({ _type: "ability", _characterid: cID }), function( abObj ) { + if( aname == Earthdawn.matchString( abObj.get( "name" ))) + abObj.remove(); + }); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:abilityRemove() error caught: " + err ); } +} // End abiltiyRemove + + + + // Supports multiple sizes. Defaults to -icon, but if size is "s" then -iconSmall, and "l" (lowercase L) or "b" (big), then -iconLarge. + // For chatwindows to see a class they must start with sheet-rolltemplate. + // The two classes sheet-rolltemplate-icons and sheet-rolltemplate-iconSmall provide formating for the icons. + // The other classes, all of the form sheet-rolltemplate-icon-(name) provide a url to the icon. + // icon: name of icon only. sheet-rolltemplate-icons- is assumed. + // size: "s" then class sheet-rolltemplate-iconSmall. "l" (lowercase l) or "b" then -iconLarge. "i" or "m" for sheet0rolltemplate-icon + // tip: Tool tip to be displayed under icon. + // pre and post: And text before or after the icon. +Earthdawn.addIcon = function ( icon, size, tip, pre, post ) { + 'use strict'; + try { + let s, outer = tip && (pre || post), t = ""; + if( !size ) s = "icon"; + else if( size.length > 11 ) + log( "Data mismatch warning. addIcon found a large size, which in the past has meant that size was left out and it was using a tip for size. Icon: " + icon ); + else { + let size2 = size.slice( 0, 1 ).toLowerCase(); + if( size2 == "s" ) s = "iconSmall"; + else if( size2 == "l" || size2 == "b" ) s = "iconLarge"; + else if( size2 == "i" || size2 == "m" ) s = "icon"; // icon or medium. + else + log( "Data mismatch warning. Invalid size of '" + size + "' in addIcon: " + icon ); + } + if( outer ) t = ''; // If we have pre or post, tooltip is around everything. If not it is just in the icons span. + if( pre ) t += pre; + if( icon ) t += ' -1 ) { // is of form D1-Novice or some such. Probably with a Discipline and a Tier. + let tier = Earthdawn.getParam( type, 2, "-"), + disc = Earthdawn.safeString( Earthdawn.getParam( type, 1, "-" )); + if( (state.Earthdawn.gED && tier == "Novice") || (state.Earthdawn.g1879 && ( bCount !== "S") && ( tier == "Initiate" || tier == "Profession" ))) // Note, bCount might have already been set in SKK. + bCount = "T"; // Only need to count 1879 if Initiate, or ED if Novice. Otherwise we certainly need accounting. + if( disc === "QD" ) { // Questor + lpBasis = 0; // Note that this is not the final value, it gets adjusted in the switch below. + misclabel = tier + " Granted Questor Devotion"; + } else if( disc === "PA" ) { // Path + lpBasis = 0; + misclabel = tier + " Path Talent Option"; + } else if( disc === "V" ) { // Versatility + lpBasis = 1; + misclabel = tier + " Versatility"; + } else if( disc === "SMO" ) { // Spell Matrix Object + lpBasis = 0; + misclabel = tier + " Spell Matrix Object"; + } else { // Most Disciplines and Skills. + if( isFinite( disc.slice( -1 ))) + lpBasis = Earthdawn.parseInt2( disc.slice( -1 )) -1; + else { + log( Earthdawn.timeStamp() + "Error! ED Attribute recordWrap:typeLP() unknown type: " + type ); + lpBasis = 0; + } + misclabel = tier + [ " ", " 2nd ", " 3rd ", " 4th " ][lpBasis] + + (state.Earthdawn.gED ? ( disc.slice( 0, 2) == "TO" ? "Talent Option" : "Discipline Talent") + : (( disc.slice( 0, 2) == "TO") ? "Optional" + : ((disc === "F1") ? "Free" + : ((tier == "Profession") ? "Prof" : "Core"))) + " Skill"); + } + if( disc === "F1") + ++lpBasis; + switch ( tier ) { + case "Master": ++lpBasis; + case "Warden": case "Exemplar": ++lpBasis; + case "Journeyman": case "Adherent": ++lpBasis; + } + } else { // Not from Discipline + switch ( type ) { + case "Master": ++lpBasis; + case "Warden": case "Exemplar": ++lpBasis; + case "Journeyman": case "Adherent": ++lpBasis; + case "Initiate": case "Novice": + break; + case "Dummy": + case "Free": // old style + case "Free-link": // v2.0 + case "Item": + case "Item-link": + case "Creature": + case "Power": + case "Other": + return -1; + case "Racial": + misclabel = "Racial Talent"; + lpBasis = 0; + break; + case "Versatility": + misclabel = "Versatility Talent"; + lpBasis = 1; + break; + case "Special": + misclabel = "Special"; + lpBasis = 0; + break; + default: + log( Earthdawn.timeStamp() + "Error! ED Attribute recordWrap typeLP for: " + sa ); + log( "Continued: Got type: " + type ); + } // end switch type + } // end else not from discipline + if( lpBasis > 3 ) + lpBasis = 3; // Can't cost higher than master. + return; + } // end typeLP + + + switch( wrapper ) { + case "DSP_Circle": + if((rankTo + rankFrom) < 2 ) // First circle in first discipline is free. First circle in all other disciplines is complex. + return; + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -10 ) + "DSP_Name", "" ); + misclabel = "Discipline"; + miscval = rankFrom + " -> " + rankTo; + if( state.Earthdawn.gED ) + note = "Discipline Abilities (such as circle based bonuses to PD or MD) are not automatically added. If Discipline Abilities are gained at this circle you need to add them yourself."; + break; + case "Durability-Rank": + header = "Durability"; + misclabel = "Novice Core Skill"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 0; + break; + case "Questor": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -10 ) + "DSP_Name", "" ); + misclabel = "Questor Devotion"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 1; + break; + case "Path-Journeyman": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -10 ) + "DSP_Name", "" ); + misclabel = "Path Talent Costs Journeyman"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 1; + break; + case "Path-Master": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -10 ) + "DSP_Name", "" ); + misclabel = "Path Talent Costs Master"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 3; + break; + case "SP_Circle": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -6 ) + "Name", "" ); + misclabel = "Spell"; + miscval = "Circle " + rankTo + (( rankFrom > 0 ) ? " (changed from circle " + rankFrom + ")" : ""); + if( rankTo > 0 ) + lp = Earthdawn.fibonacci( rankTo ) * 100; + if( rankFrom > 0 ) + lp -= Earthdawn.fibonacci( rankFrom ) * 100; + silver = rankDiff * 100; + break; + case "NAC_Requirements": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -12 ) + "Name", "" ); + misclabel = "Knack"; + miscval = "Rank " + rankTo + (( rankFrom > 0 ) ? " (changed from Rank " + rankFrom + ")" : ""); + if( rankTo > 0 ) + lp = Earthdawn.fibonacci( rankTo ) * 100; + if( rankFrom > 0 ) + lp -= Earthdawn.fibonacci( rankFrom ) * 100; + silver = rankDiff * 50; + sTime = rankTo + " days."; + break; + case "Increases": + header = Earthdawn.getParam( sa, 2, "-"); + misclabel = "Attribute"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 4; + break; + case "SP-Rank": + bCount = "T"; + header = Earthdawn.getParam( sa, 2, "-"); + miscval = rankFrom + " -> " + rankTo; + if (typeLP() === -1) + return; + break; + case "Rank": + miscval = rankFrom + " -> " + rankTo; + switch ( Earthdawn.getParam( sa, 4, "_")) { + case "SPM": + switch ( Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Type", "-10" ) ) { + case "15": header = "Enh Matrix"; break; + case "25": header = "Armor Matrix"; break; + case "-20": header = "Shared Matrix"; break; + case "-10": + default: header = "Std Matrix"; + } + type = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Origin", "Free-link" ); + if( type === "Pseudo" ) return; + // Note that this falls past SKK into Talent. + case "SKK": + if( type === undefined ) { + bCount = "S"; + misclabel = "Knowledge Skill"; + lpBasis = 1; + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + if( state.Earthdawn.gED ) + break; // Earthdawn, all knowledge skills are just skills, never Professional skills. For 1879, it falls through. + type = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Type", "F1-Novice" ); + } + case "T": + if( type === undefined ) + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + if (typeLP() === -1) + return; + break; + case "TI": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + switch ( Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Type", "Novice" )) { + case "Novice": lpBasis = 0; misclabel = "Thread Item Novice Tier"; break; + case "Journeyman": lpBasis = 1; misclabel = "Thread Item Journeyman Tier"; break; + case "Warden": lpBasis = 2; misclabel = "Thread Item Warden Tier"; break; + case "Master": lpBasis = 3; misclabel = "Thread Item Master Tier"; break; + default: return; + } break; + case "SK": + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + misclabel = "Skill"; + let t = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Type", state.Earthdawn.gED ? "Novice" : "F1-Novice" ); + if( t.indexOf( "-" ) != -1 ) + t = t.slice( t.indexOf( "-" ) + 1); + switch ( t ) { + case "Initiate": lpBasis = 1; misclabel = "Initiate Skill"; if( state.Earthdawn.g1879 ) bCount = "S"; break; + case "Novice": lpBasis = 1; misclabel = "Novice Skill"; if( state.Earthdawn.gED ) bCount = "S"; break; + case "Journeyman": lpBasis = 2; misclabel = "Journeyman Skill"; break; + case "Warden": lpBasis = 3; misclabel = "Warden Skill"; break; + case "Master": lpBasis = 4; misclabel = "Master Skill"; break; + default: return; + } break; + break; + case "SKA": + bCount = "S"; + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + misclabel = "Artisan Skill"; + lpBasis = 1; + break; + default: + header = Earthdawn.getAttrBN( cID, sa.slice( 0, -4 ) + "Name", "" ); + log( Earthdawn.timeStamp() + "Error! ED Attribute recordWrap got illegal value of: " + Earthdawn.getParam( sa, 4, "_")); + } // end case repeating section type + break; + case "ReadWrite": + case "Speak": + if( !rankFrom ) + return; // Some ranks are created at record creation. Ignore them first time we see them. + bCount = "S"; + header = wrapper + " Language"; + misclabel = "Skill"; + miscval = rankFrom + " -> " + rankTo; + lpBasis = 1; + break; + } // end switch wrapper + + let rankMin = Math.min( rankTo, rankFrom), + rankMax = Math.max( rankTo, rankFrom), + tdate = Earthdawn.getAttrBN( cID, "record-date-throalic", "" ), // First look on the current character sheet + today = new Date(); + if( !tdate ) { + let party = findObjs({ _type: "character", name: "Party" }); + if( party && party[ 0 ] ) // Look for throalic date on the "Party" sheet. + tdate = Earthdawn.getAttrBN( party[ 0 ].get( "_id" ), "record-date-throalic", "" ); + } + if( !tdate && state.Earthdawn.gED ) + tdate = "1517-1-1"; + + switch( wrapper ) { // This switch is for categories that are calculated once, no matter how many ranks have been gained. + case "SP-Circle": + case "NAC_Requirements": + break; + default: // All except DSP-Circle + for( let ind = rankMin; ind < rankMax; ++ind) { // We want this loop to go once for each rank being done. + switch( wrapper ) { // This switch is run once per rank gained. + case "DSP_Circle": + if( rankTo > 0 ) + silver = [ 0, 0, 200, 300, 500, 800, 1000, 1500, 2000, 2500, 3500, 5000, 7500, 10000, 15000, 20000 ][ ind + 1 ] + if( state.Earthdawn.gED ) sTime = "5 days."; + break; + case "Increases": + let stepValue = Math.floor(( 5 + ind + Earthdawn.getAttrBN( cID, sa.slice( 0, -9 ) + "Race", "None", true ) + + Earthdawn.getAttrBN( cID, sa.slice( 0, -9 ) + "Orig", "0", true )) / 3); + silver += stepValue * stepValue * 10; + iTime += stepValue; + sTime = iTime + " days."; + lp += Earthdawn.fibonacci( lpBasis + 1 + ind ) * 100; + break; + case "Durability-Rank": + case "Path-Journeyman": + case "Path-Master": + case "Questor": + case "ReadWrite": + case "Speak": + case "Rank": + case "SP-Rank": + lp += Earthdawn.fibonacci( lpBasis + 1 + ind ) * 100; + if( bCount === "S" ) { + iTime += ind + 1; + if( state.Earthdawn.gED ) { + silver += (ind + 1) * (ind + 1) * 10; + sTime = iTime + " weeks" + (( wrapper === "Rank") ? "." : " plus a month." ); + } } } } } + + let stem = "&{template:chatrecord} {{header=" + getAttrByName( cID, "character_name" ) + ": " + header + "}}" + + ( rankDiff < 0 ? "{{refund=Refund}}" : "") + (tdate ? "{{throalic=" + tdate + "}}" : ""); + let slink = "!Earthdawn~ charID: " + cID; + if ( miscval ) + stem += "{{misclabel=" + misclabel + "}}{{miscval=" + miscval + "}}"; + + if ( lp || silver ) { + if( lp ) + stem += "{{lp=" + lp + "}}"; + if ( silver ) + stem += "{{sp=" + silver + "}}"; + slink += "~ Record: ?{Posting Date|" + today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + + "}: ?{" + ( state.Earthdawn.gED ? "Throalic Date|" : "Game world Date|" ) + tdate + "}: "; + slink += lp ? (silver ? "LPSP: " : "LP: ") : "SP: "; + slink += (lp ? "?{" + (state.Earthdawn.g1879 ? "Action" : "Legend") + " Points to post|" + lp + "}" : "0") + ": "; + slink += (silver ? "?{" + (state.Earthdawn.g1879 ? "Money" : "Silver Pieces") + " to post|" + silver + "}" : "0") + ": "; + slink += ( rankDiff < 0 ? "Refund: " : "Spend: "); + slink += "?{Reason|" + header + (miscval ? " "+ misclabel + " " + miscval : "") + "}"; + } + if ( sTime ) { + stem += "{{time=" + sTime + "}}"; + slink += "?{Time| and " + sTime + "}"; + } + + if(( rankFrom > 3 || rankTo > 3 ) && (wrapper !== "Speak" && wrapper !== "ReadWrite")) + bCount = undefined; // We don't need to count anything, because these definitely need accounting for. +//log( stem + slink); + if( bCount === undefined && state.Earthdawn.sheetVersion <= parseFloat( Earthdawn.getAttrBN( cID, "edition_max", 0 ) )) { + let ED = new Earthdawn.EDclass(); + ED.chat( stem + "{{button1=[Press here](" + Earthdawn.colonFix( slink ) + ")}}", Earthdawn.whoTo.player | Earthdawn.whoTo.playerList | Earthdawn.whoFrom.noArchive, null, cID ); + } else { // We need to count Talents or Skills to see if these are free during character creation or need to post a cost. + let send = stem + "{{button1=[Press here](" + Earthdawn.colonFix( slink ) + ")}}", + count = Earthdawn.parseInt2( rankTo ), // Don't get the stored rank of what is being updated, use this one instead. + maxcount, + rkey, + typ, + single; + if( bCount === "T" ) { // Count Talents. Talents, most matrices, and stuff on spell tab + maxcount = state.Earthdawn.g1879 ? 11: 8; + if(state.Earthdawn.gED) + single = [ "SP-Spellcasting-Rank", "SP-Patterncraft-Rank", "SP-Elementalism-Rank", "SP-Illusionism-Rank", + "SP-Nethermancy-Rank", "SP-Shamanism-Rank" ,"SP-Wizardry-Rank", "SP-Power-Rank", "SP-Willforce-Rank" ]; + else + single = [ "SP-Spellcasting-Rank", "SP-Patterncraft-Rank", "SP-Willforce-Rank" ]; + rkey = [ "_T_Rank", "_SPM_Rank" ]; + typ = [ "_Type", "_Origin" ]; + } else { // Count skills + maxcount = state.Earthdawn.g1879 ? 9: 14; + single = [ "SKL_TotalS-ReadWrite", "SKL_TotalS-Speak", "LS-Speak-Rank", "LS-ReadWrite-Rank" ]; // SKL is old, LS is new. + rkey = [ "_SK_Rank", "_SKK_Rank", "SKA_Rank" ]; + typ = [ "_Type", , ]; + } + + for ( let item in single ) { + if( single[ item ] === sa ) + continue; + let a = Earthdawn.getAttrBN( cID, single[ item ], "0" ); + if( a ) + count += Math.min( Earthdawn.parseInt2( a ), 3); + if( count > maxcount ) // We already know this is not character creation, so don't need to bother to keep counting. + break; + } + + if( count <= maxcount ) { // If we need to bother to keep counting. + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if( att.get("name") === sa ) // If this is the one being changed, skip it. + return; + if( !att.get("name").endsWith( "_Rank" )) // If it does not end in _Rank skip it. + return; + let fnd = false; + for( let i = 0; i < rkey.length; ++i ) { + if( Earthdawn.safeString( att.get("name") ).slice( -rkey[ i ].length ) != rkey[ i ] ) + continue; + if( typ[ i ] ) { + let b = Earthdawn.getAttrBN( cID, Earthdawn.safeString( att.get("name") ).slice(0, -5 ) + typ[ i ] ); + if( (!b && typ[i] == "_Origin") || b === "Free" || b === "Free-link" || b === "Questor" || b === "Special" + || b === "Item" || b === "Item-link" || b === "Dummy" || b === "Pseudo" || b === "Other" ) + return; + } + count += Math.min( att.get( "current" ), 3); + } + }); // End for each attribute. + } + +// if( Earthdawn.getAttrBN( cID, "CreationMode", "0") === "1" ) // when value is 1, it is NOT creation. + if( count > maxcount && state.Earthdawn.sheetVersion <= parseFloat( Earthdawn.getAttrBN( cID, "edition_max", 0 ))) { + let ED = new Earthdawn.EDclass(); + ED.chat( send, Earthdawn.whoTo.player | Earthdawn.whoTo.playerList | Earthdawn.whoFrom.noArchive, null, cID ); + } + } // End count talents and skills to see if they are free or need to be paid for. + } // End recordWrap() + + + + // This is functional start of main part of attribute change handling routine. Nothing much got processed above recordWrap(). + + // When change is in a repeating section... + if( sa.startsWith( "repeating_" )) { +//log( "change " + sa); +//log( attr); // use attr.get("name") and attr.get("current"). // {"name":"Wounds","current":"1","max":8,"_id":"-MlqexKD2f4f744TgzlK","_type":"attribute","_characterid":"-MlqeuXlNO51-RYxmJv8"} + + let code = Earthdawn.repeatSection( 3, sa ), + rowID = Earthdawn.repeatSection( 2, sa ); + if( !sa.endsWith( "_RowID" )) // Don't try to read the RowID if the RowID is what is being changed! It is unnecessary and worse, the new value might not be available for reading yet. + if( !(rowID in state.Earthdawn.rowIDobj) && !Earthdawn.testNoRowID( code ) && Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + "RowID", "") !== rowID ) { // If a repeating_section is changed and no RowID is stored and it is not in the list of ones already being checked. + state.Earthdawn.rowIDobj[ rowID ] = true; + setTimeout( function() { + 'use strict'; + try{ + if( Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + "RowID", "") !== rowID ) + Earthdawn.setWW( Earthdawn.buildPre( code, rowID ) + "RowID", rowID, cID ) + delete state.Earthdawn.rowIDobj[ rowID ] + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:setRowID() error caught: " + err ); } + }, 900); + } + if( sa.endsWith( "_Name" ) || (sa.endsWith( "_Rank") )) { + if( code === "MAN" ) { // Keep a list of Maneuver RowIDs, so we can process though them quicker. + let t = rowID; + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if ( att.get("name").startsWith( "repeating_maneuvers_" )) + if( att.get("current") ) { + let a = att.get("name"); + if ( a ) { + let r = Earthdawn.repeatSection( 2, a ); + if( r && t.indexOf( r ) == -1) // Only if this rowID is not already in t. + t += ";" + r; + } + } + }); // End for each attribute. + Earthdawn.setWW( "ManRowIdList", t, cID); + } // end rep sect MAN + +// if(sa.endsWith( "_Rank" ) && !sa.endsWith( "_WPN_Rank" )) // If a rank has changed, send chat message asking if want to pay LP for it. +// recordWrap( "Rank" ); + +// if( code === "T" || code === "NAC" || code === "SK" || code === "SKA" || code === "SKK" || code === "SPM" || code === "WPN" ) +// Earthdawn.SetDefaultAttribute( cID, Earthdawn.buildPre( code, rowID ) + "CombatSlot", code === "SPM" ? 1 : 0 ); + } // End it was a name or rank. + + // If Name, or CombatSlot changes, need to mess around with the token actions. + let t = sa.endsWith( "_CombatSlot") ? 0x01 : 0x00; + if ((sa.endsWith( "_Name") && (code === "T" || code === "NAC" || code === "SK" || code === "SKA" || code === "SKK" || code === "WPN")) + || (sa.endsWith( "_Contains") && (code === "SPM" ))) + t = 0x02; + if ( t ) { // No matter what, Remove the token action associated with the old name. + let nmo, nmn, + pre = Earthdawn.buildPre( code, rowID ), + symbol = Earthdawn.constantIcon( code ); + if( t > 0x01 ) { // Name has changed, get the old name. + nmo = prev ? prev[ "current" ] : undefined; + nmn = current; + } else // Combat slot has changed, so look up name. + nmo = nmn = Earthdawn.getAttrBN( cID, pre + ( code === "SPM" ? "Contains" : "Name" ), "" ); + + if( nmo ) { + Earthdawn.abilityRemove( cID, symbol + nmo ); + if( code === "SKA" ) + Earthdawn.abilityRemove( cID, symbol + nmo + "-Cha" ); // Artisan charisma roll. + } + let cbs; // Only create new one if new supposed to. + if( t === 0x01 ) + cbs = current == "1"; + else + cbs = Earthdawn.getAttrBN( cID, pre + "CombatSlot", "0" ) == "1"; + if( cbs ) { + Earthdawn.abilityAdd( cID, symbol + nmn, "!edToken~ %{selected|" + Earthdawn.buildPre( code, rowID ) + "Roll}" ); + if( code === "SKA" ) + Earthdawn.abilityAdd( cID, symbol + nmn + "-Cha", "!edToken~ %{selected|" + Earthdawn.buildPre( code, rowID ) + "Rollc}" ); + } + } // End Token Action maint. + else if( sa.endsWith( "_MSG_toAPI" )) { // sheetworker has sent us something it wants us to do. + if( attr.get( "max" ) === "ACK" ) { // We have got an ACK from the sheetworker. Delete the row. + if( state.Earthdawn.logMsg ) log("toAPI ACK received ( " + sa + " )"); + let pre = Earthdawn.buildPre( code, rowID ); + function zot( nm ) { + 'use strict'; + let attr2 = findObjs({ _type: "attribute", _characterid: cID, name: pre + nm }); + if( attr2 ) + for( let i = attr2.length -1; i > -1; --i ) + attr2[ i ].remove(); + } + zot( "TimeStamp "); + zot( "toSheetworker" ); + zot( "toAPI" ); + } else if( current ) { // msg to API via "current". We tend to get two, one with "current" blank (when it is created), and a 2nd one with the real value. Only pay attention to the 2nd one. + Earthdawn.fromSheetworkerToAPI( current, cID, sa ); + let pre = Earthdawn.buildPre( code, rowID ); + if( state.Earthdawn.logMsg ) log("API sending ACK ( " + sa.trim() + " ): " + current.trim() + " to ( " + pre + "toSheetworker_max )" ); + Earthdawn.setWW( pre + "toSheetworker", undefined, cID, undefined, "ACK" ); // Set max but not current + } +// } // else if( sa.endsWith( "_SP_Circle" )) +// recordWrap( "SP_Circle" ); +// else if( sa.endsWith( "_NAC_Requirements" )) +// recordWrap( "NAC_Requirements" ); +// else if( sa.endsWith( "_DSP_Circle") && sa.startsWith( "repeating_discipline")) { +// switch (Earthdawn.getAttrBN( cID, sa.slice( 0, -11) + "_DSP_Type", "Discipline" )) { +// case "Path-Journeyman": recordWrap( "Path-Journeyman" ); break; +// case "Path-Master": recordWrap( "Path-Master" ); break; +// case "Questor": recordWrap( "Questor" ); break; +// case "Other": break; // Spirit & Creature +// default: recordWrap( "DSP_Circle" ); +// } + } else if( sa.endsWith( "_T_Special" )) { + if( Earthdawn.keywordCheck( current, "CorruptKarma" )) + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "Target" ) + "Activate-Corrupt-Karma", "!edToken~ SetToken: @{target|to have Karma Corrupted|token_id}~ Misc: CorruptKarma: ?{How many karma to corrupt|1}"); + else + Earthdawn.abilityRemove( cID, Earthdawn.constantIcon( "Target" ) + "Activate-Corrupt-Karma"); + } +/* + else if ( sa.endsWith( "_NAC_Requirements" )) { // If this is a PC, post an accounting entry for them. + if( Earthdawn.getAttrBN( cID, "NPC", "1" ) == Earthdawn.charType.pc ) { + let tdate = Earthdawn.getAttrBN( cID, "record-date-throalic", "" ), // First look on the current character sheet + today = new Date(), + rnk = Earthdawn.parseInt2( current ), + name = Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + "Name", "" ); + if( rnk ) { + if( !tdate ) { + let party = findObjs({ _type: "character", name: "Party" }); + if( party && party[ 0 ] ) // Look for throalic date on the "Party" sheet. + tdate = Earthdawn.getAttrBN( party[ 0 ].get( "_id" ), "record-date-throalic", "" ); + } + if( !tdate ) tdate = "1517-1-1"; + let stem = "&{template:chatrecord} {{header=" + getAttrByName( cID, "character_name" ) + ": " + name + "}}" + + "{{misclabel=Knack}}{{miscval=Rank " + rnk + "}}" + + "{{lp=" + (Earthdawn.fibonacci( rnk ) * 100) + "}}" + + "{{sp=" + (rnk * 50) + "}}" + + "{{time=" + rnk + " days}}" + let slink = "!Earthdawn~ charID: " + cID + + "~ Record: ?{Posting Date|" + today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + + "}: ?{" + ( state.Earthdawn.gED ? "Throalic Date|" : "Game world Date|" ) + tdate + + "}: LP: ?{Legend Points to post|" + (Earthdawn.fibonacci( rnk ) * 100) + + "}: ?{Silver to post|" + (rnk * 50) + "}: Spend: " + + "?{Time| and " + rnk + " days.} " + + "?{Reason|" + name + " Knack Rank " + rnk + "}"; + let ED = new Earthdawn.EDclass(); + ED.chat( stem + "{{button1=[Press here](" + Earthdawn.colonFix( slink ) + ")}}", + Earthdawn.whoTo.player | Earthdawn.whoTo.playerList | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, null, cID ); + } } } // end _NAC_Requirements changed +*/ + } // End start with "repeating" +/* + else if( sa.startsWith( "Attrib-" ) && sa.endsWith( "-Increases" )) // Attrib-Dex-Increases etc. + recordWrap( "Increases" ); + else if( sa.startsWith( "SP-" ) && sa.endsWith( "-Rank" )) { + switch( sa ) { + case "SP-Spellcasting-Rank": + case "SP-Patterncraft-Rank": + case "SP-Elementalism-Rank": + case "SP-Illusionism-Rank": + case "SP-Nethermancy-Rank": + case "SP-Shamanism-Rank": + case "SP-Wizardry-Rank": + case "SP-Power-Rank": + case "SP-Willforce-Rank": + recordWrap( "SP-Rank" ); + } } // End of the sa startsWith and sa.endsWith stuff. Next is the SA case. +*/ + + switch( sa ) { + case "API": // API or API_max has changed. If they have not changed to "1", set them to "1". + if( current !== "1" ) { + Earthdawn.setWithWorker( attr, "current", "1", "1" ); + Earthdawn.setWithWorker( attr, "max", "1", "1" ); + } break; + case "APIflag": // sheetworker has sent us something it wants us to do. + if( current ) { // We tend to get two, one with "current" blank when it is created, and a 2nd one with the real value. Only pay attention to the 2nd one. + Earthdawn.fromSheetworkerToAPI( current, cID, sa ); + Earthdawn.waitToRemove( cID, sa, 15 ); + } break; + case "Creature-Ambush": // Ambush and DiveCharge used to hold the amount. Now use Ambushing_max and DivingCharging_max. + case "Creature-Ambushing": + case "Creature-DiveCharge": + case "Creature-DivingCharging": + if( prev && ( prev[ "max" ] != attr.get( "max" ))) { + let b = sa.indexOf( "mbush") !== -1, + w = b ? "Ambush" : "Charge"; + Earthdawn.abilityRemove( cID, w ); + if( Earthdawn.parseInt2( attr.get( "max" ) )) + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "power" ) + w, "!edToken~ ForEach~ marker: " + ( b ? "ambushing" : "divingcharging") + " :t"); + } // This falls through to below on purpose, because the code above is what to do if _max changed and the code below is what to do if current changed. + case "Karma-Roll": // Changes made at the character sheet, affect all tokens, whether character or mook. Update all tokens. + case "KarmaGlobalMode": + case "Devotion-Roll": + case "DPGlobalMode": + case "SP-Willforce-Use": + case "condition-Health": + case "combatOption-AggressiveAttack": + case "combatOption-DefensiveStance": + case "combatOption-CalledShot": + case "combatOption-SplitMovement": + case "combatOption-TailAttack": + case "combatOption-Reserved": + case "condition-Blindsided": + case "condition-Blindsiding": + case "condition-Cover": + case "condition-Harried": + case "condition-KnockedDown": + case "condition-RangeLong": + case "condition-ImpairedMovement": + case "condition-NoShield": + case "condition-Surprised": + case "condition-TargetPartialCover": + case "condition-Darkness": + case "condition-Flying": + case "Misc-StrainPerTurn": + case "Adjust-All-Tests-Misc": + case "Adjust-Attacks-Misc": + case "Adjust-Damage-Misc": + case "Adjust-Defenses-Misc": + case "PD-Buff": + case "MD-Buff": + case "SD-Buff": + case "PA-Buff": + case "MA-Buff": + case "Adjust-Effect-Tests-Misc": + case "Adjust-TN-Misc": + case "condition-Buffed": // These are obsolete + case "condition-Buffed2": + case "condition-Debuffed": + case "condition-Debuffed2": { +//log("at status change"); log(attr); log(prev); + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + let code, op, + mia = _.filter( Earthdawn.StatusMarkerCollection, function(mio) { return mio[ "attrib" ] == sa; }); // get an array of menu items with this attribute. + if( mia === undefined || mia.length === 0) { + log( Earthdawn.timeStamp() + "Earthdawn: On Attribute. Warning. '" + sa + "' not be found in StatusMarkerCollection." ); + break; + } else if( mia.length === 1 ) { // If there is only one, use it. + let sm = mia[ 0 ][ "submenu" ]; + code = mia[ 0 ][ "code" ].trim(); + if( sm === undefined ) // There is no submenu, so just set the marker to match the value. value 0 unset, value 1 set. + op = (( current == "0" ) ? "u" : "s"); + else { // There is a submenu that lists all the valid values. + let i = sm.indexOf( "[" + current + "^" ); + if ( i != -1) // There is a [n^a] structure. + op = sm.charAt( sm.indexOf( "^", i) + 1); + else // The sub-menu has no [n^a] structure, so just send the value with a z in front of it. + op = current; + } + if( "a" <= op && op <= "j" ) + op = (op.charCodeAt( 0 ) - 96).toString(); + } else { // more than one menu item was found. See if any of them have "shared" set + op = "u"; // If we don't find a shared match, then we unset. + code = mia[ 0 ][ "code" ]; // Default so that if we don't find an "shared", it will attempt to unset the first one (which will unset them all). + let curr = current; + for( let i = 0; i < mia.length; ++i ) + if( mia[ i ][ "shared" ] ) + if( mia[ i ][ "shared" ] == curr ) { + op = "s"; + code = mia[ i ][ "code" ].trim(); + } else if( Earthdawn.parseInt2( curr, true )) // parseInt2 - Silent + if( Earthdawn.safeString( mia[ i ][ "shared" ] ).slice( 0, 3 ).toLowerCase() === "pos" && Earthdawn.parseInt2( curr ) > 0 ) { + op = curr; + code = mia[ i ][ "code" ].trim(); + } else if( Earthdawn.safeString( mia[ i ][ "shared" ] ).slice( 0, 3 ).toLowerCase() === "neg" && Earthdawn.parseInt2( curr ) < 0) { + op = Math.abs( Earthdawn.parseInt2( curr )); // If curr is negative, then set the badge on the neg token to a positive badge number. + code = mia[ i ][ "code" ].trim(); + } } + let tkns = findObjs({ _type: "graphic", _subtype: "token", represents: edParse.charID }); + _.each( tkns, function (TokObj) { + edParse.tokenInfo = { type: "token", tokenObj: TokObj } + edParse.MarkerSet( [ "sheetDirect", code, op ] ); + }) // End ForEach Token + } break; // end update status markers. + case "NPC": { + let rtype = (state.Earthdawn.defRolltype & (( current === "2" ) ? 0x02 : ((current === "1") ? 0x01 : 0x04))) ? "/w gm" : " "; + Earthdawn.setWW( "RollType", rtype, cID ); + } // Falls through to Attack Rank below. + case "Attack-Rank": { + if( Earthdawn.getAttrBN( cID, "NPC", "1", true) > 0 && Earthdawn.getAttrBN( cID, "Attack-Rank", 0) != 0) // NPC or Mook and have a generic attack value. + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "power" ) + "Attack", "!edToken~ %{selected|Attack}"); + else // PC + Earthdawn.abilityRemove( cID, Earthdawn.constantIcon( "power" ) + "Attack" ); + } break; +/* functionality moved to sheetworker Oct 23. + case: "character_name": + case "charName": { + let c = findObjs({ _type: "character", _id: cID }); + if (c && c[0]) c[0].set( "name", current); + } break; +*/ +// case "Durability-Rank": +// recordWrap( "Durability-Rank" ); +// break; + case "Hide-Spells-Tab": { // If we are hiding the spell pages, also remove the spell token actions. + if( current == "1") { // Check-box is being turned on + Earthdawn.abilityRemove( cID, Earthdawn.constantIcon( "Spell" ) + "-Grimoire" ); + if (state.Earthdawn.gED) + Earthdawn.abilityRemove( cID, Earthdawn.constantIcon( "Spell" ) + "-Spells" ); + } else { // Checkbox is being turned off + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "Spell" ) + "-Grimoire", "!edToken~ ChatMenu: Grimoire"); + if (state.Earthdawn.gED) + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "Spell" ) + "-Spells", "!edToken~ ChatMenu: Spells"); + } + } break; +/* + case "Karma": + if ( state.Earthdawn.g1879 || state.Earthdawn.edition != "4" ) { +// obsolete. moved to sheetworkers. + let karmaNew = Earthdawn.parseInt2( current ) - Earthdawn.parseInt2( prev["current"] ); + if( karmaNew > 0 ) { + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + edParse.funcMisc( [ "", "KarmaBuy", karmaNew ] ); + } } + break; +*/ + case "Questor": { + if( current === "None" ) { + Earthdawn.abilityRemove( cID, "DP-Roll" ); + Earthdawn.abilityRemove( cID, "DP-T" ); + } else { + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "karma" ) + "DP-Roll", "!edToken~ %{selected|DevotionOnly}" ); + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( "karma" ) + "DP-T", "!edToken~ !Earthdawn~ ForEach ~ marker: devpnt :t" ); + } + } break; +// case "SKL_TotalS-Speak": // Earthdawn sheet old +// case "Speak-Rank": // 1879 sheet +// case "LS-Speak-Rank": // Earthdawn sheet new +// recordWrap( "Speak" ); +// break; +// case "SKL_TotalS-ReadWrite": // Earthdawn sheet old +// case "ReadWrite-Rank": // 1879 sheet +// case "LS-ReadWrite-Rank": // Earthdawn sheet new +// recordWrap( "ReadWrite" ); +// break; + case "SP-WillforceShow": { + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + edParse.TokenActionToggle( Earthdawn.constantIcon( "karma" ) + "Willforce", current === "1" ); + } break; + } // End switch sa + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.attribute() error caught: " + err ); } +} // End Attribute() + + + + // Code = SP, SPM, WPN, etc. + // rowID may EATHER be a rowID, or it may be a whole repeating section attribute name, in which case this routine will extract just the rowID needed. + // due to a roll20 bug, there are some things that require the code to be lowercase instead of the standard upper. when lowercase is true instead of undefined, that happens. + // Note: keep this in sync with codeToName. +Earthdawn.buildPre = function ( code2, rowID, lowercase ) { + 'use strict'; + try { + let ret, + code = Earthdawn.safeString( code2 ).trim(); + if( !rowID ) + rowID = code; + else + rowID = rowID.trim(); + if( code.startsWith( "repeating_" )) + code = Earthdawn.repeatSection( 3, code ); + if( rowID.startsWith( "repeating_" )) + rowID = Earthdawn.repeatSection( 2, rowID ); + code = Earthdawn.safeString( code ).toUpperCase(); + switch ( code ) { + case "ARM": ret = "repeating_armor_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "BL": ret = "repeating_blood_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "DSP": ret = "repeating_discipline_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "I": // Obsolete + case "INV": ret = "repeating_inventory_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "MAN": ret = "repeating_maneuvers_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "MNT": ret = "repeating_mount_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "MSG": ret = "repeating_message_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; // Does not have RowID + case "MSK": ret = "repeating_masks_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "NAC": ret = "repeating_knacks_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "NSM": ret = "repeatingknacksummary" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "PER": ret = "repeating_personality" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; // Does not have RowID + case "SKC": ret = "repeating_skills_" + rowID + "_" + ( lowercase ? "sk" : "SK" ) + "_"; break; // Obsolete. v2.0 and greater skill artistic charisma code uses all attributes of skill artistic. + case "SK": ret = "repeating_skills_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "SKK": ret = "repeating_skillk_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; // Obsolete + case "SKAC": // Obsolete. V lower than 2.0: skill artistic charisma code uses all attributes of skill artistic. + case "SKA": ret = "repeating_skilla_" + rowID + "_SKA_"; break; // Obsolete + case "SKL": ret = "repeating_skilll_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "SPM": ret = "repeating_matrix_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "SP": ret = "repeating_spell_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "SPP": ret = "repeating_spellpreset_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "T": ret = "repeating_talents_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "TI": ret = "repeating_threads_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "TR": ret = "repeating_transaction_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + case "WPN": ret = "repeating_weapons_" + rowID + "_" + ( lowercase ? code.toLowerCase() : code ) + "_"; break; + default: log( Earthdawn.timeStamp() + "API Earthdawn:buildPre() error. Unknown code: " + code + " RowID: " + rowID ); + } + return ret; + } catch(err) { log( Earthdawn.timeStamp() + "API Earthdawn:buildPre() error caught: " + err ); } +}; // end buildPre + + + +// Central test for codes that have no RowID. +// Note that PER and MSG actually have no RowID. TI and MSK actually do, but there is no reason for them to, so unless strict is true we return true for them as well. +Earthdawn.testNoRowID = function ( code, strict ) { + 'use strict'; + code = code.trim(); + return ( code === "PER" || code === "MSG" || code === "TR" || ( !strict && code === "MSK" )) // These don't have a RowID. +}; // end testNoRowID. + + + + // Code = SP, SPM, WPN, etc. + // This is also used to test if a code is valid/recognized. If silent is true then it does not log an error and returns undefined. + // Note: keep this in sync with buildPre. +Earthdawn.codeToName = function ( code, silent ) { + 'use strict'; + try { + switch( code.trim() ) { + case "ARM": return "Armor"; + case "BL": return "Blood Magic"; + case "DSP": return "Discipline"; + case "I": // Obsolete. + case "INV": return "Inventory"; + case "MAN": return "Maneuver"; + case "MNT": return "Mount"; + case "MSG": return "Message"; // unused as name. + case "MSK": return "Mask"; // unused as name. + case "NAC": return "Knack"; + case "NSM": return "Knack Summary"; + case "PER": return "Personality"; // unused as name. + case "SKC": // Obsolete. + case "SKK": // Obsolete. + case "SKAC": // Obsolete. + case "SKA": // Obsolete. + case "SKL": // Obsolete. + case "SK": return "Skill"; + case "SP": return "Spell"; + case "SPM": return "Matrix"; + case "T": return "Talent"; + case "TI": return "Thread Item"; + case "TR": return "Transaction"; + case "WPN": return "Weapon"; + default: if( !silent ) log( Earthdawn.timeStamp() + "Earthdawn:codeToName() error. Unknown code: " + code ); + } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:codeToName() error caught: " + err ); } +}; // end codeToName + + + + // Chat buttons don't like colons. Change them to something else. They will be changed back later. +Earthdawn.colonFix = function ( txt ) { + 'use strict'; + return Earthdawn.safeString( txt ).replace( /:/g, Earthdawn.constantAlt( "colonAlt" )); +}; // end colonFix() + + // There are some very rare cases where we want a command line inside a quoted string (abilityRebuild). Change them to something else so the parser will ignore them. +Earthdawn.tildiFix = function ( txt ) { 'use strict'; return Earthdawn.safeString( txt ).replace( /~/g, Earthdawn.constantAlt( "tildiAlt" )).replace( /:/g, Earthdawn.constantAlt( "colonAlt2" )).replace( /\|/g, Earthdawn.constantAlt( "pipeAlt" )).replace( /\{/g, Earthdawn.constantAlt( "braceOpenAlt" )).replace( /\}/g, Earthdawn.constantAlt( "braceCloseAlt" )); }; +Earthdawn.tildiRestore = function ( txt ) { 'use strict'; return Earthdawn.safeString( txt ).replace( new RegExp( Earthdawn.constantAlt( "tildiAlt" ), "g" ), "~" ).replace( new RegExp( Earthdawn.constantAlt( "colonAlt2" ), "g" ), ":" ).replace( new RegExp( Earthdawn.constantAlt( "pipeAlt" ), "g" ), "\|" ).replace( new RegExp( Earthdawn.constantAlt( "braceOpenAlt" ), "g" ), "\{" ).replace( new RegExp( Earthdawn.constantAlt( "braceCloseAlt" ), "g" ), "\}" ); }; + + + + // routine to standardize the use of symboles. + // return alternates for characers we can't use in certain places. So when building a button with a comma, we will use commaAlt and then swap it for a comma later. +Earthdawn.constantAlt = function( what ) { + 'use strict'; + try { + let a; + switch ( Earthdawn.safeString( what.trim().toLowerCase() )) { + case "commaalt": a = 0x00F1; break; // Comma replacement character. Character name LATIN SMALL LETTER N WITH TILDE + case "colonalt": a = 0x00F2; break; // Colon replacement character. Buttons don't like colons, so anytime we want one in a button, replace it for a while with this. Latin Small Letter O with Grave + case "tildialt": a = 0x00F3; break; // Tildi replacement character. To get the parser to ignore tildis for a while, replace it for a while with this. Latin Small Letter O with Acute + case "colonalt2": a = 0x00F4; break; // Colon replacement character alternate. Used in conjunction with tildiFix. LATIN SMALL LETTER O WITH CIRCUMFLEX + case "pipealt": a = 0x00F5; break; // | Latin Small Letter O with Tilde + case "braceopenalt": a = 0x014D; break; // { Latin Small Letter O with Macron + case "braceclosealt": a = 0x014F; break; // } Latin Small Letter O with Breve + // Note: Unicode 0277 ( É· Latin Small Letter Closed Omega) is used by sheet-worker as a Marker to Trigger Autofill. Not used by API. + case undefined: return ""; // undefined is a legal value that returns an empty string. + default: + log( Earthdawn.timeStamp() + "Earthdawn.constantAlt: Illegal argument '" + what + "'." ); + return; + } + return String.fromCharCode( a ); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:constantAlt( " + what + " ) error caught: " + err ); } +} // end constantAlt() + + + + // routine to standardize the use of symboles. + // Due to roll20 processing, queries within queries within buttons within chat messages need increasing levels of nesting levels to be specified. + // Return the html code for certain symboles that can not appear too early. The number of nestinglevel determines the number of & prepended to the string. Each & lets it go through one roll20 interpriation without actually being interprited as the base code yet. +Earthdawn.constantButton = function( what, nestingLevel ) { + 'use strict'; + try { + if( nestingLevel === undefined ) nestingLevel = 1; + let s, r = "&"; + switch ( Earthdawn.safeString( what.trim().toLowerCase() )) { // For this upper half, we want to return html codes. + // These will eventually be converted to the real symbols later, but not until after they get past some chat command steps. + case "percent": s = 37; break; // % = 0x25 + case "parenopen": s = 40; break; // ( = 0x28 + case "parenclose": s = 41; break; // ) = 0x29 + case "comma": s = 44; break; // , = 0x2C + case "at": s = 64; break; // @ = 0x40 + case "braceopen": s = 123; break; // { = 0x7B See also Alt's below + case "pipe": s = 124; break; // | = 0x7C See also Alt's below + case "braceclose": s = 125; break; // } = 0x7D See also Alt's below + case undefined: return ""; // undefined is a legal value that returns an empty string. + default: + log( Earthdawn.timeStamp() + "Earthdawn.constantButton: Illegal argument '" + what + "'." ); + return; + } + if( nestingLevel ) { // level 1 (default) is of the form | level 2 is of the form &#124; Level 3 is of the form &amp;#124; Etc. One more amp; for each level. + for( let i = nestingLevel; i > 1; --i ) + r += "amp;"; + r += "#" + s + ";"; // of the form { + } else // level 0 is base symbol ie: |,( etc. + r = String.fromCharCode( s ); + return r; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:constantButton( " + what + " ) error caught: " + err ); } +} // end constantButton() + + + // routine to standardize the use of symboles. + // return an icon. + // We often use icons to differenciate between talents, skills, powers, knacks, spells, etc. Sometimes we just use an icon in a chat message. + // This gathers all the icons into one place. + // For this routine, we want to return an actual character, which is part of the extended UTF-16 set, + // and which roll20 one-click install library seems to corrupt if we use the code, so we can't store it as a literal. + // For Token Actions we put a bunch of zero width non breaking spaces in front of the icons to force them into sorting how we want them to. + // Those will sort numerically with the most zwnbsp to the end of the list. So for example karma and target are both sort 0, so they have no zwnbsp in front of them, but karma appears first since it is lower. After those, Power has sort 1, then all the sort 2 in assending order. + + // if sortOffset false, don't put any zwnbsp in front of the icon. + // if sortoffSet true of undefined, put the default amount. + // if sortOffset numeric, offset the number of zwnbsp by that amount. +Earthdawn.constantIcon = function( what, sortOffset ) { + 'use strict'; + try { + let c, c0, sort = 0, r = ""; + switch ( Earthdawn.matchString( what )) { // For this upper half, we have symboles that are usually used in Token Actions. They have to be sorted. + case "t": + case "talent": c = 0x2600; sort = 2; break; // Black sun with rays: &# 9728; + case "karma": c = 0x262F; sort = 0; break; // yin yang: &# 9775; + case "wpn": + case "weapon": c = 0x2694; sort = 3; break; // Crossed swords: &# 9876; + case "power": c = 0x26A1; sort = 1; break; // Lightning Bolt or High Voltage: &# 9889; + case "target": c = 0x27b4; sort = 0; break; // Black-feathered South East Arrow + case "nac": + case "knack": c0 = 0xD83D; c = 0xDCAB; sort = 2; break; // Dizzy Symbol: &# 128171; + case "sk": case "ska": case "skk": case "skl": + case "skill": c0 = 0xD83D; c = 0xDD27; sort = 2; break; // Wrench &# 128295; + case "sp": case "spm": + case "spell": c0 = 0xD83E; c = 0xDE84; sort = 3; break; // Magic Wand. &# 129668; + // This part of the list are icons we return in various chat window messages. + case "carriagereturn": case "cr": + case "return": c = 0x000D; break; // Carraige Return; + case "pointleft": c = 0x261A; break; // Black Left Pointing Index. &# 9754; + case "warning": c = 0x26A0; break; // Red Exclaimation point in a red triangle. &# 9888; This one is never an ability prefix just an icon we sometimes use. + case "zwnbsp": c = 0xFEFF; break; // Zero Width No Break SPace. Used to sort thing to the start of the ability list. The one with the most of these will go to the front of the list. + case undefined: case "none": return ""; // undefined is a legal value that returns an empty string. + default: + log( Earthdawn.timeStamp() + "Earthdawn.constantIcon: Illegal argument '" + what + "'." ); + return; + } + if( sortOffset !== false ) { + if( typeof( sortOffset ) == "number" ) + sort += sortOffset; + for( let i = 0; i < sort; ++i ) + r += String.fromCharCode( 0xFEFF ); // zwnbsp - Zero Width Non Breaking SPace. + } + if( c0 ) + return r + String.fromCharCode( c0, c ); // This is a UFT-32 character, so needs an extra character to specify the block. + else if ( c ) + return r + String.fromCharCode( c ); // This is a UFT-16 character. + else return r; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:constantIcon( " + what + " ) error caught: " + err ); } +} // end constantIcon() + + + + // At present this is just for spells, but can be expanded if needed. + // mode could be "short", "name", or "weaving" +Earthdawn.dispToName = function ( disp, mode ) { + 'use strict'; + try { + if( !["short","name","weaving"].includes(mode)) { log( Earthdawn.timeStamp() + "Earthdawn:dispToName() unknown mode: " + mode ); return; } + let t; + switch( disp.trim() ) { + case "Elementalism": + case "Elementalist": + case "5.3": t={"short": "El" , "name" : "Elementalist", "weaving" : "Elementalism" }; break; + case "Illusionist": + case "Illusionism": + case "6.3": t={"short": "Il" , "name" : "Illusionist", "weaving" : "Illusionism" }; break; + case "Nethermancy": + case "Nethemancer": + case "7.3": t={"short": "Ne" , "name" : "Nethermancer", "weaving" : "Nethermancy" }; break; + case "Wizard": + case "Wizardry": + case "16.3": t={"short": "Wz" , "name" : "Wizard", "weaving" : "Wizardry" }; break; + case "Shaman": + case "Shamanism": + case "22.3": t={"short": "Sh" , "name" : "Shaman", "weaving" : "Shamanism" }; break; + case "Other": + case "Other Weaving": + case "81": t={"short": "Oth" , "name" : "Other", "weaving" : "Other Weaving" }; break; + case "Power": + case "Spell Weaving": + default: t={"short": "Pwr" , "name" : "Power" , "weaving" : "Spell Weaving"}; + } + return t[ mode ]; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:dispToName() error caught: " + err ); } +}; // end dispToName + + + +Earthdawn.encode = (function(){ + 'use strict'; + try { + let esRE = function ( s ) { + let escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g; + return s.replace(escapeForRegexp,"\\$1"); + } + let entities = { +// ' ' : '&'+'nbsp'+';', + '/n' : '<' + 'br//' + '>', + '<' : '&' + 'lt' + ';', + '>' : '&' + 'gt' + ';', + "'" : '&' + '#39' + ';', + '?' : '&' + '#63' + ';', + '@' : '&' + '#64' + ';', + '[' : '&' + '#91' + ';', + ']' : '&' + '#93' + ';', + '{' : '&' + '#123' + ';', + '|' : '&' + '#124' + ';', + '}' : '&' + '#125' + ';', + '"' : '&' + 'quot' + ';' + }, + re = new RegExp( '(' + _.map( _.keys( entities ), esRE ).join( '|' ) + ')', 'g' ); + return function( s ) { + return s.replace(re, function( c ){ return entities[c] || c; }); + }; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:encode() error caught: " + err ); } + }()); // end encode() + + + + // Log a programming error. If command line logging of every command is turned off, log the command line as well. +Earthdawn.errorLog = function( msg, context ) { + 'use strict'; + try { + if( !context ) + log( Earthdawn.timeStamp() + "Earthdawn.errorLog did not have context."); + else if( !state.Earthdawn.logCommandline ) { // If have not already logged the command line, do so. + let m; + if( "edClass" in context && "msg" in context.edClass ) // This is a parseObj + m = context.edClass.msg; + else if ( "msg" in context ) // This is an edClass object. + m = context.msg; + else + log( Earthdawn.timeStamp + "Earthdawn.errorLog invalid context." ); + + if( m ) + log( m ); // log command line + } + log( Earthdawn.timeStamp() + msg); + } catch(err) { log( "Earthdawn:Errorlog( " + msg + " ) error caught: " + err ); } +} // end ErrorLog() + + + +Earthdawn.fibonacci = function(num, memo) { + 'use strict'; + try { + memo = memo || {}; + if (memo[ num ]) + return memo[ num ]; + if( num <= 1 ) + return 1; + return memo[ num ] = Earthdawn.fibonacci( num - 1, memo ) + Earthdawn.fibonacci( num - 2, memo); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:fibonacci() error caught: " + err ); } +}; // end fibonacci + + + + // Look for an object. If you can't find one, make one and return that. +Earthdawn.findOrMakeObj = function ( attrs, deflt, maxDeflt ) { + 'use strict'; + try { +//log(attrs); + let obj, + objs = findObjs( attrs ); + if( objs ) { + if( objs.length > 1 ) { + log( Earthdawn.timeStamp() + "Error Earthdawn:findOrMakeObj() found multiple objects: " ); + log( objs ); + let keep = 0, maxscore = 0; // pick one to keep and get rid of the rest. + for( let i = 0; i < objs.length; ++i ) { + let score = 0; + function scoreit( a, dflt ) { + 'use strict'; + if( a !== undefined && a !== null ) { + if( typeof a != "string" ) ++score; // Gain points for not being string, and not being equal to default, and not evaluating to false, on the assumption that something tried to change it to those. Note that these criteria are rather arbitrary, but wanted to make decision based on something other than first or last. + if( dflt !== undefined && a != dflt ) ++score; + if( a ) ++score; + } } + scoreit( objs[ i ].get( "current" ), deflt ); + scoreit( objs[ i ].get( "max" ), maxDeflt ); + if( score > maxscore ) { + keep = i; + maxscore = score; + } } + let txt = ""; + for( let i = objs.length -1; i > -1; --i ) + if( i !== keep ) { + txt += " attr[ " + i + " ],"; + objs[ i ].remove(); + } + obj = objs[ keep ]; + log( "removing" + txt.slice( 0, -1) + " and keeping attr[ " + keep + "]." ); + } // end found more than one. + else if( objs.length > 0 ) + obj = objs[ 0 ]; + } // end found one. + if( obj === undefined && "_type" in attrs ) { // we did not find any, create one. + let type = attrs[ "_type" ]; + delete attrs[ "_type" ]; + obj = createObj( type, attrs); + if( obj && deflt !== undefined && deflt !== null ) + Earthdawn.setWithWorker( obj, "current", deflt ); + if( obj && maxDeflt !== undefined && maxDeflt !== null ) + Earthdawn.setWithWorker( obj, "max", maxDeflt ); + } + return obj; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:findOrMakeObj() error caught: " + err ); } +}; // end findOrMakeObj() + + + + // The sheetworker has sent us a message, ether in APIflag or repeating_message +Earthdawn.fromSheetworkerToAPI = function ( msg, cID, wherefrom ) { + 'use strict'; + try { + msg = Earthdawn.safeString( msg ).trim(); + if( state.Earthdawn.logMsg ) log("toAPI ( " + wherefrom + " ): " + msg); + let ED = new Earthdawn.EDclass(), + cmdArray = msg.split( "\n" ); + Earthdawn.pseudoMsg( ED, msg ); + for( let ind = 0; ind < cmdArray.length; ++ind ) { + let cmdLine = Earthdawn.safeString( cmdArray[ ind ] ), + comma = cmdLine.indexOf( "," ), + dataLine = cmdLine.slice(comma +1).trim(); + switch ( cmdLine.slice( 0, comma).trim() ) { + case "abilityAdd": { // abilityAdd, (name)\r (text)\r optional symbol code. + Earthdawn.abilityAdd( cID, Earthdawn.constantIcon( Earthdawn.getParam( dataLine, 3, "\r" )) + + Earthdawn.getParam( dataLine, 1, "\r" ), Earthdawn.getParam( dataLine, 2, "\r" )); + } break; + case "abilityRemove": { // abilityRemove, name of ability, optional symbol code. Remove this ability. + Earthdawn.abilityRemove( cID, Earthdawn.constantIcon( Earthdawn.getParam( dataLine, 2, "," )) + Earthdawn.getParam( dataLine, 1, "," )); + } break; + case "ChatRecord": { // ChatRecord, SP: 0: (sp price): Spend: Buy a broadsword +log("ChatRecord Obsolete. If you see this, let the API developer know."); // Oct 23. Except 1879 karma buy still needs to be converted, so that still uses this. + let tdate = Earthdawn.getAttrBN( cID, "record-date-throalic", "" ), // First look on the current character sheet + today = new Date(), + lp, silver; + if(Earthdawn.getParam( dataLine, 2, ":" ) !== "0") lp = Earthdawn.getParam( dataLine, 2, ":" ); + if(Earthdawn.getParam( dataLine, 3, ":" ) !== "0") silver = Earthdawn.getParam( dataLine, 3, ":" ); + if( !tdate ) { + let party = findObjs({ _type: "character", name: "Party" }); + if( party && party[ 0 ] ) // Look for throalic date on the "Party" sheet. + tdate = Earthdawn.getAttrBN( party[0].get( "_id" ), "record-date-throalic", "" ); + } + if( !tdate && state.Earthdawn.gED ) + tdate = "1517-1-1"; + let stem = "&{template:chatrecord} {{header=" + getAttrByName( cID, "character_name" ) + ": " + Earthdawn.getParam( dataLine, 5, ":" ) + "}}" + + (tdate ? "{{throalic=" + tdate + "}}" : ""), + slink = "!Earthdawn~ charID: " + cID + + "~ Record: ?{Posting Date|" + today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + "}" // ssa[ 1] Real Date + + ": ?{" + ( state.Earthdawn.gED ? "Throalic Date|" : "Game world Date|" ) + tdate + "}: "; // ssa[ 2] Throalic Date + if ( lp || silver ) { + if( lp ) + stem += "{{lp=" + lp + "}}"; + if ( silver ) + stem += "{{sp=" + silver + "}}"; + slink += lp ? (silver ? "LPSP: " : "LP: ") : "SP: "; // ssa[ 3] Item: SPLP, SP, LP, Dev, or Other + slink += (lp ? "?{" + (state.Earthdawn.g1879 ? "Action" : "Legend") + " Points to post|" + lp + "}" : "0") + ": "; // ssa[ 4] Amount LP + slink += (silver ? "?{" + (state.Earthdawn.g1879 ? "Money" : "Silver Pieces") + " to post|" + silver + "}" : "0") + ": "; // ssa[ 5] Amount SP + slink += Earthdawn.getParam( dataLine, 4, ":" ) + ": "; // ssa[ 6] Type: Gain, Spend, Decrease (ungain), or Refund (unspend). + slink += "?{Reason|" + Earthdawn.getParam( dataLine, 5, ":" ) + "}"; // ssa[ 7] Reason - Text. + } + let ED = new Earthdawn.EDclass(); + ED.chat( stem + "{{button1=[Press here](" + Earthdawn.colonFix( slink ) + ")}}", Earthdawn.whoTo.player | Earthdawn.whoTo.playerList | Earthdawn.whoFrom.noArchive, null, cID ); + } break; + case "LinkAdd1": { //APIflag LinkAdd1,code:RowId:code:name + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + let dataArray = dataLine.split( ":" ); + if( dataArray.length !== 4 ) { + log( Earthdawn.timeStamp() + "Earthdawn - APIFlag call incorrectly formatted for LinkAdd1 : " + dataLine); + return; + } + // See if this works. + edParse.ChatMenu( [ "ChatMenu","linkadd1", dataArray[ 0 ].trim(), dataArray[ 1 ].trim(), dataArray[ 2 ].trim(), dataArray[ 3 ].trim()] ); + } break; + case "LinkAdd2": { //APIflag LinkAdd2,code:RowId:code:RowId + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + let dataArray = dataLine.split( ":" ); + if( dataArray.length !== 4 ) { + log(Earthdawn.timeStamp() + "Earthdawn - APIFlag call incorrectly formatted for LinkAdd2 : " + dataLine); + return; + } + // See if this works. + edParse.ChatMenu( [ "ChatMenu","linkadd2", dataArray[ 0 ].trim(), dataArray[ 1 ].trim(), dataArray[ 2 ].trim(), dataArray[ 3 ].trim()] ); + } break; + case "RemoveAttr": { // RemoveAttr, (fully qualified attribute name). Remove this attribute. + let attrib = findObjs({ _type: "attribute", _characterid: cID, name: (dataLine.endsWith( "_max" ) ? dataLine.slice( 0, -4) : dataLine) }); + _.each( attrib, function (att) { + att.remove(); + }); + } break; + case "RemoveRow": { // RemoveRow, (code), (rowID) // remove everything with this rowID. + let pre = Earthdawn.buildPre( Earthdawn.getParam( dataLine, 1, ","), Earthdawn.getParam( dataLine, 2, ",")).toLowerCase(), + attrib = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attrib, function (att) { + if ( Earthdawn.safeString( att.get("name") ).toLowerCase().startsWith( pre )) + att.remove(); + + }); + } break; + case "SheetUpdate": { + let brandNew = (Earthdawn.getParam( dataLine, 1, ",") == 0); + function shouldUpdate( ver ) { // "SheetUpdate," + origSheetVersion.toString() + "," + newSheetVersion.toString() + return (parseFloat( Earthdawn.getParam( dataLine, 1, ",")) < ver && ver <= parseFloat( Earthdawn.getParam( dataLine, 2, ","))); + }; + let game = Earthdawn.getParam( dataLine, 3, ","); + state.Earthdawn.sheetVersion = parseFloat( Earthdawn.getParam( dataLine, 2, ",")); // This is the version number of the html file, Earthdawn.Version is the version number of this API file. + + if( shouldUpdate( 1.001 )) + ED.updateVersion1p001( cID ); + if( shouldUpdate( 1.0021 )) + ED.updateVersion1p0021( cID ); + if( shouldUpdate( 1.0022 )) + ED.updateVersion1p0022( cID ); + if( shouldUpdate( 1.0023 )) + ED.updateVersion1p0023( cID, ED ); + if( shouldUpdate( 2.0000 )) + ED.updateVersion2p001( cID, ED ); + if( shouldUpdate( 3.0000 )) + ED.updateVersion3p000( cID, ED ); + if( shouldUpdate( 3.3300 )) + ED.updateVersion3p330( cID, ED ); + + if( brandNew ) { // if this is a brand new sheet, try automatically linking the token. + function tryLink( count ) { + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; + if( edParse.TokenFind() && ("tokenInfo" in edParse) && ("tokenObj" in edParse.tokenInfo)) { + let tID = edParse.tokenInfo.tokenObj.get( "_id" ); + if( tID ) { + Earthdawn.pseudoMsg( ED, "dummy", tID ); // fake a msg with this token selected. + edParse.LinkToken( [ "forceLink", tID ] ); + } + } else if( count < 20 ) { // Test every 3 seconds for 60 seconds. + setTimeout(function() { // Delay for 20 seconds, and then try linking. + try { + tryLink( ++count ); + } catch(err) {Earthdawn.errorLog( "ED.sheetUpdate setTimeout() error caught: " + err, ED );} + }, 3000); + } else { + Earthdawn.errorLog( "ED.sheetupdate could not link. Token ID not found.", ED ); + log( edParse.tokenInfo ); + } } // end of tryLink. + tryLink( 0 ); // Kick off the first try. + } // end brandNew character. + } break; + case "WipeMatrix": { + let edParse = new ED.ParseObj( ED ); + edParse.charID = cID; +// edParse.Spell( [ "Spell", dataLine, "WipeMatrix", "M"] ); + edParse.TuneMatrix( [ "TuneMatrix", "WipeMatrix"] ); + } break; + default: + log( Earthdawn.timeStamp() + "Unknown command in APIflag: " + cmdLine); + } // end switch cmdLine + } // end for cmdArray + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:fromSheetworkerToAPI() error caught: " + err ); } +}; // End fromSheetworkerToAPI() + + + + // This routine generates a (hopefully) unique rowID you can use to add a row to a repeating section. +// Very important note. generate UUID might need to be declared outside of .this and global. I don't know. + +Earthdawn.generateRowID = function () { + "use strict"; + var EarthdawnGenerateUUID = (function() { + "use strict"; + let a = 0, e, f, b = []; + return function() { + let c = (new Date()).getTime() + 0, d = c === a; + a = c; + for ( e = new Array(8), f = 7; 0 <= f; f--) { + e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz".charAt(c % 64); + c = Math.floor(c / 64); + } + c = e.join(""); + if (d) { + for (f = 11; 0 <= f && 63 === b[f]; f--) { + b[f] = 0; + } + b[f]++; + } else { + for (f = 0; 12 > f; f++) { + b[f] = Math.floor(64 * Math.random()); + } + } + for (f = 0; 12 > f; f++){ + c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]); + } + return c; + }; + }()); + + return EarthdawnGenerateUUID().replace(/_/g, "Z"); // I don't want underscores in my RowID! +}; + + + + // getAttrBN - get attribute by name. If does not exist, return the default value. + // This is a replacement for the official system function getAttrByName() which had bugs and I think still probably does. + // Bug in getAttrByName() when dealing with repeating values, returns undefined if the value does not exist rather than the default value. + // So in cases where the API knows what the true default value is or that we want, use this routine. In cases where the lookup name is + // variable and you don't know what the default value should be, then using getAttryByName is just as good and if they fix the bug, better. + // if toInt is true or 1, convert to int. if toInt is 2, force through safestring. +Earthdawn.getAttrBN = function ( cID, nm, dflt, toInt ) { + 'use strict'; + try { + let ret, + best = 0; + if( !cID ) { + log( Earthdawn.timeStamp() + "Invalid character_id '" + cID + "' for getAttrBN() name: " + nm + " default: " + dflt) + ret = dflt; + } else if( !nm ) { + log( Earthdawn.timeStamp() + "Invalid attribute '" + nm + "' for getAttrBN(). dflt: '" + dflt + "' cID: " + cID ) + ret = dflt; + } else { + if( nm === "character_name" ) // due to character_name being a special case that is not a true attribute, we need special handling. + ret = getAttrByName( cID, "character_name" ); + else if( dflt === undefined && !nm.startsWith( "repeating_" )) // If we are not passed a dflt and it is not a repeating section anyway, go ahead and try the getAttrByName just to see if it works better. + ret = getAttrByName( cID, nm ); + + nm = Earthdawn.safeString( nm ); + if( ret === undefined ) { // We want to do this if any of the above returned a ret of undefined. + let mx = nm.endsWith( "_max" ); + let attribBN = findObjs({ _type: "attribute", _characterid: cID, name: (mx ? nm.slice( 0, -4) : nm) }); + if( attribBN && attribBN.length > 1 ) { + log( Earthdawn.timeStamp() + "Warning Earthdawn:getAttrBN( " + cID + ", " + nm + ( dflt === undefined ? "": ", " + dflt ) + + ( toInt === undefined ? "": ", " + toInt ) + " ) returned " + attribBN.length + " attributes! Attributes are: " ); + log( JSON.stringify( attribBN )); + for( let i = 0; i < attribBN.length; ++i ) + if( attribBN[ i ].get( mx ? "max" : "current") != dflt ) + best = i; // In the weird case of having duplicate entries, we want to use one that is not the default. + ret = ((attribBN === undefined) || (attribBN.length == 0)) ? dflt : attribBN[ best ].get( mx ? "max" : "current"); + for( let i = attribBN.length - 1; i > -1; --i ) + if( i !== best ) { + log( "getAttrBN removing " + JSON.stringify( attribBN[ i ])); + attribBN[ i ].remove(); // If we found more than one attribute of this name, get rid of the extras! + } + } else + ret = ((attribBN === undefined) || (attribBN.length == 0)) ? dflt : attribBN[ best ].get( mx ? "max" : "current"); + } } + if( ret === undefined && dflt != undefined ) ret = dflt; + return ((toInt === true) || toInt == 1) ? Earthdawn.parseInt2( ret ) : (( toInt == 2) ? Earthdawn.safeString( ret ) : ret); + } catch(err) { + log( Earthdawn.timeStamp() + "Earthdawn:getAttrBN() error caught: " + err ); + log( "name: " + nm + " default: " + dflt + " cID: " + cID); + } +}; // end getAttrBN() + + + +Earthdawn.getIcon = function ( mi ) { + return mi[ "customTag" ] ? mi[ "customTag" ] : mi[ "icon" ] +}; // end getIcon() + + + + // Find the nth parameter in str. ie: getParam( "11, 22, 33", 2 ) is "22". + // num is 1 or more. delim defaults to comma. +Earthdawn.getParam = function ( str, num, delim ) { + 'use strict'; + try { + if( typeof str !== 'string' ) { + log( Earthdawn.timeStamp() + "Error getParam argument not string." ); + log( str ); log( num); log( delim); + return; + } + str = str.trim(); + if( !delim ) + delim = ","; + let found, count = 0, j, i = -1; + do { + j = i +1; + i = str.indexOf( delim, j); + } while ( (++count < num) && (i !== -1) ); + if( count === num) { + if( i < 0) + i = str.length; + found = str.slice( j, i).trim(); + } + return found; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:getParam() error caught: " + err ); } +}; // end getParam() + + + + // Earthdawn.getStatusMarkerCollection() + // Get a collection of objects that fully describe status markers available. Used by Markerset and other routines. + // Note: submenu is a bit of a bastardization of the real submenu and the codes used on the character sheets. + // In submenu, [x,y] is as follows: x is code for this option on the character sheet ie: 2 for partial. y is coded for menu. + // In submenu, [x,y] ether [x,] or [,y] will usually be removed at start of processing, leaving only x or y, not both. + // + // Icon is the icon name in the roll20 default token set. customIcon is the name in the Earthdawn Fasa Official custom icon set (available for free in the marketplace). + // A startup routine looks for the customIcons, and if finds it, loads the customTag (which is runtime dependent). + // + // The rules for more than one icon sharing an attribute are as follows: Each code must be unique. There should not be a submenu. + // There should be an element "shared", which holds ether the work Negative (digits 1 to 9 representing -1 to -9), Positive (digits 1 to 9), + // or the one value of attrib that the icon will be displayed (checked value). + // For example when the karmaauto icon is set, KarmaGlobalMode gets set to "x". when karmaask, it is set to "?". +Earthdawn.getStatusMarkerCollection = function() { + 'use strict'; + try { +// These are the default token set, with the ones we are not using marked with xx. +// xx "red", xx "blue", xx "green", xx "brown", "purple", xx "pink", xx "yellow", "dead", +// "skull", "sleepy", "half-heart", "half-haze", "interdiction", "snail", "lightning-helix", xx "spanner", +// xx "chained-heart", "chemical-bolt", xx "death-zone", "drink-me", xx "edge-crack", xx "ninja-mask", "stopwatch", xx "fishing-net", +// "overdrive", xx "strong", xx "fist", xx "padlock", "three-leaves", "fluffy-wing", "pummeled", "tread", +// "arrowed", xx "aura", "back-pain", xx "black-flag", "bleeding-eye", xx "bolt-shield", "broken-heart", xx "cobweb", +// "broken-shield", xx "flying-flag", xx "radioactive", xx "trophy", xx "broken-skull", xx "frozen-orb", "rolling-bomb", "white-tower", +// xx "grab", xx "screaming", "grenade", "sentry-gun", "all-for-one", "angel-outfit", "archery-target" + let smc = []; // IMPORTAINT NOTE!!! if you make changes that affect any attrib, also edit attribute section of the on ready event near the bottom of this file and the chat menu section dealing with status's. + smc.push({ code: "karmaauto", prompt: "Karma Auto", attrib: "KarmaGlobalMode", shared: "x", icon: "lightning-helix", customIcon: "001-Karma-On", customTag: "" }); + smc.push({ code: "karmaask", prompt: "Karma Ask", attrib: "KarmaGlobalMode", shared: "?", icon: "drink-me", customIcon: "002-Karma-Ask", customTag: "" }); + if( state.Earthdawn.gED ) { + smc.push({ code: "devpntauto", prompt: "DP Auto", attrib: "DPGlobalMode", shared: "x", icon: "angel-outfit", customIcon: "003-Devotion-On", customTag: "" }); + smc.push({ code: "devpntask", prompt: "DP Ask", attrib: "DPGlobalMode", shared: "?", icon: "broken-heart", customIcon: "004-Devotion-Ask", customTag: "" }); + } + smc.push({ code: "healthunconscious", prompt: "Unconscious", attrib: "condition-Health", shared: "5", icon: "pummeled", customIcon: "005-Unconscious", customTag: "" }); + smc.push({ code: "healthdead", prompt: "Dead", attrib: "condition-Health", shared: "-5", icon: "dead", customIcon: "006-Dead", customTag: "" }); + smc.push({ code: "strain", prompt: "Strain per round", icon: "grenade", attrib: "Misc-StrainPerTurn", customIcon: "007-Strain", customTag: "", + submenu: "?{Strain per round|0,[0^u]|1,[1^a]|2,[2^b]|3,[3^c]|Increase,++|Decrease,--}"}); +// 008-Pink, then line 2 of token markers (there are 8 per line). + // Combat options: - Not shown - Attack to knockdown, Attack to Stun, Jump-up, setting against a charge, shatter shield. + smc.push({ code: "aggressive", prompt: "Aggressive Attack", attrib: "combatOption-AggressiveAttack", icon: "sentry-gun", customIcon: "021-Agressive-Attack", customTag: ""}); + smc.push({ code: "defensive", prompt: "Defensive Stance", attrib: "combatOption-DefensiveStance", icon: "white-tower", customIcon: "022-Defensive-Stance", customTag: ""}); + smc.push({ code: "called", prompt: "Called Shot", attrib: "combatOption-CalledShot", icon: "archery-target", customIcon: "023-Called-Shot", customTag: "" }); + smc.push({ code: "split", prompt: "Split Movement", attrib: "combatOption-SplitMovement", icon: "tread", customIcon: "024-Split-Movement", customTag: ""}); + smc.push({ code: "reserved", prompt: "Reserved Action", attrib: "combatOption-Reserved", icon: "stopwatch", customIcon: "025-ReservedAction", customTag: ""}); + smc.push({ code: "tail", prompt: "Tail Attack", attrib: "combatOption-TailAttack", icon: "purple", customIcon: "026-Tail-Attack", customTag: ""}); + smc.push({ code: "blindsiding", prompt: "Blindsiding", attrib: "condition-Blindsiding", icon: "interdiction", customIcon: "027-Blindsiding", customTag: ""}); + smc.push({ code: "targetpartialcover", prompt: "Tgt Partial Cover", attrib: "condition-TargetPartialCover", icon: "half-heart", customIcon: "028-Cover-Target", customTag: ""}); +// third line + smc.push({ code: "knocked", prompt: "Knocked Down", attrib: "condition-KnockedDown", icon: "back-pain", customIcon: "101-KnockedDown", customTag: ""}); + smc.push({ code: "harried", prompt: "Harried", attrib: "condition-Harried", icon: "all-for-one", customIcon: "102-Harried", customTag: "", + submenu: "?{Harried|Not Harried,[0^u]|Harried,[2^s]|Overwhelmed,[3^c]|Overwhelmed II,[4^d]|Overwhelmed III,[5^e]|Increase,++|Decrease,--}"}); + smc.push({ code: "blindsided", prompt: "Blindsided", attrib: "condition-Blindsided", icon: "arrowed", customIcon: "103-Blindsided", customTag: ""}); + smc.push({ code: "surprised", prompt: "Surprised", attrib: "condition-Surprised", icon: "sleepy", customIcon: "104-Surprised", customTag: ""}); + smc.push({ code: "noshield", prompt: "NoShield", attrib: "condition-NoShield", icon: "broken-shield", customIcon: "105-NoShield", customTag: ""}); + smc.push({ code: "move", prompt: "Movement Impaired", attrib: "condition-ImpairedMovement", icon: "snail", customIcon: "106-Move-Impaired", customTag: "", + submenu: "?{Impaired Movement|None,[0^u]|Partial,[2^b]|Full,[4^d]}"}); + smc.push({ code: "flying", prompt: "Flying", icon: "fluffy-wing", attrib: "condition-Flying", customIcon: "107-Flying", customTag: "", + submenu: "?{Flying|Not Flying,[-1^u]|Flying,[0^s]|Flying altitude 1,[1^a]|Flying altitude 2,[2^b]|Flying altitude 3,[3^c]|Flying altitude 4,[4^d]|Flying altitude 5,[5^e]|Flying altitude 6,[6^f]|Flying altitude 7,[7^g]|Flying altitude 8,[8^h]|Flying altitude 9,[9^i]|Increase,++|Decrease,--}"}); +// smc.push({ code: "flying", prompt: "Flying", icon: "fluffy-wing", attrib: "condition-Flying", submenu: "?{Amount|0}", customIcon: "107-Flying", customTag: "" }); + smc.push({ code: "range", prompt: "Long Range", attrib: "condition-RangeLong", icon: "half-haze", customIcon: "108-Range-Long", customTag: ""}); +// fourth line + smc.push({ code: "cover", prompt: "Cover", attrib: "condition-Cover", icon: "three-leaves", customIcon: "109-Cover-Partial", customTag: "", + submenu: "?{Cover|None,[0^u]|Partial,[2^b]|Full,[99^i]}"}); + smc.push({ code: "vision", prompt: "Vision Impaired", attrib: "condition-Darkness", icon: "bleeding-eye", customIcon: "110-Darkness-Partial", customTag: "", + submenu: "?{Impaired Vision|None,[0^u]|Partial,[2^b]|Full,[4^d]}"}); + + smc.push({ code: "divingcharging", prompt: "Diving/Charging", attrib: "Creature-DivingCharging", icon: "rolling-bomb", customIcon: "301-DivingCharging", customTag: ""}); + smc.push({ code: "ambushing", prompt: "Ambushing", attrib: "Creature-Ambushing", icon: "overdrive", customIcon: "302-Ambushing", customTag: "" }); +// 400-Black, 401-Bordeaux, 402-Green, 403- Greenish, then fifth line. + smc.push({ code: "alltestsdebuff", prompt: "All tests debuff", icon: "", attrib: "Adjust-All-Tests-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "401-Action-Debuff", customTag: "" }); + smc.push({ code: "alltestsbuff", prompt: "All tests buff", icon: "", attrib: "Adjust-All-Tests-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "402-Action-Buff", customTag: "" }); + smc.push({ code: "attacksdebuff", prompt: "Attacks debuff", icon: "", attrib: "Adjust-Attacks-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "403-Attack-Debuff", customTag: "" }); + smc.push({ code: "attacksbuff", prompt: "Attacks buff", icon: "", attrib: "Adjust-Attacks-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "404-Attack-Buff", customTag: "" }); + smc.push({ code: "damagedebuff", prompt: "Damage debuff", icon: "", attrib: "Adjust-Damage-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "405-Damage-Debuff", customTag: "" }); + smc.push({ code: "damagebuff", prompt: "Damage buff", icon: "", attrib: "Adjust-Damage-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "406-Damage-Buff", customTag: "" }); + smc.push({ code: "defensesdebuff", prompt: "Defenses debuff", icon: "", attrib: "Adjust-Defenses-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "407-Defenses-Debuff", customTag: "" }); + smc.push({ code: "defensesbuff", prompt: "Defenses buff", icon: "", attrib: "Adjust-Defenses-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "408-Defenses-Buff", customTag: "" }); +// Sixth line + smc.push({ code: "pddebuff", prompt: "PD debuff", icon: "", attrib: "PD-Buff", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "409-PD-Debuff", customTag: "" }); + smc.push({ code: "pdbuff", prompt: "PD buff", icon: "", attrib: "PD-Buff", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "410-PD-Buff", customTag: "" }); + smc.push({ code: "mddebuff", prompt: "MD debuff", icon: "", attrib: "MD-Buff", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "411-MD-Debuff", customTag: "" }); + smc.push({ code: "mdbuff", prompt: "MD buff", icon: "", attrib: "MD-Buff", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "412-MD-Buff", customTag: "" }); + smc.push({ code: "sddebuff", prompt: "SD debuff", icon: "", attrib: "SD-Buff", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "413-SD-Debuff", customTag: "" }); + smc.push({ code: "sdbuff", prompt: "SD buff", icon: "", attrib: "SD-Buff", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "414-SD-Buff", customTag: "" }); + smc.push({ code: "padebuff", prompt: "PA debuff", icon: "", attrib: "PA-Buff", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "415-PA-Debuff", customTag: "" }); + smc.push({ code: "pabuff", prompt: "PA buff", icon: "", attrib: "PA-Buff", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "416-PA-Buff", customTag: "" }); +// Seventh line + smc.push({ code: "madebuff", prompt: "MA debuff", icon: "", attrib: "MA-Buff", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "417-MA-Debuff", customTag: "" }); + smc.push({ code: "mabuff", prompt: "MA buff", icon: "", attrib: "MA-Buff", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "418-MA-Buff", customTag: "" }); + smc.push({ code: "effectsdebuff", prompt: "Effects debuff", icon: "", attrib: "Adjust-Effect-Tests-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "419-Effect-Debuff", customTag: "" }); + smc.push({ code: "effectsbuff", prompt: "Effects buff", icon: "", attrib: "Adjust-Effect-Tests-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "420-Effect-Buff", customTag: "" }); + smc.push({ code: "tndebuff", prompt: "TN debuff", icon: "", attrib: "Adjust-TN-Misc", shared: "Negative", + submenu: "?{Amount|0}", customIcon: "421-TN-Debuff", customTag: "" }); + smc.push({ code: "tnbuff", prompt: "TN buff", icon: "", attrib: "Adjust-TN-Misc", shared: "Positive", + submenu: "?{Amount|0}", customIcon: "422-TN-Buff", customTag: "" }); +// 600-Kakadoi, 601-Superblue. then eighth line. Note that the first three are like the colors, the sheet does not do anything with t hem. +// smc.push({ code: "entangled", prompt: "Entangled/Grappled", attrib: "condition-Entangled-Grappled", icon: "fishing-net", customIcon: "601-Entangled-Grappled", customTag: ""}); +// smc.push({ code: "poison", prompt: "Poisoned", icon: "death-zone", attrib: "condition-Poisoned", customIcon: "602-Poisoned", customTag: ""}); +// smc.push({ code: "stealth", prompt: "Stealthy", icon: "ninja-mask", attrib: "condition-Stealthy", customIcon: "603-Stealthy", customTag: ""}); +// 700-Grey, 700-Skyblue, 700-Yellowish, end of list. + + // Go through each item in StatusMarkerCollection and see if it has a customIcon (some don't). + // If it does, see if this particual campain has the custum icons loaded, and if so, record the tag. Tags variy from campaign to campaign. + let customcollection = JSON.parse(Campaign().get( "token_markers" )), + txt, found = 0; + for( let j = 0; j < smc.length; j++) { + let sm = smc[ j ]; + if( "customIcon" in sm) + for( let i = 0; i < customcollection.length; i++) + if( Earthdawn.safeString( sm.customIcon ).toLowerCase() === Earthdawn.safeString( customcollection[ i ].name ).toLowerCase()) { + smc[ j ].customTag = customcollection[ i ].tag + ++found; + } + }; + Earthdawn.StatusMarkerCollection = smc; + if( customcollection.lenth < 12 ) + txt = "Warning! There are only " + customcollection.lenth + " token markers loaded."; + else if( found < 12 ) + txt = "The Earthdawn Token Marker set has not been installed. Found only " + found + " of " + smc.length + " custom markers from the set."; + return txt; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.getStatusMarkerCollection() error caught: " + err ); } +} // End Earthdawn.getStatusMarkerCollection() + + + +Earthdawn.isString = function ( str ) { return (str != null && typeof str.valueOf() === "string") ? true : false } + + + + // keywordCheck( klist, canidate1, canidate2, etc ) + // Look for all the other arguments within the keyword list. + // return a bitmap of each keyword found. if second and third canidates found but first is not, then would return 0x110; (third, second, first) + // if any argument is a boolean true, then the next argument should be tested as a "startsWith" + // Called standalone and via ED.keyword +Earthdawn.keywordCheck = function( klist ) { + 'use strict'; + try { + let ret = 0, swith = false; + if( klist && arguments.length > 1 ) { + let kl = Earthdawn.safeString( klist ).replace( /[^\w\,\-]/g, "").toLowerCase(); // strip out whitespace (and everything that is not a word, comma or hyphen), lowercased. + for( let i = 1; i < arguments.length; ++i ) { + if((( typeof arguments[ i ] ) === "boolean" ) && arguments[ i ] ) + swith = true // Next argument is tested for "starts with" + else { + let a = arguments[ i ].toLowerCase(); + if(( swith && kl.includes( "," + a )) || // this first half is testing if it starts with this argument (starts at a comma, but does not need to end with a comma). + ( !swith && kl.includes( "," + a + "," ))) // This second half is testing for an exact match (starts and ends with a comma). + ret += 1 << (i - 1); + swith = false; + } } +//log( "keywordCheck '" + kl + "' ret " + ret); + } + return ret; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.keywordCheck() error caught: " + err ); } +} // end keywordCheck() + + + +Earthdawn.matchString = function( str ) { // return this string with everything that is not a /W stripped out, and lower cased. Used to test for matches. + return Earthdawn.safeString( str ).replace( /\W/g, "").toLowerCase(); +} + + + // for chat menu messages made with htmlBuilder, we need a section (this.newSect) and a body (this routine passed the sect). +Earthdawn.newBody = function ( sectnew ) { + return sectnew.append( ".body", "", { class:"sheet-rolltemplate-body" }); +}; + + + + // makeButton() + // Make a self contained html button that can be sent to the chat window. + // noColonFix true: don't do colonFix or encode, false: do colonFix and Encode it. +Earthdawn.makeButton = function( buttonDisplayTxt, linkText, tipText, colr, noColonFix ) { + 'use strict'; + try { + return new HtmlBuilder( "a", buttonDisplayTxt, Object.assign( {}, { + href: noColonFix ? linkText : Earthdawn.encode( Earthdawn.colonFix( linkText )), + class: "sheet-rolltemplate-chatbutton sheet-rolltemplate-ireallymeanit" + ((colr) ? " sheet-rolltemplate-color-" + colr : "") }, + tipText ? { // Optional tipText section + title: Earthdawn.encode( Earthdawn.encode( tipText )) } : {} + )) + " "; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.makeButton error caught: " + err ); } +} // end makeButton() + + + + // This function "protects" against NaN parseInts (when input is not a number) by default to 0 instead of NaN +Earthdawn.parseInt2 = function ( i, silent ) { +/* + // For quick debugging, uncomment this, which will throw an error to the calling routine. + if(( i === undefined ) || ( i === null ) || (i === "" )) return 0; // if it is an empty string, just quietly return a zero. + let x = parseInt( i ); + if( isNaN( x )) { + if( !silent ) + log( Earthdawn.timeStamp() + "Earthdawn, parseInt2 was passed not a number " + i ); + throw new Error('Parameter is not a number! ' + i);} + } else + return x || 0; +*/ + try { + if(( i === undefined ) || ( i === null ) || (i === "" ) || (i === false)) return 0; // if it is an empty string, just quietly return a zero. + else if( i === true ) return 1; + let x = parseInt( i ); + if( isNaN( x )) { + if( !silent ) + log( Earthdawn.timeStamp() + "Earthdawn, parseInt2 was passed not a number " + i ); + return 0; + } else + return x || 0; + } catch(err) { + log( Earthdawn.timeStamp() + "Earthdawn.parseInt2() error caught: " + err ); + return 0; + } +} //end parseInt2 + + + + // pseudoMsg + // We don't seem to have a valid .msg (because we are running from "on ready" or "APIflag", but we need one. + // So fake one. +Earthdawn.pseudoMsg = function( pclass, msg, sel ) { // msg is the text of the message. sel is a tokenID that should be selected. + 'use strict'; + try { + if( pclass.msg === undefined ) { // fake up a playerID. + pclass.msg = {}; + pclass.msg.content = msg ? msg : ""; + pclass.msg.type = "api"; + } + if( !pclass.msg.playerid ) { + let players = findObjs({ _type: "player", _online: true }); // lets just find the first gm that is online. + let found = _.find( players, function( plyr ) { return playerIsGM( plyr.get( "_id" ))}); + if( !found ) { + players = findObjs({ _type: "player" }); // There are no GMs online, so lets just find a GM that is offline. + found = _.find( players, function( plyr ) { return playerIsGM( plyr.get( "_id" ))}); + if( !found && players && players.length > 0 ) + found = players[ 0 ]; + } + if( found ) + pclass.msg.playerid = found.get( "_id" ); + pclass.msg.who = found.get( "_displayname" ); + } + if( sel && !pclass.msg.selected ) + pclass.msg.selected = [{ _id: sel, _type: "graphic" }]; + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.pseudoMsg error caught: " + err ); } +} // End Earthdawn.pseudoMsg() + + + + // This routine returns just one section of a repeating section name. + // section: 0 = repeating. 1 = talents, knacks, weapons, etc. 2 = rowID, 3 = code (SP, WPN, etc.), 4 is the attribute name. + // Note that this assumes that the attribute name does NOT contain an underscore, but allows for the rowID to contain one. +Earthdawn.repeatSection = function ( section, str ) { + 'use strict'; + try { + if( !section ) { + log( Earthdawn.timeStamp() + "Earthdawn:repeatSection() error, invalid section: " + section + " str : " + str ); + return; + } + if( !str ) { + log( Earthdawn.timeStamp() + "Earthdawn:repeatSection() error, invalid str: " + str + " section : " + section ); + return; + } + let x = str.split( "_" ); + if( x[ x.length -1 ] === "max" ) { // If we have a 5th section, get rid of it and add "_max" to the 4th section. + x.pop(); + x[ x.length -1] += "_max"; + } + if( section < 2 ) // talents (etc). + return x[ section ]; + else if (section == 3 ) { // Code + let z1 = Earthdawn.safeArray( x ); + if( z1.length > 2 ) + return Earthdawn.safeString( z1[ z1.length -2 ] ).toUpperCase(); + else return; + } + else if (section == 4 ) // This is the attribute name. + return x[ x.length -1 ]; + else { // There is a possibility that the RowID might contain an underscore. So this is if they want section 2. Get rid of sections 0, 1, 3, and 4. Return whatever is left. + x.pop(); + x.pop(); + x.shift(); + x.shift(); + return x.join("_"); + } + } catch(err) { + log( Earthdawn.timeStamp() + "Earthdawn:repeatSection() error caught: " + err ); + } +}; // end repeatSection + + + + + + // Make sure the returned value is safely an object, and can be used with .length(). + // If it is not already an object, return an array +Earthdawn.safeArray = function( arr ) { + 'use strict'; + try { + let r; + switch( typeof arr ) { + case "object": r = arr; + break; + case "string": + case "number": + case "bolean": r = [ arr ]; + break; + default: r = []; + } + return r; + } catch(err) { + log( Earthdawn.timeStamp() + "Earthdawn:safeString() error caught: " + err ); + } +}; // end safeArray + + + + // Make sure the returned value is safely a string, and can be used with .toUpperCase() or .replace(), Etc. + // If it is not already a string and can't be converted to a string, return an empty string. +Earthdawn.safeString = function( str ) { + 'use strict'; + try { + return (( typeof str === "string" ) ? str: ( typeof str === "number" ) ? str.toString() : "" ); + } catch(err) { + log( Earthdawn.timeStamp() + "Earthdawn:safeString() error caught: " + err ); + } +}; // end safeString + + + + // This is a wrapper for the attribute .set() function, that checks to make sure val is not undefined. + // in the basic .set() function, If val is undefined it errors out the entire API, requiring a restart. + // Worse, the error preempts logging that should have happened and the error message gives you no clue as to where your code erred out. + // This checks for undefined, writes an error message, and substitutes a default value. + // + // Rats. Can't test for NaN here, because use same routine for string and numbers. But NaN fails as well. +Earthdawn.set = function( obj, type, val, dflt ) { // type is often "current" or "max" for attributes, but could be name, bar3_value, bar3_max, or even status_xxx. + 'use strict'; + try { +// log( "set " + obj.get("name") + " val " + val); + if(( val === undefined && dflt != undefined ) || (val !== val)) { // val !== val is the only way to test for it equaling NaN. Can't use isNan() because many values are not supposed to be numbers. But we do want to test for val having been set to NaN. + log( Earthdawn.timeStamp() + "Warning!!! Earthdawn:set() Attempting to set '" + att + "' to " + val + " setting to '" + dflt + "' instead. Object is ..."); + log( obj ); + obj.set( type, (dflt === undefined) ? "" : dflt ); + } else + obj.set( type, (val === undefined) ? "" : val ); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:set() error caught: " + err ); } +} // end of set() + +Earthdawn.setWithWorker = function( obj, type, val, dflt ) { // type is "current" or "max" + 'use strict'; + try { + if( !obj ) { + log( Earthdawn.timeStamp() + "Earthdawn:setWithWorker() Error: obj is undefined. Type: " + type + " val: " + val + " dflt: " + dflt ); + return; + } +// log( "setww " + obj.get("name") + " val " + val); + if(( val === undefined && dflt != undefined ) || (val !== val)) { // val !== val is the only way to test for it equaling NaN. Can't use isNan() because many values are not supposed to be numbers. But we do want to test for val having been set to NaN. + log( Earthdawn.timeStamp() + "Warning!!! Earthdawn:setWithWorker() Attempting to set '" + att + "' to " + val + " setting to '" + dflt + "' instead. Object is ..."); + log( obj ); + obj.setWithWorker( type, (dflt === undefined) ? "" : dflt ); + } else + obj.setWithWorker( type, (val === undefined) ? "" : val ); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:setWithWorker() error caught: " + err ); } +} // end of setWithWorker() + + + // setWW helper routine that sets a value into an attribute and nothing else. + // (Note that there is also a ParseObj version that has access to this.charID, which this version does not) +Earthdawn.setWW = function( attName, val, cID, dflt, maxVal, maxDflt ) { + 'use strict'; + try { + if( !cID ) { + log( "Eearthdawn.SetWW() Error, no cID: " + attName + " : " + val ); + } else { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: attName }); + if( val !== undefined ) + Earthdawn.setWithWorker( aobj, "current", val, dflt ); + if( maxVal !== undefined ) + Earthdawn.setWithWorker( aobj, "max", maxVal, maxDflt ); + } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.SetWW() error caught: " + err ); } +} // End Earthdawn.SetWW() + + + + // Look to see if an attribute exists for a certain character sheet. If not - create it with a default attribute. +Earthdawn.SetDefaultAttribute = function( cID, attr, dflt, maxdflt ) { + 'use strict'; + try { + let aobj = findObjs({ _type: 'attribute', _characterid: cID, name: attr }) [ 0 ]; + if ( aobj === undefined ) { // If we actually found an existing attribute, then do nothing, as this routine only does defaults. + aobj = createObj("attribute", { name: attr, characterid: cID }); + if( dflt === null || dflt === undefined ) + dflt = getAttrByName( cID, attr, "current"); // This looks weird, but what it is doing is getting any default defined in the html. + if( maxdflt === null || maxdflt === undefined ) + maxdflt = getAttrByName( cID, attr, "max"); + if ( dflt != undefined && isNaN( parseInt(aobj.get("current"))) ) + Earthdawn.setWithWorker( aobj, "current", dflt ); + if ( maxdflt != undefined && isNaN( parseInt(aobj.get("max"))) ) + Earthdawn.setWithWorker( aobj, "max", maxdflt ); + } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:SetDefaultAttribute() error caught: " + err ); } +} // end of SetDefaultAttribute() + + + + // return a self contained html fragment that has a tooltip. +Earthdawn.texttip = function ( txt, tip ) { + 'use strict'; + try { + return new HtmlBuilder( "span", txt, { class: "sheet-rolltemplate-texttip", title: Earthdawn.encode( Earthdawn.encode( tip )) }); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:texttip() error caught: " + err ); } +}; // end texttip() + + + + // return string with timestamp in it. +Earthdawn.timeStamp = function () { + 'use strict'; + try { + let today = new Date(); + return today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + " " + today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() + " (UTC) "; + } catch(err) { log( "Earthdawn:timeStamp() error caught: " + err ); } +} // End timeStamp + + + + // Refresh a token. Set the token markers to match the status, rebuild the abilities, clear any hits or targets, etc. + // This is done on a token drop, and also to all tokens on a page when the gm moves a player or all players to a page. +Earthdawn.tokenRefresh = function ( obj ) { // token object + 'use strict'; + try { +// if( obj && obj.get( "name" )) { // Ignore any token without a name (probably not a real token. Don't do this. name is often blank. + if( obj && obj.get( "_subtype" ) === "token") { +//log( "refreshing token " + obj.get("name")); + let rep = obj.get( "represents" ); + if( rep && rep != "" ) { + let ch = getObj( "character", rep ); + if( ch ) { + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = rep; + edParse.tokenInfo = { type: "token", name: obj.name, tokenObj: obj, characterObj: getObj("character", rep ) }; + edParse.abilityRebuild( [ "addGraphic" ] ); + edParse.SetStatusToToken(); + edParse.TokenSet( "clear", "TargetList" ); + edParse.TokenSet( "clear", "Hit" ); + edParse.TokenSet( "clear", "SustainedSequence" ); + edParse.toSheet( "SheetUpdate", "" ); + } } } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:tokenRefresh() error caught: " + err ); } +} // End tokenRefresh + + + + // Given a tokenID, return the represented charID. +Earthdawn.tokToChar = function ( tokenID ) { + 'use strict'; + try { + if ( tokenID ) { + let TokObj = getObj("graphic", tokenID); + if( TokObj !== undefined ) { + let targetChar = TokObj.get("represents"); + if( targetChar ) + return targetChar; + } } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:tokToChar() error caught: " + err ); } +}; // end tokToChar() + + + + // The import routines want to get rid of values shortly after they appear, and in fact sometimes before the database has been updated. + // Loop until the object appears, and then remove it. +Earthdawn.waitToRemove = function ( cID, nm, timelimit ) { + 'use strict'; + try { + function waitForIt( n ) { + 'use strict'; + setTimeout( function() { + if( n < 1 ) { + let a = findObjs({ _type: "attribute", _characterid: cID, name: nm }); + if( a ) + while ( a.length > 0 ) + a.pop().remove(); + } else if ( findObjs({ _type: "attribute", _characterid: cID, name: nm })) // Every second we are going to test to see if we found it. If we do, we wait one additional second before deleting it. + waitForIt( 0 ); + else // We nether found the attribute we were waiting for, nor has the timer ran out yet. + waitForIt( n-1); + }, 1000); } + + waitForIt( timelimit ? timelimit : 15 ); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn:waitToRemove() error caught: " + err ); } +}; // end waitToRemove() + + + + + // Define the Earthdawn Class EDclass +Earthdawn.EDclass = function( origMsg ) { + 'use strict'; + + // define any class variables + this.msg = origMsg; + this.msgArray = []; // msg parsed by tilde ~ characters. msgArray[0] will hold !Earthdawn which we have already tested for and can ignore. + this.countSuccess = 0; // If there is a target number, this is a count of how many attacks, or attacks on separate targets had at least one success. + this.countFail = 0; + this.rollCount = 0; // This is a count of how many async rolls are still outstanding. + // Note: This thread CONTINUES to execute at the bottom of object declaration - after all functions are defined. + + + + // Log the ready event. + this.Ready = function () { + 'use strict'; // Check if the namespaced property exists, creating it if it doesn't + let firstRun; + if( state.Earthdawn === undefined ){ + state.Earthdawn = {}; + firstRun = true; + } + + if( state.Earthdawn.game === undefined ) { + state.Earthdawn.game = "ED"; + state.Earthdawn.gED = true; + state.Earthdawn.edition = 4; + state.Earthdawn.g1879 = undefined; + } +// if( state.Earthdawn.effectIsAction === undefined ) state.Earthdawn.effectIsAction = false; +// if( state.Earthdawn.karmaRitual === undefined ) state.Earthdawn.karmaRitual = "-1"; + if( state.Earthdawn.logStartup === undefined ) state.Earthdawn.logStartup = true; + if( state.Earthdawn.defRolltype === undefined ) state.Earthdawn.defRolltype = 0x03; // Bitfield set for who is GM only. NPC and Mook gm only, PC public; + if( state.Earthdawn.style === undefined ) state.Earthdawn.style = Earthdawn.style.VagueRoll; + if( state.Earthdawn.showDice === undefined ) state.Earthdawn.showDice = true; + // Everything works best if API and Sheet version are compatible, but some effort is made to let them limp along on different versions. + if( state.Earthdawn.version === undefined ) state.Earthdawn.version = Earthdawn.Version; // Note: This is the API (this file) version. Earthdawn.Version is hardcoded constant. state.Earthdawn.version is record of last version run with. + if( state.Earthdawn.sheetVersion === undefined ) state.Earthdawn.sheetVersion = 0.000; // This is the Sheet (html file) version that we think we are dealing with. + if( state.Earthdawn.Rolltype === undefined ) { + state.Earthdawn.Rolltype = {}; + state.Earthdawn.Rolltype.Override = false; + state.Earthdawn.Rolltype.PC = {}; + state.Earthdawn.Rolltype.PC.Default = "Public"; + state.Earthdawn.Rolltype.PC.Exceptions = {"horrormark": {name: "Horror Mark", display: "GM Only"}}; // set with key of Earthdawn.matchString( name ) = { name: name, display: ssa[ 5 ] }; + state.Earthdawn.Rolltype.NPC = {}; + state.Earthdawn.Rolltype.NPC.Default = "GM Only"; + state.Earthdawn.Rolltype.NPC.Exceptions = {"horrormark": {name: "Horror Mark", display: "GM Only"}}; + } + if( state.Earthdawn.linking === undefined ) { + state.Earthdawn.linking = {}; + state.Earthdawn.linking.PC = {}; + state.Earthdawn.linking.NPC = {}; + } + state.Earthdawn.newChars = []; // set the array of new characters to empty. + state.Earthdawn.actionCount = {}; // erase all the old actionCounts + state.Earthdawn.rowIDobj = {}; // erase collection of rowIDs waiting to be checked. + + // Check to see if the current version of code is the same number as the previous version of code. + // This will update all character sheets when a new API is loaded. + // If a new character sheet is loaded without a new API version, each will be updated individually when the sheet is first opened. + if( state.Earthdawn.version != Earthdawn.Version ) { // This code will be run ONCE when a new API version is loaded. However the update routines below will be run once for each character. + + + // It is possible that some update routines might take a lot of time to update, depending upon what updates are needed. + // If a campaign has many characters, the servers might timeout with an infinite loop message before compleating (there is no infinite loop, just a loop that takes too much time). + // Therefore use setTimeout to call each character one at a time, this lets the system know that progress is being made between characters. + function vUpdate( ed, routine, version) { + 'use strict'; + let count = 0, + charQueue = findObjs({ _type: "character" }); // create the queue we'll be processing. + if( charQueue ) { + const charBurndown1 = () => { // create the function that will process the next element of the queue + if( charQueue.length ) { + let c = charQueue.shift(); + let attCount = routine( c.get( "_id" ), ed, count++ ); + Earthdawn.errorLog( "Updated " + attCount + " things for " + c.get( "name" ), ed); + setTimeout( charBurndown1, 1); // Do the next character + } else // Have finished the last attribute. + ed.chat( count + " character sheets updated.", Earthdawn.whoFrom.apiWarning ); + }; + ed.chat( "Updating all characters (" + charQueue.length + ") to new character sheet version " + version, Earthdawn.whoFrom.apiWarning ); + charBurndown1(); // start the execution by doing the first element. Each element will call the next. + } + } // end vUpdate + + + if( state.Earthdawn.version < 1.001) // Note, this tests JS version number, not sheet version number. + vUpdate( this, this.updateVersion1p001, 1.001 ); + if( state.Earthdawn.version < 1.0021) + vUpdate( this, this.updateVersion1p0021, 1.0021 ); + if( state.Earthdawn.version < 1.0022) + vUpdate( this, this.updateVersion1p0022, 1.0022 ); + if( state.Earthdawn.version < 1.0023) + vUpdate( this, this.updateVersion1p0023, 1.0023 ); + if( state.Earthdawn.version < 2.001) + vUpdate( this, this.updateVersion2p001, 2.001 ); + if( state.Earthdawn.version < 3.000) + vUpdate( this, this.updateVersion3p000, 3.000 ); + if( state.Earthdawn.version < 3.330) { + let edParse = new this.ParseObj( this ); + edParse.funcMisc( [ "funcMisc", "macroCreate", "refresh" ] ); // This version needs new macros, so refreash them. + vUpdate( this, this.updateVersion3p330, 3.330 ); + } + + state.Earthdawn.version = Earthdawn.Version; + } + + let style; + switch (state.Earthdawn.style) { + case Earthdawn.style.VagueSuccess: style = " - Vague Successes."; break; + case Earthdawn.style.VagueRoll: style = " - Vague Roll."; break; + case Earthdawn.style.Full: + default: style = " - Full."; break; + } + + log( Earthdawn.timeStamp() + "---Earthdawn.js Version: " + Earthdawn.Version + + " loaded. Earthdawn.html Version: " + state.Earthdawn.sheetVersion + " loaded. For " + + state.Earthdawn.game + " Edition: " + Math.abs( state.Earthdawn.edition ) + " ---"); + if( state.Earthdawn.logStartup ) { + log( "--- Roll Style: " + state.Earthdawn.style + style ); + log( "--- CursedLuckSilent is " + ((state.Earthdawn.CursedLuckSilent && state.Earthdawn.CursedLuckSilent & 0x04 ) ? "Silent" : "not Silent") + + ". NoPileonDice is " + state.Earthdawn.noPileonDice + ((state.Earthdawn.CursedLuckSilent && state.Earthdawn.CursedLuckSilent & 0x02 ) ? " Silent" : " not Silent") + + ". NoPileonStep is " + state.Earthdawn.noPileonStep + ((state.Earthdawn.CursedLuckSilent && state.Earthdawn.CursedLuckSilent & 0x01 ) ? " Silent" : " not Silent") + + ". ---"); + } + + let smcText = Earthdawn.getStatusMarkerCollection(); // load the status marker collection, and see if the custom status markers are loaded. + if( smcText ) { + let edParse = new this.ParseObj( this ); + smcText += " For instructions open this link " + + Earthdawn.makeButton( "Wiki Link", "https://wiki.roll20.net/Earthdawn_-_FASA_Official_V2#Import_the_Custom_Marker_Set", + "This button will open this character sheets Wiki Documentation, which should answer most of your questions about how to use this sheet.", "dflt", true ) + + " in another tab and go to section 4.1.2."; + this.chat( smcText, Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + } + + + // for each character, set API and API_max to 1 + let chars = findObjs({ _type: "character" }); + _.each( chars, function ( charObj ) { + let cid = charObj.get( "_id" ); + let aObj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cid, name: "API" }); + if( aObj.get( "current" ) != "1" ) + aObj.setWithWorker( "current", "1" ); + if( aObj.get( "max" ) != "1" ) + aObj.setWithWorker( "max", "1" ); + }) // End ForEach character + + + + // + // Put anything that you want to happen 5 seconds after startup here. + // + let edc = this; + setTimeout(function() { + try { + StatusTracker.RegisterCallback( "AnnounceTurn", callbackStAnnounceTurn ); + StatusTracker.RegisterCallback( "TokenType", callbackStTokenType ); + } catch(err) { +// log( "Warning! Earthdawn.js -> StatusTracker integration failed! Error: " + err ); +// log( " The sheet will still work, but without StatusTracker integration!" ); + } + try { + if( typeof WelcomePackage !== 'undefined' ) + if( typeof WelcomePackage.onAddCharacter !== 'undefined' ) + WelcomePackage.onAddCharacter( callbackWelcomePackage ); + else if( typeof WelcomePackage.OnAddCharacter !== 'undefined' ) + WelcomePackage.OnAddCharacter( callbackWelcomePackage ); + } catch(err) { +// if( err.indexOf( "WelcomePackage is not defined" ) == -1 ) +// log( "Warning! Earthdawn.js -> WelcomePackage integration failed! Error: " + err ); + } + try { + let sectPlayer = new HtmlBuilder( "", "", {class: "sheet-rolltemplate-background-white" }); + } catch(err) { + log( Earthdawn.timeStamp() + "Error! Earthdawn.js -> HtmlBuilder integration failed! Error: " + err ); + log( " ***** The Earthdawn character sheet compainion API will ***NOT*** work, unless htmlBuilder is also installed! *****" ); + log( " ***** go to the API page and add htmlBuilder.js" ); + } + + + + // Every time the API is restarted, go through each character and look for old repeating_message's that have not been processed. + // Process and/or delete them. + try { + let ApiUnproc = 0, Expired = 0, ApiAck = 0, SheetAck = 0, kept = 0, rowCount = 0, charCount = 0, d = new Date(), + charQueue = findObjs({ _type: "character" }); // create the queue we'll be processing. + if( charQueue && charQueue.length > 0 ) { + const charBurndown3 = () => { // function that will process the next element of the queue + try { + if( charQueue.length > 0 ) { + let c = charQueue.shift(), cDel; + let id = c.get( "_id" ); + let attributes = findObjs({ _type: "attribute", _characterid: id }), + rowIndex = [], + rowattribs = [], + toDel = []; + _.each( attributes, function (att) { + if (att.get("name").startsWith( "repeating_message_" )) { + let rowID = Earthdawn.safeString( Earthdawn.repeatSection( 2, att.get( "name" ))), + t; + if( rowID ) { + t = rowIndex.indexOf( rowID ); // One-dimensional array of message rowIDs. + if( t == -1 ) { + t = rowIndex.push( rowID ) - 1; + rowattribs.push( [] ); + } + rowattribs[ t ].push( att ); // Array or arrays of attributes. First dimension is RowID. rowAttribs[0][0] and [0][1] will have the same rowID. + } + } + }); // End for each attribute. // We have now ordered things into neat two dimensional arrays. + + for( let i = 0; i < rowIndex.length; ++i ) { + let toAPI, toApiAck, toSheet, toSheetAck, tstamp, rDel; + for( let j = 0; j < rowattribs[ i ].length; ++j ) { // we don't know what order we are going to get these, so now put each row into variables. + let n = rowattribs[i][j].get( "name" ), + c = rowattribs[i][j].get( "current" ), + m = rowattribs[i][j].get( "max" ); + if( n.endsWith( "_MSG_TimeStamp" )) + tstamp = c; + else if( n.endsWith( "_MSG_toAPI" )) { + if( c ) toAPI = c; + if( m ) toApiAck = m; + } else if( n.endsWith( "_MSG_toSheetworker" )) { + if( c ) toSheet = c; + if( m ) toSheetAck = m; + } else if ( !n.endsWith( "_MSG_RowID" )) { // We should not see RowID's, but if we do, don't freak out. + Earthdawn.errorLog( "Earthdawn messageCleanup error. invalid attribute: " + n, edc); + rDel = true; // If we do see something truly weird, delete the whole row. + } + } // end column Now that we have the row organized into variables, check to see if we should delete this row. + + if( toAPI && !toSheetAck ) { // Message to API was never ACK. So process it now, but delete it instead of sending ACK. + Earthdawn.fromSheetworkerToAPI( toAPI, id, "unacknowledged" ); + rDel = true; + ++ApiUnproc; + } + // if( toSheet && !toApiAck ) { + // rDel = true; + // ++SheetUnproc; + // } + if( tstamp ) { + let tdiff = Math.abs(d - (new Date( tstamp ))), + told = tdiff > 50000; // Milliseconds, so 50 seconds. + //log("tdiff " + tdiff + " told " + told + " d " + d.toString() + " dold " + (new Date( tstamp ))); + if(told) { + rDel = true; + ++Expired; + } + } + if( toApiAck ) { // Message has been ACK, but was not deleted + rDel = true; + ++ApiAck; + } + if( toSheetAck) { // Message has been ACK, but was not deleted. + rDel = true; + ++SheetAck; + } + if( !toAPI && !toSheet ) + rDel = true; // badly formed message. + + if( rDel ) { + cDel = true; + ++rowCount; + for( let j = 0; j < rowattribs[ i ].length; ++j ) + toDel.push( rowattribs[ i ][ j ] ) + } else ++kept; + } // end row + if( cDel ) ++charCount; + for( let j = 0; j < toDel.length; ++j ) + toDel[ j ].remove(); + + setTimeout( charBurndown3, 1); // Do the next character + } else { // Have finished the last character. + if( charCount || rowCount) { + log( "Message cleanup deleted " + rowCount + " messages for " + charCount + " characters."); + if( ApiUnproc ) log( ApiUnproc + " messages to the API that were unprocessed. "); + if( ApiAck ) log( ApiAck + " messages to the API that were processed but not deleted. "); + // if( SheetUnproc ) log( SheetUnproc + " messages to the Sheetworker that were unprocessed. "); + if( SheetAck ) log( SheetAck + " messages to the Sheetworker that were processed but not deleted. "); + if( Expired ) log( Expired + " messages that were expired. "); + if( kept ) log( kept + " messages kept." ); + } } + } catch(err) { + log( Earthdawn.timeStamp() + " Message cleanup error caught: " + err ); + } + }; // end charBurndown3() + charBurndown3(); // start the execution by doing the first element. Each element will call the next. + } + } catch(err) { log( Earthdawn.timeStamp() + "Error! Earthdawn.js -> Message cleanup failed! Error: " + err ); } + + + if( firstRun ) { // This is the first time the API has been run. + let edParse = new edc.ParseObj( edc ), + charQueue = findObjs({ _type: "character" }), // create the queue we'll be processing. + count = 0; + Earthdawn.pseudoMsg( edc, "dummy" ); // fake a msg with no token selected. + if( charQueue && charQueue.length > 0) { + const charBurndown2 = () => { // function that will process the next element of the queue + try { + if( charQueue.length > 0 ) { + let c = charQueue.shift(); + log( "Verifying " + c.get( "name" )); + edParse.charID = c.get( "_id" ); + edParse.Debug( [ "Debug", "repSecFix", "silent", "firstRun", count ? "notFirst" : "first" ] ); + ++count; + setTimeout( charBurndown2, 1); // Do the next character + } else { // Have finished the last character. + edc.chat( "We recommend relinking all existing characters using the process described in the WiKi.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + edc.chat( "Suggest opening " + Earthdawn.makeButton( "Wiki Link", "https://wiki.roll20.net/Earthdawn_-_FASA_Official_V2" + , "This button will open this character sheets Wiki Documentation, which should answer most of your questions about how to use this sheet." + , "dflt", true ) + " in another tab and reading it." + , Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + } + } catch(err) { + log( "firstRun error caught: " + err ); + } }; + edc.chat( "This is the first time the API has run in this campaign space.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + edParse.funcMisc( [ "funcMisc", "macroCreate", "Refresh", "firstRun" ] ); + edc.chat( "Earthdawn Macros created.", Earthdawn.whoTo.public ); + edc.chat( 'Suggest to your players that they put "Roll-Public", "Roll-Player-GM", and "Roll-GM-Only" in their macro bar.', Earthdawn.whoTo.public ); + edc.chat( 'As GM, you should also have "NpcReInit", "ResetChars", and "Dur-Track" in your macro bar.', Earthdawn.whoTo.public ); + edc.chat( "We are now Verifying all preexisting characters.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + edc.chat( charQueue.length + " characters detected.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + charBurndown2(); // start the execution by doing the first element. Each element will call the next. + } } // end firstRun + + + }, 3500); // end timeout + }; // End ED.Ready() + + + + // chat - The EDclass version of chat. Note that the edParse version calls this one with edParse class in po, so this is the one that does everything. + // newMsg: Text to send. + // iFlags: whoFrom and whoTo flags. + // customFrom: Text string that will be used instead of whoFrom and whoTo flags. + // po: If this is being called back from parseObj, then this is the parseObj. Else, if it exists, it holds cID. +this.chat = function ( newMsg, iFlags, customFrom, po ) { + 'use strict'; + let edc = this; + try { + let haveMsg = edc && edc.msg; + let cID, wf = "API", specialTo, whoTo = "", cnt = 0; // Number of players that actually got the message + iFlags = iFlags || 0; +//log("this.chat " + iFlags + " customFrom " + customFrom); + + // Set the cID if any + if( typeof po === "string" ) { + cID = po; // This was never parseObj, we were passed cID. + po = undefined; + } else if( po !== undefined) + cID = po.charID; // after this either we have a cID, or we were not passed any (which means it is a general message, not attached to a character). + + // Deal with error and warnings that have default whoto and whofom + if ( iFlags & (Earthdawn.whoFrom.apiError | Earthdawn.whoFrom.apiWarning )) { + Earthdawn.errorLog( newMsg, edc ); // Errors and Warnings log to console log. + iFlags |= Earthdawn.whoFrom.api | Earthdawn.whoTo.player | ((iFlags & Earthdawn.whoFrom.apiError) ? Earthdawn.whoTo.gm : 0); // Chat messages to player who did it, If an error, send to gm as well. + } + // Find the whoFrom + if( customFrom && customFrom.startsWith( " sent Roll" )) { // When a roll is sent to GM Only, a Player Card message is sent to the player saying it was sent. + specialTo = customFrom; + customFrom = undefined; + } + if( customFrom ) // if we specified a customFrom as argument, it overrides everything + wf = customFrom; + else if ((iFlags & Earthdawn.whoFrom.player) && (iFlags & Earthdawn.whoFrom.character) && po && po.tokenInfo && ("name" in po.tokenInfo ) + && haveMsg && edc.msg.who ) // From a player and we have the name of the token + wf = edc.msg.who.replace(" (GM)","") + " - " + po.tokenInfo[ "name" ]; + else if ((iFlags & Earthdawn.whoFrom.character) && po) { + let fnd; + if( po && po.tokenInfo ) { + fnd = po.tokenInfo[ "name" ]; + if( (!fnd || fnd.length < 2) && "characterObj" in po.tokenInfo ) { + fnd = po.tokenInfo.characterObj.get( "name" ); + if( (!fnd || fnd.length < 2) && "tokenObj" in po.tokenInfo ) { + fnd = po.tokenInfo.tokenObj.get( "name" ); + } } } + else if( po.charID ) { + let charObj = getObj("character", po.charID); + if( charObj ) + fnd = charObj.get( "name" ); + } + if( fnd && fnd.length > 0 ) + wf = fnd; + } else if (!haveMsg || (iFlags & Earthdawn.whoFrom.api) || (edc.msg.type === "api") || (edc.msg.playerid === "API")) + wf = "API"; + // end find whoFrom + + // Find the WhoTo + if( specialTo ) + whoTo = specialTo; + else if ( iFlags & Earthdawn.whoTo.gm && iFlags & Earthdawn.whoTo.player ) + whoTo = " to GM&P "; + else if ( iFlags & Earthdawn.whoTo.gm ) + whoTo = " to GM "; + else if ( iFlags & Earthdawn.whoTo.player ) + whoTo = " to player "; + + function sc( speakingas, inp ) { // send chat wrapper. + sendChat( speakingas, inp, null, (iFlags & Earthdawn.whoFrom.noArchive) ? {noarchive:true} : null ); + cnt++; + } // end sc wrapper + function whisper( speakingas, whisperto, dataline ) { + if( whisperto.indexOf( "\"" ) !== -1 ) { + sendChat( "API", "Warning: One the roll20 Display names (" + whisperto + ") API is trying to send a message to contains a double quote mark, which makes it impossible to send a whisper to that person." ); + sc( speakingas, dataline ); // send it to public (as well as everybody on the list - duplicate message). + } else + sc( speakingas, '/w "' + whisperto +'" ' + dataline ); + } // end whisper wrapper + +/* + if ( !(iFlags & Earthdawn.whoTo.playerList )) { // Don't do this section if specified to do the playerList section. + // Send to player, unless already sending to the gm, and the player is the gm. + if( haveMsg && edc.msg.who && ( iFlags & Earthdawn.whoTo.player ) + && ( !(iFlags & Earthdawn.whoTo.gm) || ((iFlags & Earthdawn.whoTo.gm) && edc.msg.playerid && !playerIsGM( edc.msg.playerid )))) + whisper( wf + whoTo, edc.msg.who.replace( " (GM)", "" ), newMsg); + else if( iFlags & Earthdawn.whoTo.gm ) + sc( wf + whoTo, "/w gm " + newMsg ); + else if( !( iFlags & Earthdawn.whoTo.mask)) // If no whoTo specified, send to all. + sc( wf + " to Public", newMsg ); + } // end NOT playerList +*/ +// When there is a player and a GM, and the player asks for a pgm, it is sent to player, but not GM. + + if ( !(iFlags & Earthdawn.whoTo.playerList )) { // Don't do this section if specified to do the playerList section. + // Send to player, unless already sending to the gm, and the player is the gm. + if( haveMsg && edc.msg.who && ( iFlags & Earthdawn.whoTo.player ) + && ( !(iFlags & Earthdawn.whoTo.gm) || ((iFlags & Earthdawn.whoTo.gm) && edc.msg.playerid && !playerIsGM( edc.msg.playerid )))) + whisper( wf + whoTo, edc.msg.who.replace( " (GM)", "" ), newMsg); + if( iFlags & Earthdawn.whoTo.gm ) + sc( wf + whoTo, "/w gm " + newMsg ); + if( !( iFlags & Earthdawn.whoTo.mask)) // If no whoTo specified, send to all. + sc( wf + " to Public", newMsg ); + } // end NOT playerList + + if( cnt === 0 ) { // If we did not send any messages above, send to everybody on the token control list and every gm. + let lplr = findObjs({ _type: "player", _online: true }), //list of online players + lplrctrl = [], ctrall; //List of players that control this player id + if( cID ) { + let c = findObjs({ _type: "character", _id: cID })[0]; + if (c && c.get( "controlledby" )) + lplrctrl = c.get( "controlledby" ).split( "," ); + ctrall = lplrctrl && lplrctrl.includes("all"); + } + + if( lplr ) { + for( let i = 0; i < lplr.length; i++ ) { //go through the list of on-line players and send to the right ones + let snd = false, + pid = lplr[ i ].get( "_id" ), + pn = lplr[ i ].get( "_displayname" ); + snd |= (iFlags & Earthdawn.whoTo.gm) && playerIsGM( pid ); // Send to GM if flagged for it + if( iFlags & Earthdawn.whoTo.player) { + snd |= haveMsg && edc.msg.playerid && ( edc.msg.playerid == pid ); // Send to the originator of the message, if there is one. + snd |= (ctrall || lplrctrl.includes( pid )) && (!playerIsGM( pid ) || !haveMsg); // Player(s) controlling the same token.character. Unless it is not a real message GM is excluding because vague results are sent in 2 separate messages + } + if( snd ) + whisper( wf + whoTo, pn, newMsg ); + } } } // end playerList + if( cnt == 0 ) //If we didn't identify any player to send it to just send it to all + sc( wf + " to Public", newMsg ); + } catch(err) { Earthdawn.errorLog( "Earthdawn.chat() error caught: " + err, edc ); } +}; // end chat() + + + + + // Somehow script library messed up character set and all token actions ended up with weird and wrong names (starting with 0xFFFD). + // Go through all abilities that start with 0xFFFD. + // look inside it, and figure out what it is, and fix the name. + this.updateVersion1p001 = function( cID ) { + 'use strict'; + let edc = this, + count = 0; + try { + function setNameSafe( obj, typ, nm, val ) { // We are changing objects names, but we DONT want duplicates. Before changing a name, see if the new name already exists, and if it does, delete the object we were about to change. + 'use strict'; // This could solve a problem where multiple version of a sheet got used at different times. + let attrib = findObjs({ _type: typ, _characterid: cID, name: nm }); + if( attrib && attrib.length > 0 ) { + obj.remove(); + while( attrib.length > 1 ) + attrib.pop().remove(); // if have more than one of the same name, delete all except one. + } + else + Earthdawn.set( obj, "name", val); + ++count; + } + + let a = findObjs({ _type: "ability", _characterid: cID }); + if ( a ) + for( let i = 0; i < a.length; ++i) { + let name = Earthdawn.safeString( a[i].get( "name" ) ); + let e = name.lastIndexOf( String.fromCharCode( 0xFFFD ) ); + if( e !== -1 ) { + let act = Earthdawn.safeString( a[i].get("action") ).trim(), + symbol; + if( act.endsWith( "willforce:t" )) setNameSafe( a[i], "ability", name, Earthdawn.constantIcon( "karma" ) + "WillFrc-T" ); + else if( act.endsWith( "_T_Roll}" )) symbol = Earthdawn.constantIcon( "talent" ); + else if( act.endsWith( "_NAC_Roll}" )) symbol = Earthdawn.constantIcon( "knack" ); + else if( act.endsWith( "_SK_Roll}" )) symbol = Earthdawn.constantIcon( "skill" ); + else if( act.endsWith( "_WPN_Roll}" )) symbol = Earthdawn.constantIcon( "weapon" ); + else if( act.endsWith( "Target: Set" )) symbol = Earthdawn.constantIcon( "target" ); + else if( act.endsWith( "TargetsClear" )) symbol = Earthdawn.constantIcon( "target" ); + else if( act.endsWith( "Grimoire" )) symbol = Earthdawn.constantIcon( "spell" ); + else if( act.endsWith( "Spells" )) symbol = Earthdawn.constantIcon( "spell" ); + else if (name.match( /[A-Z][a-z]+-\d '/)) symbol = Earthdawn.constantIcon( "spell" ); + if( symbol ) + setNameSafe( a[i], "ability", name, symbol + name.slice( e +1 )); // Fix the name with the correct symbol + } else if( name === "Attack") + a[i].set("action", "!edToken~ %{selected|Attack}"); + } + + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + let nm = Earthdawn.safeString( att.get("name") ); + if ( nm.endsWith( "_DSP_Code" )) { + if( Earthdawn.safeString( att.get( "current" )) == "99.0" ) // I changed the code for Questors. + att.set( "current", "89.0"); + } else if ( nm === "SP-WillForce-Karma-Control" ) // Willforce, not WillForce. + setNameSafe( att, "attribute", nm, "SP-Willforce-Karma-Control"); + else if ( nm === "Damage" ) { + let t = Earthdawn.safeString( att.get( "max" )); + if( t ) + Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: "Damage-Unc-Rating"}, t); + } else if ( nm === "SP-WillForce-DP-Control" ) + setNameSafe( att, "attribute", nm, "SP-Willforce-DP-Control"); + else if ( nm.endsWith( "show-T-details" )) + setNameSafe( att, "attribute", nm, "T_showDetails"); + else if ( nm.endsWith( "show-NAC-details" )) + setNameSafe( att, "attribute", nm, "NAC_showDetails"); + else if ( nm.endsWith( "show-SK-details" )) + setNameSafe( att, "attribute", nm, "SK_showDetails"); + else if ( nm.endsWith( "show-SPM-details" )) + setNameSafe( att, "attribute", nm, "SPM_showDetails"); + else if ( nm.endsWith( "show-SP-details" )) + setNameSafe( att, "attribute", nm, "SP_showDetails"); + else if ( nm.endsWith( "_WilEffect" ) && att.get( "current" ) != "0" ) + Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: nm.slice(0, -10) + "_WilSelect"}, "Wil"); + }); // End for each attribute. + + a = findObjs({ _type: "macro" }); + if ( a ) { + for( let i = 0; i < a.length; ++i) { + let name = a[i].get( "name" ); + let e = name.lastIndexOf( String.fromCharCode( 0xFFFD ) ); + if( e !== -1 ) { + let act = a[i].get("action").trim(), + symbol; + if( act.endsWith( "Attrib" )) setNameSafe( a[i], "macro", name, "Attrib"); + else if( act.endsWith( "Target: Set" )) symbol = Earthdawn.constantIcon( "target" ); + else if( act.endsWith( "TargetsClear" )) symbol = Earthdawn.constantIcon( "target" ); + if( symbol ) + setNameSafe( a[i], "macro", name, symbol + name.slice( e +1 )); // Fix the name with the correct symbol + } } } + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion1p001() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion1p001() + + + + this.updateVersion1p0021 = function( cID ) { + 'use strict'; + let edc = this, + count = 0; + try { + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if ( att.get("name").endsWith( "_Mod-Type" )) { + if( att.get( "current" ) == "@{IP}" ) + att.set( "current", "(-1*@{IP})"); + else if( att.get( "current" ) == "@{Armor-IP}" ) + att.set( "current", "(-1*@{Armor-IP})"); + } + }); // End for each attribute. + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion1p0021() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion1p0021() + + + + this.updateVersion1p0022 = function( cID ) { + 'use strict'; + let edc = this, + count = 0; + try { + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if ( att.get("name").endsWith( "_Mod-Type" )) { + if( att.get( "current" ).search( /Armor-IP/ ) != -1) + att.set( "current", "@{Adjust-All-Tests-Total}+(-1*@{Armor-IP})"); + else if( att.get( "current" ).search( /\{IP\}/ ) != -1) + att.set( "current", "@{Adjust-All-Tests-Total}+(-1*@{IP})"); + } + }); // End for each attribute. + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion1p0022() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion1p0022() + + + + this.updateVersion1p0023 = function( cID, ed, charCount ) { + 'use strict'; + let edc = this, + count = 0; + try { + if( charCount === 0 ) { // If this is true then this is being called because a new API version has been detected (as opposed an old character sheet being imported) and this is the very first character. So do this once when new API is detected. + let macs = findObjs({ _type: "macro", visibleto: "all" }); // These will be deleted in the macro refresh below, but lets specifically target them for deletion just in case. + _.each( macs, function (macObj) { + let n = macObj.get( "name" ); + if( n.startsWith( Earthdawn.constantIcon( "Target" )) && n.endsWith( "r-Targets" )) + macObj.remove(); + }); + + if( ed.msg === undefined ) // fake up a playerID. + Earthdawn.pseudoMsg( ed ); + let edp = new ed.ParseObj( ed ); + edp.funcMisc( [ "funcMisc", "macroCreate", "Refresh" ] ); + } // do once when new API detected. + + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if ( att.get( "name" ).startsWith( "repeating_")) { + let nm = att.get( "name" ); + if ( nm.endsWith( "_CombatSlot") || (nm.endsWith( "_Contains") && (Earthdawn.repeatSection( 3, nm) === "SPM" ))) { + let nmn, + rowID = Earthdawn.repeatSection( 2, nm), + code = Earthdawn.repeatSection( 3, nm), + symbol = Earthdawn.constantIcon( code ), + cbs = att.get( "current" ), + lu = "Name"; + if( code === "SPM" ) { + cbs = "1"; + lu = "Contains"; + } + if ( code !== "SP" ) { // skip if it is SP, we don't do those token actions. + nmn = Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + lu, "" ); + Earthdawn.abilityRemove( cID, symbol + nmn ); + if( cbs == "1" ) + Earthdawn.abilityAdd( cID, symbol + nmn, "!edToken~ %{selected|" + Earthdawn.buildPre( code, rowID ) + "Roll}" ); + } + } // End Token Action maint. + } + }); // End for each attribute. + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion1p0023() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion1p0023() + + + + this.updateVersion2p001 = function( cID, ed, charCount ) { + 'use strict'; + let edc = this, + count = 0; + try { + if( charCount === 0 ) { // If this is true then this is being called because a new API version has been detected (as opposed an old character sheet being imported) and this is the very first character. So do this once when new API is detected. + let macs = findObjs({ _type: "macro", visibleto: "all" }); // These will be deleted in the macro refresh below, but lets specifically target them for deletion just in case. + _.each( macs, function (macObj) { + let n = macObj.get( "name" ); + if( n.startsWith( Earthdawn.constantIcon( "Target" )) && n.endsWith( "r-Targets" )) { + macObj.remove(); + ++count; + } + }); + + if( ed.msg === undefined ) // fake up a playerID. + Earthdawn.pseudoMsg( ed ); + let edp = new ed.ParseObj( ed ); + edp.funcMisc( [ "funcMisc", "macroCreate", "Refresh" ] ); + } // do once when new API detected. + + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + if ( att.get( "name" ).startsWith( "repeating_")) { + let nm = att.get( "name" ); + if ( nm.endsWith( "_CombatSlot") || (nm.endsWith( "_Contains") && (Earthdawn.repeatSection( 3, nm) === "SPM" ))) { + let nmn, + rowID = Earthdawn.repeatSection( 2, nm), + code = Earthdawn.repeatSection( 3, nm), + symbol = Earthdawn.constantIcon( code ), + cbs = att.get( "current" ), + lu = "Name"; + if( code === "SPM" ) { + cbs = "1"; + lu = "Contains"; + } + if ( code !== "SP" ) { // skip if it is SP, we don't do those token actions. + nmn = Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + lu, "" ); + Earthdawn.abilityRemove( cID, symbol + nmn ); + if( cbs == "1" ) + Earthdawn.abilityAdd( cID, symbol + nmn, "!edToken~ %{selected|" + Earthdawn.buildPre( code, rowID ) + "Roll}" ); + ++count; + } + } // End Token Action maint. + } + }); // End for each attribute. + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion1p0023() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion2p001() + + + + this.updateVersion3p000 = function( cID, ed, charCount ) { + 'use strict'; + let edc = this, + count = 0; + try { + let tabGood, tabBad; + function setDefault( att, maxdef ) { // This only sets a new max value if the old value was zero. + if( maxdef ) { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: cID, name: att }, 0, 0); + if( aobj.get( "max" ) == 0) + Earthdawn.setWithWorker( aobj, "max", maxdef ); + } + } + + let attributes = findObjs({ _type: "attribute", _characterid: cID }); + _.each( attributes, function (att) { + switch( att.get( "name" )) { + case "Hide-Spells-tab": + tabBad = att; break; + case "Hide-Spells-Tab": + tabGood = att; break; + case "Creature-Ambush": + setDefault("Creature-Ambushing", att.get( "current" )); + att.remove(); + break; + case "Creature-DiveCharge": + setDefault("Creature-DivingCharging", att.get( "current" )); + att.remove(); + break; + case "Flight": + setDefault("Fly", att.get( "current" )); + att.remove(); + break; + case "playerWho": + att.remove(); + break; + } + }); // End for each attribute. + // if Hide-Sheet-tab exists, make it Hide-Sheet-Tab. + if( tabGood && tabBad ) { // we have both, just delete the bad one. + tabBad.remove(); + ++count; + } else if ( tabBad ) { // We only have bad, so rename it to the good one. + tabBad.set( "name", "hide-Spells-Tab" ); + ++count; + } + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion3p000() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion3p000() + + + + // Note, before vUpdate is called, we will have already run macroCreate - Refreash + this.updateVersion3p330 = function( cID, ed, charCount ) { + 'use strict'; + let edc = this, // Debug: RepSecFix: forceNameRefresh + count = 0; + try { + if( ed.msg === undefined ) // fake up a playerID. + Earthdawn.pseudoMsg( ed ); + let edp = new ed.ParseObj( ed ); + edp.charID = cID; + count += edp.abilityRebuild( [ "abilityRebuild", "batchFix", "forceNameRefresh" ]); + return count; + } catch(err) { Earthdawn.errorLog( "ED.updateVersion3p330() cID=" + cID + " error caught: " + err, edc ); } + }; // end updateVersion3p330() + + + +/* +Step/Action Dice Table +1 D4-2 11 D10+D8 21 D20+2D8 31 2D20+D8+D6 +2 D4-1 12 2D10 22 D20+D10+D8 32 2D20+2D8 +3 D4 13 D12+D10 23 D20+2D10 33 2D20+D10+D8 +4 D6 14 2D12 24 D20+D12+D1 34 2D20+2D100 +5 D8 15 D12+2D6 25 D20+2D12 35 2D20+D12+D10 +6 D10 16 D12+D8+D6 26 D20+D12+2D 36 2D20+2D126 +7 D12 17 D12+2D8 27 D20+D12+D8 37 2D20+D12+2D6+D6 +8 2D6 18 D12+D10+D8 28 D20+D12+2D 38 2D20+D12+D8+D68 +9 D8+D6 19 D20+2D6 29 D20+D12+D1 39 2D20+D12+2D80+D8 +10 2D8 20 D20+D8+D6 30 2D20+2D6 40 2D20+D12+D10+D8 +*/ + // Takes an Earthdawn step number, and returns a string containing the dice to be rolled. + // If step is less than 1, returns an empty string. + this.StepToDice = function( stepNum ) { + 'use strict'; + try { + let dice = ""; + + if ( stepNum < 0 ) + stepNum = 0; + if ( stepNum < 8 ) { // The step numbers less than 8 don't follow the same pattern as the rest of the table and should just be set to the correct value. + switch( stepNum ) { + case 1: dice = ((state.Earthdawn.gED && state.Earthdawn.edition == 3) ? "{{1d6!-3}+d1}kh1+" : "{{1d4!-2}+d1}kh1+" ); break; // Roll a d4 minus something, but also roll a "d1" and keep only the highest one. + case 2: dice = ((state.Earthdawn.gED && state.Earthdawn.edition == 3) ? "{{1d6!-2}+d1}kh1+" : "{{1d4!-1}+d1}kh1+" ); break; + case 3: dice = ((state.Earthdawn.gED && state.Earthdawn.edition == 3) ? "{{1d6!-1}+d1}kh1+" : "d4!+" ); break; + case 4: dice = "d6!+"; break; + case 5: dice = "d8!+"; break; + case 6: dice = "d10!+"; break; + case 7: dice = "d12!+"; break; + } + } // end step 7 or less + else if( state.Earthdawn.gED && state.Earthdawn.edition == 3 ) { // Earthdawn 3rd edition. + let baseNum = stepNum - 6, + twelves = 0; + if( stepNum > 12 ) // Calculate the number of d12's we need to roll. + twelves = Math.floor( baseNum / 7); + baseNum = ( baseNum % 7 ) + 6; // We now have a number between 6 and 12. The chart repeats the same sequence, differing only in the number of d12's, which we have already calculated. + + switch( baseNum ) { + case 6: dice = "d10!+"; break; + case 7: ++twelves; break; + case 8: dice = "2d6!+"; break; + case 9: dice = "d8!+d6!+"; break; + case 10: dice = "2d8!+"; break; + case 11: dice = "d10!+d8!+"; break; + case 12: dice = "2d10!+"; break; + } + if( twelves > 0 ) + dice = ((twelves === 1) ? "" : twelves.toString()) + "d12!+" + dice; + } else { // This is Earthdawn 4th edition. + let baseNum = stepNum - 8; + if( stepNum > 18 ) // Calculate the number of d20's we need to roll. + dice = Math.floor( baseNum / 11).toString() + "d20!+"; + baseNum = ( baseNum % 11 ) + 8; // We now have a number between 8 and 18. The chart repeats the same sequence, differing only in the number of d2o's, which we have already calculated. + switch( baseNum ) { + case 8: dice += "2d6!+"; break; + case 9: dice += "d8!+d6!+"; break; + case 10: dice += "2d8!+"; break; + case 11: dice += "d10!+d8!+"; break; + case 12: dice += "2d10!+"; break; + case 13: dice += "d12!+d10!+"; break; + case 14: dice += "2d12!+"; break; + case 15: dice += "d12!+2d6!+"; break; + case 16: dice += "d12!+d8!+d6!+"; break; + case 17: dice += "d12!+2d8!+"; break; + case 18: dice += "d12!+d10!+d8!+"; break; + } + } // End 4th edition + return Earthdawn.safeString( dice ).slice(0,-1); // Trim off the trailing "+" + } catch(err) { Earthdawn.errorLog( "ED.StepToDice() error caught: " + err, this ); } + }; // End Earthdawn.EDclass.StepToDice() + + + + + // If a message includes any inline rolls, go through the message and replace the in-line roll markers with the roll results. + this.ReconstituteInlineRolls = function( origMsg ) { + 'use strict'; + try { + let msg = origMsg; + if( _.has( msg, "inlinerolls" )) { + msg.content = _.chain( msg.inlinerolls ) + .reduce( function( m, v, k){ + m['$[['+k+']]'] = v.results.total || 0; + return m; + },{}) + .reduce(function( m, v,k ){ + return m.replace( k, v ); + }, msg.content) + .value(); + } + return msg; + } catch(err) { Earthdawn.errorLog( "ED.ReconstituteInlineRolls() error caught: " + err, this ); } + }; + + + // edsdr - Earthdawn Step Dice Roller + this.StepDiceRoller = function() { + 'use strict'; + // looks for !edsdr and converts Earthdawn Step Numbers to Dice which are then sent back to the chat system to be rolled. + // Expected format: !edsdr~ (StepNum)~ (KarmaStep)~ (Reason)~ (Target Number) + // Everything other than the !edsdr~ tag and step number is optional. + // "!edsdr~ num~ karmastep (: Karma Control)~ reason" gets turned to "/roll (dice) (reason)" where dice includes the step and karma dice + // For Example: "!edsdr~ 11~ 4~ for Attack" Gets turned into "/r d10!+d8!+d6! for Attack" + // If (Target Number) is present, when the dice roller is finished, a callback routine will display the number of levels of success achieved. + // This is currently set up for 4th edition, but the code could be modified to report other edition result levels. + // + // This routine will also process the initial tag edsdrGM which will display the results only to the GM and the player who rolled the dice and + // edsdrHidden which will display the roll to the GM only. + // + // The following roll20 macro will generate a string that this will process. + // !edsdr~ ?{Step|0}~ ?{Karma Step|0} : karma control~ for ?{reason| no reason}~ Target Number + let edc = this; + try { + let MsgType, + newMsg, + rollMsg, + third = ""; + + if ( this.msgArray[ 0 ] === "!edsdrGM") { + rollMsg = "/gmroll "; + MsgType = Earthdawn.whoTo.gm | Earthdawn.whoTo.player; + newMsg = " PGM Rolls step "; // This is the base step number. + } + else if ( this.msgArray[ 0 ] === "!edsdrHidden") { + rollMsg = "/gmroll "; + MsgType = Earthdawn.whoTo.gm; + newMsg = " GmOnly Rolls step "; + } else { // public + rollMsg = "/r "; + MsgType = 0; + newMsg = " Rolls step "; + } + let step = Earthdawn.parseInt2( this.msgArray[1] ); + if( step < 1 ) { + this.chat( "Warning!!! Step Number " + step, Earthdawn.whoFrom.apiWarning ); + step = 1; + } + rollMsg += this.StepToDice( step ); + newMsg += step; + + if (this.msgArray.length > 2) { + let karmaControl = Earthdawn.getParam( this.msgArray[2], 2, ":"), + karmaDice; + if( karmaControl === "-1" ) + karmaDice = 0; + else if( Earthdawn.parseInt2( karmaControl, true ) > 0 ) + karmaDice = this.StepToDice( Earthdawn.parseInt2( karmaControl ) * 4 ); + else + karmaDice = this.StepToDice( Earthdawn.parseInt2( Earthdawn.getParam( this.msgArray[2], 1, ":" ))); // karma or bonus step number + if( karmaDice != "" ) { + newMsg = newMsg + " plus " + karmaDice.replace( /!|\+/g, ""); + rollMsg = rollMsg + "+" + karmaDice; + } + if (this.msgArray.length > 3) + third = " " + this.msgArray[ 3 ] + "."; // This is the "reason" or flavor text + } // End msgArray has at least two elements. + + if(( MsgType != (Earthdawn.whoTo.gm | Earthdawn.whoTo.player)) && (this.msgArray.length < 5 || Earthdawn.parseInt2( this.msgArray[ 4 ] ) < 1)) { + this.chat( rollMsg + newMsg + third, 0); + } else { // We have a target number, so have the results of the roll sent to a callback function to be processed. + sendChat("player|" + this.msg.playerid, rollMsg, function( ops ) { // This is a callback function that sendChat will callback as soon as it finishes rolling the dice. + 'use strict'; + // NOTE THAT THIS IS THE START OF A CALLBACK FUNCTION + // Standard. Tell what rolled, vague about how much missed by. + // Full: Tell exact roll result and how much made or missed by. + // Vague. Done tell roll result, but tell how much made or missed by. + let RollResult = JSON.parse(ops[0].content); +// let EchoMsg = "Rolling " + ops[0].origRoll.replace( /!/g, "") + ":"; + let EchoMsg = newMsg + " (" + ops[0].origRoll.replace( /!/g, "") + "):"; + if ( state.Earthdawn.style != Earthdawn.style.VagueRoll) + EchoMsg += " Rolled a " + RollResult.total + "."; + EchoMsg += third; + + if (edc.msgArray.length > 4 && Earthdawn.parseInt2( edc.msgArray[ 4 ] ) > 0) { + let result = RollResult.total - Earthdawn.parseInt2( edc.msgArray[4] ); + if( result < 0 ) { + EchoMsg += " FAILURE" + (( state.Earthdawn.style != Earthdawn.style.VagueSuccess ) ? " by " + Math.abs(result) + "!" : "!" ); + } else if ( result < 5 ) { + EchoMsg += " SUCCESS" + (( state.Earthdawn.style != Earthdawn.style.VagueSuccess ) ? " by " + Math.abs(result) + "." : "." ); + } else + EchoMsg += " SUCCESS" + (( state.Earthdawn.style != Earthdawn.style.VagueSuccess ) ? " by " + Math.abs(result) : "" ) + " (" + ( Math.floor(result / 5) ).toString() + " extra" + ((result < 10) ? "!)" : "es!)"); + } // End we have a target number + else if ( state.Earthdawn.style == Earthdawn.style.VagueRoll) + EchoMsg += " Rolled a " + RollResult.total + "."; + edc.chat( EchoMsg, MsgType); + }); // End of callback function + } // 4th element. + } catch(err) { Earthdawn.errorLog( "ED.StepDiceRoller() error caught: " + err, edc ); } + }; // End StepDiceRoller(); + + + + // edInit - Initiative + this.Initiative = function() { + 'use strict'; + // Expects an initiative step, rolls the initiative for each selected token and puts them in the turn order the selected token. + // There may also optionaly be a karma step. + // + // The following roll20 macro will generate a string that this will process. + // edInit~ ?{Initiative Step}~ ?{Karma Step | 0}~ for Initiative + let edc = this; + try { + let step = Earthdawn.parseInt2( this.msgArray[1] ), + rollMsg; + if ( step < 1 ) { + this.chat( "Illegal Initiative step of " + step, Earthdawn.whoFrom.apiWarning ); + step = 0; + rollMsg = "/r {{1d4!-2}+d1}kh1"; + } + else + rollMsg = "/r " + this.StepToDice( step ); + let newMsg = " rolled step " + step; + if (this.msgArray.length > 2) { + let karmaDice = this.StepToDice( Earthdawn.parseInt2( this.msgArray[2] ) ); // karma or bonus step number + if( karmaDice != "" ) { + newMsg = newMsg + " plus " + karmaDice.replace( /!|\+/g, ""); + rollMsg = rollMsg + "+" + karmaDice; + } } + + let Count = 0; + _.each(edc.msg.selected, function( sel ) { + let TokenObj = getObj( "graphic", sel._id ); + if (typeof TokenObj === 'undefined' ) + return; + + let TokenName = TokenObj.get( "name" ); + let CharObj = getObj( "character", TokenObj.get( "represents" )) || ""; + if (typeof CharObj === 'undefined') + return; + + if( TokenName.length < 1 ) + TokenName = CharObj.get( "name" ); + Count = Count + 1; + sendChat( "player|" + edc.msg.playerid, rollMsg + "~" + TokenName + "~" + sel._id, function( ops ) { + 'use strict'; // NOTE THAT THIS IS THE START OF A CALLBACK FUNCTION + let RollResult = JSON.parse(ops[0].content), + result = RollResult.total, + ResultArray = ops[0].origRoll.split( "~" ), + Reason = "for initiative"; + if (edc.msgArray.length > 3 ) + Reason = edc.msgArray[ 3 ]; + edc.chat( ResultArray[1] + newMsg + ": [" + ResultArray[0] +"] " + Reason +" and got " + result + "."); + let tt = Campaign().get( "turnorder" ); + let turnorder = (tt == "") ? [] : JSON.parse( tt ); + turnorder = _.reject(turnorder, function( toremove ){ return toremove.id === sel._id }); + turnorder.push({ id: sel._id, _pageid: TokenObj.get( "pageid" ), pr: result }); + turnorder.sort( function(a,b) { 'use strict'; return (b.pr - a.pr) }); + Campaign().set( "turnorder", JSON.stringify(turnorder)); + }); // End of callback function + }); // End for each selected token + if( Count == 0) + edc.chat( "Error! Need to have a token selected to Roll Initiative.", Earthdawn.whoFrom.apiWarning ); + } catch(err) { Earthdawn.errorLog( "EDInit error caught: " + err, edc ); } + }; // End EDInitiative(); + + + +// +// NOTE: Everything above this point (plus the last routine in this file) is what is needed to get the stepdice and initiative macros working. +// Everything below this point is used with the PARSE command and interacts with the character sheet. +// +// So if you are not using my Earthdawn character sheets, you can cut everything between this point and the similer note near the end of this file. +// + + + // This routine is a StatusTracker callback function that tells StatusTracker what type of token it is. PC, NPC, or Mook. + var callbackStAnnounceTurn = function( token ) { + 'use strict'; + let rep; + if (rep = token.get('represents')) + return (Earthdawn.getAttrBN(rep, "AnnounceTurn", "0" ) == "1"); + return; + }; // End callbackStAnnounceTurn; + + + + // This routine is a StatusTracker callback function that tells StatusTracker what type of token it is. PC, NPC, or Mook. + var callbackStTokenType = function( token ) { + 'use strict'; + let ret = "none"; + switch (Earthdawn.getAttrBN(token.get('represents'), "NPC", "1" )) { + case "-1": ret = "Object"; break; + case "0": ret = "PC"; break; + case "1": ret = "NPC"; break; + case "2": ret = "Mook"; break; + } + return ret; + }; // End callbackStTokenType; + + + + // This routine runs when a new character is added. + // It can be called from events "on character add" when one is created manually, or it can be called from WelcomePackage when it makes one. + // Note, it also seem to be triggered on character add, when a character is imported from the character vault (in which case it is not truly a new character!). + this.newCharacter = function( cID ) { + 'use strict'; + let edc = this; + try { + state.Earthdawn.newChars.push( cID ); // Add this character ID to the list of brand new characters. This is so that abilityRebuild will do something slightly different for brand new characters. + setTimeout(function() { // When a character is imported from the character vault, the recalc caused by edition has been observed to cause a race condition. Delay this processing long enough for the import to have been done. + try { + let npc = ( typeof WelcomePackage === 'undefined' ) ? Earthdawn.charType.pc : Earthdawn.charType.npc; + let plr = findObjs({ _type: "player", _online: true }); // If there is only one person on-line, that is the player. + if( plr && plr.length === 1 ) + npc = playerIsGM( plr[0].get( "_id")) ? Earthdawn.charType.npc : Earthdawn.charType.pc; + let CharObj = getObj( "character", cID); // See if we can put a default value in the player name. + if ( CharObj ) { + let lst = CharObj.get( "controlledby" ); + let arr = _.without( lst.split( "," ), "all" ); + if( arr && arr.length === 1 && arr[ 0 ] !== "" ) { // If there is only one person who can control the character, use their name. + let pObj = getObj( "player", arr[ 0 ]); + if( pObj ) + Earthdawn.SetDefaultAttribute( cID, "player-name", pObj.get( "_displayname" )); + npc = playerIsGM( arr[ 0 ] ) ? Earthdawn.charType.npc : Earthdawn.charType.pc; // character was created by welcome package, if for GM, make it an NPC else PC. + } } + + Earthdawn.setWW( "API", 1, cID ); + + // If a character was created by Welcome package for a Player, or if Welcome Package is not installed, default to PC. + // If a character was created by Welcome package for a GM, or if Welcome Package is installed, default to NPC. + Earthdawn.SetDefaultAttribute( cID, "NPC", npc ? npc : Earthdawn.charType.pc ); + Earthdawn.SetDefaultAttribute( cID, "RollType", (state.Earthdawn.defRolltype & (( npc === Earthdawn.charType.pc ) ? 0x04 : 0x01)) ? "/w gm" : " " ); + + if( Earthdawn.getAttrBN( cID, "edition", "4" ) != state.Earthdawn.edition ) + if( (Earthdawn.getAttrBN( cID, "edition", "99" ) == "") && state.Earthdawn.edition ) + Earthdawn.setWW( "edition", state.Earthdawn.edition, cID, "4" ) + else + edc.chat( "Error, settings mismatch. New character had 'edition' set to '" + Earthdawn.getAttrBN( cID, "edition", "99" ) + + "'. While API state variable has 'edition' set to '" + state.Earthdawn.edition + + "'. Go to Campaign settings Default Sheet Settings and set 'What Game/Edition is your campaign' to correct value to ensure that all future sheets will be " + + "set correctly, then go to any character sheets 'Special Functions - GM: Special Commands' and set 'Change Edition' to the correct value " + + "as well to update the API and all existing sheets.", Earthdawn.whoFrom.apiWarning ); + + setTimeout(function() { // 10 seconds seems to be enough for all of the above, but not the ability rebuild. Give it more time. + try { + let ind = state.Earthdawn.newChars.indexOf( cID ); // remove this character from the list of brand new characters. + if( ind > -1 ) + state.Earthdawn.newChars.splice( ind, 1 ); + } catch(err) { Earthdawn.errorLog( "ED.newCharacter setTimeout2() error caught: " + err, edc ); } + }, 20000); // end delay 20 more seconds. + } catch(err) { Earthdawn.errorLog( "ED.newCharacter setTimeout() error caught: " + err, edc ); } + }, 10000); // end delay 10 seconds. + } catch(err) { Earthdawn.errorLog( "newCharacter error caught: " + err, edc ); } + }; // End newCharacter; + + + + // This routine is a callback that WelcomePackage runs when a new character is added. + var callbackWelcomePackage = function( character ) { + 'use strict'; + let ED = new Earthdawn.EDclass(); + ED.newCharacter( character.get( "_id" ) ); + return; + }; // End callbackWelomePackage; + + + + + // Define the ParseObj Class + // By making a ParseObj we can insure that each command msg has it's own instance and don't have to worry about global variables being overwritten. + this.ParseObj = function( edc ) { + 'use strict'; + // Parameters + this.edClass = edc; // This is a pointer back to the class that has created this object. + this.bFlags = 0; // See Earthdawn.flagsArmor, .flagsTarget, and .flags for description of what is stored here. + this.charID = undefined; // Character ID associated with this action, additional information is stored in tokenInfo. + this.doLater = ""; // This is a command we are saving to do immediately before the Roll() + this.indexMsg = 0; // The command is a tilde (~) segmented list. As we parse this message, this is the index that points to the current segment being parsed. + this.indexTarget = undefined; // Index to targetIDs. + this.indexToken = undefined; // Index to tokenIDs. +// this.SWflag = ""; // This holds commands that will later be sent to the sheetworker. Newline delimited list of command lines that start with a command and a comma. + this.targetIDs = []; // array of IDs of targets. + this.tokenIDs = []; // array of IDs of tokens we are processing with this command. + this.rollQueue = []; // Array to hold information on each roll the system wants to make when multiple rolls are launched at the same time. Right now it only holds this.misc in the state wanted. + this.tokenAction = false; // If this is set to true, then we were called from a Token Action. If false, from a character sheet button. + // Note that this controls how many of the commands behave. For example ForEach behaves differently if called from a Token Action. + this.tokenInfo = undefined; // { type: "token" or "character", name: (name), tokenObj: (API token object), characterObj: (API character object) } + this.uncloned = this; // ForEach makes a copy of this class that each loop can modify at will. If you want to make changes or check values in the original, use this.uncloned. + // eachTargets is not defined here, but is stored at this level. + this.misc = {}; // An object to store miscellaneous values that don't really need a dedicated space on this top level. It starts out empty, but many routines store various stuff here. + // Among the things stored here in .misc are: + // bonusStep: This holds bonus steps to be added to a step dice roll. + // bonusDice: This holds bonus dice to be added to a step dice roll. It is held as a string ready to be appended to a roll query. + // charIDsUnique: When MarkerSet is called to toggle status markers, store unique character ids between iterations of each token. + // karmaNum: This holds karma steps to be added to a step dice roll. + // karmaDice: This holds karma dice to be added to a step dice roll. It is held as a string ready to be appended to a roll query. + // reason: Text that is sent back as part of the result message. + // result: Result of the roll is stored here. Before the roll, modifiers to the result can be stored here. + // rollWhoSee: If this action has a specific customizable roll type (whether roll is public, player/gm, or GM-only), the string describing it is stored here. + // step: This is the step number to be rolled for the talent or skill being used. Set with Value. + // strain: How much strain was taken this action. + // targetName: Name of the current target token. + // targetNum: If this action involves a target number, it is stored here. + // targetNum2: On an Action command when the target is Riposte, this stores the 2nd target number. + // There are also a number of values that are prepared in the Action, Roll or other routines to be outputted in the RollFormat routine. + // These include: + // headcolor, subheader, displayMsg, successMsg, failMsg, endNote, endNoteSucc, endNoteFail, targetName, targettype, etc. + + + + +/* + // ParseObj.addSWflag() This routine builds a list of commands for the sheetworkers to process. + // The list is newline delimited. + // Each item on the list has a command, a comma, and some data for the command. Each command has its own requirements for format. + // cmd tells the sheetworkers what exact type of communication this is. + // line is the details needed to process this command. + // Trigger, (attribute name to trigger) (: colon) (data to be written to activate the trigger). (newline). + // + // Note: Code obsolete after Feb 2023 + this.addSWflag = function( cmd, line ) { + try { + this.SWflag += cmd + "," + line + "\n"; + } catch(err) { Earthdawn.errorLog( "ED.addSWflag() error caught: " + err, this ); } + } // end addSWflag() + + + // ParseObj.sendSWflag() + // if SWflag is not blank, send it to the sheet workers. + // No arguments, if there is a msg, it will already be stored in SWflag. + // + // Note: code obsolete after Feb 2023. replaced by toSheet + this.sendSWflag = function() { + try { + if( this.SWflag ) + if( this.charID ) { +//log(this.SWflag); + this.setWW( "SWflag", this.SWflag.trim()) + } else + Earthdawn.errorLog( "ED.checkSWflag error, no charID found! SWflag was " + this.SWflag, this ); + } catch(err) { Earthdawn.errorLog( "ED.checkSWflag() error caught: " + err, this ); } + } // end sendSWflag() +*/ + + + + // ParseObj.toSheet() + // Send a message to a sheetworker asking it to do something. + // + // cmd tells the sheetworkers what exact type of communication this is. + // line is the details needed to process this command. + // Trigger, (attribute name to trigger) (: colon) (data to be written to activate the trigger). + this.toSheet = function( cmd, line ) { + try { + if( this.charID ) { + let t = cmd + "," + line, + pre = Earthdawn.buildPre( "MSG", Earthdawn.generateRowID() ); + if( state.Earthdawn.logMsg ) log( "toSheet ( " + pre + " ): " + t); + createObj( "attribute", { _characterid: this.charID, name: pre + "TimeStamp", current: (new Date()).toString() }); + createObj( "attribute", { _characterid: this.charID, name: pre + "toSheetworker", current: t }); + } else + Earthdawn.errorLog( "ED.toSheet error, no charID for this operation: " + t ); + } catch(err) { Earthdawn.errorLog( "ED.toSheet( " + t + " ) error caught: " + err, this ); } + } // end toSheet() + + + // ParseObj.Action() + // We are passed an action to take. Lookup the necessary values from the appropriate character sheet(s) and roll the correct dice. + // ssa[1]: Action - T, SK, SKA, SKK, SKL, WPN. + // ssa[2]: RowID + // ssa[3]: Mods + // value="!Earthdawn~ charID: @{character_id}~ foreach: sct: ust~ Target: @{T_Target}~ Action: T: @(T_RowID): ?{Modification|0}" + this.Action = function( ssa ) { + 'use strict'; + try { + if( ssa.length < 3 ) { + this.chat( "Error! Action() parameters not correctly formed. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + return; + } + if( ssa[ 2 ].length < 1 ) { + this.chat( "Error! Action() not passed RowID. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + return; + } + if( this.tokenInfo === undefined ) + if( this.charID === undefined ) { + this.chat( "Error! tokenInfo and charID undefined in Action() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } else { + let CharObj = getObj("character", this.charID); + if (typeof CharObj != 'undefined') + this.tokenInfo = { type: "character", name: CharObj.get("name"), characterObj: CharObj }; // All we have is character information. + else { + this.chat( "Error! Invalid charID in Action() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } } + + let step = 0, + stepAttrib = "Rank", + pre = Earthdawn.buildPre( ssa[ 1 ], ssa[ 2 ] ), + def_attr, // This is the default attribute if it can't read one from the sheet. + modtype, + special, + armortype; + ssa[ 1 ] = Earthdawn.safeString( ssa[ 1 ] ).toLowerCase(); + switch ( ssa[ 1 ] ) { + case "sk": { + let cls = Earthdawn.getAttrBN( this.charID, pre + "Class", "General"); + if( cls !== "General" ) + this.misc[ "skillClass" ] = cls; + } + case "t": + case "nac": { + this.misc[ "result" ] = (this.misc[ "result" ] || 0) + this.getValue( pre + "Result-Mods"); + let fx = Earthdawn.getAttrBN( this.charID, pre + "FX", ""); + if( fx ) + this.misc[ "FX" ] = fx; + stepAttrib = "Effective-Rank"; + if( ssa[ 1 ] === "skc") + def_attr = "Cha"; + else + def_attr = "0"; + modtype = Earthdawn.getAttrBN( this.charID, pre + "Mod-Type", "Action"); + armortype = Earthdawn.getAttrBN( this.charID, pre + "ArmorType", "PA"); + this.misc[ "displayMsg" ] = Earthdawn.getAttrBN( this.charID, pre + "DisplayText", ""); + this.misc[ "successMsg" ] = Earthdawn.getAttrBN( this.charID, pre + "SuccessText", ""); + this.misc[ "failMsg" ] = Earthdawn.getAttrBN( this.charID, pre + "FailText", ""); + if(Earthdawn.getAttrBN( this.charID, pre + "Notes", "")!=="") + this.misc[ "endNote" ] = "Description "+ Earthdawn.texttip("(Hover)",Earthdawn.getAttrBN( this.charID, pre + "Notes", "").replace( /\n/g, Earthdawn.constantIcon( "cr" ))); + if( modtype != "(0)" && modtype != "NoRoll" ) + this.doLater += "~Karma: " + pre + "Karma"; + this.strainAdd( pre ); // This is the strain for the base Talent. + if( modtype === "Attack" || modtype === "Attack CC" ) { + let aa = this.getValue( "combatOption-AggressiveAttack"); + if( aa != 0 ) + this.strainAdd( "Aggressive Attack", aa * Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Strain", "1", true)); + let cs = this.getValue( "combatOption-CalledShot"); + if( cs != 0 ) + this.strainAdd( "Called Shot", cs); + } + if( Earthdawn.getAttrBN( this.charID, pre + "Action", "Standard") === "Standard" ) { + let sm = this.getValue( "combatOption-SplitMovement"); + if( sm != 0 ) + this.strainAdd( "Split Movement", sm ); + } + special = Earthdawn.getAttrBN( this.charID, pre + "Special", "None"); + if( special !== "None" && special !== "" && special !== ", ," ) + this.misc[ "Special" ] = special; + let kw = Earthdawn.keywordCheck( special, "Recovery", "Recovery-Woodskin", "Initiative", "Knockdown" ); + if( kw & 0x02 ) + this.misc[ "Recovery-WoodSkin" ] = true; + if( (kw & 0x01) || (kw & 0x02)) { + this.bFlags |= Earthdawn.flags.Recovery; + if (Earthdawn.getAttrBN( this.charID, "NPC", "1") != Earthdawn.charType.mook) { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Recovery-Tests" }, 0, 2); + if( (aobj.get( "current" ) || 0) <= 0) { + this.chat( this.tokenInfo.name + " does not have a Recovery Test to spend.", Earthdawn.whoFrom.apiWarning ); + return; + } else + Earthdawn.setWithWorker( aobj, "current", Earthdawn.parseInt2( aobj.get( "current" )) -1 ); + } } + + if(( kw & 0x01) | ( kw & 0x02)) this.misc[ "headcolor" ] = "recovery"; // Recovery or Recovery-Woodskin + else if( kw & 0x04 ) this.misc[ "headcolor" ] = "initrep"; // Initiative + else if( kw & 0x08 ) this.misc[ "headcolor" ] = "knockdown"; // Knockdown + else if( modtype === "Action" ) this.misc[ "headcolor" ] = "action"; + else if( modtype === "Effect" ) this.misc[ "headcolor" ] = "effect"; + else if( modtype === "Attack" ) this.misc[ "headcolor" ] = "attack"; + else if( modtype === "Attack CC" ) this.misc[ "headcolor" ] = "attackcc"; + else if( modtype === "Damage" ) this.misc[ "headcolor" ] = "damage"; + else if( modtype === "Damage Poison" ) this.misc[ "headcolor" ] = "damagepoison"; + else if( modtype === "Damage CC" ) this.misc[ "headcolor" ] = "damagecc"; + else if( modtype === "JumpUp" ) this.misc[ "headcolor" ] = "jumpup"; + else this.misc[ "headcolor" ] = "none"; + + this.misc[ "rollWhoSee" ] = pre + "RollType"; + let tType = Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, pre + "Target", "None")); + if( Earthdawn.getAttrBN( this.charID, "condition-Blindsided", "0") === "1" && tType.startsWith( "Ask:" ) && tType.slice( 6,7) === "D" ) + this.chat( "Warning! Character " + this.tokenInfo.name + " is Blindsided. Can he take this action?", Earthdawn.whoFrom.apiWarning ); + + + if( tType && tType !== "None" ) + if( tType.startsWith( "Ask" )) + this.misc[ "targettype" ] = tType.substring( 0, tType.lastIndexOf( ":" )); + else if( tType.slice( 1, 3) === "D1") // PD1, MD1, and SD1, go to just the first two characters. + this.misc[ "targettype" ] = tType.slice( 0, 2); + else if( tType.startsWith( "Riposte" )) + this.misc[ "targettype" ] = "Riposte"; + else + this.misc[ "targettype" ] = tType; + + if( Earthdawn.getAttrBN( this.charID, pre + "ActnEfct", "1" ) === "1") { // 1 is action, -1 is effect, 0 is no roll. + if( Earthdawn.getAttrBN( this.charID, "combatOption-DefensiveStance", "0" ) === "1" ) { + let x = Earthdawn.getAttrBN( this.charID, pre + "Defensive", "0", true ); + if ( [ 1, 2, 3, 4, 5, 6, 7 ].includes( x )) { + step -= Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Penalty", "-3", true ); // Since this talent is defensive, we need to add this value that is about to be subtracted with the standard modifiers, back in. + this.misc[ "Defensive" ] = x; // Since step was modified for being defensive, tell people. + } + if ( [ 1, 2, 4, 5 ].includes( x )) + step += Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Bonus", "3", true ); // This Talent actually gets a bonus for being in defensive stance. + } + if( Earthdawn.getAttrBN( this.charID, "combatOption-AggressiveAttack", "0" ) === "1" ) { + let x = Earthdawn.getAttrBN( this.charID, pre + "Defensive", "0", true ); + if( [ 1, 2, 3 ].includes( x )) { + step += Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Penalty", "-3", true ); // Since this talent is defensive, it gets a penalty for being in aggressive stance. + this.misc[ "Aggressive" ] = x; // Since step was modified for being defensive, tell people. + } } + + if( Earthdawn.getAttrBN( this.charID, "condition-KnockedDown", "0" ) === "1" ) + if( Earthdawn.getAttrBN( this.charID, pre + "Resistance", "0" ) === "1" ) { + step += 3; + this.misc[ "Resistance" ] = true; // Resistance is depreciated 8/22 + } } + if( Earthdawn.getAttrBN( this.charID, pre + "MoveBased", "0" ) == "1" ) { + let tstep = Earthdawn.getAttrBN( this.charID, "condition-ImpairedMovement", "0" ); + if( tstep > 0 ) { + step -= tstep; + this.misc[ "MoveBased" ] = (tstep == 2) ? "Partial" : "Full"; + } } + if( Earthdawn.getAttrBN( this.charID, pre + "VisionBased", "0" ) == "1" ) { // VisionBased depreciated 8/22 + let tstep = Earthdawn.getAttrBN( this.charID, "condition-Darkness", "0" ); + if( tstep > 0 ) { + step -= tstep; + this.misc[ "VisionBased" ] = (tstep == 2) ? "Partial" : "Full"; + } } + this.misc[ "sayTotalSuccess" ] = Earthdawn.getAttrBN( this.charID, pre + "sayTotalSuccess", "0" ) == "1"; + } break; + case "ska": + case "skc": + def_attr = "Cha"; + this.misc[ "rollWhoSee" ] = pre + "RollType"; + this.misc[ "headcolor" ] = "action"; + break; + case "skk": + this.doLater += "~Karma: kcdef: -1: " + pre + "Karma"; + def_attr = "Per"; + this.misc[ "rollWhoSee" ] = pre + "RollType"; + this.misc[ "headcolor" ] = "action"; + break; + case "wpn": + stepAttrib = "Effective-Rank"; + this.strainAdd( pre ); + this.doLater += "~Karma: " + pre + "Karma"; + switch (Earthdawn.getAttrBN( this.charID, pre + "CloseCombat", "1" )) { + case "-1": case "-2": // Missile, Thrown + modtype = "Damage"; + def_attr = "Str"; + this.misc[ "headcolor" ] = "damage"; + break; + case "-3": case "-4": // Firearm, Heavy. + modtype = "Damage Firearm"; + def_attr = "0"; + this.misc[ "headcolor" ] = "damage"; + break; + default: // 1 Melee, 2 Unarmed, 3 Attached. + modtype = "Damage CC"; + def_attr = "Str"; + this.misc[ "headcolor" ] = "damagecc"; + } +/* if (state.Earthdawn.gED) + modtype = ((Earthdawn.getAttrBN( this.charID, pre + "CloseCombat", "1", true ) < 0) ? "Damage": "Damage CC" ); + else { // WPN_Type exists in 1879 only + let typ = Earthdawn.getAttrBN( this.charID, pre + "Type", "0" ); + switch (typ) { + case "0": case "1" case "2": + modtype = "Damage CC"; + break; + case "2": case "3": + modtype = "Damage"; + break; + case "4": case "5": + modtype = "Damage Firearm"; + def_attr = "0"; + break; + } } + this.misc[ "headcolor" ] = ((Earthdawn.getAttrBN( this.charID, pre + "CloseCombat", "1" ) == "1") ? "damagecc": "damage" );; +*/ + armortype = "PA"; + break; + default: + this.chat( "Error! Action() parameter ssa[1] not 'T', 'NAC', 'SK', 'SKA', 'SKK', or 'WPN'. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + } // end case ssa[1] + + if( modtype === undefined ) + modtype = "Action"; + else if (modtype.startsWith( "Attack" )) + this.Bonus( [ 0, "Adjust-Attacks-Bonus" ] ); + else if (modtype.startsWith( "Damage" ) && modtype !== "Damage Poison" ) { + this.Bonus( [ 0, "Adjust-Damage-Bonus" ] ); + if( ssa[ 1 ].toUpperCase() === "WPN" ) + this.Bonus([ 0, pre + "BonusDice" ]); + } + this.misc[ "ModType" ] = modtype; + this.misc[ "rsPrefix" ] = pre; + + if( armortype !== undefined && armortype !== "N/A" && modtype.startsWith( "Damage" )) + switch ( Earthdawn.safeString( armortype ).trim().toLowerCase() ) { + case "pa": this.bFlags |= Earthdawn.flagsArmor.PA; break; + case "ma": this.bFlags |= Earthdawn.flagsArmor.MA; break; + case "pa-nat": this.bFlags |= Earthdawn.flagsArmor.PA | Earthdawn.flagsArmor.Natural; break; + case "ma-nat": this.bFlags |= Earthdawn.flagsArmor.MA | Earthdawn.flagsArmor.Natural; break; + case "na": + case "noarmor": + case "none": this.bFlags |= Earthdawn.flagsArmor.None; break; + case "unknown": + case "unk": this.bFlags |= Earthdawn.flagsArmor.Unknown; break; + } + + // First we want to know what attribute this action uses, and what it's value is. + let attr, modtypevalue = 0; + if( ssa[1] !== "skk" && ssa[1] !== "skac" && ssa[1] !== "skc" && ssa[1] !== "wpn" ) + attr = Earthdawn.getAttrBN( this.charID, pre + "Attribute", undefined); + if( attr === undefined ) + attr = def_attr; + if( attr != "0" && attr !== "" && attr !== undefined) { // There is an attribute other than "None". Find it's value. + step += this.getValue( attr + "-Step"); + step += this.getValue( attr + "-Mods"); + } + + + if( modtype == "JumpUp" + || modtype.search( /Armor/ ) != -1 // Armor and "@{Adjust-All-Tests-Total}+(-1*@{Armor-IP})" are obsolete with V3.1 + ) // JumpUp has replaced Armor IP, and has full IP applying + modtypevalue = this.getValue( "Adjust-All-Tests-Total" ) + + (Earthdawn.getAttrBN( this.charID, "condition-KnockedDown", "0" ) == "1" ? 3 : 0) - this.getValue( "IP" ); + else if( modtype == "Init" ) // Init calculation has been modified, so the Misc-Adjust was not necessary anymore because included in the Attribute + modtypevalue = this.getValue( "Initiative-Mod-Auto"); + else if (modtype == "0" || modtype == "(0)" || modtype == "NoRoll" ) + modtypevalue = 0; + else if( modtype == "Action" ) + modtypevalue = this.getValue( "Adjust-All-Tests-Total" ); + else if( modtype == "Effect" ) + modtypevalue = this.getValue( "Adjust-Effect-Tests-Total" ); + else { + let postfix = modtype.endsWith( "CC" ) ? "-CC" : ""; + if( modtype.search( /Attack/ ) != -1 ) + modtypevalue = this.getValue( "Adjust-Attacks-Total" + postfix ); + else if( modtype === "Damage Poison" ) // Damage Poison is NOT adjusted by Adjust-Damage-Total + modtypevalue = 0; + else if( modtype.search( /Damage/ ) != -1 ) + modtypevalue = this.getValue( "Adjust-Damage-Total" + postfix ); + else modtypevalue = 0; + } + + let actType = Earthdawn.getAttrBN( this.charID, pre + "Action", (ssa[ 1 ] === "wpn" ) ? "Simple" : "Standard" ); + if( actType !== "NA" ) + this.misc[ "actType" ] = actType; + this.misc[ "effRank" ] = this.getValue( pre + stepAttrib); + this.misc[ "step" ] = (this.misc[ "step" ] || 0) + step + modtypevalue + this.misc[ "effRank" ] + this.getValue( pre + "Mods" ) - this.mookWounds(); +//log("Step Calculation " + " thistep " +(this.misc[ "step" ] || 0) + " step " +step + " modtype " +modtypevalue + " rank " +this.getValue( pre + stepAttrib) + " mods " +this.getValue( pre + "Mods" )); + this.misc[ "ModValue" ] = modtypevalue; + this.misc[ "rollName" ] = Earthdawn.getAttrBN( this.charID, pre + "Name", "").trim(); + this.misc[ "reason" ] = this.misc[ "rollName" ] + ((ssa[ 1 ] === "wpn") ? " damage" : ""); + let newssa = ssa.splice( 2); + + if ( special != undefined && special == "Initiative" ) { + if( Earthdawn.getAttrBN( this.charID, "Creature-Ambushing", "0" ) == "1" ) + this.misc[ "step" ] += Earthdawn.getAttrBN( this.charID, "Creature-Ambushing_max", "0", true ); + newssa[ 0 ] = "Init"; + this.rollPre( newssa ); + + } else if ( modtype == "(0)" || modtype == "NoRoll" ) { + this.doNow(); + this.rollFormat( this.WhoSendTo()); + } else { + newssa[ 0 ] = "Roll"; + this.ForEachHit( newssa ); + } + } catch(err) { Earthdawn.errorLog( "ED.Action() error caught: " + err, this ); } + } // End ParseObj.Action() + + + + + // Delete and rebuild all abilities for this character in order to make sure they are all correct. + // On every token drop, check to see if all token actions that should exist do, and that none that should not exist don't. Ask user what to do. + // ssa[0] addGraphic: from on add graphic (graphic drop event). Test to see if Token Actions are up to date. + // ssa[0] abilityRebuild: addGraphic event might present user with menu choices that they can take. + // ssa[1] + // forceTest: FixRepSec forces this routine to run by user command even when user does not want it automatically run on token drop. + // Menu: test, and present the user with a menu of possible fixes. + // fixAll: Usually called from Menu, fix everything. + // addOne ssa[2]: Usually called from menu. Fix one named ability. + // removeOne ssa[2]: Usually called from menu, remove one named ability. + // Never: Usually called from menu, Never test this characters abilities unless specifically told to by running repsecfix. + // batch: This is being run by RepSecBatch, and ALL characters are calling this in quick succession. Mostly same as forceTest but with minimum verbiage. + // batchFix: Same as Batch and Fixall. Just quietly fix everything that can be fixed. + // Standard: This can be passed, but we don't ever use it, it is just the opposite of forceNameRefresh. On standard we only refresh abilities is the ability text is changed, a different name does not matter. + // forceNameRefresh: We will refresh an ability if ether the name or text is different. + this.abilityRebuild = function ( ssa ) { + 'use strict'; + let po = this; + try { + let cID = this.charID, + problems = 0, + txt = "", + oldTlength = 0, + addGraphic = ssa.includes( "addGraphic" ), + batch = ssa.includes( "batch" ), // batch is GM command to verify / fix all characters, asking what to do. + batchFix = ssa.includes( "batchFix" ), // batchFix is GM command to verify / fix all characters, automatically fixing everything fixable without asking. + forceTest = ssa.includes( "forceTest" ), + refreshNames = ssa.includes( "refreshNames" ), // force a refresh of the ability names (otherwise it does not matter if the names are different). + silent = ssa.includes( "silent" ), +// standard = ssa.includes( "Standard" ), // Present if forceNameRefresh is not true, but no need to use it. In Standard it only updates if the ability text changes + forceNameRefresh = ssa.includes( "forceNameRefresh" ), // Also updates if the ability name has changed. + newChar = (state.Earthdawn.newChars.indexOf( cID ) != -1); // This character was created within the last few seconds. + + // what == -1 do a fix all on everything. + // what == 0 just basic test, If problems are found then we will give them a menu button and suggest they run it. + // what == 1 test and set the details into txt for the menu. Menu will give options for fixall, or for each specific problem found. + function buildTA( what) { // go through and get information needed to figure out what the token actions should be. + 'use strict'; // This routine also does the actual destruction and rebuilding of the TA depending upon the value of what. + let oldab = [], // List of token actions that DO exist. + newab = [], // List we build of token actions we think ought to exist. + spmrowid = new Map(), + contains = new Map(), + candidate = []; // List of attributes ending in _name that might ought to have token actions. + + function addit( nm, ab ) { // Make this token action. + 'use strict'; + if( what === -1 ) // If fixing all, then just recreate all candidates. + Earthdawn.abilityAdd( cID, nm, ab); + else + newab.push( { name: nm ,action: ab }); + } // end addit() + + // first look at the existing Token Actions. Remove them or put them into oldab. + _.each( findObjs({ _type: "ability", _characterid: cID}), function( abObj ) { + let act = abObj.get( "action" ), + nm = abObj.get( "name" ); + if( act.startsWith( "!Earthdawn" ) || act.startsWith( "!edToken" ) || act.startsWith( "!edsdr" ) || act.startsWith( "!edInit" ) ) + if( what === -1 || nm === "Untitled" ) // If fixing everything, just get rid of all existing token actions and build from scratch. + abObj.remove(); + else + oldab.push({ id: abObj.get( "_id" ), name: nm, action: act }); + }); + + // second, look at all talents, knacks, etc. that ought to have a token action. Put them into candidate or one of the specialty lists. + _.each( findObjs({ _type: "attribute", _characterid: cID }), function( att ) { + let sa = att.get( "name" ) || ""; + if( sa.startsWith( "repeating_" )) { // This loop only deals with repitems + if( sa.endsWith( "T_Special" ) && Earthdawn.keywordCheck( att.get( "current" ), "CorruptKarma" )) { + addit( Earthdawn.constantIcon( "Target" ) + "Activate-Corrupt-Karma", + "!edToken~ SetToken: @{target|to have Karma Corrupted|token_id}~ Misc: CorruptKarma: ?{How many karma to corrupt|1}" ); + } else if( sa.endsWith( "_spmRowID" )) { + if( att.get( "current" ).length > 2 ) + spmrowid.set( Earthdawn.repeatSection( 2, sa ), att.get( "current" )); + } else if( sa.endsWith( "_SPM_Contains" )) { + contains.set( Earthdawn.repeatSection( 2, sa ), att.get( "current" )); + } else if( sa.endsWith( "_Name" )) { //Dealing with T, NAC, SK, SKA, SKK, SPN and SP/SPM + let code = Earthdawn.repeatSection( 3, sa ); + if ((code === "T" || code === "NAC" || code === "SK" || code === "SKA" || code === "SKK" || code === "WPN" || code === "SP")) // All except SPM + candidate.push( att ); + } } + }); + + // Now we actually process the candidates, using the list of SPM rowid's and the spell contains list we got in the previous loop. Among other things this builds newab + _.each( candidate, function( att ) { + let sa = att.get( "name" ), + code = Earthdawn.repeatSection( 3, sa); + if( getAttrByName( cID, Earthdawn.buildPre( sa ) + "CombatSlot" ) == "1" ) { + if( code === "SP" ) { + let spmID = spmrowid.get( Earthdawn.repeatSection( 2, sa ) ); + if( spmID && spmID.length > 1 ) { + let cont = contains.get( spmID ); + if( cont && cont.length > 1 ) + addit( Earthdawn.constantIcon( "Spell" ) + cont, "!edToken~ %{selected|" + Earthdawn.buildPre("SPM", spmID) + "Roll}" ); + else + Earthdawn.errorLog( "abilityRebuild:BuildTA: " + sa + " could not find out what: " + spmID + " contains.", po ); + } + } else // All except SP/SPM + addit( Earthdawn.constantIcon( code ) + att.get("current"), "!edToken~ %{selected|" + Earthdawn.buildPre( code, Earthdawn.repeatSection( 2, sa ) ) + "Roll}" ); + } + }); + + //Attack, Charge, Ambush, Grimoire, Spell, and Quester DP. + if( Earthdawn.getAttrBN( cID, "NPC", "1", true) > 0 && Earthdawn.getAttrBN( cID, "Attack-Rank", 0) != 0) // NPC or Mook and have a generic attack value. + addit( Earthdawn.constantIcon( "power" ) + "Attack", "!edToken~ %{selected|Attack}"); + if( Earthdawn.getAttrBN( cID, "Creature-DivingCharging_max", "0" ) != "0" ) + addit( Earthdawn.constantIcon( "power" ) + "Charge", "!edToken~ ForEach~ marker: divingcharging: t") + if( Earthdawn.getAttrBN( cID, "Creature-Ambushing_max", "0" ) != "0" ) + addit( Earthdawn.constantIcon( "power" ) + "Ambush", "!edToken~ ForEach~ marker: ambushing: t"); + if( Earthdawn.getAttrBN( cID, "Hide-Spells-Tab", "1" ) != "1" ) { + addit( Earthdawn.constantIcon( "Spell" ) + "-Grimoire", "!edToken~ ChatMenu: Grimoire"); + addit( Earthdawn.constantIcon( "Spell" ) + "-Spells", "!edToken~ ChatMenu: Spells"); + } + if( Earthdawn.getAttrBN( cID, "Questor", "None" ) != "None" ) { + addit( Earthdawn.constantIcon( "karma" ) + "DP-Roll", "!edToken~ %{selected|DevotionOnly}" ); + addit( Earthdawn.constantIcon( "karma" ) + "DP-T", "!edToken~ !Earthdawn~ ForEach~ marker: devpnt: t" ); + } + + // We now have a list of old abilities and new abilities. compare them. + // For each thing that is not up to date, give button to fix just that. + let oldmap, newmap; + if( forceNameRefresh ) { // oldmap and newmap are the names and the actions joined. If ether are different it will trigger a rebuild. + oldmap = _.map( oldab, function( o ) { return o.name + Earthdawn.constantAlt( "pipealt" ) + o.action; }); + newmap = _.map( newab, function( o ) { return o.name + Earthdawn.constantAlt( "pipealt" ) + o.action; }); + } else { // oldmap and newmap are JUST the actions, not the id or name. We want to test only the actions and don't care if the names are different. + oldmap = _.map( oldab, function( o ) { return o.action; }); + newmap = _.map( newab, function( o ) { return o.action; }); + } + + let extra = _.difference( oldmap, newmap ); // For each entry in extra (which is a list of things to be removed), find the record in oldab (which has the ID, which extra does not) and remove the ability. + if( extra.length > 0 ) { + txt += "Unknown Token Action" + ((extra.length > 1) ? "s" : "") + " found. " + ((what == 1) ? "Do you want to remove: " : "Removing: " ); + _.each( extra, function( a ) { + ++problems; + let old = _.filter( oldab, function ( ab ) { return (forceNameRefresh ? ab.name + Earthdawn.constantAlt( "pipealt" ) + ab.action : ab.action) === a}); + _.each( old, function( b ) { + if( what == 1 ) // menu entry asking if want to remove it. + txt += " " + Earthdawn.makeButton( b.name, "!Earthdawn~ charID: " + po.charID + "~ abilityRebuild: removeOne: " + b.id + , "This will remove the named Token Action.", "param" ); + else if( what == -1 ) { // automatically remove it. + txt += " " +b.name; + abiltiyRebuild( "removeOne", b.id ); + } + }); + }); + if( txt.length > oldTlength ) { + txt += "?
    "; + oldTlength = txt.length + } } + + let build = _.difference( newmap, oldmap ); // We want to know newab that are not in oldab (probably need to be created) + if( build.length > 0 ) { + txt += "Missing Token Action" + ((build.length > 1) ? "s" : "") + ". " + ((what == 1) ? "Do you want to create: " : "Creating: " ); + _.each( build, function( a ) { + ++problems; + let nw = _.filter( newab, function ( ab ) { return (forceNameRefresh ? ab.name + Earthdawn.constantAlt( "pipealt" ) + ab.action : ab.action) === a}); + _.each( nw, function( b ) { + if( what == 1 ) // menu entry asking if want to create it. + txt += " " + Earthdawn.makeButton( b.name, "!Earthdawn~ charID: " + po.charID + "~ abilityRebuild: buildOne: " + + b.name + ": " + Earthdawn.tildiFix( b.action ) + , "This will create a Token Action for the named ability.", "param" ); + else if( what == -1 ) { // automatically build it. + txt += " " + b.name; + abiltiyRebuild( "buildOne", b.id, Earthdawn.tildiFix( b.action ) ); + } + }); + }); + if( txt.length > oldTlength ) { + txt += "?
    "; + oldTlength = txt.length + } } + + // look for duplicate token actions (they might have different names, but the action is identical). Ask if any should be deleted. + let group = _.groupBy( oldmap, function( a ) { return a; }); + _.each( group, function( act ) { + if( act.length > 1 ) { // duplicates + txt += "Duplicate Token Actions found. Do you want to delete:"; + let old = _.filter( oldab, function ( ab ) { return (forceNameRefresh ? ab.name + Earthdawn.constantAlt( "pipealt" ) + ab.action : ab.action) === act[ 0 ]}); + _.each( old, function( a ) { + ++problems; + if( what == 1 ) // menu entry asking if want to remove it. + txt += " " + Earthdawn.makeButton( a.name, "!Earthdawn~ charID: " + po.charID + "~ abilityRebuild: removeOne: " + a.id + , "This will remove the named Token Action.", "param" ); + else if( what == -1 ) { // automatically remove it. + txt += " " + a.name; + abiltiyRebuild( "removeOne", a.id ); + } + }); + if( txt.length > oldTlength ) { + txt += "?
    "; + oldTlength = txt.length + } } + }); + if (problems > 0) { + log( problems +" problems found. Details: " + txt); + } + } // end buildTA + + if( newChar ) // This is a brand new character, just created from roll20 or a compendium drop. + buildTA( -1 ); + // case addGraphic, case forceTest, and case batch (token drop, repsecfix is run, or the GM special command is run). + else if( addGraphic || forceTest || batch ) { + if ( addGraphic && (Earthdawn.getAttrBN( cID, "abilityRebuildControl", "" ) === "Never" )) + return; // If this character is set to never do this action on an addGraphic, exit this routine. + buildTA( 0 ); + + if( problems > 0 ) // This block just writes a chat window message and a button warning that there is a problem. + this.chat( problems + " token actions were not as expected. " + + Earthdawn.makeButton( "Menu", "!Earthdawn~ charID: " + this.charID + "~ abilityRebuild: Menu : " + (forceNameRefresh ? "forceNameRefresh" : "Standard") + , "This will present a menu of options on how to deal with this.", "param" ) + " to fix." + , Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive ); + } else if ( ssa.length < 2 ) + Earthdawn.errorLog( "ED:abilityRebuild() Warning, bad SSA " + JSON.stringify( ssa )); + else { // We have something in ssa[1] + switch ( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() ) { + case "buildone": // Rebuild a Token Action. + if( ssa.length > 3 ) { + let abObj = getObj( "ability", ssa[ 2 ]); + if( abObj ) // If already there, get rid of it. + abObj.remove(); + Earthdawn.abilityAdd( cID, ssa[ 2 ], Earthdawn.tildiRestore( ssa[ 3 ])); + this.chat( ssa[ 2 ] + " added.", Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + } else + Earthdawn.errorLog( "ED:abilityRebuild() warning bad build: " + JSON.stringify( ssa ), po); + break; + case "batchfix": + case "fixall": // Rebuild all Token Actions. + buildTA( -1 ); + if( !batchFix ) this.chat( "Attempting to fix all issues. Retesting...", Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + this.abilityRebuild( [ "abilityRebuild", "Menu", "silent" ] ); + break; + case "menu": // Present a menu asking what to do about it. + buildTA( 1 ); + if( problems > 0 ) + this.chat( Earthdawn.makeButton( "Fix All", "!Earthdawn~ charID: " + this.charID + "~ abilityRebuild: fixAll", + "This will delete all Token Actions for this character that use this API module and recreate them correctly.", "param" ) + "
    " + + Earthdawn.makeButton( "Never", "!Earthdawn~ charID: " + this.charID + "~ abilityRebuild: Never", + "This button will set it so that this character will never perform this test of Token Actions again except as part of the fix/restore special function." + , "param" ) + " test TA again.
    " + + txt.trim(), Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + else if (!silent) + this.chat( "No problems found.", Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "API" ); + break; + case "never": // skip this routine in the future. + Earthdawn.setWW( "abilityRebuildControl" , "Never", cID ); + this.chat( "It will no longer test the token actions when the token is dropped.", Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + break; + case "removeone": // remove one token action by name + let abObj = getObj( "ability", ssa[ 2 ]); + if( abObj ) { + this.chat( abObj.get( "name" ) + " removed.", Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + abObj.remove(); + } else { + this.chat( "Warning bad remove.", Earthdawn.whoFrom.character | Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive); + Earthdawn.errorLog( "ED:abilityRebuild() warning bad remove: " + JSON.stringify( ssa ), po); + } + break; + default: + Earthdawn.errorLog( "ED:abilityRebuild() Warning, unknown command " + JSON.stringify( ssa )); + } } + return problems; + } catch(err) { Earthdawn.errorLog( "ED:abilityRebuild() error caught: " + err, po ); } + } // End abilityRebuild + + + + // ParseObj.Bonus ( ssa ) + // Add a bonus dice to the next roll. + // ssa is an array that holds the parameters. + // 1 - Step of the bonus dice (defaults to step 0). + this.Bonus = function( ssa ) { + 'use strict'; + try { + let kstep = 0; + if( ssa.length > 1 ) + kstep = this.getValue( ssa[ 1 ] ); + if( kstep > 0 ) { // do we really have a bonus dice? + this.misc[ "effectiveStep" ] = (("effectiveStep" in this.misc) ? this.misc[ "effectiveStep" ] : 0 ) + kstep; + let t = this.edClass.StepToDice( kstep ); + this.misc[ "bonusStep" ] = ( ("bonusStep" in this.misc) ? this.misc[ "bonusStep" ] + "+" : "" ) + kstep; + this.misc[ "bonusDice" ] = ( ("bonusDice" in this.misc) ? this.misc[ "bonusDice" ] : "" ) + "+" + t; + } + } catch(err) { Earthdawn.errorLog( "ED.Bonus() error caught: " + err, this ); } + } // End ParseObj.Bonus() ssa ) + + + + // ParseObj.CalculateStep() + // This subroutine has a purpose similar to Lookup, but the processing needed is too complex to be passed on the command line. + // So we are passed an identifier in ssa [ 1 ] which tells us what needs to be calculated. + // This routine looks up whatever info is needed and performs the calculation. + // Anything after ssa[1] is added to this total. + // The results are put in this.misc.step. + // + // So far this routine can calculate: + // Jump-up step + this.calculateStep = function( ssa ) { + 'use strict'; + try { + if( ssa.length < 2 ) { + this.chat( "Error! calculateStep() not passed a value to lookup. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + return false; + } + if( this.charID === undefined ) { + this.chat( "Error! charID undefined in calculateStep(). Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return false; + } + + let lu = 0; + switch ( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() ) { + case "jumpup": + lu = Earthdawn.getAttrBN( this.charID, "Dex", 5, true ); // This includes All-Tests-Total. + lu += Earthdawn.getAttrBN( this.charID, "condition-KnockedDown", "0", true ) * 3; + lu += Earthdawn.getAttrBN( this.charID, "Jumpup-Adjust", "0", true ); + lu -= Earthdawn.getAttrBN( this.charID, "Armor-IP", 0, true ) + this.mookWounds(); // Note that this is only Armor-IP, not shield or misc mods. + break; + } // End switch + + lu += this.ssaMods( ssa, 2); + this.misc[ "step" ] = ( this.misc[ "step" ] || 0) + lu; + } catch(err) { Earthdawn.errorLog( "ED.calculateStep() error caught: " + err, this ); } + return false; + } // End calculateStep() + + + + // ParseObj.chat() + // For handiness, we have a version of this in both edClass and edParse. + // Just call the edClass one. + this.chat = function ( newMsg, iFlags, customFrom ) { + 'use strict'; + this.edClass.chat( newMsg, iFlags, customFrom, this ); + }; // end chat() + + + + // ParseObj.ChatMenu() + // We have a request to display a menu in the chat window. + // attrib, damage, editspell2: (dur, MenuAddExtraThread, AddExtraThread, MenuRemoveExtraThread, RemoveExtraThread), + // fxSet: (sequnce of submenus), gmstate / gmspecial, grimoire, help, languages, link, linkAdd1, linkAdd2, linkRemove, linkRemoveHalf, + // oppmnvr, RolltypeEdit, RolltypeMulti, skills, spells, stateEdit, status, strainChange, talents. + this.ChatMenu = function( ssa ) { + 'use strict'; + let edParse = this; + try { + let lst, + entryPoint, + id, + s = "", + ind = 0; + + function addSheetButtonCall( label, item, tip ) { + s += Earthdawn.makeButton( label, + "!Earthdawn~ foreach: st: tuc~ SetAttrib: " + item + ":?{" + Earthdawn.getParam(label, 1, " ") + + Earthdawn.constantButton( "pipe" ) + Earthdawn.constantButton( "at" ) + Earthdawn.constantButton( "braceOpen" ) + "selected" + + Earthdawn.constantButton( "pipe" ) + item + Earthdawn.constantButton( "braceClose" ) + "}", tip, "dflt" ); + } + function addButtonWithCharID( label, item, tip, color) { + s += Earthdawn.makeButton( label, "!Earthdawn~ charID: " + edParse.charID + "~ " + item, tip, color); + } + + function buildLabel( label, item ) { // builds a label that is the label text in the first param, and the looked up value of the 2nd param. + if( !item ) + item = label; + if( id ) { + let t = edParse.getValue(item, id); + if( isFinite( t ) ) + return label + " " + t.toString(); + } + return label; + } + + + switch( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase()) { + case "attrib": { + lst = this.getUniqueChars( 1 ); + if ( Earthdawn.safeArray( lst).length === 1 ) + for( let k in lst ) + id = k; + let spacer = " / ", + nl = "
    "; + + s +=nl; + addSheetButtonCall( buildLabel( "Actn Tests", "Adjust-All-Tests-Misc" ), "Adjust-All-Tests-Misc", "Adjust the modifier to all Action Tests." ); + addSheetButtonCall( buildLabel( "Attacks", "Adjust-Attacks-Misc" ), "Adjust-Attacks-Misc", "Adjust the modifier to all Close Combat Attack tests." ); + addSheetButtonCall( buildLabel( "Attack Bonuses", "Adjust-Attacks-Bonus" ), "Adjust-Attacks-Bonus", "Adjust the bonus step added to all Attacks." ); + addSheetButtonCall( buildLabel( "Damage", "Adjust-Damage-Misc" ), "Adjust-Damage-Misc", "Adjust the modifier to all Close Combat Damage tests." ); + addSheetButtonCall( buildLabel( "Bonus Damage", "Adjust-Damage-Bonus" ), "Adjust-Damage-Bonus", "Adjust the bonus step to all Damage rolls." ); + addSheetButtonCall( buildLabel( "Defenses", "Adjust-Defenses-Misc" ), "Adjust-Defenses-Misc", "Adjust the modifier to Physical and Mystic (but not Social) Defenses." ); + addSheetButtonCall( buildLabel( "Efct Tests", "Adjust-Effect-Tests-Misc" ), "Adjust-Effect-Tests-Misc", "Adjust the modifier to all Effect Tests." ); + addSheetButtonCall( buildLabel( "TN", "Adjust-TN-Misc" ), "Adjust-TN-Misc", "Adjust the modifier to all Action Target Numbers." ); + + s += nl + buildLabel( "PD" ) + " "; + addSheetButtonCall( buildLabel( "Buff", "PD-Buff" ), "PD-Buff", "Adjust the modifier to Physical Defense." ); + s += spacer + buildLabel( "MD" ) + " "; + addSheetButtonCall( buildLabel( "Buff", "MD-Buff" ), "MD-Buff", "Adjust the modifier to Mystic Defense." ); + s += spacer + buildLabel( "SD" ) + " "; + addSheetButtonCall( buildLabel( "Buff", "SD-Buff" ), "SD-Buff", "Adjust the modifier to Social Defense." ); + s += spacer + buildLabel( "PA", "Physical-Armor" ) + " "; + addSheetButtonCall( buildLabel( "Buff", "PA-Buff" ), "PA-Buff", "Adjust the modifier to Physical Armor." ); + s += spacer + buildLabel( "MA", "Mystic-Armor" ) + " "; + addSheetButtonCall( buildLabel( "Buff", "MA-Buff" ), "MA-Buff", "Adjust the modifier to Mystic Armor." ); + s += nl + buildLabel( "Move", "Movement" ) + spacer; + s += buildLabel( "Wnds", "Wounds" ) + spacer; + s += buildLabel( "Dmg", "Damage" ) + spacer; + if( id ) + s += "Unc " + Earthdawn.getAttrBN( id, "Damage-Unc-Rating", 20 ).toString() + spacer; + s += buildLabel( "Dth", "Damage-Death-Rating" ) + spacer; + s += buildLabel( "W Thr", "Wound-Threshold" ) + spacer; + s += buildLabel( "Karma" ) + spacer; + if( id ) + s += "K max " + Earthdawn.getAttrBN( id, "Karma_max", 0 ).toString() + spacer; + s += buildLabel( "DP" ) + spacer; + if( id ) + s += "DP max " + Earthdawn.getAttrBN( id, "DP_max", 0 ).toString() + spacer; + s += nl; + s += Earthdawn.makeButton( buildLabel( "Dex" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Dex-Check}", "Dex Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Dex-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Dex-Effect-Check}", "Dex Effect test", "effect" ); + s += Earthdawn.makeButton( buildLabel( "Str" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Str-Check}", "Str Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Str-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Str-Effect-Check}", "Str Effect test", "effect" ); + s += Earthdawn.makeButton( buildLabel( "Tou" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Tou-Check}", "Toughness Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Tou-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Tou-Effect-Check}", "Toughness Effect test", "effect" ); + s += Earthdawn.makeButton( buildLabel( "Per" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Per-Check}", "Per Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Per-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Per-Effect-Check}", "Per Effect test", "effect" ); + s += Earthdawn.makeButton( buildLabel( "Wil" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Wil-Check}", "Will Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Wil-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Wil-Effect-Check}", "Will Effect test", "effect" ); + s += Earthdawn.makeButton( buildLabel( "Cha" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Cha-Check}", "Charisma Action test", "action" ); + s += Earthdawn.makeButton( buildLabel( "FX", "Cha-Effect" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Cha-Effect-Check}", "Charisma Effect test", "effect" ); + s += nl; + s += Earthdawn.makeButton( buildLabel( "Init", "Initiative"), "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Dex-Initiative-Check}", + "Roll Initiative for this character.", "effect" ); + s += Earthdawn.makeButton( "JumpUp", "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|JumpUp}", + "Make a jump-up test. 2 strain, Target #6", "action" ); + let x; + if( id ) + x = this.getValue("Knockdown", id) + this.getValue("Adjust-Effect-Tests-Total", id); + + s += Earthdawn.makeButton( "Knockdown" + (x ? " " + x.toString() : ""), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Knockdown}", "Make a standard Knockdown test.", "action" ); + + s += Earthdawn.makeButton( buildLabel( "Recovery", "Recovery-Step" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Recovery}", "Make a Recovery test for this character.", "damage" ); + if( id ) + x = this.getValue("Recovery-Step", id) + this.getValue("Wil", id); + s += Earthdawn.makeButton( "Recov Stun" + (x ? " " + x.toString() : ""), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|RecoveryStun}", "Make a Stun Damage only Recovery test for this character.", "damage" ); + s += Earthdawn.makeButton( "New Day", "!Earthdawn~ charID: " + Earthdawn.constantButton( "at" ) + "{selected|character_id}~ ForEach: c~ Misc: NewDay", + "Set characters recovery tests used to zero and refill karma to max", "damage" ); + if( state.Earthdawn.g1879 ) { + s += Earthdawn.makeButton( buildLabel( "Language Speak", "Speak-Step" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|Speak-Roll}", null, "action" ); + s += Earthdawn.makeButton( buildLabel( "Language Read/Write", "ReadWrite-Step" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{selected|ReadWrite-Roll}", null, "action" ); + } + if( id && state.Earthdawn.gED ) { // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_DSP_Name" )) + s += Earthdawn.makeButton( att.get("current") + " " + edParse.getValue( Earthdawn.buildPre( "DSP", att.get("name") ) + "Circle", id), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.buildPre( "DSP", att.get("name") ) + "halfMagic}", + "Half Magic test.", "action" ); + }); // End for each attribute. + } + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "Attributes" ); + } break; // end attrib + case "damage": { + s = "
    Selected Tokens Take Dmg - "; + s += Earthdawn.makeButton( "PA", "!Earthdawn~ foreach: st~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun} : PA", + "The Selected Token(s) take damage. Physical Armor applies.", "damage" ); + s += Earthdawn.makeButton( "MA", "!Earthdawn~ foreach: st~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun} : MA", + "The Selected Token(s) take damage. Mystic Armor applies.", "damage" ); + s += Earthdawn.makeButton( "NA", "!Earthdawn~ foreach: st~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun|Strain} : NA", + "The Selected Token(s) take damage. No Armor applies.", "damage" ); + s += Earthdawn.makeButton( "Other", "!Earthdawn~ foreach: st~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun|Strain} : ?{Armor| None,NA| Physical,PA| Mystic,MA| Nat PA,PA-Nat| Nat MA,MA-Nat}", + "The Selected Token(s) take damage, which might be of type Stun, Strain, or Normal. Armors may be applied.", "damage" ); + s += Earthdawn.makeButton( "1 Strain", "!Earthdawn~ foreach: st ~ Damage: Strain: 1: Verbose", + "The Selected Token(s) take 1 strain.", "damage" ); + s += Earthdawn.makeButton( "X Strain", "!Earthdawn~ foreach: st~ Damage: Strain: ?{How much Strain|2} : Verbose", + "The Selected Token(s) take 1 strain.", "damage" ); + s += "
    Give Dmg to Target Token - "; + s += Earthdawn.makeButton( "PA", "!Earthdawn~ setToken: @{target|token_id}~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun} : PA", + "Give damage to a Target token. Physical Armor applies.", "damage" ); + s += Earthdawn.makeButton( "MA", "!Earthdawn~ setToken: @{target|token_id}~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun} : MA", + "Give damage to a Target token. Mystic Armor applies.", "damage" ); + s += Earthdawn.makeButton( "NA", "!Earthdawn~ setToken: @{target|token_id}~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun|Strain} : NA", + "Give damage to a Target token. No Armor applies.", "damage" ); + s += Earthdawn.makeButton( "Other", "!Earthdawn~ setToken: @{target|token_id}~ Damage: ?{Damage)|1}: ?{Type of damage|Damage|Stun|Strain} : ?{Armor| None,NA| Physical,PA| Mystic,MA| Nat PA,PA-Nat| Nat MA,MA-Nat }", + "Give damage to a Target token, which might be of type Stun, Strain, or Normal. Armors may be applied.", "damage" ); + s += "
    Roll Dmg - "; + s += Earthdawn.makeButton( "Fire", "!Earthdawn~ Quick: Fire: ?{Size of fire|Torch,Torch: 4|Small Campfire,Small Campfire: 6" + + "|Large Campfire,Large Campfire: 8|House fire,House fire: 10|Forest fire,Forest fire: 12}~ foreach: st~ Roll", + "The Selected Token(s) take Fire damage. Physical Armor applies.", "damage" ); + s += Earthdawn.makeButton( "Fall", "!Earthdawn~ Quick: Fall: " + + "?{Distance Fallen|2-3 Yrd,2-3: 5|4-6 Yrd,4-6: 10|7-10 Yrd,7-10: 15|11-20 Yrd,11-20: 20 (2)|21-30 Yrd,21-30: 25 (2)|31-50 Yrd,31-50: 25 (3)" + + "|51-100 Yrd,51-100: 30 (3)|101-150 Yrd,101-150: 30 (4)|151-200 Yrd,150-200: 35 (4)|201 Yrd or more,201 or more: 35 (5)}~ foreach: st~ Roll", + "The Selected Token(s) take Falling damage. No Armor applies.", "damage" ); + lst = this.getUniqueChars( 1 ); + if ( Earthdawn.safeArray( lst).length === 1 ) + for( let k in lst ) + id = k; + if (id) { + let t = "", t1 = "", + attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_WPN_Name" )) +// if( Earthdawn.getAttrBN(id, att.get( "name" ).slice( 0, -5) + "_CombatSlot", "0") != "1") + t += Earthdawn.makeButton( att.get( "current" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.safeString( att.get( "name" )).slice(0, -4) + "Roll}", + "Roll a Weapon Damage.", "damage" ); + if( att.get( "name" ).endsWith( "_Mod-Type" ) && att.get( "current" ).startsWith( "Damage" )){ + let pre = Earthdawn.buildPre( att.get( "name" )), + name = Earthdawn.getAttrBN( id, pre + "Name", "" ) + t1 += Earthdawn.makeButton( name, "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + pre + "Roll}", "Roll an Ability Damage.", "damage" );} + }); // End for each attribute. + if( t.length > 1 ) + s += "
    Roll for Weapon: " + t; + if( t1.length > 1 ) + s += "
    Talents/Knacks: " + t1; + } + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "Damage" ); + } break; // end damage + case "durationtracker": { + if( ssa.length < 4) return; + let name= ssa[ 2 ], + dur = Earthdawn.parseInt2( ssa[ 3 ] ); + let tt = Campaign().get( "turnorder" ); + let tracker = (tt == "") ? [] : JSON.parse( tt ); + tracker.push( { id: "-1", pr: "-" + dur, custom: name, formula: "1" } ); + Campaign().set( 'turnorder' ,JSON.stringify( tracker )); + } break; //end duration tracker + case "editspell2": { + let nm = Earthdawn.buildPre( "SP", ssa[ 2 ]); + switch ( ssa[ 3 ] ) { + case "Dur": { + let dur = ""; + if( ssa[ 4 ] != "Nothing" ) + dur += ssa[ 4 ] + " "; + if( ssa[ 5 ] > 0 ) + dur += (( dur.length > 0 ) ? "+" : "") + ssa[ 5 ] + " "; + dur += ssa[ 6 ]; + this.setWW( nm + "Duration", dur ); + s = "Duration updated"; + } break; // end Dur + case "MenuAddExtraThread": { + addButtonWithCharID( "ET Add Target", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Add Tgt (+?{How many Additional Targets or Additional Effects|Rank})", null, "param" ); + addButtonWithCharID( "ET Inc Area", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Area (?{How much increased area|2 Yards})", null, "param" ); + addButtonWithCharID( "ET Inc Dur (minutes)", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Dur (min)", null, "param" ); + addButtonWithCharID( "ET Inc Dur", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Dur (?{How many units})", null, "param" ); + addButtonWithCharID( "ET Inc Effect Step", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Efct Step (?{How many units|2})", null, "param" ); + addButtonWithCharID( "ET Inc Effect (other)", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Efct (Other) (?{How many units|2})", null, "param" ); + addButtonWithCharID( "ET Inc Range", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Inc Rng (?{How many units|10 yards})", null, "param" ); + addButtonWithCharID( "ET Special", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Special (?{What is the text of the Extra Thread Effect})", null, "param" ); + addButtonWithCharID( "ET Remove Targets", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: Rmv Tgt (?{How many targets to remove|-Rank})", null, "param" ); + addButtonWithCharID( "ET Not Applicable", "chatMenu: editSpell2: " + ssa[ 2 ] + ": AddExtraThread: NA", null, "param" ); + } break; // end MenuAddExtraThread + case "AddExtraThread": { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: nm + "ExtraThreads" }, ""); + let et = "," + Earthdawn.safeString( aobj.get( "current" )) + ","; + if( et.length < 4 ) + et = ","; + Earthdawn.set( aobj, "current", (et + Earthdawn.safeString( ssa[ 4 ] ) + ",").slice( 1, -1 )); + s = "Updated"; + } break; // end AddExtraThread + case "MenuRemoveExtraThread": { + let et = Earthdawn.getAttrBN( this.charID, nm + "ExtraThreads", "" ); + let aet = et.split( "," ); + for( let i = 0; i < aet.length; ++i ) + if( aet[ i ].trim().length > 0 ) + addButtonWithCharID( "Remove " + aet[ i ].trim(), "chatMenu: editSpell2: " + ssa[ 2 ] + ": RemoveExtraThread: " + aet[ i ].trim(), null, "param2" ); + } break; // end MenuRemoveExtraThread + case "RemoveExtraThread": { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: nm + "ExtraThreads" }, ""); + let et = "," + Earthdawn.safeString( aobj.get( "current" )).trim() + ","; + let net = et.replace( "," + ssa[ 4 ] + ",", "," ); + if( et != net ) { + Earthdawn.set( aobj, "current", net.slice( 1, -1 )); + s = "Updated"; + } + } break; // end RemoveExtraThread + } // end switch ssa[ 3 ] within editspell2 + if( s.length > 1 ) + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "Edit Spell: " + + Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( "SP", ssa[ 2 ] ) + "Name", "" )); + } break; // end editSpell2 + case "fxset": { // chatMenu: fxSet: (subMenu): (code): (rowID): [additional parameters already specified in previous menus] + // This is a sequence of menus. subMenu 1 is the first question, subMenu 2 is the 2nd etc. Each submenu adds it's parameter to what has already been collected. + // Starting at ssa[5] they are [Set/Clear]: When (Attempt/Success): What Effect: Color: Where does effect appear. + let pre = Earthdawn.buildPre( ssa[ 3 ], ssa[ 4 ]), + submenu = ssa[ 2 ], + ttip = "Special Effects appear on the virtual table top when this action is performed. They are set via a series of questions here in the chat window. Answer each question in turn."; + ssa[ 2 ] = Earthdawn.parseInt2( submenu ) + 1; + let already = ssa.join( ":" ); + switch (submenu) { + case "0": // set or Clear + addButtonWithCharID( "Set FX", already + ": Set", " This sets a new Special Effect, replacing any old FX.", "param" ); + addButtonWithCharID( "Clear FX", "fxSet: " + ssa[ 3 ] + ": " + ssa[ 4 ] + ": Clear", " This removes any Special Effects for this ability.", "param2" ); + break; + case "1": // When does this FX display. + s += " When should this Effect be displayed? Every time the ability is "; + addButtonWithCharID( "Attempted", already + ": Attempt", " Effect will be displayed every time the ability is attempted, whether it succeeds or not.", "param" ); + s += " or only when it "; + addButtonWithCharID( "Succeeds", already + ": Success", " Effect will be displayed only when the ability succeeds.", "param" ); + break; + case "2": + s += " What type of effect? "; + addButtonWithCharID( "Beam", already + ": Beam", " A beam effect (and other buttons of this color) travels from the caster to one or more targets.", "param2" ); + addButtonWithCharID( "Bomb", already + ": Bomb", " A bomb effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Breath", already + ": Breath", " A breath effect (and other buttons of this color) travels from the caster to one or more targets.", "param2" ); + addButtonWithCharID( "Bubbling", already + ": Bubbling", " A bubbling effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Burn", already + ": Burn", " A burn effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Burst", already + ": Burst", " A burst effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Explode", already + ": Explode", " An explode effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Glow", already + ": Glow", " A glow effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Missile", already + ": Missile", " An missile effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Nova", already + ": Nova", " A nova effect (and other buttons of this color) takes place ether at the caster location, or the target locations.", "param" ); + addButtonWithCharID( "Splatter", already + ": Splatter", " A splatter effect (and other buttons of this color) travels from the caster to one or more targets.", "param2" ); + addButtonWithCharID( "Other/Custom", already + ": Custom ?{Name of Custom FX}", + + " In order to create your own custom FX, first select the FX Tool (lightning bolt) from the left toolbar, then select the " + + "'[New Custom FX]' option under the '--Custom FX--. Read the help wiki (above) for additional instructions.' header.", "param2" ); + s += " For more information " + + Earthdawn.makeButton( "FX Help Wiki", "https://help.roll20.net/hc/en-us/articles/360037258714-F-X-Tool#F/XTool-CustomFXTool", + "This button will open Roll20's help page for custom Special Effects. Important! Open this in a new tab, not this tab.", "dflt", true ); + break; + case "3": // Color. Also, if last one was a Custom Effect, verify that it is Good. + if( Earthdawn.safeString( ssa[ 7 ]).startsWith( "Custom" ) ) { + let cust = findObjs({ _type: 'custfx', name: Earthdawn.safeString( ssa[ 7 ]).slice( 7 ).trim() })[0]; + if( cust && cust.get( "_id" )) { + ssa[ 8 ] = ""; // This is to fall though to the next question with this question left blank. + ssa[ 2 ] = Earthdawn.parseInt2( submenu ) + 2; + already = ssa.join( ":" ); + } else { + this.chat( "Error! Can not find a Custom FX of name: '" + Earthdawn.safeString( ssa[ 7 ]).slice(7) + "'.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoFrom.noArchive ); + return; + } + } else { + s += " What color of effect "; + addButtonWithCharID( "Acid", already + ": Acid", " Acid effects are a dark green.", "param" ); + addButtonWithCharID( "Blood", already + ": Blood", " Blood effects are crimson.", "param" ); + addButtonWithCharID( "Charm", already + ": Charm", " Charm effects are a light purple.", "param" ); + addButtonWithCharID( "Death", already + ": Death", " Death effects are black.", "param" ); + addButtonWithCharID( "Fire", already + ": Fire", " Fire effects are a mix of red and orange.", "param" ); + addButtonWithCharID( "Frost", already + ": Frost", " Frost effects are a mix of white and blue.", "param" ); + addButtonWithCharID( "Holy", already + ": Holy", " Holy effects are a light yellow.", "param" ); + addButtonWithCharID( "Magic", already + ": Magic", " Magic effects are a mix of many differnt colors with black and blue predominating.", "param" ); + addButtonWithCharID( "Slime", already + ": Slime", " Slime effects are a sickly green.", "param" ); + addButtonWithCharID( "Smoke", already + ": Smoke", " Smoke effects are white.", "param" ); + addButtonWithCharID( "Water", already + ": Water", " Water effects are sea blue.", "param" ); + break; + } // Note the top option falls through here. + case "4": // Where effect appears. + let ss2 = Array.from( ssa ); // Force a new instance + ss2.splice( 0, 1); + ss2.splice( 1,1); // We get rid of chatMenu, keep fxSet, get rid of sub-menu, Deep code, RowID and everytghing else. + already = ss2.join( ":" ); + s += " Where does the Effect appear? "; + let typ = Earthdawn.safeString( ss2[ 5 ] ).toLowerCase(); + if( typ !== "beam" && typ !== "breath" && typ !== "splatter" ) { + addButtonWithCharID( "Caster Only", already + ": CO", " The effect appears on the token taking the action.", "param" ); + addButtonWithCharID( "First Target Only", already + ": FTO", " The effect appears only on the first target.", "param" ); + addButtonWithCharID( "All Targets", already + ": AT", " The effect appears multiple times, once on each targets.", "param" ); + } + if( typ === "beam" || typ === "breath" || typ === "splatter" || typ.startsWith( "custom ") ) { + addButtonWithCharID( "Caster to 1st Target", already + ": CtFTO", " The effect appears on the caster, and travels to the first target.", "param" ); + addButtonWithCharID( "Caster to all Targets", already + ": CtAT", " The effect appears multiple times, starting on the caster and traveling to each target.", "param" ); + } + break; + default: + edParse.chat( "Error! chatMenu fxSet, Invalid sub-menu: " + JSON.stringify( ssa), Earthdawn.whoFrom.apiWarning | Earthdawn.whoFrom.noArchive ); + } // end switch submenu within fxSet. + if( s.length > 1 ) + this.chat( "Setting Special Effects for " + Earthdawn.codeToName( ssa[ 3 ] ) + " " + Earthdawn.getAttrBN( this.charID, pre + "Name", "") + ". " + + (!ttip ? "" : Earthdawn.texttip( "(hover)", ttip)) + s + , Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive ); + } break; // end fxSet + case "gmspecial": + case "gmstate": { + let cid; + if( this.charID ) + cid = this.charID; + if( !cid && this.edClass && this.edClass.msg && this.edClass.msg.rolledByCharacterId ) + cid = this.edClass.msg.rolledByCharacterId; + if( !playerIsGM( this.edClass.msg.playerid )) { +// this.chat( "Only GM can do this!", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + macroDetail( "ResetChars", "!Earthdawn~ ", false ); + + s = "Most GM Special commands are not allowed for players. Players can " + + Earthdawn.makeButton("ResetChars", "!Earthdawn" + (cid ? "~ charID: " + cid : "") + "~ Misc: ResetChars: ?{(de)buffs Only or Full (including damage and karma) Reset|Mods Only|Full}", + "Remove all buffs and debuffs, reset all status markers, and optionaly set karma and health to full.", "param" ) + + Earthdawn.makeButton("Clean Tokens", "!Earthdawn" + (cid ? "~ charID: " + cid : "") + "~ Debug: CleanTokens: Character", + "Clean this characters tokens from all pages.", "param" ); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + } else { + let t = JSON.parse( JSON.stringify( state.Earthdawn ) ); // To get a deep copy, turn it into a string and back into an object. + t.RollType = undefined; // We want to get rid of RollType from the initial listing, since it can be very long and has its own section. + s += JSON.stringify( t ) + " "; + s += Earthdawn.makeButton("ResetChars", "!Earthdawn" + (cid ? "~ charID: " + cid : "") + + "~ Misc: ResetChars: ?{(de)buffs Only or Full (including damage and karma) Reset|Mods Only|Full}", + "Remove all buffs and debuffs, reset all status markers, and optionaly set karma and health to full.", "param" ); + s += Earthdawn.makeButton("Clean Tokens", "!Earthdawn" + (cid ? "~ charID: " + cid : "") + "~ Debug: CleanTokens: ?{Which Tokens|This Character,Character|All Tokens Ask for each page,Ask|All Tokens All Pages,All}", + "Clean Tokens from the campaign. This character cleans this characters tokens from all pages. Ask for each page presents a list of pages with tokens and asks which ones to clean all tokens from. All tokens cleans all tokens from all pages (except pages with 'bullpen' in their name)." , "param" ); + s += Earthdawn.makeButton("Verify/Fix All", "!Earthdawn~ Debug: ?{Do you want to run Verify Fix on ALL characters (use Special Function to run on just one sheet). " + + "Have console log open to see results.|Cancel|Yes but ask about each fix,RepSecBatch|Yes and automatically fix them,RepSecBatchFixAll}" + + ": ?{Refresh Abilities|Only if Ability text changes,Standard|If Ability name or text changes,forceNameRefresh}" + , "Verify/Fix All sheets will run a routine for each character that will check all attributes looking for problems and fixing what it can." , "param" ); + s += Earthdawn.makeButton("Rolltype Editor", "!Earthdawn~ ChatMenu: RolltypeEdit: state.Earthdawn.Rolltype: : Display", + "Change Rolltype. Change defaults, add or remove exceptions, Set an override." , "param" ); + s += Earthdawn.makeButton("Linking options", "!Earthdawn~ Misc: State: Linking", + "This allows various linking options to be changed (whether or not nameplates are automatically shown on PCs and NPCs for example)" , "param" ); + s += Earthdawn.makeButton("Result Style", "!Earthdawn~ Misc: State: Style: ?{What roll results style do you want? " + + "Value when this button was generated was '" + ((state.Earthdawn.style == 2) ? "Vauge Result" :((state.Earthdawn.style == 1) ? "Vauge Success" : "Full")) + + "'|Vague Roll Result,2|Vague Success (not recommended),1|Full,0}" + , "Switch API to provide different details on roll results. Vague Roll result is suggested. It does not say what the exact result is, but says how much it " + + "was made by. Vague Success says exactly what the roll was, but does not say the TN or how close you were.", "param") + s += Earthdawn.makeButton("Show Dice", "!Earthdawn~ Misc: State: showDice: ?{Show roll results dice on seperate line. " + + "Value when this button was generated was '" + state.Earthdawn.showDice + "'|False,0|True,1}", + "If True dice roll details are displayed as a seperate line. Otherwise they are in the tooltip of the yellow Results box.", "param"); + s += Earthdawn.makeButton("No Pile on - Dice", "!Earthdawn~ Misc: State: NoPileonDice: ?{Enter max number of times each die may explode. -1 to disable" + + "|" + ((state.Earthdawn.noPileonDice == undefined) ? "-1" : state.Earthdawn.noPileonDice) + "}" + , "If a GM desires a less lethal game, or to reduce risk in specific encounters, they can use this option to keep NPC dice from exploding too many times. " + + "This controls the maximum number of times a single dice can 'explode' (be rolled again after rolling a max result). " + + "-1 is the standard default of unlimited. 0 means no dice will ever explode. 2 (for example) means a dice can explode twice, but not three times. " + + "This can done quietly, without announcing it to the players (see silent)." , "param" ); + s += Earthdawn.makeButton("No Pile On - Step", "!Earthdawn~ Misc: State: NoPileonStep: ?{Enter multiple of step number to limit result to. -1 to disable" + + "|" + ((state.Earthdawn.noPileonStep == undefined) ? "-1" : state.Earthdawn.noPileonStep) + "}" + , "If a GM desires a less lethal game, or to reduce risk in specific encounters, they can use this option to keep NPC roll results from exceeding a specific " + + "multiple the step number. If zero or -1, this is disabled and roll results are unlimited. if 1, " + +"then the dice result will never be very much greater than the step result. If (for example) the value is 3.5, then the result will never get very much " + + "greater than 3.5 times the step being rolled. This can done quietly, without announcing it to the players (see silent)." , "param" ); + s += Earthdawn.makeButton("Curse Luck / Pile On Silent", "!Earthdawn~ Misc: State: CursedLuckSilent: ?{Does the Cursed Luck Horror power work silently " + + "Value when this button was generated was '" + ((state.Earthdawn.CursedLuckSilent & 0x04) ? "Yes" : "No") + "'.|No,0|Yes,1}" + + "?{Does No-pile-on-dice work silently. Value when this button was generated was '" + ((state.Earthdawn.CursedLuckSilent & 0x02) ? "Yes" : "No") + "'.|No,0|Yes,1}" + + "?{Does No-pile-on-step work silently. Value when this button was generated was '" + ((state.Earthdawn.CursedLuckSilent & 0x01) ? "Yes" : "No") + "'.|No,0|Yes,1}" + , "Normally the system announces when a dice roll has been affected by Cursed Luck or Pile on Dice or Step. However the possibility exists that a " + + "GM might want it's effects to be unobtrusive. When these are set to 'yes', then the program just silently changes the dice rolls without announcing it." , "param" ); + s += Earthdawn.makeButton("API Logging", "!Earthdawn~ Misc: State: ?{What API logging event should be changed|LogStartup|LogCommandline|LogMsg}: ?{Should the API log this event|Yes,1|No,0}", + "What API events should the API log to the console?" , "param" ); +// s += Earthdawn.makeButton("Version", "!Earthdawn~ Misc: State: ?{API version or HTML version|API|HTML}: ?{Version number to store}", +// "You can change the html or javascript version number stored in the state. This can force character sheet update routines to be run next startup." , "param" ); + s += Earthdawn.makeButton("Change Edition", "!Earthdawn~ Misc: State: edition: ?{What rules Edition " + + "Value when this button was generated was '" + state.Earthdawn.edition + "'|Earthdawn Forth Ed,4 ED|1879 1st Ed,-1 1879|Earthdawn Third Ed,3 ED|Earthdawn First Ed,1 ED}", + "Switch API and Character sheet to Earthdawn 1st/3rd/4th Edition or 1879 1st Edition. Note this NEEDs to also be done in campaign default sheet settings." , "param" ); + s += Earthdawn.makeButton("Set all attribute", "!Earthdawn~ Debug: SetAttribAll: ?{Attribute name} : ?{Attribute Value}: ?{Confirm really do this|No|Yes}", + "Go through every character in the whole campaign and set a variable to the same value." , "param" ); + s += Earthdawn.makeButton("State Editor", "!Earthdawn~ ChatMenu: StateEdit: state.Earthdawn: : Display", + "A general state editor. Can change anything in the state variable." , "param" ); +// s += Earthdawn.makeButton("RollType", "!Earthdawn~ ChatMenu: RollType: Menu", +// "Determine the visibility of different roll results by type. For example some rolls might be public, others might be displayed to GM only." , "param" ); +// s += Earthdawn.makeButton("Default RollType", "!Earthdawn~ Misc: State: ?{What category of character do you want to change|NPC|Mook|PC}: ?{Should new characters of that type default to Public or GM Only rolls|Public,0|GM Only,1}", +// "By default, PCs default to public rolls, but NPCs and Mooks default to rolls visible only by the GM. This can be changed." , "param" ); +// s += Earthdawn.makeButton("Default Karma Ritual", "!Earthdawn~ Misc: State: KarmaRitual: ?{What should the default for each new characters Karma Ritual be set to?|Fill to max,-1|Refill by Circle,-2|Refill by Racial Modifier,-3|Refill Nothing,0|Refill 1,1|Refill 2,2|Refill 3,3|Refill 4,4|Refill 5,5|Refill 6,6|Refill 7,7|Refill 8,8|Refill 9,9|Refill 10,10}", +// "At charcter creation each charcters Karma Ritual behavior is set to this value. By default, The 'New Day' button refills a characters Karma pool up to Full." , "param" ); +// s += Earthdawn.makeButton("ToAPI", "!Earthdawn~ Misc: toAPI: ?{Remove API|Never Mind|Set Characters noAPI,Set|Fully remove API,noAPI}", +// "FromAPI will set all sheets to use noAPI buttons and remove all Earthdawn state variables (can be run just before deactivating API or uninstalling sheet to clean things up)" , "param" ); +// s += Earthdawn.makeButton("ToAPI", "!Earthdawn~ Misc: toAPI: ?{Set all characters to use API or noAPI|API|noAPI}", +// "ToAPI will set all sheets to use API. FromAPI will set all sheets to use noAPI buttons and remove all Earthdawn state variables (can be run just before deactivating API or uninstalling sheet to clean things up)" , "param" ); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + } } break; // end gmstate and gmspecial + case "grimoire": { + lst = this.getUniqueChars( 1 ); + for( let k in lst ) { + id = k; + let attributes = findObjs({ _type: "attribute", _characterid: id }); + s = ""; + let attflt= attributes.filter( function (att) { return att.get( "name" ).endsWith( "_SPP_Name" )}); + _.each( attflt, function ( att ) { + let tmpPre = Earthdawn.buildPre( att.get( "name" )); + s += Earthdawn.makeButton( att.get("current"), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + tmpPre + "Load}" + , "Load Preset " + //+ Earthdawn.getAttrBN( id, tmpPre + "Preset", "") + , "param"); + }); // End for each attribute. + if(s.length>0) + this.chat( "What Spell Preset ?" + s, Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, getAttrByName( id, "character_name" ) + " - Grimoire" ); + s = "What Spell? "; + attflt= attributes.filter( function (att) { return att.get( "name" ).endsWith( "_SP_Name" )}); + _.each( attflt, function ( att ) { + let tmpPre = Earthdawn.buildPre( att.get( "name" )); + let matrix = Earthdawn.getAttrBN( id, tmpPre + "spmRowID", "0" ); + let bnot = (typeof matrix !== 'string') || (matrix.length < 2); + if( Earthdawn.getAttrBN( id, tmpPre + "Type", "Spell") == "Spell") { //Only display Spells, not Knacks and Binding Secrets + s += Earthdawn.makeButton( att.get("current"), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + tmpPre + "Roll}" + , (bnot ? "" :"Spell in Matrix, use the Spells Menu instead! - ") + + Earthdawn.getParam( Earthdawn.getAttrBN( id, tmpPre + "DisplayText", ""), 1, "~") + " " + + Earthdawn.getParam( Earthdawn.getAttrBN( id, tmpPre + "SuccessText", ""), 1, "~"), bnot ? "param" : "param2" ); + } + }); // End for each attribute. + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, getAttrByName( id, "character_name" ) + " - Grimoire" ); + } + } break; // end grimoire + case "help": { + s = Earthdawn.makeButton( "Wiki Link", ( state.Earthdawn.sheetVersion < 1.8 ) ? "https://wiki.roll20.net/Earthdawn_-_FASA_Official" + : "https://wiki.roll20.net/Earthdawn_-_FASA_Official_V2", + "This button will open this character sheets Wiki Documentation, which should answer most of your questions about how to use this sheet.", "dflt", true ) + " Important! Open this in a new tab, not this tab."; + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "API" ); + } break; // end help + case "inspect": { + s = "Inspect: " + + Earthdawn.makeButton( "Lookup Value", "!Earthdawn~ foreach: st: tuc~ Debug: Inspect : GetValue: ?{Enter full name}", + "Enter an attribute name (simple or whole repeating section name), and get the value.", "param" ) + + Earthdawn.makeButton( "IDs from Name", "!Earthdawn~ foreach: st: tuc~ Debug: Inspect : GetIDs: ?{Enter Name fragment}", + "Enter a text fragment, and get IDs of every repeating section name that contains that fragment.", "param" ) + + Earthdawn.makeButton( "TokenObj", "!Earthdawn~ foreach: st~ Debug: Inspect : TokenObj", + "Test code to cause selected tokens to display TokenInfo to chat.", "param" ) + + Earthdawn.makeButton( "Repeating section", "!Earthdawn~ charID: ?{Char ID|@{selected|character_id}}~ Debug: Inspect: RepeatSection: ?{RowID}: ?{Detail|Short|Full}", + "Test code to show information on a row.", "param" ) + + Earthdawn.makeButton( "Object ID", "!Earthdawn~ Debug: Inspect: ObjectID: ?{Object ID}", + "Test code to show information on what an ID is. Character ID, Token ID, etc.", "param" ) + + Earthdawn.makeButton( "Statusmarkers", "!Earthdawn~ foreach: st~ Debug: Inspect : statusmarkers", + "Test code to cause selected tokens to display statusmarkers to Chat.", "param" ); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "API" ); + } break; // end inspect + case "languages": { + lst = this.getUniqueChars( 1 ); + for( let k in lst ) { + id = k; + s = '
    '; + let cnt = 0; + // got through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function ( att ) { + if( att.get( "name" ).endsWith( "_SKL_Name" )) { + ++cnt; + s += "
    "; + s += att.get( "current" ); + let pre = Earthdawn.buildPre( "SKL", att.get("name") ); + s += ""; + if( Earthdawn.getAttrBN( id, pre + "Speak", "0") == "1") + s += " Sk-Spk"; + if( Earthdawn.getAttrBN( id, pre + "ReadWrite", "0") == "1") + s += " Sk-RW"; + if( Earthdawn.getAttrBN( id, pre + "Speak-T", "0") == "1") + s += " Ta-Spk"; + if( Earthdawn.getAttrBN( id, pre + "ReadWrite-T", "0") == "1") + s += " Ta-RW"; + s += "
    "; + } + }); // End for each attribute. + s += "
    "; + if( cnt ) + edParse.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, + getAttrByName( id, "character_name" ) + " - Languages" ); + } + } break; // end languages + case "link": { // chatmenu: Link: (code - T, NAC, SK, SPM, WPN): rowID + // List all existing links for this rowID and ask if to be removed. And a button to add a link. + let pre = Earthdawn.buildPre( ssa[ 2 ], ssa[ 3 ]); + s = "Managing links for " + Earthdawn.codeToName( ssa[ 2 ] ) + " " + Earthdawn.getAttrBN( this.charID, pre + "Name", "") + ". " + + Earthdawn.texttip( "(hover)", "Links are used to automatically integrate a value from another ability in the calculation of this ability. " + + "For example, a Free Talent or Free Matrix should be linked to a Discipline so that its Rank automatically updates when the Circle is updated. " + + "A Knack would be linked to the referenced Talent, so that the Knack rank automatically uses the Talent Rank. " + + "Links can also be used to create combos between elements, such as Surprise Short Sword, " + + "that could be linked with the Talent Surprise Strike and a Weapon Short Sword to get the total damage roll, " + + "or Tiger Dance that could be linked to Talents Tiger Spring and Air Dance to roll the combination of the 2 Talents)"); + + addButtonWithCharID( "Add a Link", "ChatMenu: Linkadd1: " + ssa[ 2 ] + ": " + ssa[ 3 ] + + ": ?{What are we linking this to|Talent,T|Discipline,DSP|Skill,SK|Weapon,WPN|Thread Item,TI|Attribute}: " + + "?{Name to link to (Make sure substring is an exact match, including case. Example - Melee)}", + "Link this to an Talent, Discipline Circle, Skill, Weapon, Thread Item or Attribute, such that this item will use that items Rank." , "param"); + lst = Earthdawn.getAttrBN( this.charID, pre + "LinksGetValue", "" ); + // repeating_talents_xxx_T_name+repeating_talents_xxx_T_name2,another + let lst2 = Earthdawn.getAttrBN( this.charID, pre + "LinksGetValue_max", "" ); + if( lst2.length < 5 ) + lst2 = ""; // strip out any (0) that was put here. + if( lst ) + lst = lst.split( "," ); + if( lst2 ) + lst2 = lst2.split( "," ); + if( !lst || !lst2 || lst.length == 0 ) + s += "Existing links: None."; + else for( let i = 0; i < lst.length; ++i ) { + let t = lst2[ i ], + att = !t.startsWith( "repeating_" ), + code = Earthdawn.repeatSection( 3, t ); + s += "Remove "; + if( att ) + addButtonWithCharID( lst[ i ].trim(), "ChatMenu: LinkRemove: " + ssa[ 2 ] + ": " + ssa[ 3 ] + ": Attribute: " + lst2[ i ], "Make this item no longer linked to this Attribute.", "param2"); + else { + let tmp = "item."; + tmp = Earthdawn.codeToName( code ) + "."; + addButtonWithCharID( lst[ i ].trim(), + "ChatMenu: LinkRemove: " + ssa[ 2 ] + ": " + ssa[ 3 ] + ": " + code + ": " + Earthdawn.repeatSection( 2, t ), + "Make this item no longer linked to this " + tmp, "param2"); + } } + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "API" ); + } break; // end link + case "linkadd1": { // ChatMenu: linkadd1: (code: T, NAC, SK, SPM, WPN): (rowID): (Attribute, T, SK, DSP, TI, or WPN) : (name string to search for) + // User has given us a string to attempt to link to. Search all entries to see if there is a match. Confirm the match. +// debug code +//if( ssa[ 5 ].length < 2 ) ssa[5] = "Air Sailor"; + if( ssa.length < 6 || !ssa[ 2 ] || !ssa[ 3 ] || !ssa[ 4 ] || ssa[ 5 ].length < 2 ) { // Make sure got a valid command line. + this.chat( "Error! linkadd1 not correctly formed.", Earthdawn.whoFrom.apiError ); + log( ssa ); + return; + } + let attrib = (ssa[ 4 ] === "Attribute"), skipmost = false; + if( attrib ) { // Check for the "real" attributes first, and if you find one, don't search for any more. + let tar = [ "dex", "str", "tou", "per", "wil", "cha" ], + t = Earthdawn.safeString( ssa[ 5 ] ).toLowerCase(), + effect = ( t.indexOf( "-effect" ) !== -1 ), + step = ( t.indexOf( "-step" ) !== -1 ); + for( let i = 0; i < tar.length; ++i ) + if( t.indexOf( tar[ i ] ) !== -1 ) { + ssa[ 5 ] = Earthdawn.safeString( tar[ i ].slice(0, 1)).toUpperCase() + tar[ i ].slice( 1 ) + (effect ? "-Effect" : "") + (step ? "-Step" : ""); + skipmost = true; + } + } + if( !skipmost ) { + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }), + found = []; + // if( ssa[ 4 ] === "T" ) { + // let tar = [ "SP-Spellcasting-Effective-Rank", "SP-Patterncraft-Effective-Rank", "SP-Elementalism-Effective-Rank", "SP-Illusionism-Effective-Rank", + // "SP-Nethermancy-Effective-Rank", "SP-Shamanism-Effective-Rank", "SP-Wizardry-Effective-Rank", "SP-Power-Effective-Rank", "SP-Willforce-Effective-Rank" ]; + // for( let i = 0; i < tar.length; ++i ) + // if( tar[ i ].indexOf( ssa[ 5 ] ) !== -1 ) { + // attrib = true; + // ssa[ 4 ] = "Attribute"; + // ssa[ 5 ] = tar[ i ]; + // } + // } + if( attrib ) { + _.each( attributes, function (att) { + if ( att.get( "name" ).indexOf( ssa[ 5 ] ) != -1) + found.push( att ); + }); // End for each attribute. + } else { // It is a repeating section, check each name. + let lookfor = "_" + ssa[ 4 ] + "_Name", + strMatch = Earthdawn.safeString( ssa[ 5 ] ).toLowerCase(); + _.each( attributes, function (att) { + if( Earthdawn.safeString( att.get("name")).endsWith( lookfor ) && Earthdawn.safeString( att.get( "current")).toLowerCase().indexOf( strMatch ) != -1) + found.push( att ); + }); } // End for each attribute. + if( found.length == 0 ) { + this.chat( "No matches found.", Earthdawn.whoFrom.apiWarning); + break; + } else if ( found.length == 1 ) + ssa[ 5 ] = attrib ? found[ 0 ].get( "name" ) : Earthdawn.repeatSection( 2, found[ 0 ].get( "name" ) ); + // This option immediately falls into lindadd2. + else { + s = found.length + " matches found. Select which to Link: "; + for( let i = 0; i < found.length; ++i ) + addButtonWithCharID( ssa[ 4 ] + " " + ( attrib ? found[ i ].get( "name" ) + " " : "") + found[ i ].get( "current" ) + , "ChatMenu: Linkadd2: " + ssa[ 2 ] + ":" + ssa[ 3 ] + ": " + ssa[ 4 ] + ": " + + (attrib ? found[ i ].get( "name" ) : Earthdawn.repeatSection( 2, found[ i ].get( "name" ) )), + "Add this Link." , "param"); + this.chat( s.trim(), Earthdawn.whoFrom.apiWarning | Earthdawn.whoFrom.noArchive, "API" ); + break; + } } } // end linkAdd1 + case "linkadd2": { // User has pressed a button telling us which exact one to link to OR we fell through from above due to only finding only one candidate. + // Here we do the actual linking. + // ChatMenu: linkadd2: (code: T, NAC, SK, SPM, WPN): (rowID): (Attribute, T, SK, DSP, TI, or WPN): link rowID or attribute name + // First code/rowID combo is row that is making link (getting a value). + // Second code/rowID combo is row that is being linked to (providing a value). + // Can make a link T, NAC, SK, SPM, WPN + // Can link to T, SK, WPN, DSP, TI, attributes???. Maybe static talents. // note removed NAC + if( ssa.length < 6 || !ssa[ 2 ] || !ssa[ 3 ] || !ssa[ 4 ] || ssa[ 5 ].length < 2 ) { // Make sure got a valid command line. + this.chat( "Error! linkadd2 not correctly formed.", Earthdawn.whoFrom.apiError ); + log( ssa ); + return; + } + // walk links. Check to make sure that nothing links back to this (or anything else in the tree). + function walkLinks( nextCode, nextRow, badLink, nameList ) { // Check to make sure row being linked does not reference row adding link. + let links = Earthdawn.getAttrBN( edParse.charID, Earthdawn.buildPre( nextCode, nextRow) + "LinksGetValue_max" ); + if( links ) { + let alinks = links.split(); + for( let i = 0; i < alinks.length; ++i ) { + if( alinks[ i ].indexOf( badLink ) !== -1 ) { + edParse.chat( "Error! attempted circular reference: " + nameList + " to " + + Earthdawn.getAttrBN( edParse.charID, Earthdawn.buildPre( nextCode, nextRow) + "Name", Earthdawn.whoFrom.apiWarning )); + return false; + } else if( alinks[ i ].startsWith( "repeating_" )) + if ( !walkLinks( Earthdawn.repeatSection( 3, alinks[ i ]), Earthdawn.repeatSection( 2, alinks[ i ]), badLink, + nameList + " to " + Earthdawn.getAttrBN( edParse.charID, Earthdawn.buildPre( nextCode, nextRow) + "Name", "" ))) + return false; + } } + return true; + }; // end walkLinks() + + if( !walkLinks( ssa[ 4 ], ssa[ 5 ], ssa[ 3 ], Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( ssa[ 2 ], ssa[ 3 ]) + "Name", "" ))) + return; + let att = (ssa[ 4 ] === "Attribute"); + + // Given a fully qualified argument name, it will Add the Display and Links. + function linkAdd( linkName, dispAdd, linkToAdd) { + let obj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: edParse.charID, name: linkName }); + let dlst = obj.get( "current" ), + llst = obj.get( "max" ); + if( !llst || llst.length < 5 ) { // there are no existing links, so we are adding the first one. + llst = []; + dlst = []; + } else { // There are existing links. + llst = llst.split( "," ); + dlst = dlst.split( "," ); + if( llst.length != dlst.length ) { + edParse.chat( "Warning! internal data mismatch in linkadd2.", Earthdawn.whoFrom.apiError); + Earthdawn.errorLog( "Warning! internal data mismatch in linkadd2.", edParse); + log( dlst ); + log( llst); + llst = []; + dlst = []; + } + } + dlst.push( dispAdd ); + llst.push( linkToAdd ); + Earthdawn.set( obj, "max", llst.join(), linkToAdd ); + //log( "setww " + obj.get( "name" ) + " from: " + obj.get( "current" ) + " to: " + dlst.join()); + Earthdawn.setWithWorker( obj, "current", dlst.join(), dispAdd ); + + let sflag = false; + if( linkName.startsWith( "repeating_" ) ) { + let code = Earthdawn.repeatSection( 3, linkName ) + if( code === "T" || code === "NAC" || code === "SK" || code === "WPN" || code === "SPM" ) + sflag = true; + } else + sflag = true; + if( sflag ) +//{ +// edParse.addSWflag( "Trigger2", linkName ); + edParse.toSheet( "Trigger2", linkName ); +//log(333); log(linkName);} + + } // end linkAdd + + let t = att ? ssa[ 5 ] : Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( ssa[ 4 ], ssa[ 5 ] ) + "Name", "" ), + l; + function lnk() { + l = ""; + for( let i = 0; i < arguments.length; ++i ) { + let tmp = (att ? arguments[ i ] : Earthdawn.buildPre( ssa[ 4 ], ssa[ 5 ] )) + arguments[ i ]; + l += ((i === 0) ? "" : "+" ) + tmp; + } + }; + // repeating_talents_xxx_T_name+repeating_talents_xxx_T_name2,another + switch( ssa[ 4 ] ) { + case "Attribute": lnk( ssa[ 5 ] ); break; + case "DSP": lnk( "Circle" ); break; + case "NAC": lnk( "Step" ); break; + case "TI": lnk( "Rank" ); break; + default: lnk( "Effective-Rank" ); break; // T, SK, WPN + } + linkAdd( Earthdawn.buildPre( ssa[ 2 ], ssa[ 3 ]) + "LinksGetValue", + t.replace( /[\,|\+]/g, ""), // absolutely can't have any commas or plus signs in the name. + l); // LinksGetValue are of form comma delimited list of fully qualified attributes, maybe more than one separated by plus signs. + if( !att ) // LinksProviceValue are comma delimited lists of form (code);(rowID). + linkAdd( Earthdawn.buildPre( ssa[ 4 ], ssa[ 5 ]) + "LinksProvideValue", + Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( ssa[ 2 ], ssa[ 3 ] ) + "Name", "" ).replace( /[\,|\+]/g, ""), + ssa[ 2 ] + ";" + ssa[ 3 ]); + this.chat( "Linked " + ssa[ 4 ] + "-" + t, Earthdawn.whoFrom.apiWarning | Earthdawn.whoFrom.noArchive, "API" ); +// this.sendSWflag(); + } break; // end linkAdd2 + case "linkremove": // ChatMenu: linkRemove: (code: T, NAC, SK, SPM, WPN): (rowID): ( Attribute, T, SK, WPN, DSP, NAC, or WPN): linked rowID to be removed or Attrubite name. + case "linkremovehalf": { // ChatMenu: linkRemoveHalf: attribute name, rowID to remove. + // linkRemove if the main entry point, called when the user presses a button requesting a link be removed. + // linkRemoveHalf is a secondary entry point, called by the on destroy routine when it detects that a linked row has been deleted, it removes the other half of the link. + let done = 0; + + // Given a fully qualified argument name (repeating_talents_XXX_T_LinksProvideValue), and a rowID, + // will remove any links matching that rowID from the named argument. + function linkRemove( linkName, rowID, countIt ) { + let obj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: edParse.charID, name: linkName }, ""); + let dlst = obj.get( "current" ), + llst = obj.get( "max" ); + if( !llst || llst.length < 5 ) { // there are no existing links. + llst = []; + dlst = []; + } else { // There are existing links. + llst = llst.split( "," ); + dlst = dlst.split( "," ); + if( llst.length != dlst.length ) { + edParse.chat( "Warning! internal data mismatch in linkRemove.", Earthdawn.whoFrom.apiError); + Earthdawn.errorLog( "Warning! internal data mismatch in linkRemove.", edParse); + log( dlst ); + log( llst); + llst = []; + dlst = []; + } + } + for( let i = llst.length - 1; i > -1; --i ) + if(llst[ i ].indexOf( rowID ) !== -1 ) { + llst.splice( i, 1); + dlst.splice( i, 1); + if( countIt ) + ++done; + } + Earthdawn.set( obj, "max", llst.join(), "" ); +//log( "setww " + obj.get( "name" ) + " from: " + obj.get( "current" ) + " to: " + dlst.join()); + Earthdawn.setWithWorker( obj, "current", dlst.join(), "" ); + + let sflag = false; + if( linkName.startsWith( "repeating_" ) ) { + let code = Earthdawn.repeatSection( 3, linkName ) + if( code === "T" || code === "NAC" || code === "SK" || code === "WPN" || code === "SPM" ) + sflag = true; + } else + sflag = true; + if( sflag ) +// edParse.addSWflag( "Trigger2", linkName ); + edParse.toSheet( "Trigger2", linkName ); + } // end linkRemove + + + if( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() === "linkremovehalf") { + if( ssa.length < 3 ) // Make sure got a valid command line. + return; + linkRemove( ssa[ 2 ], ssa[ 3 ] ); + } else { // this is the full linkRemove + // Can link to T, SK, WPN, DSP, TI, attributes???. Maybe static talents. // note removed NAC + if( ssa.length < 5 || ssa[ 5 ].length < 2 ) // Make sure got a valid command line. + return; + let att = (ssa[ 4 ] === "Attribute"); + linkRemove( Earthdawn.buildPre( ssa[ 2 ], ssa[ 3 ]) + "LinksGetValue", ssa[ 5 ], true); + if( !att ) + linkRemove( Earthdawn.buildPre( ssa[ 4 ], ssa[ 5 ]) + "LinksProvideValue", ssa[ 3 ], false); + this.chat( done + " links removed.", Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "API" ); + } +// this.sendSWflag(); + } break; // end linkRemove + case "oppmnvr": { // Opponent Maneuverer. Here we are just displaying a list of buttons that are possible. + // Called from a button on RollESbuttons(). (If have extra successes) + // Input string is "!Earthdawn~ chatmenu: oppmnvr: SetToken/CharID: id: target ID + let cID = Earthdawn.tokToChar( ssa[ 2 ] ), // NOTE: This is the Target cID. Not the acting cID. + lst = Earthdawn.getAttrBN( cID, "ManRowIdList", "bad" ), + pIsGM = playerIsGM( this.edClass.msg.playerid ), + t = ""; + function makeButtonLocal( txt, lnk, es, tip ) { + let tssa = "!Earthdawn~ setToken: " + ssa[ 2 ] + "~ OpponentManeuver: " + lnk + (es ? es : ""); + t += Earthdawn.makeButton( txt, tssa, tip, "damage" ); + }; + + let sectPlayer = new HtmlBuilder( "", "", { class: "sheet-rolltemplate-chatbox" }); + let sectSpell = edParse.newSect(); + if ( Earthdawn.getAttrBN( cID, "Opponent-ClipTheWing", "0", true ) > 0 ) + makeButtonLocal( "Clip the Wing", "ClipTheWing", 2, + "The attacker may spend two additional successes from an Attack test to remove the creature’s ability to fly until the end of the next round. If the attack causes a Wound, the creature cannot fly until the Wound is healed. If the creature is in flight, it falls and suffers falling damage for half the distance fallen." ); + if ( Earthdawn.getAttrBN( cID, "Opponent-CrackTheShell", "0", true ) > 0 ) + makeButtonLocal( "Crack the Shell", "CrackTheShell", ": ?{How many successes to spend Cracking the Shell|1}", + "Importaint! Press this button AFTER rolling and applying damage, then press this button to record your manuver. The attacker may spend extra successes from physical attacks (not spells) to reduce the creature’s Physical Armor by 1 per success spent. This reduction takes place after damage is assessed, and lasts until the end of combat." ); + if ( Earthdawn.getAttrBN( cID, "Opponent-Defang", "0", true ) > 0 ) + makeButtonLocal( "Defang", "Defang", ": ?{How many successes to spend Defanging|1}", + "The opponent may spend additional successes to affect the creature’s ability to use its poison. Each success spent reduces the Poison’s Step by 2. If the attack causes a Wound, the creature cannot use its Poison power at all until the Wound is healed." ); + if ( Earthdawn.getAttrBN( cID, "Opponent-Enrage", "0", true ) > 0 ) + makeButtonLocal( "Enrage", "Enrage", ": ?{How many successes to spend Enraging|1}", + "An opponent may spend additional successes from an Attack test to give a -1 penalty to the creature’s Attack tests and Physical Defense until the end of the next round. Multiple successes may be spent for a cumulative effect." ); + if ( Earthdawn.getAttrBN( cID, "Opponent-Provoke", "0", true ) > 0 ) + makeButtonLocal( "Provoke", "Provoke", undefined, + "The attacker may spend two additional successes from an Attack test to enrage the creature and guarantee he will be the sole target of the creature’s next set of attacks. Only the most recent application of this maneuver has any effect." ); + if ( Earthdawn.getAttrBN( cID, "Opponent-PryLoose", "0", true ) > 0) + makeButtonLocal( "Pry Loose", "PryLoose", ": ?{How many successes to spend Prying Loose|1}", + "The attacker may spend additional successes from an Attack test to allow a grappled ally to immediately make an escape attempt with a +2 bonus per success spent on this maneuver." ); + if( lst !== "bad" && lst.length > 1 ) { + let arr = lst.split( ";" ); + for( let i = 0; i < arr.length; ++i ) { + let pre = Earthdawn.buildPre( "MAN", arr[ i ] ); + if(( Earthdawn.getAttrBN( cID, pre + "Type", "1") === "-1") && (pIsGM || ( Earthdawn.getAttrBN( cID, pre + "Show", "") === "1" ))) { // type -1 is Opponent + let n = Earthdawn.getAttrBN( cID, pre + "Name", ""), + d = Earthdawn.getAttrBN( cID, pre + "Desc", "").replace(/\n/g, " "); + if( n.length > 0 || d.length > 0 ) + makeButtonLocal( n, "Custom" + arr[ i ], ": ?{How many successes to spend on this maneuver|1}", d.replace( "\n", " ") ); + } } } + sectPlayer.append( "", t); + this.chat( "Opponent Maneuvers " + sectPlayer.toString(), Earthdawn.whoTo.gm | Earthdawn.whoTo.player | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character | Earthdawn.whoFrom.noArchive); // added the GM : objective is to have the GM see the buttons pushed in combat to help novice players + } break; // end oppmnvr + case "skills": { // List out all skills + // Called from a macro Token action (visible when any character is selected). + lst = this.getUniqueChars( 1 ); + for( let k in lst ) { + id = k; + let s1 = [], s2 = [], s3 = []; + s = ""; + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_SK_Name" )) { + s1.push( { a: Earthdawn.getAttrBN( id, Earthdawn.buildPre( "SK", att.get("name")) + "Name", ""), + b: "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.buildPre( att.get("name")) + "Roll}", + c: Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "Notes", "" ).replace( /\n/g, Earthdawn.constantIcon( "cr" )) }); + } else if (att.get("name").endsWith( "_SKK_Name" )) { + s2.push( { a: Earthdawn.getAttrBN( id, Earthdawn.buildPre( "SKK", att.get("name") ) + "Name", ""), + b: "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.buildPre( att.get("name")) + "Roll}"}); + } else if (att.get("name").endsWith( "_SKA_Name" )) + s3.push( { a: Earthdawn.getAttrBN( id, Earthdawn.buildPre( "SKA", att.get("name") ) + "Name", ""), + b: "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.buildPre( att.get("name")) + "Roll}"}); + }); // End for each attribute. + for( let j = 0; j < s1.length; ++j ) + s += Earthdawn.makeButton( s1[j].a, s1[j].b, s1[j].c, "action"); + for( let j = 0; j < s2.length; ++j ) + s += Earthdawn.makeButton( s2[j].a, s2[j].b, null, "action" ); + for( let j = 0; j < s3.length; ++j ) + s += Earthdawn.makeButton( s3[j].a, s3[j].b, null, "action" ); + s += Earthdawn.makeButton( "Languages", "!Earthdawn~ chatmenu: languages", null, "dflt" ); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, getAttrByName( id, "character_name" ) + " - Skills" ); + } + } break; // end Skills + case "spells": { // List out all the spellcasting skills, spells in matrix, and spells in grimour. + // Called from a macro Token Action. Visible when a character who does not have spells hidden is selected. + lst = this.getUniqueChars( 1 ); + let s2 = ""; + for( let k in lst ) { + id = k; + s = ""; + + let attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function (att) { // First we are listing all the spellcasting Talents. + if( att.get( "name" ).endsWith( "_Special" ) && Earthdawn.keywordCheck( att.get( "current" ), true, "SPL-" )) { + let pre = Earthdawn.buildPre( att.get( "name" )); + s += Earthdawn.makeButton( getAttrByName( id, pre + "Name" ), "!edToken~ " + Earthdawn.constantButton( "percent" ) + + "{" + id + "|" + pre + "Roll}" + , Earthdawn.getParam( Earthdawn.getAttrBN( id, pre + "DisplayText", "" ), 1, "~") + " " + + Earthdawn.getParam( Earthdawn.getAttrBN( id, pre + "SuccessText", "" ), 1, "~") + , att.get( "current" ).includes("-Willforce") ? "effect" : "action" ); + } + if( att.get("name").endsWith( "_SPM_Contains" )) // Then all the spells in the grimoire + s2 += Earthdawn.makeButton( att.get("current"), "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + + Earthdawn.buildPre( "SPM", att.get("name")) + "Roll}" + , Earthdawn.getParam( Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name")) + "DisplayText", "" ), 1, "~") + " " + + Earthdawn.getParam( Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name")) + "SuccessText", "" ), 1, "~") + , "param" ); + }); // End for each attribute. + } + this.chat( s.trim() + "
    " + s2.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Spells" ); + } break; // end Spells + case "rolltypeedit": // specialized case of stateedit. + entryPoint = 2; + case "stateedit" : { // a general purpose state editor. Other options in GM Special commands change specific state values. With this you can add, change or remove ANY state value or collection. + // chatMenu: stateEdit: (scope): (unused): Display: + // write all of the scope to the chat menu, and if not at top level (state) give button to go up a level. + // Also give a button to go down into each lower level that is already a collection. + // and give buttons to add a new sub-level, and add a new value. + // chatMenu: stateEdit: (scope): : Up. Go up one level of scope and then do a Display. + // chatMenu: stateEdit: (2)(scope): (3) (name): (4) Add: (5)Type: (6)value (string and number only) + // chatMenu: stateEdit: (2)(scope): (3) (name): (4) Change: (5)value (string and number only) + // chatMenu: stateEdit: (scope): (name) : Delete + if( !playerIsGM( this.edClass.msg.playerid ) ) + this.chat( "Only GM can do this stuff!", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + else { + if( entryPoint === undefined ) entryPoint = 1; + let scope = ssa[ 2 ].trim(), + name = ssa[ 3 ].trim(), + currentScope, + up = 0; + let walk = scope.split( "." ); + + function walkit( s ) { // get a pointer to a subset of state that only holds scope. If scope is state.Earthdawn.Rolltype.NPC, then currentScope would only hold NPC and it's values. + let cs = s; + for( let i = 1; i < (walk.length - up); ++i ) + if( walk[ i ] in cs ) + cs = cs[ walk[ i ]]; + else { + edParse.chat( "Warning! internal data mismatch in stateEdit. " + walk[ i ] + " not found.", Earthdawn.whoFrom.apiError); + return; + } + return cs; + } + + function change( whereFrom, value, type ) { // get a value of the correct type, ready to be set into currentScope[name] (defined external). + // wherefrom 1 = addArray, 2 = add, 3 = edit. + let r, t2, set = (whereFrom !== 1), doMsg = (whereFrom !== 1), recursive = false, fake = false; + if( type === undefined ) + if((typeof (currentScope[ name ])) !== undefined ) + type = typeof (currentScope[ name ]); + else if( value !== undefined ) + type = typeof value; + if( type === "object" && Array.isArray( currentScope[ name ] )) + type = "array"; + + switch( Earthdawn.safeString( type ).toLowerCase()) { + case "number": + r = ( Earthdawn.safeString( value ).indexOf( "." ) == -1 ) ? Earthdawn.parseInt2( value ) : parseFloat( value ); + t2 = r; + break; + case "null": + r = null; + t2 = "null"; + break; + case "undefined": + r = undefined; + t2 = "undefined"; + break; + case "string": + r = Earthdawn.safeString( value ).trim(); + t2 = r; + break; + case "boolean": + if( value === "true" || value == "1" ) + r = true; + else + r = false; + t2 = r.toString(); + break; + case "boolean true": + r = true; + t2 = "true"; + break; + case "boolean false": + r = false; + t2 = "false"; + break; + case "array": + r = []; // Here an array is simply set to empty. Adding and removing properties are other routines. + t2 = "empty array"; + fake = true; + break; + case "object": + r = {}; // Here an object is simply set to empty. Adding and removing properties are other routines. + recursive = true; + t2 = "empty object"; + fake = true; + break; + default: + } + + if( set || fake ) // AddArray does its own setting. Add and Edit are set here. + currentScope[ name ] = r; + if( doMsg || fake ) + edParse.chat( name + " changed to " + t2, Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + if( recursive ) + edParse.ChatMenu( ["ChatMenu", "StateEdit", scope + "." + name, "", "Display" ] ); // recursive call with the new scope. + + return fake ? "fake" : r; + } // end change() + + // chatMenu, stateEdit, (2) scope, (3) property name to operate on, ***(4)*** subcommand, (others). + switch( Earthdawn.safeString( ssa[ 4 ] ).toLowerCase()) { + case "up": + up = 1; + case "in": // chatMenu: stateEdit: (scope): In: (a key to be added to scope). + if( !up ) { // if we are in in, without falling down from up. + scope += "." + name; + walk.push( name ); + } + case "display": { // chatMenu: stateEdit: (scope): : Display: + + function tipText( k ) { + let t4; + switch( k ) { + case false: + case "No Override": t4 = "Override not in effect. Dice Rolls sent as specified in defaults and exceptions."; break; + case "GM Only": t4 = "Dice Rolls sent to GMs only. Useful for making test rolls with monsters."; break; + case "Public": t4 = "Dice Rolls sent to everybody."; break; + case "Player and GM": t4 = "Dice Rolls sent to Player and GM but not to anybody else."; break; + case "Controlling Only": t4 = "Dice Rolls sent only to the Player who requested the roll. Useful for a GM making test rolls with monsters."; break; + case "Sheet": t4 = "Turn this feature off, and send Dice Rolls where the sheet tells them to using the old system."; break; + default: t4 = "rolltypeedit data mismatch " + k; + } + return t4; + } + + currentScope = walkit( state ); + if( entryPoint === 2 ) { // RolltypeEdit + if( scope.endsWith( "PC" )) { // We are in the PC or NPC section. + s += "" + scope + " " + "
    "; + s += Earthdawn.makeButton( "Back to main level." // + walk.slice( 0, -1).join( ".") + ,"!Earthdawn~ ChatMenu: rolltypeEdit: " + walk.slice( 0, -1).join( "." ) + ": : Display" + ,"Move back to the Rolltype root." , "param" ); + s += Earthdawn.makeButton( "Default", "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": Default: Change : " + + "?{Change Dice Roll Display Default to|Public|GM Only|Player and GM|Controlling Only}", + + "Change Default Dice Roll display." + , "param" ) + + " " + currentScope[ "Default" ] + " "; + Object.entries( currentScope[ "Exceptions" ] ).forEach(([key, val]) => { + s += Earthdawn.makeButton( val[ "name" ] + ,"!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": " + key + ": " + + "?{Edit or Delete|Edit, exceptionEdit " + + ": ?{change roll display to" + Earthdawn.constantButton( "pipe", 2 ) + "Public" + Earthdawn.constantButton( "pipe", 2 ) + "GM Only" + + Earthdawn.constantButton( "pipe", 2 ) + "Player and GM" + Earthdawn.constantButton( "pipe", 2 ) + "Controlling Only" + + Earthdawn.constantButton( "braceClose", 2 ) + "|Delete, exceptionDelete}" + ,"Edit or Delete exception." , "param" ); + s += " " + val[ "display" ] + " "; + }); // end foreach exception(); + s += Earthdawn.makeButton( "Add new Exception", "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": " + + "?{Talent Skill or Knack name (exactly as it appears in the roll header)}: exceptionAdd : " + + "?{Display|Public|GM Only|Player and GM|Controlling Only}", + + "Add a new exception." + , "param" ); + s += Earthdawn.makeButton( "Refresh", + "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": : Display", + "Display this again, so that you can see what has changed." , "param" ); + } else { // We are in the main menu. + s += "" + scope + " " + "
    "; + s += Earthdawn.makeButton( "Override", "!Earthdawn~ ChatMenu: StateEdit: " + scope + ": Override: Change :" + + "?{Change Dice Roll Override to|No Override, false: boolean|GM Only, GM Only: string|Public, Public: string|" + + "Player and GM, Player and GM: string|Controlling Only, Controlling Only: string}" + , "Change Override. Force all Dice Rolls to go to ther override destination. " + + "Useful when testing monsters and you don't want the players to see the rolls." + , "param" ); + s += Earthdawn.makeButton( currentScope[ "Override" ] === false ? "None" : currentScope[ "Override" ] + , "", tipText( currentScope[ "Override" ] ), "dflt" ); // This button does not do anything, it just providees a place to hang a tooltip. + [ "PC", "NPC" ].forEach( function( what ) { + 'use strict'; + s += "
    "; + s += Earthdawn.makeButton( what, "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": " + what + ": In", "Edit " + what + " default and exceptions", "param" ); + s += "Default: " + currentScope[ what ][ "Default" ] + " "; + s += "Exceptions : " + Object.keys( currentScope[ what ][ "Exceptions" ] ).length; +// s += "Exceptions : " + currentScope[ what ][ "Exceptions" ].length; + }); // end forEach() + s += Earthdawn.makeButton( "Refresh", + "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": : Display", + "Display this again, so that you can see what has changed." , "param" ); + s += Earthdawn.makeButton( "Help", + "!Earthdawn~ ChatMenu: rolltypeEdit: " + scope + ": : Help", + "Helpful hints as to how to set exceptions." , "param" ); + } + } else { // stateEdit + s += "" + scope + " " + JSON.stringify( currentScope ) + "
    "; // Display all of the current scope + if( walk.length > 1 ) + s += Earthdawn.makeButton( "Up to level " + walk.slice( 0, -1).join( "."), + "!Earthdawn~ ChatMenu: StateEdit: " + walk.slice( 0, -1).join( "." ) + ": : Display", + "Move one step closer to the root." , "param" ); + s += Earthdawn.makeButton( "Refresh", + "!Earthdawn~ ChatMenu: StateEdit: " + scope + ": : Display", + "Display this again, so that you can see what has changed." , "param" ); + s += Earthdawn.makeButton( "Add new element or collection ", + "!Earthdawn~ ChatMenu: StateEdit: " + scope + ": ?{Name}: Add: ?{Type" + + "|Number" + Earthdawn.constantButton( "comma" ) + "Number: ?{Value" + Earthdawn.constantButton( "pipe", 2 ) + "0" + Earthdawn.constantButton( "braceClose", 2 ) + + "|String" + Earthdawn.constantButton( "comma" ) + "String: ?{Value" + Earthdawn.constantButton( "braceClose", 2 ) + "|Boolean true|Boolean false|Array|Object}", + "Add an Element, Array or Collection." , "param" ); + _.each( currentScope, function( v, k ) { + let str, tip, bad = false; + + switch( typeof v ) { + // Note, we use nesting level two coding for the pipes and closing brackets. + case "null": + case "undefined": + case "string": str = "?{Edit or Delete string " + k + " Old value ( " + v + " ) |Edit, Edit: ?{New Value" + Earthdawn.constantButton( "braceClose", 2 ) + "|Delete}"; + tip = "Edit or delete this string."; + break; + case "number": str = "?{Edit or Delete number " + k + " Old value ( " + v + " ) |Edit, Edit: ?{New Value" + Earthdawn.constantButton( "pipe", 2 ) + "0" + + Earthdawn.constantButton( "braceClose", 2 ) + "|Delete}"; + tip = "Edit or delete this number."; + break; + case "boolean": str = "?{New Value " + k + " Old value ( " + v + " ) |True, Edit: true|False, Edit: false|Delete}"; + tip = "Set this boolean value to true or false, or delete it."; + break; + case "object": + if( Array.isArray( v ) ) { // This is best practices for building Roll20 nested chat menu queries. + // The outermost query can be of the form ?{}. Inner queires must call Earthdawn.constantButton for pipe, comma, and braseClose. + str = "?{What do you want to do with array \'" + k + "\'|" + + "Delete|Add Elements, arrayAdd: " + + "?{Add Where (F for first. L for last. or after a zero based index number)" + Earthdawn.constantButton( "braceClose", 2 ) + ": " + + "?{Add What type of element" + + Earthdawn.constantButton( "pipe", 2 ) + "Number" + Earthdawn.constantButton( "comma", 2 ) + + "Number: ?{Value" + Earthdawn.constantButton( "pipe", 3 ) + "0" + Earthdawn.constantButton( "braceClose", 3 ) + + Earthdawn.constantButton( "pipe", 2 ) + "String" + Earthdawn.constantButton( "comma", 2 ) + "String: ?{Value" + Earthdawn.constantButton( "braceClose", 3 ) + + Earthdawn.constantButton( "pipe", 2 ) + "Boolean true" + Earthdawn.constantButton( "pipe", 2 ) + "Boolean false" + + Earthdawn.constantButton( "pipe", 2 ) + "Array" + Earthdawn.constantButton( "pipe", 2 ) + "Object" + Earthdawn.constantButton( "braceClose", 2 ) + + "|Remove Elements, arrayRemove: " + + "?{Remove starting at what index number (zero based)" + Earthdawn.constantButton( "pipe", 2 ) + "0" + Earthdawn.constantButton( "braceClose", 2 ) + + ": ?{Number of items to remove" + Earthdawn.constantButton( "pipe", 2 ) + "1" + Earthdawn.constantButton( "braceClose", 2 ) + +"|Set to Empty, change}"; + tip = "This routine can not do anything with arrays yet except delete them, or set them to empty."; + } else { + str = "?{What do you want to do with object|Edit|Into object " + k + ",In|Delete}"; + tip = "Make this object the current object, so that you can manipulate (edit or delete) its properties."; + } + break; + default: + log("stateEdit error. Unknown typeof " + typeof v + " for " + k + " Old value ( " + v + " )"); + bad = true; + } + if( !bad ) s += Earthdawn.makeButton( k, "!Earthdawn~ ChatMenu: StateEdit: " + scope + ": " + k + ":" + str, tip , "param" ); + }); + } // end entrypoint 1 + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + } break; // end display + case "add": { + currentScope = walkit( state ); + change( 2, ssa[ 6 ], ssa[ 5 ] ); // for array and object, ssa6 will be undefined, which is OK. + } break; + case "arrayadd": { // Add an element to an existing array. Chatmenu: stateEdit: state: name of array: ArrayAdd: (5) F, L, or zero based index number: (6) type : (7) Value (number or string only). + if( ssa.length < 7 ) { + this.chat( "Warning!!! Data mismatch in editState: arrayAdd. Not enough arguments.", Earthdawn.whoFrom.apiWarning ); + log( ssa ); + } else { + currentScope = walkit( state ); + + if( !Array.isArray( currentScope[ name ] )) + this.chat( "Warning!!! Data mismatch in editState: arrayAdd. " + (typeof (currentScope[ name ])), Earthdawn.whoFrom.apiWarning ); + else { + let t = change( 1, ssa[ 7 ], ssa[ 6 ]); + if( t !== "fake" ) { + let t3 = JSON.stringify( currentScope[ name ]); + if( ssa[ 5 ].toUpperCase() == "F" ) + currentScope[ name ].unshift( t ); + else if( ssa[ 5 ].toUpperCase() == "L" ) + currentScope[ name ].push( t ); + else + currentScope[ name ].splice( Earthdawn.parseInt2( ssa[ 5 ]), 0, t ); + + this.chat( name + " changed from " + t3 + " to " + JSON.stringify( currentScope[ name ] ), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + } } } } break; // end arrayAdd + case "arrayremove": { // ChatMenu: StateEdit: (scope): arrayRemove: (index): (number) + if( ssa.length < 6 ) { + this.chat( "Warning!!! Data mismatch in editState: arrayRemove.", Earthdawn.whoFrom.apiWarning ); + log( ssa ); + } else { + currentScope = walkit( state ); + if( !Array.isArray( currentScope[ name ] )) + this.chat( "Warning!!! Data mismatch in editState: arrayRemove." + (typeof currentScope[ name ]), Earthdawn.whoFrom.apiWarning ); + else { + + let i = Earthdawn.parseInt2( ssa[ 5 ] ), // index number + n = Earthdawn.parseInt2( ssa[ 6 ] ), // number to remove + l = currentScope[ name ].length; + if( l < i ) + this.chat( "Warning!!! can't delete array element starting at " + i + " of an array of length " + l, Earthdawn.whoFrom.apiWarning ); + else { + let t3 = JSON.stringify( currentScope[ name ]); + currentScope[ name ].splice( i, n ); + this.chat( name + " changed from " + t3 + " to " + JSON.stringify( currentScope[ name ] ), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + } } } } break; + case "edit": + case "change": { + currentScope = walkit( state ); + change( 3, ssa[ 5 ], ssa[ 6 ] ); + } break; + case "delete": { + currentScope = walkit( state ); + delete currentScope[ name ]; + this.chat( name + " deleted.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + } break; + case "exceptionadd": + currentScope = walkit( state )[ "Exceptions" ]; + let ms = Earthdawn.matchString( name ); + currentScope[ ms ] = { name: name, display: ssa[ 5 ] }; + this.chat( currentScope[ ms ][ "name" ] + " added and set to " + ssa[ 5 ] + ".", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + break; + case "exceptiondelete": + currentScope = walkit( state )[ "Exceptions" ]; + if( name in currentScope ) { + this.chat( "Exception '" + currentScope[ name ][ "name" ] + "' deleted.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + delete currentScope[ name ]; + } else + this.chat( "Exception not found.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + break; + case "exceptionedit": + currentScope = walkit( state )[ "Exceptions" ]; + currentScope[ name ][ "display" ] = ssa[ 5 ]; + this.chat( currentScope[ name ][ "name" ] + " changed to " + ssa[ 5 ] + ".", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + break; + case "help": // helpful hints on how to use this subsystem. + this.chat( "Roll results can be sent to everybody (public) or the GM only. Player & GM allows rolls to go only to the two people who need to see them.
    " + + "Setting an override changes all roll results systemwide. This would normally be done by a GM who wants to test out some creatures before a game without " + + "the players seeing what the creatures are. Setting to GM Only will do that. If there is more than one person with GM permissions, then setting to " + + "'Controling player only' will make it so that only the person who requested the roll will see the results.
    " + + "By default all PC rolls are set to Public, and all NPC rolls are set to GM only, but these can be changed.
    " + + "Exceptions can also be set on a per Talent or Knack name basis.
    " + + "The easiest way to set an exception is to make a roll, and click on the icon in the upper right header.
    " + + "That will give a menu where you can set an exception for rolls exactly like that roll.
    " + + "Alternativly, you can use this menu to add an exception, spelling the header of the roll " + + "(for example Awareness) exactly as it is spelled in the Talent or Skill name." + , Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + break; + case "nothing": // they changed their mind. Do nothing. + break; + default: + this.chat( "Earthdawn stateEdit Error. Unknown command " + ssa[ 4 ], Earthdawn.whoFrom.apiWarning ); + log( ssa ); + } } } + break; // end stateEdit + case "rolltypemulti": { // ssa[ 2 ] = state.Earthdawn.Rolltype.NPC, Others are ether exceptionIs or exceptionWouldBe entries. + let except = ssa[ 2 ].endsWith( ".NPC" ) ? state.Earthdawn.Rolltype.NPC.Exceptions : state.Earthdawn.Rolltype.PC.Exceptions, + wkey, wname, dname, + s = "Do you want to: "; + for( let i = 3; i < ssa.length; ++i ) { + let tip; + switch( ssa[ i ].toLowerCase() ) { + case "skillexceptionis": + tip = "This display exception is setup by skill type: Knowledge or Artisan. Exceptions can also be setup by skill name."; + case "exceptionis": + if( tip === undefined ) tip = "This display exception is setup by skill name. They can also be setup by skill type: Knowledge or Artisan."; + wname = ssa[ ++i ]; + if( wname in except ) { + dname = except[ wname ][ "name" ]; + s += "Edit display exception for " + + Earthdawn.makeButton( dname, "!Earthdawn~ ChatMenu: RolltypeEdit: " + ssa[ 2 ] + ": " + wname + + ": ?{Edit display exception for '" + dname + "'|Change to Public, exceptionEdit: Public|Change to GM Only, exceptionEdit: GM Only" + + "| Change to Player and GM, exceptionEdit: Player and GM|Delete exception, exceptionDelete}", tip, "action" ) + "."; + } else + this.chat( "Earthdawn rolltypeMulti data mismatch Error. " + wname + " not found.", Earthdawn.whoFrom.apiWarning ); + break; + case "skillexceptionwouldbe": + case "exceptionwouldbe": + tip = "You can setup rolltype exceptions for Knowledge and Artisan skills ether by name or type."; + wname = ssa[ ++i ]; + s += "Create a display exception for " + + Earthdawn.makeButton( wname, "!Earthdawn~ ChatMenu: RolltypeEdit: " + ssa[ 2 ] + ": " + wname + ": ?{Create a display exception for '" + wname + + "'|Public, exceptionAdd: Public|GM Only, exceptionAdd: GM Only|Player and GM, exceptionAdd: Player and GM}", tip, "action" ) + "."; + break; + default: + this.chat( "Earthdawn rolltypeMulti Error. Unknown command " + ssa[ i ], Earthdawn.whoFrom.apiWarning ); + log( ssa ); + } } + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmStateEdit" ); + } break; // end RolltypeMulti + case "status": { // Called from a macro Token action. Visible when any character is selected. + let basic = true; + + function buildLabelStatus( mi, markers ) { + let label = mi["prompt"], + bset = (markers.indexOf( "," + Earthdawn.getIcon( mi ) + ",") != -1), + vset = (markers.indexOf( "," + Earthdawn.getIcon( mi ) + "@") != -1); + basic = false; + if( !bset && !vset) // this icon is not set on the token - just use label + { + basic = true; + return label; + } + else if ( mi["submenu"] === undefined ) // There is no submenu, so it is a boolean toggle that is on. + return label; + else if( mi[ "code" ] === "health" ) { // Health is special case that needs to be hardcoded as it uses two icons. + if( markers.indexOf( "," + "skull" + ",") != -1 ) + return label + "-Dying"; + else + return label + "-Unconscious"; + } else { // The icon is set on the token, and there is a submenu. Figure out what the submenu item is that is set. + let res = markers.match( new RegExp( "," + mi["icon"] + "@\\d" ) ); + if( res === null ) + return label; + let badge = res[0].slice(-1), + badge2 = String.fromCharCode( badge.charCodeAt( 0 ) + 48 ), // This should translate 1, 2, 3, etc into a, b, c, etc. + t = mi["submenu"], + res2 = t.match( new RegExp( "\\|[\\w\\s]+,\\[\\d\\^" + badge2 ) ); + if( res2 === null ) + return label + "-" + badge; + else + return label + " - " + Earthdawn.getParam( res2[0], 1, "," ).slice( 1 ).trim(); + } + } // end function buildLabelStatus + + lst = this.getUniqueChars( 2 ); + let markers = ",,"; + if ( lst.length > 0 ) { + id = lst[ 0 ].token; // Use first token as template + let TokObj = getObj("graphic", id); + markers = "," + TokObj.get( "statusmarkers" ) + ","; + } + + _.each( Earthdawn.StatusMarkerCollection, function( menuItem ) { + let sm = menuItem[ "submenu" ], + shared = menuItem[ "shared" ]; + if(( shared !== undefined ) && ( Earthdawn.safeString( shared ).toLowerCase().startsWith( "pos" ) || Earthdawn.safeString( shared ).toLowerCase().startsWith( "neg" ))) + return; // The ones that meet this condition are all listed on the Attribs menu, so don't need to be listed here. + // If there is no submenu, toggle the current value from set to unset or visa versa. + // If there is a submenu, but it just asks for a numeric value ( no [n^a] structure ), prefix the value with a "z". + // Otherwise, strip the [n^a] structure of everything except the alpha bit. + s += Earthdawn.makeButton( buildLabelStatus( menuItem, markers ), + "!Earthdawn~ foreach: st~ marker: " + menuItem[ "code" ] + + ((sm === undefined) ? ": t" : (( sm.indexOf( "^" ) === -1) ? ":z" + sm : ":" + sm.replace(/\[([\w\-]+)\^([\w\-]+)\]/g, "$2"))), + null, basic ? "statusoff" : "statuson" ); + }); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Status" ); + } break; // end Status + case "strainchange": { // strainChange, old strain, new strain. + let att = state.Earthdawn.g1879 ? "Damage-Stun" : "Damage" + let dmg = Earthdawn.getAttrBN( this.charID, att ); + this.Damage( [ "STRAINSILENT", "NA", "AllowNeg", ( Earthdawn.parseInt2( ssa[ 2 ] ) * -1) + Earthdawn.parseInt2( ssa[ 3 ] ) ]); // adjust with difference between old and new values. + this.chat( "Strain changed from " + ssa[ 2 ] + " to " + ssa[ 3 ] + ". Old '" + att + "' was " + dmg + + ". New value " + Earthdawn.getAttrBN( this.charID, att ), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Strain Change" ); + } break; + case "talents": // Called from a macro token action. Visible when any character is selected. + case "talents-non": { // Note that the current version DOES display talents that are token actions, unless this tag is used. + lst = this.getUniqueChars( 1 ); + let non = Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() === "talents-non"; + for( let k in lst ) { + id = k; + s = ""; + + let attributes = findObjs({ _type: "attribute", _characterid: id }); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_T_Name" ) || att.get("name").endsWith( "_NAC_Name")) + if( !non || (Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "CombatSlot", "") != "1")) { + let kw = Earthdawn.keywordCheck( Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "Special", ""), true, "Recovery", "Initiative"), + modtype = Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "Mod-Type", ""), + bg; + if( kw & 0x02 ) bg = "health"; // Recovery or Recovery-Woodskin + else if( kw & 0x04 ) bg = "effect"; // Initiative + else if( modtype === "Action" ) bg = "action"; + else if( modtype === "Effect" ) bg = "effect"; + else if( modtype.startsWith( "Attack" )) bg = "attack"; + else if( modtype.startsWith( "Damage" )) bg = "damage"; + else bg = "action"; + + s += Earthdawn.makeButton( Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "Name", ""), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + id + "|" + Earthdawn.buildPre( att.get("name")) + "Roll}", + Earthdawn.getAttrBN( id, Earthdawn.buildPre( att.get("name") ) + "Notes", "" ).replace( /\n/g, Earthdawn.constantIcon( "cr" )), bg ); + } + }); // End for each attribute. + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, + getAttrByName( id, "character_name" ) + (non ? " - Talents Non-combat" :" - Talents" )); + } + } break; // end Talents + default: + Earthdawn.errorLog( "edParse.ChatMenu() Illegal ssa. ", edParse ); + log( ssa ); + } // end main switch + } catch(err) { Earthdawn.errorLog( "ED.ChatMenu() error caught: " + err, edParse ); log(ssa); } + return false; + } // End ChatMenu() + + + + + // ParseObj.CreaturePower ( ssa ) + // user has pressed button to spend successes activating a creature power. + // ssa[0]: + // CreaturePower: Name of Power: Target ID + // CreaturePower: Custom-MpeF4fBTanxnZtZlKij: char ID + // OpponentManeuver: Name of Power: Target ID: [Optional] number of hits. + // OpponentManeuver: oCustom1: -MejRq_ZKS9AGKSKIwtX: 1 + this.CreaturePower = function( ssa ) { + 'use strict'; + try { + if( ssa[ 1 ].startsWith( "Custom" )) { + let row = Earthdawn.safeString( ssa[ 1 ] ).slice( 6 ).trim(), // trim off the custom, leaving the rowID + cID = Earthdawn.safeString( ssa[ 2 ] ), + pre = Earthdawn.buildPre( "MAN", row), + linenum = 0; + if( Earthdawn.getAttrBN( cID, pre + "Type", "1") ) { // type 1 is Creature + let d = Earthdawn.getAttrBN( this.charID, pre + "Desc", ""); + if( d ) { + let sectCP1 = this.newSect(); + let bodyCP1 = Earthdawn.newBody( sectCP1 ); + bodyCP1.append( (( ++linenum % 2) ? ".odd" : ".even"), d.replace( /\n/g, " ")); + this.chat( sectCP1.toString(), Earthdawn.whoFrom.noArchive, Earthdawn.getAttrBN( this.charID, pre + "Name", "") ); + } + } else // not type is Opponent + if( Earthdawn.getAttrBN( cID, pre + "Show", "0") == "1" ) { + let sectCP2 = this.newSect(); + let bodyCP2 = Earthdawn.newBody( sectCP2 ); + bodyCP2.append( (( ++linenum % 2) ? ".odd" : ".even"), ssa[3] + " successes spent on " + Earthdawn.getAttrBN( cID, pre + "Name", "") + ". " + + Earthdawn.getAttrBN( cID, pre + "Desc", "").replace( /\n/g, " ")); + this.chat( sectCP2.toString(), Earthdawn.whoFrom.noArchive ); + } else + this.chat( "GM has not enabled this Maneuver.", Earthdawn.whoFrom.noArchive ); + } else { + switch (ssa[ 0 ] ) { + case "CreaturePower": + switch (ssa[ 1 ] ) { + case "GrabAndBite": + this.chat( "One success spent Grab and Bite. Automatically grapple opponent. Grappled opponents automatically take bite damage each round until the grapple is broken.", Earthdawn.whoFrom.noArchive ); + break; + case "Hamstring": + this.chat( "One success spent Hamstringing. Movement halved until the end of the next round. If Wound, the penalty lasts until the Wound is healed.", Earthdawn.whoFrom.noArchive ); + break; + case "Overrun": + this.chat( "One success spent Overrunning. Only opponent with a lower Strength Step. Make a Knockdown test against a DN equal to the Attack test result.", Earthdawn.whoFrom.noArchive ); + break; + case "Pounce": + this.chat( "One success spent Pouncing. Force the opponent to make a Knockdown test against a DN equal to the Attack test result if reached on a leap and not too much larger.", Earthdawn.whoFrom.noArchive ); + break; + case "SqueezeTheLife": + this.chat( "Two successes spent on Squeeze the Life. Automatically grapple opponent. Grappled opponents automatically take claw damage each round until the grapple is broken.", Earthdawn.whoFrom.noArchive ); + break; + default: + Earthdawn.errorLog( "CreaturePower had illegal ssa[1]", this ); + log( ssa ); + } + break; + case "OpponentManeuver": // {"content":"!Earthdawn~ setToken: -MejRq_ZKS9AGKSKIwtX~ OpponentManeuver: Provoke" + let oflags = Earthdawn.getAttrBN( this.charID, "CreatureFlags", 0 ); + + switch (ssa[ 1 ] ) { + case "ClipTheWing": + if( oflags & Earthdawn.flagsCreature.ClipTheWing ) + this.chat( "Two successes spent Clipping the Wing. Creature can not fly until the end of next round. If flying, creature falls. If wound, can't fly until heal it.", Earthdawn.whoFrom.noArchive ); + else this.chat( "You can/'t Clip the Wing of this opponent.", Earthdawn.whoFrom.noArchive ); + break; + case "CrackTheShell": + if( oflags & Earthdawn.flagsCreature.CrackTheShell ) { + let attr = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "PA-Buff" }, 0); + let newpa = Earthdawn.parseInt2(Earthdawn.getAttrBN( this.charID, "Physical-Armor", "0")) - Earthdawn.parseInt2(ssa[ 2 ]); + if( newpa < 0 ) { + let newpabuff = Earthdawn.parseInt2( attr.get( "current" )) - (newpa + Earthdawn.parseInt2(ssa[ 2 ])); + Earthdawn.setWithWorker( attr, "current", newpabuff , 0 ); + this.chat( "Spent " + ssa[2] + " successes Cracking the Shell but only needed " + (newpa + Earthdawn.parseInt2(ssa[ 2 ])) + " to remove all remaining armor. Physical armor has been reduced. Important! This is supposed to be done AFTER rolling for damage and applying it.", Earthdawn.whoFrom.noArchive ); + this.MarkerSet([ "sheetDirect", "padebuff" , -newpabuff ]); + } else { + let newpabuff = Earthdawn.parseInt2( attr.get( "current" )) - Earthdawn.parseInt2(ssa[ 2 ]); + Earthdawn.setWithWorker( attr, "current", newpabuff, 0 ); + this.chat( ssa[2] + " successes spent Cracking the Shell. Physical armor has been reduced. Important! This is supposed to be done AFTER rolling for damage and applying it.", Earthdawn.whoFrom.noArchive ); + this.MarkerSet([ "sheetDirect", "padebuff" , -newpabuff ]); + } + } else this.chat( "You can't Crack the Shell of this opponent.", Earthdawn.whoFrom.noArchive ); + break; + case "Defang": + if( oflags & Earthdawn.flagsCreature.Defang ) + this.chat( ssa[2] + " successes spent Defanging. Each one is -2 to Poison steps. If Wound then creature can't use Poison at all.", Earthdawn.whoFrom.noArchive ); + else this.chat( "You can't defang this opponent.", Earthdawn.whoFrom.noArchive ); + break; + case "Enrage": + if( oflags & Earthdawn.flagsCreature.Enrage ) + this.chat( ssa[2] + " successes spent Enraging. Each one is -1 to Attack tests and PD until the end of next round.", Earthdawn.whoFrom.noArchive ); + else this.chat( "You can't Enrage this opponent.", Earthdawn.whoFrom.noArchive ); + break; + case "Provoke": + if( oflags & Earthdawn.flagsCreature.Provoke ) + this.chat( "Two successes spent Provoking. This creature will not attack anybody but you for it's next set of attacks.", Earthdawn.whoFrom.noArchive ); + else this.chat( "You can't provoke this opponent.", Earthdawn.whoFrom.noArchive ); + break; + case "PryLoose": + if( oflags & Earthdawn.flagsCreature.PryLoose ) + this.chat( ssa[2] + " successes spent Prying Loose. A Grappled ally may make an immediate escape attempt at +2 per.", Earthdawn.whoFrom.noArchive ); + else this.chat( "You can't pry anybody loose from this opponent.", Earthdawn.whoFrom.noArchive ); + break; + default: + Earthdawn.errorLog( "Opponent Maneuverer had illegal ssa[1]", this ); + log( ssa ); + } + break; + default: + Earthdawn.errorLog( "CreaturePower had illegal ssa[0]", this ); + log( ssa ); + } } // end else end switch + } catch(err) { Earthdawn.errorLog( "ED.CreaturePower() error caught: " + err, this ); } + } // End CreaturePower() + + + + // This routine is passed a roll result. If this.misc.CursedLuck has been set, Curse the dice as per the horror power. + // This routine also calculates and does + // noPileonDice: If this is set in state, then no individual NPC die will explode more than this many times. + // noPileonStep: If this is set in state, then the total roll result of NPCs will not be very much greater than this multiple of the step number. + // See notes on BuildRoll(). + // roll result examples: + // {"type":"V","rolls":[{"type":"R","dice":1,"sides":20,"mods":{"exploding":""},"results":[{"v":20},{"v":16}]},{"type":"M","expr":"+"},{"type":"R","dice":1,"sides":8,"mods":{"exploding":""},"results":[{"v":6}]},{"type":"M","expr":"+"},{"type":"R","dice":1,"sides":6,"mods":{"exploding":""},"results":[{"v":1}]}],"resultType":"sum","total":43} + // {"type":"V","rolls":[{"type":"R","dice":2,"sides":8,"mods":{"exploding":""},"results":[{"v":8},{"v":6},{"v":3}]}],"resultType":"sum","total":17} + // {"type":"V","rolls":[{"type":"R","dice":2,"sides":8,"mods":{"exploding":""},"results":[{"v":8},{"v":5},{"v":8},{"v":4}]}],"resultType":"sum","total":25} + this.CursedLuck = function( curse, roll ) { + 'use strict'; + try { + let stuffDone = 0; + if( curse == undefined ) + curse = 0; + + // The Cursed Luck horror power has been used. Find The highest dice rolled, and replace them with 1's. + while ( "CursedLuck" in this.misc && curse-- > 0 ) { + let highVal = -1, + highItem, // pointer to item with the highest roll (to be cursed). + highj, highjEnd, + resMod, // Value of the dice to be cursed (and all exploding dice from it). + rollCopy = JSON.parse( JSON.stringify( roll ) ), // We want a brand new copy of this, so turn it into a string and back into an object. + working = rollCopy; // working and rollCopy will point to the same underlying data structure, but they will point to different PARTS of the structure. ie: working will only point to subgroups within the main working structure. + + function walkCurse( item ) { // Walk through the roll structure, extracting what we need. + 'use strict'; + switch ( item[ "type" ] ) { + case "V": // This is the outermost container. It should have ,"resultType":"sum","total":6 at the end. + for( let k1 = 0; k1 < item[ "rolls" ].length; ++k1) + walkCurse( item[ "rolls" ][ k1 ] ); + break; + case "G": // This is a group delimited by brackets. {{1d4!-1}+d1}kh1 is two nested groups. "1d4!-1" and that and d1, with a keep highest 1. + for( let k2 = 0; k2 < item[ "rolls" ].length; ++k2) + for( let k3 = 0; k3 < item[ "rolls" ][ k2 ].length; ++k3 ) + walkCurse( item[ "rolls" ][k2][ k3 ] ); + break; + case "R": // This is a sub-roll result. {"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]} + if( "results" in item ) + for( let j = 0; j < item[ "results" ].length; ++j ) + if( "v" in item[ "results" ][ j ] ) { + let val = item[ "results" ][ j ][ "v" ]; + if( val > highVal || (val == highVal && item[ "sides" ] == val)) { // Gives highest value, In case of a tie it favors dice that will explode. If still tied, takes first non-exploding dice or last rolled exploding dice. + highVal = val; + resMod = val; // value of the dice to be cursed. + highItem = item; // pointer to the curse candidate. + highj = j; // index within item of the roll to be cursed. + + if( ( "mods" in item) && ( "exploding" in item[ "mods" ])) + while( item[ "sides" ] == item[ "results" ][ j ][ "v" ] && ++j < item[ "results" ].length ) + resMod += item[ "results" ][ j ][ "v" ]; // skip through any dice that are just explosions of this one. + highjEnd = j; + } } + break; + case "M": // This is an expression, such as "+" between two items, or "-1" as a modifier to a roll. + break; + default: + Earthdawn.errorLog( "Error in ED.CursedLuck()-A. Unknown type '" + item[ "type" ] + "' in rolls " + item + ". Complete roll is ...", this ); + log( JSON.stringify( rolls )); + } + } // end walkCurse() + walkCurse( working ); // Find the highest result within this roll. + if( highVal > 1 ) { // Then curse that dice. Which is to say, replace the high roll and all dice that exploded from it with a 1. + highItem[ "results" ].splice( highj, highjEnd + 1 - highj, { v: 1}); + rollCopy[ "total" ] = rollCopy[ "total" ] + 1 - resMod; + } + roll = rollCopy; + stuffDone |= 0x04; + } // end while cursed luck + + // No Pile-On Dice + if( Earthdawn.getAttrBN( this.charID, "NPC", "1", true ) > 0 ) { // Pile-on prevention is only done on NPC dice rolls. PCs can pile on all they want. + // Pile on prevention. Dice explosion method. Dice are only allowed to explode so many times (maybe zero times, maybe more). + // Find occurrences of when the dice exploded too many times, and remove the excess. + if( state.Earthdawn.noPileonDice != undefined && state.Earthdawn.noPileonDice >= 0) { + let rollCopy = JSON.parse( JSON.stringify( roll ) ), // We want a brand new copy of this, so turn it into a string and back into an object. + working = rollCopy; // working and rollCopy will point to the same underlying data structure, but they will point to different PARTS of the structure. ie: working will only point to subgroups within the main working structure. + + function walkPileDice( item ) { // Walk through the roll structure, extracting what we need. + 'use strict'; + let expcount = 0, + resMod = 0, + piled; + + switch ( item[ "type" ] ) { + case "V": // This is the outermost container. It should have ,"resultType":"sum","total":6 at the end. + for( let k1 = 0; k1 < item[ "rolls" ].length; ++k1) + walkPileDice( item[ "rolls" ][ k1 ] ); + break; + case "G": // This is a group delimited by brackets. {{1d4!-1}+d1}kh1 is two nested groups. "1d4!-1" and that and d1, with a keep highest 1. + for( let k2 = 0; k2 < item[ "rolls" ].length; ++k2) + for( let k3 = 0; k3 < item[ "rolls" ][ k2 ].length; ++k3 ) + walkPileDice( item[ "rolls" ][k2][ k3 ] ); + break; + case "R": // This is a sub-roll result. {"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]} + if( ("results" in item) && ("mods" in item) && ("exploding" in item[ "mods" ])) + for( let j = 0; j < item[ "results" ].length; ++j ) + if( "v" in item[ "results" ][ j ] ) { + let val = item[ "results" ][ j ][ "v" ]; + if( item[ "sides" ] == val) { // explosion + if( ++expcount > state.Earthdawn.noPileonDice ) { + resMod += val; + if( piled === undefined ) + piled = j; // index of first explosion that was to much. + } + } else { // not explosion. Every results list will end with a dice that did not explode. + if( piled !== undefined ) { + item[ "results" ].splice( piled, j - piled ); // Remove everything between the start of the pile, and the die that did not explode. + rollCopy[ "total" ] -= resMod; + resMod = 0; + j = piled; // We removed entries from the list we were cycling through, so reset out index. + piled = undefined; + stuffDone |= 0x02; + } + expcount = 0; + } } + break; + case "M": // This is an expression, such as "+" between two items, or "-1" as a modifier to a roll. + break; + default: + Earthdawn.errorLog( "Error in ED.CursedLuck()-B. Unknown type '" + item[ "type" ] + "' in rolls " + item + ". Complete roll is ...", this ); + log( JSON.stringify( rolls )); + } + } // end walkPileDice() + + walkPileDice( working ); + roll = rollCopy; + } // End noPileonDice + + + // No Pile-On Step + // Pile on prevention, Step method. The final result is not allowed to greatly exceed a certain multiple of the + // effective step (converting karma, bonuses, and modifiers to steps) of the roll, or the Target Number, whichever is greater. + // For example if noPileonStep is 2.0, then a step 20 roll should not produce a roll very much in excess of 40. + // This section removes excess exploding dice or reduces non-exploding dice until the result is close to the multiple. + let done = false, + lowLimit = Math.max( Math.max( ("targetNum" in this.misc) ? this.misc[ "targetNum" ] : 0, this.misc[ "effectiveStep" ] || 0) * (state.Earthdawn.noPileonStep || 1), 14); // No matter how low the TN, step and multiplier, let it be at least 14 (number picked at arbitrarily) + if( state.Earthdawn.noPileonStep ) + while ( !done && roll[ "total" ] >= (lowLimit + 3)) { + done = true; + let highVal = -1, + highItem, + highj, highjEnd, exploding, + resMod, + rollCopy = JSON.parse( JSON.stringify( roll ) ), // We want a brand new copy of this, so turn it into a string and back into an object. + working = rollCopy; // working and rollCopy will point to the same underlying data structure, but they will point to different PARTS of the structure. ie: working will only point to subgroups within the main working structure. + + function walkPileStep( item ) { // Walk through the roll structure, extracting what we need. + 'use strict'; + switch ( item[ "type" ] ) { + case "V": // This is the outermost container. It should have ,"resultType":"sum","total":6 at the end. + for( let k1 = 0; k1 < item[ "rolls" ].length; ++k1) + walkPileStep( item[ "rolls" ][ k1 ] ); + break; + case "G": // This is a group delimited by brackets. {{1d4!-1}+d1}kh1 is two nested groups. "1d4!-1" and that and d1, with a keep highest 1. + for( let k2 = 0; k2 < item[ "rolls" ].length; ++k2) + for( let k3 = 0; k3 < item[ "rolls" ][ k2 ].length; ++k3 ) + walkPileStep( item[ "rolls" ][k2][ k3 ] ); + break; + case "R": // This is a sub-roll result. {"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]} + if( ("results" in item) && ("mods" in item) && ("exploding" in item[ "mods" ])) + for( let j = 0; j < item[ "results" ].length; ++j ) + if( "v" in item[ "results" ][ j ] ) { + let val = item[ "results" ][ j ][ "v" ]; + // highest dice that will not send total below the threshold we want. + if( val > highVal && (roll[ "total" ] - val) >= lowLimit ) { + highVal = val; + resMod = val; + exploding = (item[ "sides" ] == val); + highItem = item; + highj = j; + } } + break; + case "M": // This is an expression, such as "+" between two items, or "-1" as a modifier to a roll. + break; + default: + Earthdawn.errorLog( "Error in ED.CursedLuck()-C. Unknown type '" + item[ "type" ] + "' in rolls " + item + ". Complete roll is ...", this ); + log( JSON.stringify( rolls )); + } + } // end walkPileStep() + + walkPileStep( working ); + if( highVal > 3 ) { // Remove or reduce this dice. + if( exploding ) { + highItem[ "results" ].splice( highj, 1); + rollCopy[ "total" ] -= resMod; + } else { // replace it with a 1, 2, or 3. + let random = Math.floor(Math.random() * 3) + 1; + highItem[ "results" ][ highj ][ "v" ] = random; + rollCopy[ "total" ] += random - resMod; + } + stuffDone |= 0x01; + done = false; + } + roll = rollCopy; + } // End while noPileonStep + } // End not NPC. + + this.misc[ "FunnyStuffDone" ] = stuffDone; + return roll; + } catch(err) { Earthdawn.errorLog( "ED.CursedLuck() error caught: " + err, this ); } + } // End CursedLuck() + + + + // ParseObj.Damage ( ssa ) + // Apply Damage to Token/Char specified in tokenInfo. + // ssa is an array that holds the parameters. + // Note: These top notes describe the old expected order of arguments, but the routine has been rewritten to accept the + // arguments in any order because sometimes it was difficult to supply them in order. + // 0 - (optional) Damage (default), Strain, Stun, Recovery, Woodskin, notWoodskin; + // Also StrainSilent which does the strain, but does not send a message saying so. + // 1 - Armor Mods: NA = No Armor, PA = subtract Physical Armor before applying damage, MA = Mystic Armor. PA-Nat, MA-Nat. + // 2 - Damage: Amount of damage to apply. For "Recovery", "Woodskin" or "notWoodskin" this will be a negative number. + this.Damage = function( ssa ) { + 'use strict'; + try { + if( ssa.length < 3 ) { + this.chat( "Error! Not enough arguments passed for Damage() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + return; + } + if( this.tokenInfo === undefined ) { + this.chat( "Error! tokenInfo undefined in Damage() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + + let bToken = ( this.tokenInfo.type === "token" ), // We have both a token and a character object. + armor = 0, + armorType = "", armorAbrieve = "", + dmg = 0, + bRecovery = false, + npc = Earthdawn.getAttrBN( this.charID, "NPC", "1" ), + bMook = !((npc == Earthdawn.charType.pc) || (npc == Earthdawn.charType.npc)), + Woodskin = 0, // Obsolete Nov 2023. + bStrain = false, + bStrainSilent = false, + bAllowNeg = false, + bVerbose = false, + bStun = (this.bFlags & Earthdawn.flags.RecoveryStun); // true if RecoveryStun, or if we get the stun flag below. + for( let ind = 0; ind < ssa.length; ++ind) { // Loop though getting any expected parameter, which may show up in various orders. + if( isNaN( ssa[ ind ] )) { + switch ( Earthdawn.safeString( ssa[ ind ] ).toUpperCase() ) { + case "ALLOWNEG": + bAllowNeg = true; + break; + case "PA": + armor = Earthdawn.getAttrBN( this.charID, "Physical-Armor", "0" ); + armorType = " Physical"; + armorAbrieve = "PA"; + break; + case "PA-NAT": + armor = Earthdawn.getAttrBN( this.charID, "PA-Nat", "0" ); + armorType = " Natural Physical"; + armorAbrieve = "PA"; + break; + case "MA": + armor = Earthdawn.getAttrBN( this.charID, "Mystic-Armor", "2" ); + armorType = " Mystic"; + armorAbrieve = "MA"; + break; + case "MA-NAT": + armor = Earthdawn.getAttrBN( this.charID, "MA-Nat", "2" ); + armorType = " Natural Mystic"; + armorAbrieve = "MA"; + break; + case "NOTWOODSKIN": // Obsolete Nov 2023. + ++Woodskin; // Woodskin = 2 + case "WOODSKIN": // Obsolete Nov 2023. + ++Woodskin; // Woodskin = 1. falls into recovery, as woodskin is also recovery. + case "RECOVERY": + case "RECOVERY-WOODSKIN": + bRecovery = true; + break; + case "STRAINSILENT": + bStrainSilent = true; + case "STRAIN": + bStrain = true; + if( state.Earthdawn.g1879 ) bStun = true; + break; + case "STUN": + bStun = true; + break; + case "VERBOSE": + bVerbose = true; + break; + case "DAMAGE": // This is the normal case and requires no handling. In fact in some cases we get this even when it really is Stun or Strain. So don't trust this to mean it is normal damage just because we are here. + case "NA": // No Armor, No action required. + break; + default: + Earthdawn.errorLog( "ED.Damage() unparsable argument " + ind + ": " + JSON.stringify(ssa), this); + } + } else // is number. + dmg += Earthdawn.parseInt2( ssa[ ind ] ); + } // loop getting parameters. + + if( armor < 1 ) + armorType = ""; + if( !bAllowNeg && (bRecovery ? (dmg >= 0) : (dmg <= 0 ))) // If the passed damage evaluates to zero, just exit. + return; + if( bStrain ) { // Edge case. Strain should normally not have armor specified, but Take-Damage and Damage-Target buttons allows user to choose strain and an armor. If they do, remove them. + armor = 0; + armorType = ""; + } + + dmg -= armor; + if( dmg <= 0 && !bRecovery && !bAllowNeg ) { + this.chat( "Attack glances off of " + this.tokenInfo.name +"'s" + (armorAbrieve ? " " + Earthdawn.addIcon( armorAbrieve, "l"): "") + armorType + " Armor." ); + return; + } + + let newMsg = "", gmMsg = "", recMsg = this.tokenInfo.name; + if( bStrain && !bVerbose ) { // Strain normally (when part of some other action) does not have a separate strain message, since the strain is reported as part of the roll results. + if( !bStrainSilent ) + this.misc[ "strain" ] = dmg; + } else if (armor < 1 ) { // No armor + newMsg = this.tokenInfo.name + " took " + dmg + " " + (bStrain ? Earthdawn.addIcon( "strain", "l") + "Strain" + : (bStun ? Earthdawn.addIcon( "stun", "l") + "Stun" : Earthdawn.addIcon( "damage", "l") + "Damage")); + } else if ( npc == Earthdawn.charType.pc ) { + newMsg = this.tokenInfo.name + " took " + dmg + " " + (bStrain ? Earthdawn.addIcon( "strain", "l") + "Strain" + : (bStun ? Earthdawn.addIcon( "stun", "l") + "Stun" : Earthdawn.addIcon( "damage", "l") + "Damage")) + + " above" + (armorAbrieve ? " " + Earthdawn.addIcon(armorAbrieve, "l"): "") + armorType + " armor"; + } else { // NPC + newMsg = this.tokenInfo.name + "'s" + (armorAbrieve ? " " + Earthdawn.addIcon(armorAbrieve, "l"): "") + armorType + " armor absorbs "; + if( dmg < armor ) + newMsg += "most"; + else if ( dmg <= (armor * 2)) + newMsg += "some"; + else + newMsg += "little"; + newMsg += " of the damage"; + } + + let currDmg, attributeDmg; + if( bToken && ( bMook || !(this.tokenInfo.tokenObj.get( "bar3_link" )) )) // If it is a mook, get it from the token. + currDmg = Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar3_value" )); + if( !bMook ) { // if not a mook, get damage from the character sheet. + attributeDmg = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Damage" }, 0); + currDmg = Earthdawn.parseInt2( attributeDmg.get( "current" )); + } + if( !bStun ) { // normal damage. This is identical for characters and mooks. + currDmg += dmg; + if( bRecovery ) { + if( currDmg < 0 ) { // If all damage has been healed, we have to know whether this is a wood skin test or not, since those are handled differently. + if( "Recovery-WoodSkin" in this.misc ) + recMsg += " Wood Skin added " + (dmg * -1) + Earthdawn.addIcon( "damagehealth", "l" ) + " Health. New damage value " + currDmg + "."; + else { // We know this is NOT woodskin and we have negative damage, have the leftover heal stun damage. + recMsg += " recovered " + ((dmg - currDmg) * -1) + Earthdawn.addIcon( "damage", "l" ) + " damage. New value 0."; + bStun = true; + dmg = currDmg; + currDmg = 0; + } + } else // after recovery, there is still damage. + recMsg += " recovered " + (dmg * -1) + Earthdawn.addIcon( "damage", "l" ) + " damage. New value " + currDmg + "."; + } + + if( !bMook ) // if not a mook, set it on the character sheet. + Earthdawn.setWithWorker( attributeDmg, "current", currDmg, 0 ); + if( bToken && ( bMook || !(this.tokenInfo.tokenObj.get( "bar3_link" )) )) // If it is a mook, set it on the token. + Earthdawn.set( this.tokenInfo.tokenObj, "bar3_value", currDmg ); + } // end normal damage section + + let unc = Earthdawn.getAttrBN( this.charID, "Damage_max", 20 ), // for pc's and npc's unc is current unc rating (with stun damage already subtracted). For mooks it is the core characters unc rating. + WoundThreshold = Earthdawn.getAttrBN( this.charID, "Wound-Threshold", 7 ) || 0; + if( bStun ) { // Stun uses a different procedure than normal damage because of the calculated fields on the character sheet. + let attributeStun, stunDmg; + if( !bMook ) { // This is not a Mook, so add the Stun to the character sheet where it will automatically flow to the token value 3. + attributeStun = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Damage-Stun" }, 0); + stunDmg = Earthdawn.parseInt2(attributeStun.get( "current" )); + } else // Only if this is a Mook, take the core characters real unc rating and Subtract the value in bar3_max, which will give this mooks current stun damage. + stunDmg = unc - Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar3_max" )); + stunDmg += dmg; + if( bRecovery && stunDmg != 0 && dmg != 0 ) { + recMsg += (( this.tokenInfo.name.length === recMsg.length ) ? "" : " and" ) + " recovered " + ((stunDmg < 0) ? ((dmg - stunDmg) * -1) : (dmg * -1) ) + + Earthdawn.addIcon( "stun", "l" ) + " stun. New value " + ((stunDmg < 0) ? 0 : stunDmg ) + "."; + if( stunDmg < 0) + stunDmg = 0; + } + if( !bMook ) { + Earthdawn.setWithWorker( attributeStun, "current", stunDmg, 0 ); + unc -= dmg; // Also subtract new dmg from the unc rating here, since we use it later, and the event that sets it probably will not have triggered yet. + } else { + Earthdawn.set( this.tokenInfo.tokenObj, "bar3_max", unc - stunDmg ); // For mooks, bar3 max is character unc rating, minus stunDmg. + unc -= stunDmg; // for PCs (above) we just subtract out the new damage dealt. For Mooks we subtract out all stun damage ever dealt to get the number used in the calculations below. + } + if( dmg >= WoundThreshold ) { + this.MarkerSet( ["d", "harried", "++"] ); // Stun damage does not cause wounds, set harried until end of round. + newMsg += " and is harried until end of round"; + } } // end stun damage section. + + if( !bStrain && !bStun && dmg >= WoundThreshold ) { // wound. + let currWound; + if( !bMook ) { // if not a mook, get from the character sheet. + let attribute = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Wounds" }, 0); + currWound = Earthdawn.parseInt2(attribute.get( "current" )); + currWound += 1; + Earthdawn.setWithWorker( attribute, "current", currWound, 0 ); + } + if( bToken && ( bMook || !(this.tokenInfo.tokenObj.get( "bar2_link" )) )) { // If it is a mook, do token. + currWound = Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar2_value" )); + if( isNaN( currWound ) ) + currWound = 1; + else + currWound += 1; + Earthdawn.set( this.tokenInfo.tokenObj, "bar2_value", currWound ); + newMsg += ".
    Takes wound " + currWound; + } } // end wound + + if( currDmg >= Earthdawn.getAttrBN( this.charID, "Damage-Death-Rating", 25 )) { + newMsg += ".
    Character is DEAD"; + this.MarkerSet( ["d", "healthdead", "s"] ); + } else if( currDmg >= ( unc || 0)) { + newMsg += ".
    Character is Unconscious"; + this.MarkerSet( ["d", "healthunconscious", "s"] ); + } else { // not dead or unconscious, so OK. + this.MarkerSet( ["d", "healthunconscious", "u"] ); + if( dmg >= (WoundThreshold + 5) // Character is wounded and need to make a Knockdown test + && ( Earthdawn.getAttrBN( this.charID, "Knockdown-Adjust", "0", true ) < 990 ) // but is not immune to knockdown + && ( Earthdawn.getAttrBN( this.charID, "condition-KnockedDown", "0" ) != "1" )) { // and is not already knocked down. + let cname = getAttrByName( this.charID, "character_name" ); + newMsg += ". Need to make a Knockdown Test"; + gmMsg += " TN " + ( dmg - WoundThreshold ) + "
    " + + Earthdawn.makeButton( "Knockdown", + "!Earthdawn~ " + (( this.tokenInfo !== undefined && this.tokenInfo.tokenObj !== undefined) + ? "setToken: " + this.tokenInfo.tokenObj.get( "id" ) : "charID: " + this.charID ) + + "~ TargetNum: " + ( dmg - WoundThreshold ) + + ": Adjust-TN-Auto: Adjust-TN-Misc~ modValue: ?{Modification|0} ~ K-ask: @{" + cname + "|KarmaGlobalMode}@{" + + cname + "|Str-Karma-Ask}: @{" + getAttrByName( this.charID, "character_name") + "|DPGlobalMode}@{" + + cname + "|Str-DP-Ask}~ Value: Knockdown: Adjust-All-Tests-Total: Defensive: Resistance~ Roll" + ,"Make a standard Knockdown test.", "action" ); + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }), + po = this; + _.each( attributes, function (att) { + if( att.get( "name" ).endsWith( "_Special" ) && Earthdawn.keywordCheck( att.get("current"), "Knockdown" )) { + let pre = Earthdawn.buildPre( att.get( "name")), + name = Earthdawn.getAttrBN( po.charID, pre + "Name", "" ), + code = Earthdawn.repeatSection( 3, att.get( "name") ), + rid = Earthdawn.repeatSection( 2, att.get( "name") ); +//log( "Talent Found " + pre + " " + name + " " + code +" " + rid + " " + cname); + gmMsg += Earthdawn.makeButton( name + " test", + "!Earthdawn~ " + (( po.tokenInfo !== undefined && po.tokenInfo.tokenObj !== undefined) + ? "setToken: " + po.tokenInfo.tokenObj.get( "id" ) : "charID: " + po.charID ) + + "~ TargetNum: " + ( dmg - WoundThreshold ) + + ": Adjust-TN-Auto: Adjust-TN-Misc~ modValue: ?{Modification|0} ~ K-ask: @{" + cname + "|KarmaGlobalMode}@{" + + cname + "|" + pre + "Karma-Ask}: @{" + cname + "|DPGlobalMode}@{" + + cname + "|" + pre + "DP-Ask}~ Action: "+ code +":" + rid + ,"Make a Knockdown test.", "action" ); + } + }); // End for each attribute. + } } + + if(gmMsg && npc == Earthdawn.charType.pc) { //gm message is sent separately to GM for NPCs and appended for PCs + newMsg += gmMsg; + gmMsg = ""; + } + if( bRecovery && recMsg.length > 0) + this.chat( recMsg, Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + if( !bRecovery && newMsg.length > 0) + this.chat( newMsg + "." ); + if( !bRecovery && gmMsg.length > 0) + this.chat( gmMsg, Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + } catch(err) { Earthdawn.errorLog( "ED.Damage() error caught: " + err, this ); } + } // End ParseObj.Damage() ssa ) + + + + // ParseObj.Debug() + // This is a collection of test, repair, or diagnostic commands. + // + // Most are probably invoked from the "Special Function" drop-down and button on the Adjustments page. + // Cancel, CleanTokens, Inspect, RepSecFix, RepSecBatch, RepSecBatchFixAll, SetAttribAll, sheetworkertest, showeach, test. + this.Debug = function( ssa ) { + 'use strict'; + let batchFix = false, + po = this; + try { + switch ( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() ) { + case "cancel": // This does nothing. It is just a target for a repsecBatch - cancel that is meant to be ignored. + break; + case "cleantokens": // Clean tokens from the campaign. All tokens of current character (available to players or gm), all tokens in campaign, or all tokens on requested pages. + if( ssa[ 2 ] != "Character" && !playerIsGM( this.edClass.msg.playerid ) ) + this.chat( "Only GM can do this!", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + else { + let tkns; + switch ( Earthdawn.safeString( ssa[ 2 ] ).toLowerCase() ) { + case "ask": // Give a list of pages with tokens. If they press one it calls with Page. + tkns = findObjs({ _type: "graphic", _subtype: "token" }); + let s = "Which map pages do you wish to clean all tokens from: ", + pages = {}; + _.each( tkns, function( TokObj ) { + if( TokObj.get( "_pageid" ) in pages ) + ++pages[ TokObj.get( "_pageid" )]; + else + pages[ TokObj.get( "_pageid" )] = 1; + }); // End ForEach Token + _.each( pages, function( v, k ) { + let pg = getObj("page", k) + if( pg ) + s += Earthdawn.makeButton( pg.get( "name" ) + ": " + v, "!Earthdawn~ charID: " + po.charID + "~ Debug: CleanTokens: Page: " + k, + "Clean all " + v + " tokens from this page.", "param" ); + }); + this.chat( s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + break; + case "all": // Clean all tokens from all pages (except bullpen). + tkns = findObjs({ _type: "graphic", _subtype: "token" }); + break; + case "character": // clean tokens for the current character whereever they may be (except bullpen). + tkns = findObjs({ _type: "graphic", _subtype: "token", represents: po.charID }); + break; + case "page": // Clean all tokens from this page. Called from "Ask". + tkns = findObjs({ _type: "graphic", _subtype: "token", _pageid: ssa[ 3 ] }); + break; + default: + Earthdawn.errorLog( "ED.Debug:CleanToken() invalid argument: " + ssa.toString(), po ); + } + if( ssa[ 2 ] != "Ask" ) { + let bpage = (ssa[ 2 ] == "Page"), + count = 0, + bullpens = []; + if( !bpage) { + let pgs = findObjs({ _type: "page" }); + if( pgs ) + _.each( pgs, function( pg ) { + if( Earthdawn.safeString( pg.get( "name" )).toLowerCase().includes( "bullpen" )) // Find all pageid's that include Bullpen in the name. Unless cleaning named pages. + bullpens.push( pg[ "_id" ] ); + }); + } + _.each( tkns, function( tokobj ) { + if( !bullpens.includes( tokobj[ "_pageid" ] )) { + ++count + tokobj.remove(); + } + }); + this.chat( count + " Tokens removed.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive, "gmState" ); + } } + break; // end CleanToken + case "inspect": // This is test code that lets me see info on tokens and/or characters. + log( ssa); + switch( Earthdawn.safeString( ssa[ 2 ] ).toLowerCase() ) { + case "getids": { // Given a text fragment. Look in every attribute that ends in "_Name" to see if it contains the fragment. + // go through all attributes for this character and look for ones we are interested in + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_Name" ) && att.get( "current" ).indexOf( ssa[ 3 ] ) != -1 ) + po.chat( getAttrByName( po.charID, "character_name" ) + " " + "CharID: " + po.charID + " " + att.get( "name" ) + ": " + att.get( "current" ), + Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + }); // End for each attribute. + } break; + case "getvalue": // Given an attribute name, give the value of the attribute. + this.chat( getAttrByName( this.charID, "character_name" ) + " " + ssa[ 3 ] + ": " + getAttrByName( this.charID, ssa[ 3 ]), + Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + break; + case "objectid": { // Given an Object ID. Show what type of object it is and it's name. + let objs = findObjs({ _id: ssa[ 3 ] }); + _.each(objs, function(obj) { + let typ = obj.get( "type" ); + let name = obj.get( "name" ); + if( typ ) + if( name ) + po.chat( "Type: " + typ + "\nName: " + name, Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + else + po.chat( "Type: " + typ, Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + po.chat( JSON.stringify( obj ), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + }); + } break; + case "repeatsection": { // Given a character ID (defaulting to current character) and a repeating section ID. show what it is for. + if( ssa[ 4 ] === "Full" ) { + let attributes = findObjs({ _type: "attribute", _characterid: po.charID }); + _.each( attributes, function (att) { + let name = att.get( "name" ); + if ( name.startsWith( "repeating_" ) && name.indexOf( ssa[3] ) > -1) { + po.chat( "_id: " + att.get( "_id" ) + " name: " + att.get( "name" ) + " current: " + att.get( "current" ) + + ( att.get( "max" ) ? " max: " + att.get( "max" ) : ""), + Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + log( att ); + } + }); + } else { // Short report + function checkRepeat() { + for( let i = 1; i < arguments.length; ++i ) { + let aobj = findObjs({ _type: 'attribute', _characterid: po.charID, name: arguments[ 0 ] + arguments[ i ] })[0]; + if ( aobj != undefined ) + po.chat( JSON.stringify( aobj ), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + } + }; + if( state.Earthdawn.gED ) + checkRepeat( Earthdawn.buildPre( "DSP", ssa[3] ), "Code", "Name", "Circle" ); + else + checkRepeat( Earthdawn.buildPre( "DSP", ssa[3] ), "Name", "Circle", "Tier" ); + checkRepeat( Earthdawn.buildPre( "T", ssa[3] ), "Name", "Rank" ); + checkRepeat( Earthdawn.buildPre( "NAC", ssa[3] ), "Name", "Rank" ); + checkRepeat( Earthdawn.buildPre( "SK", ssa[3] ), "Name", "Rank" ); + checkRepeat( Earthdawn.buildPre( "SKK", ssa[3] ), "Name" ); + checkRepeat( Earthdawn.buildPre( "SKA", ssa[3] ), "Name" ); + checkRepeat( Earthdawn.buildPre( "SKL", ssa[3] ), "SKL_Name" ); + checkRepeat( Earthdawn.buildPre( "SPM", ssa[3] ), "Type", "Origin", "Contains" ); + checkRepeat( Earthdawn.buildPre( "SP", ssa[3] ), "Name", "Circle", "Discipline" ); + checkRepeat( Earthdawn.buildPre( "WPN", ssa[3] ), "Name" ); + checkRepeat( Earthdawn.buildPre( "MNT", ssa[3] ), "Name" ); + checkRepeat( Earthdawn.buildPre( "TI", ssa[3] ), "Name" ); + } + } break; + case "statusmarkers": // Show status markers of selected tokens. + this.chat( this.tokenInfo.tokenObj.get( "statusmarkers" ), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + this.chat( "pseudoToken: " + Earthdawn.getAttrBN( this.charID, "pseudoToken", ""), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + break; + case "tokenobj": // Show tokenInfo for selected tokens. + if ( this.tokenInfo ) { + this.chat( "Type: " + this.tokenInfo[ "type" ] + + "\nName: " + this.tokenInfo[ "name" ] + + (("tokenObj" in this.tokenInfo) ? "\ntokenID: " + this.tokenInfo.tokenObj.get("id") + "\nToken" + JSON.stringify( this.tokenInfo.tokenObj ) : "") + + (("characterObj" in this.tokenInfo) ? "\ncharID: " + this.tokenInfo.characterObj.get("id") + "\nChar" + JSON.stringify( this.tokenInfo.characterObj ) : ""), + Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive, "Inspect" ); + } else + this.chat( "tokenInfo undefined.", Earthdawn.whoTo.player| Earthdawn.whoFrom.noArchive, "Inspect" ); + break; + } // End Inspect + break; + case "repsecfix": { // RepSecFix: Repeating Section Verify and Fix. Note: This is now labeled Character Sheet Verify/Fix and fixes some things not in repeating sections. + // Sort the attributes by name, and see if any are duplicates, if so remove one. + // Find any row that does not have a RowID, and fix it. + // For each RowID found, see if any two are duplicates except for case, and if so merge them into the correct (mixed case) rowID. + // This will also repeat the AbilityRebuild test (This will force it to be done even if user has set it to Never do it automatically). + // LinkToken calls this routine in silent mode. If called by the menu, it puts notes into the console log. + try { + let orig = [], + lcase = [], + dup = [], + needFixi = [], + needFix = [], + silent = ssa.includes( "silent" ), // silent is not really silent, just logs less stuff to the console log. + firstRun = ssa.includes( "firstRun" ), // The API has never been run in this campaign before. Not used in current implmentation, but could be. + first = ssa.includes( "first" ), // This is the first character in a firstRun. + issues = 0, + rep = 0, + nonrep = 0, + timer = Date.now(); + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + let sorted = _.sortBy( attributes, function( att ){ return att.get("name"); }); + if( !silent ) log( "Debug RepSecFix: " + attributes.length + " attributes found. Sorting took " + (Date.now() - timer) + " ms."); + + let save = sorted[ 0 ]; + for( let i = 1; i < sorted.length; ++i ) { // Go through each and every attribute in sorted order. + let att = sorted[ i ]; + if( att.get( "name" ) === save.get( "name" )) { // We have two attributes with identical names. get rid of one of them. + let a = att.get( "current" ), + s = save.get( "current" ), + which; // Which to get rid of 1 att, 2 save + if( !a ) which = 1; // pick which one is most likely to be the best value to keep. + else if( !s ) which = 2; + else if( typeof a === "string" ) { + if( typeof s !== "string" ) which = 2; // pick one that is not string. + else if( a.length < s.length ) which = 1; + else if( a.length > s.length ) which = 2; + else if( a < s ) which = 1; + else if( a > s ) which = 2; + } else { + if( typeof s == "string" ) which = 1; + else if( a < s ) which = 1; + else if( a > s ) which = 2; + } + if( which === 2 ) { + dup.push( save ); // mark the save attribute for deletion and skip to the next loop with the current attribute as the save. + save = att; + continue; + } else { + dup.push( att ); // mark this attribute for deletion and skip to th next loop with the same "save" as this loop. + continue; + } } // end have two attributes with identical names, get rid of one. + + save = att; // We are done with save for this loop, so can set it up for next loop. + if( att.get( "name" ).startsWith( "repeating_")) { // Now we are specifically fixing repeating sections that have no rowID, or that are all lower or upper case. + ++rep; + let nm = Earthdawn.safeString( att.get("name") ), + rowID = Earthdawn.safeString( Earthdawn.repeatSection( 2, nm)), + code = Earthdawn.safeString( Earthdawn.repeatSection( 3, nm)); + if( !rowID || !code ) { + ++issues; + po.chat( "Error found, badly formated repeating section name. Deleting: " + nm, Earthdawn.whoFrom.apiWarning ) + log( "Deleting " + JSON.stringify( att )); + att.remove(); // (From API to player ): Error found, badly formated repeating section name. Deleting: repeating_inventory_-MeU4hpDClwHqdkpTEmI_show-Inventory-details + continue; + } + if( rowID && Earthdawn.codeToName( rowID, true )) { // We have seen cases where somehow a Code gets placed where a rowID should be. If we find that the rowID matches a valid code, then just delete the attribute. + po.chat( "Error found, badly formated rowID. Deleting: " + nm, Earthdawn.whoFrom.apiWarning ) + ++issues; + log( "Deleting " + JSON.stringify( att )); + att.remove(); + continue; + } + if( code && Earthdawn.testNoRowID( code )) // Certain types of repeating sections do not need to test if there is a rowID. + continue; + if( nm.endsWith( "_Name" )) { // When we find a repeating section name, Make sure we have a RowID that matches the rowID of the name. + // Note, for a while I had a routine to look though the attributes we already had for the RowID, but I could not get it to work. So using system tools. + let attrib = findObjs({ _type: "attribute", _characterid: po.charID, name: nm.slice( 0, -5) + "_RowID" }); + if( !attrib || attrib.length == 0 ) { // RowID does not exist at all for this item. + if( rowID !== rowID.toLowerCase()) { // Don't do this if this row is likely corrupt anyway. + attrib = createObj( "attribute", { _characterid: po.charID, name: nm.slice( 0, -5) + "_RowID","current": rowID }); + po.chat( "Error found, " + att.get( "current" ) + " at " + nm + " did not have a rowID.", Earthdawn.whoFrom.apiWarning ) + ++issues; + } + } else if( !attrib[ 0 ].get( "current" ) || attrib[ 0 ].get( "current" ) !== rowID) { + po.chat( "Error found, " + att.get( "current" ) + " at " + nm + " had incorrect rowID of - " + attrib[ 0 ].get( "current" ) + ". Fixed", Earthdawn.whoFrom.apiWarning ) + attrib[ 0 ].set( "current", rowID); + ++issues; + } + } // end _name + if( orig.indexOf( rowID ) === -1 ) { // This is a list of all unique rowID's we found. + orig.push( rowID ); + lcase.push( rowID.toLowerCase() ); + } + if( rowID === rowID.toLowerCase() || rowID === rowID.toUpperCase() ) { // This rowID is all the same case which is almost certainly got stored wrong. + let t = needFixi.indexOf( rowID ); + if( t == -1 ) { + t = needFixi.push( rowID ) -1; + needFix.push( [] ); + } + needFix[ t ].push( att ); + } + } else { // end repeating. start not repeating. + if( att.get( "name" ) === "edition" ) { // Also don't do the tests below of edition and/or sheetVersion is zero. + if( first ) { // This is called automatically by firstRun the very first time the API is detected and furthermore this is the first character sheet processed. + // state sheetVersion is almost certainly not correct and state edition might not be correct ether. + // So if there is an edition that is not zero, set the state values using what we find in this first record. + if(( !state.Earthdawn.edition || state.Earthdawn.edition === 4) && att.get( "current" ) + && (att.get( "current" ) != "0") && (att.get( "current" ) != Earthdawn.safeString( state.Earthdawn.edition ))) { + po.chat( "Note: state.Earthdawn.edition was: '" + state.Earthdawn.edition + "' and was different from the first characters (" + + Earthdawn.getAttrBN( po.charID, "character_name", "" ) + ") edition, so state.Earthdawn.edition was set to: " + att.get( "current" ) + + ". If this is not desired, change edition using Parameters / Sheet / Special Functions / Do SF / GM Special Commands / Change Edition.", Earthdawn.whoFrom.apiWarning ) + state.Earthdawn.edition = att.get( "current" ); + } + if(( state.Earthdawn.sheetVersion == 0.0) && att.get( "max" ) && ( parseFloat( att.get( "max" )) != 0.0 )) { + po.chat( "Note: state.Earthdawn.sheetVersion was: '" + state.Earthdawn.sheetVersion + "' and was different from the first characters (" + + Earthdawn.getAttrBN( po.charID, "character_name", "" ) + ") sheetVersion, so state.Earthdawn.sheetVersion was set to: " + att.get( "max" ) + + ". If this is incorrect it should eventually fix itself.", Earthdawn.whoFrom.apiWarning ) + state.Earthdawn.sheetVersion = parseFloat( att.get( "max" )); + } + } else { // Not first. We want to set everything to use the systems edition and sheetVersion. + if( state.Earthdawn.edition && (att.get( "current" ) != Earthdawn.safeString( state.Earthdawn.edition ))) { + log( "was game edition " + att.get( "current" ) + " new " + Earthdawn.safeString( state.Earthdawn.edition )); + Earthdawn.setWithWorker( att, "current", Earthdawn.safeString( state.Earthdawn.edition )); + } + if(( state.Earthdawn.sheetVersion ) && (att.get( "max" ) != Earthdawn.safeString( state.Earthdawn.sheetVersion ))) { + log( "Warning, sheet edition is still " + att.get( "max" ) + " should be " + Earthdawn.safeString( state.Earthdawn.sheetVersion )); +// Earthdawn.setWithWorker( att, "max", Earthdawn.safeString( state.Earthdawn.sheetVersion )); + } } + ++nonrep; + } } + } // End for each attribute. + + _.each( dup, function (att) { + log( "repSecFix deleting duplicate attribute " + JSON.stringify( att )); + att.remove(); + }); + + // needFixi is a one-dimensional array that contains rowIDs that need fixing because they are all lower case. It is used as the index for needFix. + // needFix is an array of arrays of objects that contain attributes that need fixing. For example there are two attributes that have the same rowID in needFixi [ 0 ], then needFix[0][0] and needFix[0][1] will contain them. + if( needFixi.length > 0 ) { +//log( "repSecFix" ); log( needFixi); log( orig); log( lcase); log( needFix); + for( let i = 0; i < needFixi.length; ++i ) { // This is for each issue that needs fixed. RowIDs that are bad. + let cnt = [], // This will hold the indexes of the RowIDs we are looking for. + f = lcase.indexOf( needFixi[ i ].toLowerCase()); + while( f !== -1) { // Look in list of all attributes (not just bad ones) for unique rowID's, that have the same value when lowercased. + cnt.push( f ); + f = lcase.indexOf( needFixi[ i ].toLowerCase(), f +1); + } + f = undefined; + if( cnt.length > 1 ) // We have found more than one entry with the same lowercased rowID. Almost certainly at least one of these is bad, hopefully at least one is good. + for( let j = 0; j < cnt.length; ++j ) { + let ocj = Earthdawn.safeString( orig[ cnt[ j ]] ); + if( ocj !== ocj.toLowerCase() && ocj !== ocj.toUpperCase()) + f = ocj; // a rowID that is GOOD, that is the same when lowercased as a bad rowID. + } + if( f ) { // This is the good (mixed case) RowID we want to use for all loop i attributes. + let lst = ", ", + pre; +//log( "needFix[ " + i + " ] length is " + needFix[ i ].length); log( "new RowID " + f); + for( let j = 0; j < needFix[ i ].length; ++j ) { // For each attribute that has the rowID we are correcting. + let obj = needFix[ i ][ j ]; // This is the "bad" attribute object + let nm = obj.get( "name" ); + let n = Earthdawn.repeatSection( 4, nm); + pre = Earthdawn.buildPre( Earthdawn.repeatSection( 3, nm), f); // This is the "good" rowID. +//log( nm); + let attrib = findObjs({ _type: "attribute", _characterid: po.charID, name: pre + n }); + if( attrib ) { + if( attrib.length > 0 ) { // We found a correct entry of this name. delete the one that was bad. + po.chat( "Error found, both bad and good RowIds found for " + attrib[0].get( "name" ) + " deleting bad one.", Earthdawn.whoFrom.apiWarning ) + obj.remove(); + ++issues; // We have already removed duplicates, so we don't need to test for more than one. + } else { // There is not already a "good" attribute of this rowID. Fix in place the bad attribute. + lst += n + ", "; + Earthdawn.set( obj, "name", pre + n ); + } } } + if( lst.length > 5) { + po.chat( "Errors found, " + lst.slice( 2 ).trim() + " fixed for " + pre, Earthdawn.whoFrom.apiWarning ) + ++issues; + } + + } else { // We identified a problem rowID. But there were no attributes with a mixed case rowID we could use to know what it should be. + po.chat( "Errors found with " + needFix[ i ].length + " attributes of row " + needFix[ i ][0].get( "name") + " Deleting whole row.", Earthdawn.whoFrom.apiWarning ) + log( needFix[ i ] ); + for( let j = 0; j < needFix[ i ].length; ++j ) + needFix[ i ][ j ].remove(); + ++issues; + } + } // end for each needFix + } else if ( issues ) + this.chat( "All other RowID's found appear valid.", Earthdawn.whoFrom.apiWarning ); + + let problems = this.abilityRebuild( [ "abilityRebuild", ssa.includes( "batchFix" ) ? "batchFix" : ( ssa.includes( "batch" ) ? "batch" : "forceTest" ), + ssa.includes( "forceNameRefresh" ) ? "forceNameRefresh" : "Standard" ]); + + if( !silent ) { + log( rep + " attributes were in repeating sections. " + nonrep + " were in non-repeating sections." ); + log( "Debug RepSecFix: Finished after " + (Date.now() - timer) + " ms."); + if( !issues && !problems ) this.chat( Earthdawn.getAttrBN( this.charID, "character_name", "" ) + ": Verify / Fix: No issues found." ); + } + } catch(err) { Earthdawn.errorLog( "Debug RepSecFix error caught: " + err, po ); } + } break; // end repsecfix + case "repsecbatchfixall": + batchFix = true; // don't ask before fixing. + case "repsecbatch": // Run the previous routine for all characters (instead of just one), Ask before fixing anything. These are called for SF func GM commands. + let charQueue = findObjs({ _type: "character" }); // create the queue we'll be processing. + if( charQueue && charQueue.length > 0) { + const fixBurndown = () => { // function that will process the next element of the queue + try { + if( charQueue.length > 0 ) { + let c = charQueue.shift(); + log( "Verifying " + c.get( "name" )); + po.charID = c.get( "_id" ); + po.Debug( [ "Debug", "repSecFix", "silent", batchFix ? "batchFix" : "batch", ssa.includes( "forceNameRefresh") ? "forceNameRefresh" : "Standard" ] ); + setTimeout( fixBurndown, 1); // Do the next character + } else // Have finished the last character. + this.chat( "RepSecBatch finished. Look in API console log for details.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + } catch(err) { + log( "RepSecBatch error caught: " + err ); + } } + this.chat( "We are now Verifying all " + charQueue.length + " characters found.", Earthdawn.whoFrom.apiWarning | Earthdawn.whoTo.public ); + fixBurndown(); // start the execution by doing the first element. Each element will call the next. + } + break; // end repsecBatch + case "setattriball": { // Set all attributes to a value. SetAttribAll: attribute name: Attribute value: Confirm ("yes") + if( ssa.length < 5 || !ssa[ 2 ] ) { + this.chat( "Error! badly formated command.", Earthdawn.whoFrom.apiError); + log( ssa ); + return; + } + if( ssa[ 4 ].toLowerCase() !== "yes" ) // command was not confirmed, quietly exit. + return; + + let a = ssa[ 2 ]; + let m = a.endsWith( "_max" ); + if( m ) a = a.slice( 0, -4); // trim the _max off if it was present. + let v = ssa[ 3 ]; // value is a string. empty strings are accepted. If (undefined) or (delete) then set to undefined. If (delete) and if the other value (current or max) is already undefined, delete the whole thing. + let s = ( v === "(delete)" ) ? 1: 0; + + let count = 0, + charQueue = findObjs({ _type: "character" }); // create the queue we'll be processing. + if( charQueue ) { + const charBurndownSAA = () => { // create the function that will process the next element of the queue + if( charQueue.length ) { + let aObj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: charQueue.shift().charID, name: a }, 0, 0); + if( s === 0 ) + aObj.set( m ? "max" : "current", v ); + else if( s === 1 ) + Aobj.remove(); + setTimeout( charBurndownSAA, 1); // Do the next character + } else // Have finished the last attribute. + ed.chat( count + " characters updated.", Earthdawn.whoFrom.apiWarning ); + } }; + ed.chat( "Updating all (" + charQueue.length + ") characters '" + ssa[ 2 ] + "' to '" + ssa[ 3 ] + "'", Earthdawn.whoFrom.apiWarning ); + charBurndownSAA(); // start the execution by doing the first element. Each element will call the next. + } break; // end setAttribAll + case "sheetworkertest": // Do nothing (this function is done by sheet-worker) + break; + case "showeach": // showeach is basically just debug code that lets me look inside of repeating sections. Show attributes that match the passed parameters. + if( this.charID === undefined ) { + this.chat( "Error! Trying ShowEach() when don't have a CharID.", Earthdawn.whoFrom.apiError); + return; + } + let searchString = "CB2", // Whatever you want to look for can be hard-coded here. + searchString2 = undefined; + + if( ssa.length > 2 ) + if( ssa[ 2 ] !== "API" ) { + searchString = ssa[ 2 ]; + if( ssa.length > 3 ) + searchString2 = ssa[ 3 ]; + } + // Go through all attributes for this character and look for ones that have the search strings + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + _.each( attributes, function (indexAttributes) { + if ( indexAttributes.get("name").indexOf( searchString ) > -1 ) + if (( searchString2 === undefined ) || (indexAttributes.get("name").indexOf( searchString2 ) > -1 ) ) { + log( "Name: " + indexAttributes.get("name") + " Val: " + indexAttributes.get("current") ); + } + }); // End for each attribute. + break; + case "test": // This is just temporary test code. Try stuff out here and see if it works. Make sure nothing dangerous is here when released. + try { + log("Debug Test"); +/* + // Go through all attributes for this character and look for ones that have the search strings + function btest( obj ) { + let ret = 0; + if( obj != undefined ) { + let c = obj.get( "current" ), m = obj.get( "max" ); + if( c != undefined ) ret += (typeof c === "string") ? c.length : c.toString().length; + if( m != undefined ) ret += (typeof m === "string") ? m.length : m.toString().length; + } + return ret; + } + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + log( attributes.length + " attributes found" ); + let sorted = _.sortBy( attributes, function( att ){ return btest( att ) }); + function atest( ind ) { + let obj = sorted[ ind ]; + return btest( obj ); + } + log( sorted.length + " after sorting" ); +// log( "shortest length " + sorted[ 0 ][ "current " ].length + sorted[ 0 ][ "max" ].length ); + let step = Math.floor( sorted.length / 10); + for( let i = 0; i < sorted.length; i += step ) + log( i + " length " + atest( i ) + " " + JSON.stringify( sorted[ i ])); + log( "longest length " + atest( sorted.length -1 )); + + let cleanind = 0, cleanchar = 0, done = false; + while( !done ) { + let obj = sorted.pop(); + ++cleanind; + let totlen = btest( obj ); + cleanchar += totlen; + log( "removed " + obj.get( "name" ) + " current length " + obj.get( "current" ).length + " max length " + obj.get( "max" ).length); + obj.remove(); + if( totlen < 32 ) done = true; + } + log( "removed " + cleanind + " objects with " + cleanchar + " total characters. " + sorted.length + " attributes remaining" ); +*/ + + +// _.each( attributes, function (indexAttributes) { +// if ( indexAttributes.get("name").indexOf( searchString ) > -1 ) +// if (( searchString2 === undefined ) || (indexAttributes.get("name").indexOf( searchString2 ) > -1 ) ) { +// log( "Name: " + indexAttributes.get("name") + " Val: " + indexAttributes.get("current") ); +// } +// }); // End for each attribute. + + +/* + log( i + " length " + (sorted[ i ].get( "current " ).length ) + + (sorted[ i ].get( "max" ).length )); + log( "longest length " + (sorted[ sorted.length -1 ].get( "current" ).length ) + + (sorted[ sorted.length -1 ].get( "max" ).length )); + + for( let i = 0; i < sorted.length; i += step ) + log( i + " length " + ((("current" in (sorted[ i ])) ? sorted[ i ][ "current " ].length : 0) + + (("max" in (sorted[ i ])) ? sorted[ i ][ "max" ].length : 0))); + log( "longest length " + ((("current" in (sorted[ sorted.length -1 ])) ? sorted[ sorted.length -1 ][ "current" ].length : 0) + + (("max" in (sorted[ sorted.length -1 ])) ? sorted[ sorted.length -1 ][ "max" ].length : 0 ))); +*/ +// Findtest, finddebug, cdd tttt + } catch(err) { Earthdawn.errorLog( "ED Test error caught: " + err, po ); } + log( Earthdawn.timeStamp() + "Test Done" ); + break; + } + } catch(err) { Earthdawn.errorLog( "ED.Debug() error caught: " + err, po ); } + } // End ParseObj.Debug() + + + + // If there are any commands that we saved to do later. Do them now. + this.doNow = function() { + 'use strict'; + try { + if( this.doLater !== "" ) { // These are commands we did not want to do when we first saw them, and only want to do at the last moment. + let ta = this.doLater.split( "~" ); + for( let i = 1; i < ta.length; ++i) + this.Parse( ta[ i ] ); + this.doLater = ""; + } + } catch(err) { Earthdawn.errorLog( "ED.doNow() error caught: " + err, this ) } + } // End ParseObj.DoNow() + + + + // ParseObj.findAllPagesAnyPlayterIsOn() + // + // Returns an array of pageIDs. + // if testOnline is false then list is of all pages, whether players are online or not. Otherwise it is only online players. Defaults to true. + this.findAllPagesAnyPlayterIsOn = function( testOnline ) { + 'use strict'; + try { + if( testOnline === undefined || testOnline ) testOnline = true; // testOnline defaults to true. Set to native boolean so don't have to convert each loop. + else testOnline = false; + let pgs = Campaign().get( "playerspecificpages" ), // object of pages that have players on them. + players = findObjs({ _type: "player", _online: true }), // array of online players. + ret = [ Campaign().get( "playerpageid" ) ]; // the all players page. + if( pgs ) + Object.entries( pgs ).forEach(([key, val]) => { + if(( testOnline || ( key in players )) && ( !( key in ret))) // If page is not already in ret, and page is an online player, add it to the list. + ret.push( val ); + }); + players.forEach( function( item ) { + 'use strict'; + if( playerIsGM( item.get( "_id" )) && !ret.includes( item.get( "_lastpage" ))) // if player is gm and last page they visited is not already in ret, add it. + ret.push( item.get( "_lastpage" )); + }); + return ret; + } catch(err) { Earthdawn.errorLog( "ED.findAllPagesAnyPlayterIsOn() error caught: " + err, this ); } + }; // End ParseObj.findAllPagesAnyPlayterIsOn + + + + // ParseObj.FindPageOfPlayer() + // + // Returns pageID of page this player is on. + // + // Note that Jun 2024 I think the only routine that called this now calls findAllPagesAnyPlayerIsOn instead. So routine might be useless. + this.FindPageOfPlayer = function( playerID ) { + 'use strict'; + try { + let pgs = Campaign().get( "playerspecificpages" ), + ret; + if ( pgs && ( playerID in pgs )) + ret = pgs[ playerID ]; + else // player is on the all players page. + ret = Campaign().get( "playerpageid" ); + return ret; + } catch(err) { Earthdawn.errorLog( "ED.FindPageOfPlayer() error caught: " + err, this ); } + }; // End ParseObj.FindPageOfPlayer + + + + // ParseObj.ForEachToken () + // For Each selected token, perform some command. + // ssa is an array that holds any modifiers for the ForEach command. Look in the switch statement below for description of options. + this.ForEachToken = function( ssa ) { + 'use strict'; + let edParse = this; + try { + this.tokenIDs = []; + let bst = false, // Do we want all selected tokens? + bsct = false, // Do we want all selected character tokens? + bust = false, // Do we want to look in unselected tokens if we can't find with the above? + binmt = false, // Do we want to ignore all selected tokens that do not match the character ID? + btuc = false, // Token Unique Character - Ignore all except the first token for each unique character. + bc = false, // Do we want character (if found nothing else)? + flag = 0, // Instead of doing a ForEachToken loop: 1 - return a list of tokens. + mooks = false, // Do we want to ignore all selected non-mooks? + notMooks = false, // Do we want to ignore all selected mooks? + PCs = false, // Do we want to ignore all selected NPCs? + NPCs = false, // Do we want to ignore all selected PCs? + notPCs = false, // Do we want to ignore all selected non-PCs? + objarr = [], + stat; + + for ( let i = 1; i < ssa.length; i++) { + switch ( Earthdawn.safeString( ssa[ i ] ).toLowerCase() ) { + case "character": // Character Returns the character matching charID only. + case "c": + bc = true; + break; + case "list": // Don't do a ForEachToken, simply return a list of tokens for which you would have done a ForEachToken. + flag = 1; + break; + case "status": // Don't do a ForEachToken, simply return true/false if any token has a token status set starting with next parameter. + flag = 2; + stat = ssa[ ++i]; + break; + case "mooks": // Keep Mooks only. Ignore selected characters that are not mooks. + case "mook": + mooks = true; + break; + case "notmooks": // Keep PCs and NPCs that are not mooks. Ignore selected characters that are mooks. + case "notmook": + notMooks = true; + break; + case "npcs": // Keep NPCs (including mooks) only. Ignore selected characters that are PCs. + case "npc": + NPCs = true; + break; + // Note: Could add NpcNotMook here if needed. So far have not needed it. + case "pcs": // Keep PCs only. Ignore selected characters that are NPCs or Mooks. + case "pc": + PCs = true; + break; + case "selectedtokens": // Selected Tokens - ForEachToken processes selected tokens only with no variation for token action or character ID. + case "st": + bst = true; + break; + case "selectedcharactertokens": // Selected Character Tokens - It processes only the selected tokens that match charID. + case "sct": + case "selectedcharacter": + case "sc": + bsct = true; + break; + case "ignorenonmatchingtokens": // Ignore Non-matching Tokens - Any selected token that does not match the character ID is ignored. + case "ignorenonmatching": // Note that this is different from sct in that if this one is set, it still performs the logic described below to figure out which options are wanted. + case "inmt": + binmt = true; + break; + case "tuc": + case "uc": // Token unique character + btuc = true; // NEEDS TO BE combined with some other directive such as st. Will give only one token for each selected character or mook. + break; + case "unselectedtokens": // If you did not find any selected character tokens, look in the unselected tokens as well. + case "ust": // Note that if you select this option WITHOUT st or sct, it will always find all character tokens, selected or not. + bust = true; + break; + } } // end ssa[1] switch + + if( !bst && !bsct && !bust && !bc ) { + // Without any modifiers, it tries it's hardest to find whatever it can. + // When called from a token action, perform the action for all selected tokens. + // when called from a button on a character sheet that is a mook, perform for all selected tokens for that character. If none of the characters tokens are selected, do all that are selected. + // when called from a button on a character sheet that is non-mook, FIND the token even if not selected. If it finds more than one it is an error. + // If there is no token on this page, see what can do with the character itself. This may cause called routines to error. + bst = this.tokenAction; // all selected tokens + bsct = !this.tokenAction; // all selected character tokens + bust = !this.tokenAction; // look in unselected tokens + bc = true; // character (if found nothing else) + } + + if(( bst || bsct || binmt ) && this.edClass.msg !== undefined ) + _.each( this.edClass.msg.selected, function( sel ) { // Check selected tokens + let TokObj = getObj("graphic", sel._id); + if (typeof TokObj === 'undefined' ) + return; + let cID = TokObj.get("represents"); + if( btuc ) { + for( let i = 0; i < objarr.length; ++i ) + if( objarr[ i ]["characterObj"]["_id"] === cID ) + return; + } + if( bst || (( edParse.charID !== undefined) && (cID === edParse.charID ))) { // This will get all selected tokens on a token action, and all selected tokens that match this character on a sheet action. + let CharObj = getObj("character", cID); + if (typeof CharObj === 'undefined') + return; + let npc = Earthdawn.getAttrBN( cID, "NPC", "1" ); +// if(( mooks && ( npc == "2" )) || ( notMooks && ( npc != "2" )) || ( NPCs && ( npc != "0" )) || ( PCs && ( npc == "0" ))) { + if(( !mooks || ( npc == Earthdawn.charType.mook )) && ( !notMooks || ( npc != Earthdawn.charType.mook )) + && ( !NPCs || ( npc != Earthdawn.charType.pc )) && ( !PCs || ( npc == Earthdawn.charType.pc ))) { + let TokenName = TokObj.get("name"); + if( TokenName.length < 1 ) + TokenName = CharObj.get("name"); + edParse.tokenIDs.push(sel._id); + objarr.push({ type: "token", name: TokenName, tokenObj: TokObj, characterObj: CharObj }); + } } + }); // End for each selected token + + if( this.charID !== undefined ) { // If we did not find any of this character in the selected tokens, look for unselected tokens with this charID. + if( bust && ((objarr.length || 0) < 1 )) { + let CharObj = getObj("character", this.charID); + if ((typeof CharObj != 'undefined') && ( this.edClass.msg !== undefined )) { + let pages = this.findAllPagesAnyPlayterIsOn(), + tkns = []; + pages.forEach(( page ) => { + tkns = _.union( tkns, findObjs({ _pageid: page, _type: "graphic", _subtype: "token", represents: this.charID })); + }); + _.each( tkns, function( TokObj ) { // Check all tokens found + if( btuc && objarr.length > 0 ) + return; + let TokenName = TokObj.get("name"); + if( TokenName.length < 1 ) + TokenName = CharObj.get("name"); + edParse.tokenIDs.push( TokObj.get("id") ); + objarr.push({ type: "token", name: TokenName, tokenObj: TokObj, characterObj: CharObj }); + }) // End ForEach Token + }; // End charObj found + } // End - This characters token was not selected. Search for it. + + if( bc && (objarr.length || 0) == 0) { + let CharObj = getObj("character", this.charID); + if (typeof CharObj != 'undefined') + objarr.push({ type: "character", name: CharObj.get("name"), characterObj: CharObj }); // All we have is character information. + } + else if ( (bsct || binmt) && objarr.length > 1 && Earthdawn.getAttrBN( this.charID, "NPC", "1" ) != Earthdawn.charType.mook ) { +// cdd see if we can get this to not happen on skills! + this.chat( "Error! ForEachToken() with non-mook character and more than one token for that character, none of which were selected.", Earthdawn.whoFrom.apiError); + objarr = []; + } } + + // Now that we have a list of tokens, have the parser go through each remaining items in the command line individually, once for each token. + if( (objarr.length || 0) > 0) { + if( flag === 1 ) // list + return objarr; + else if ( flag === 2) { // Status + let edpS1 = edParse.tokenInfo, + edpS2 = edParse.charID, + ret = false; + _.each( objarr, function ( obj ) { + edParse.tokenInfo = obj; + edParse.charID = obj.characterObj.get( "id"); + if( edParse.TokenGet( stat ).length > 0 ) + ret = true; + }); // End for each Token + edParse.tokenInfo = edpS1; + edParse.charID = edpS2; + return ret; + } else { // This is the more normal case. Call all the rest of the command line, with each token found. + let miscsave = _.clone( edParse.misc ); // Otherwise this gets passed by reference and all copies end up sharing the same object. So save a clone, and explicitly clone the clone back in. + this.indexToken = 0; + _.each( objarr, function ( obj ) { + let newParse = _.clone( edParse ); + newParse.misc = _.clone( miscsave ); + newParse.tokenInfo = obj; + newParse.charID = obj.characterObj.get( "id"); // Make sure that charID is set to the correct character for this token. + if( newParse.tokenInfo.type === "token" && newParse.tokenInfo.tokenObj.get("id") !== edParse.tokenIDs[ edParse.indexToken] ) + newParse.chat( "Warning! Possible error in ForEachToken() tokenID of " + newParse.tokenInfo.tokenObj.get("id") + " not equal " + edParse.tokenIDs[ edParse.indexToken], Earthdawn.whoFrom.apiError); + newParse.checkForStoredTargets(); + ++edParse.indexToken; + }); // End for each Token + } + edParse.indexMsg = edParse.edClass.msgArray.length; // Set edParse to be Done for the original copy (since have already done it for each copy). + } else { + this.chat( "Error! No token selected.", Earthdawn.whoFrom.apiWarning ); + this.indexMsg = this.edClass.msgArray.length; + } + } catch(err) { Earthdawn.errorLog( "ED.ForEachToken() error caught: " + err, edParse ); } + } // End ParseObj.ForEachToken() + + + + // ParseObj.ForEachTokenList() + // We have been passed a list of Token IDs. (generated by a previous threads call to ForEachToken() + // One by one, do a ForEach loop for each token id. + // Note that this is usually the list of tokens we are doing the command for (not to). IE: Tokenlist is attacking TargetSet. + this.ForEachTokenList = function( ssa ) { + 'use strict'; + let edParse = this; + try { + this.tokenIDs = []; + for( let i = 1; i < ssa.length; i++) + this.tokenIDs.push( ssa[ i ] ); + for( this.indexToken = 0; this.indexToken < this.tokenIDs.length; this.indexToken++) { + let TokObj = getObj("graphic", this.tokenIDs[ this.indexToken ]); + if (typeof TokObj === 'undefined' ) + continue; + + let cID = TokObj.get("represents"); + let CharObj = getObj("character", cID) || ""; + if (typeof CharObj === 'undefined') + continue; + + let TokenName = TokObj.get("name"); + if( TokenName.length < 1 ) + TokenName = CharObj.get("name"); + let newParse = _.clone( edParse ); + newParse.charID = cID; + newParse.tokenInfo = { type: "token", name: TokenName, tokenObj: TokObj, characterObj: CharObj }; + newParse.checkForStoredTargets(); + } + edParse.indexMsg = edParse.edClass.msgArray.length; // Set edParse to be Done for the original copy (since have already done it for each copy). + } catch(err) { Earthdawn.errorLog( "ED.ForEachTokenList() error caught: " + err, edParse ); } + edParse.indexMsg = edParse.edClass.msgArray.length; // Set edParse to be Done for the original copy (since have already done it for each copy). + return; + } // End ParseObj.ForEachTokenList() + + + + // If the current token has previously set targets, call forEachTarget(). + // otherwise just call to ParseLoop as a targetlist is coming in a separate command. + this.checkForStoredTargets = function() { + 'use strict'; + try { + if( this.targetIDs.length !== 0 ) + this.chat( "Warning! checkForStoredTargets() already has targetIDs defined. Check for bugs. Msg: " + this.edClass.msg.content + " Index = " + this.indexMsg, Earthdawn.whoFrom.apiError); + + if( !( this.bFlags & Earthdawn.flagsTarget.Mask ) + || ( this.bFlags & (Earthdawn.flagsTarget.Ask | Earthdawn.flagsTarget.Riposte)) + || ( this.bFlags & Earthdawn.flagsTarget.Set )) // target type is none or ask, so no target tokens involved. + this.ParseLoop(); + else { + let t = this.TokenGet( "TargetList" ); + if( t.length === 0) + this.ParseLoop(); + else { // a target list was stored for this token. Do all following commands for each target. + this.targetIDs = t; + this.forEachTarget(); + } } + } catch(err) { Earthdawn.errorLog( "ED.checkForStoredTargets() error caught: " + err, this ); } + return; + } // End ParseObj.checkForStoredTargets() + + + + + // If coming from checkForStoredTargets() then we don't have ssa. + // If the current token has previously set targets, loop through for each target. + // otherwise just call to ParseLoop as a targetlist is coming in a separate command. + // If ssa defined, then coming from Parse(). + // + // Note that this routine is also the one that evaluates target defenses and finds the highest among all targets: + // In which case the highest is the only target processed. + // It also distributes multiple targets among multiple tokens. + this.forEachTarget = function( ssa ) { + 'use strict'; + try { + this.TokenSet( "clear", "Hit"); // Clear any hits that may be attached to any tokens. + if( ssa !== undefined ) { // ssa is a target list that should be stored. + this.targetIDs = []; + for( let i = 1; i < ssa.length; i++ ) + this.targetIDs.push( ssa[ i ] ); + } + if( this.targetIDs.length === 0 ) + this.chat( "Warning! ForEachTarget() has no targets. Check for bugs. Msg: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else { // If we have targets (ether from preset targets or a TargetSet command) + if( this.bFlags & Earthdawn.flagsTarget.Highest ) { // If target is looking for highest one in the list, find it and then continue with the rest of the commands (we don't do anything for each target, we just find the one target). + let highnum = -999, + highsr = -999, + highindex; + for( let i = 0; i < this.targetIDs.length; i++ ) { + let x = this.TargetCalc( this.targetIDs[ i ], this.bFlags ); + if( x !== undefined ) { + if( x[ "val" ] > highnum ) { + highindex = i; + highnum = x[ "val" ]; + this.misc[ "targetName" ] = x[ "name" ]; + } + if(( "spiritRating" in x) && x[ "spiritRating"] && x[ "spiritRating" ] > highsr ) + highsr = x[ "spiritRating" ]; + } } + if( highindex !== undefined ) { + this.indexTarget = highindex; + if( this.targetIDs.length > 1 ) + this.misc[ "targetName" ] += " and " + (this.targetIDs.length === 2 ? "1 other" : (( this.targetIDs.length -1 ).toString() + " others")); + if( highnum ) + this.misc[ "targetNum" ] = Math.max( highnum, ( "targetNum" in this.misc ) ? this.misc[ "targetNum" ] : 0); + } + if( highsr > 0 ) + this.misc[ "highSR" ] = highsr; + } else if( this.bFlags & Earthdawn.flagsTarget.Each ) { // Save all the target numbers, since we will be comparing each of them to the rolled value later. Again this does not setup multiple rolls. + + let highsr = -999; + this.eachTargets = []; + for( let i = 0; i < this.targetIDs.length; ++i ) { + let x = this.TargetCalc( this.targetIDs[ i ], this.bFlags ); + if( x !== undefined ) { + if( x[ "val" ] ) + this.eachTargets.push({ tID: this.targetIDs[ i ], tName: x[ "name" ], tNum: x[ "val" ] }); + if(( "spiritRating" in x) && x[ "spiritRating"] && x[ "spiritRating" ] > highsr ) + highsr = x[ "spiritRating" ]; + } } + if( highsr > 0 ) + this.misc[ "highSR" ] = highsr; + } else { // Do the action once for each target in target list. + this.misc[ "queueRolls" ] = true; + let saveMisc = JSON.stringify( this.misc ), + rq = []; + for( this.indexTarget = 0; this.indexTarget < this.targetIDs.length; this.indexTarget++ ) { + // We are at a place where the action is done once for each target list. + // However, if we have multiple tokens, and multiple targets, evenly distribute the targets among the tokens. + // At this point we do this by ignoring any targets that don't get assigned to this token. + if( this.tokenIDs.length < 2 || ( this.indexTarget % this.tokenIDs.length) == (this.indexToken || 0 )) { + let newParse = _.clone( this ); + newParse.misc = JSON.parse( saveMisc ); // Make certain we get rid of stuff from the last loop. + let x = newParse.TargetCalc( newParse.targetIDs[ newParse.indexTarget ], newParse.bFlags ); + if( x !== undefined ) { + newParse.misc[ "targetNum" ] = (( "targetNum" in newParse.misc) ? newParse.misc[ "targetNum" ] : 0) + x[ "val" ]; + newParse.misc[ "targetName" ] = x["name"]; + if(( "spiritRating" in x) && x[ "spiritRating"] ) + newParse.misc[ "highSR" ] = x[ "spiritRating" ]; + else + newParse.misc[ "highSR" ] = undefined; + newParse.ParseLoop(); + rq.push( JSON.stringify( newParse[ "misc" ] )); // save the state for a roll we will be making later. + } } } + this.indexMsg = this.edClass.msgArray.length; // Set edParse to be Done for the original copy (since have already done it for each copy). + this.rollQueue = rq; + if( this.rollQueue.length > 0 ) { // For each roll we want to make, we have collected the information we need in this.misc. Now we launch the first roll (subsequent rolls will be launched by rollFormat() + this.indexTarget = 0; + this.misc = JSON.parse( this.rollQueue.shift() ); + this.rollPost(); + } } } + } catch(err) { Earthdawn.errorLog( "ED.ForEachTarget() error caught: " + err, this ); } + return; + } // End ParseObj.ForEachTarget() + + + + + // If this is not a damage roll, just call Roll() + // If this is a damage roll, see if any of the tokens being processed, have a recorded hit. + // If so, call Roll once for each hit (which might be zero, one, or more). + // Otherwise, just call Roll once for each token. + this.ForEachHit = function( ssa ) { + 'use strict'; + try { + if( !( this.bFlags & Earthdawn.flagsArmor.Mask )) { // This is not a damage roll, so just proceed to Roll(). + this.rollPre( ssa ); + return; + } + // If this is the first token to be processed, find out if any tokens have hits. + if( !(this.uncloned.bFlags & (Earthdawn.flags.HitsFound | Earthdawn.flags.HitsNot ))) { + let fnd = false; + for( let i = 0; !fnd && i < this.tokenIDs.length; ++i ) + if( this.TokenGetWithID( "Hit", this.tokenIDs[ i ] ).length > 0 ) + fnd = true; + this.uncloned.bFlags |= fnd ? Earthdawn.flags.HitsFound : Earthdawn.flags.HitsNot; + } + + if( this.uncloned.bFlags & Earthdawn.flags.HitsFound ) { + this.targetIDs = this.TokenGet( "Hit" ); + if( this.bFlags & Earthdawn.flags.WillEffect ) { // if it is a will effect damage roll, there is only one roll made. +//log( "willeffect dmg roll"); +// cdd note, we also want a keyword for this, not only willeffect. Or possibly a keyword for the else below. + this.indexTarget = 0; + this.rollPre( [ "Roll"] ); + } else { // Otherwise we make one roll per hit + this.misc[ "queueRolls" ] = true; + let saveMisc = JSON.stringify( this.misc ); + for( this.indexTarget = 0; this.indexTarget < this.targetIDs.length; this.indexTarget++ ) { + if( this.indexTarget > 0 ) + this.misc = JSON.parse( saveMisc ); // restore this.misc to what it was before the first loop through. + this.rollPre( [ "Roll"] ); + this.rollQueue.push( JSON.stringify( this[ "misc" ] )); // save the state for a roll we will be making later. + } + if( this.rollQueue.length > 0 ) { // For each roll we want to make, we have collected the information we need in this.misc. Now we launch the first roll (subsequent rolls will be launched by rollFormat() + this.indexTarget = 0; + this.misc = JSON.parse( this.rollQueue.shift() ); + this.rollPost(); + } } + } else // No token has hits recorded. Just do it once for each token. + this.rollPre( [ "Roll"] ); + } catch(err) { Earthdawn.errorLog( "ED.ForEachHit() error caught: " + err, this ); } + return; + } // End ParseObj.ForEachHit() + + + + // Special Effects. + // This routine does two things. Records the user requested FX in the character sheet, and actually displays it on the VTT. + // If ssa [ 0 ] == FXset then a button has been pressed to set FX to a Talent, Knack or Spell. + // If it is a string that contains an FX entry, Then we need to generate an FX display upon the VTT. + this.FX = function( ssa ) { + 'use strict'; + try { + if( typeof ssa === "string" ) { // Being called from one of two places in Roll(). + let txt = "", + ss = ssa.split( "," ); + if( ss[ 3 ].endsWith( "FTO" ) && (this.indexTarget || 0) != 0 ) + return; // Effect is for first target only, and this is not the first target. + + let start = this.indexTarget || 0, + end = start + 1; + if( Earthdawn.safeString( ss[ 0 ] ).toLowerCase() === "effect" && start == 0 ) // Effect tests usually (possibly always) only come through here once, so we need to simulate going through once per target. Everything else should be going through this routine once per target already. + end = this.targetIDs.length; + + for( let ind = start; ind < end; ++ind ) { + let typ = (ss[ 1 ] + "-" + Earthdawn.safeString( ss[ 2 ]) ).toLowerCase(); + if( Earthdawn.safeString( ss[ 1 ]).startsWith( "Custom " )) { // Custom Effect + let cust = findObjs({ _type: 'custfx', name: Earthdawn.safeString( ss[ 1 ] ).slice( 7 ) })[0]; + if( cust && cust.get( "_id" )) + typ = cust.get( "_id" ); + else { + this.chat( "Error! Invalid Custom FX Name: '" + ss[ 1 ] + "'. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiWarning ); + return; + } + } // End custom effect. + if (Earthdawn.safeString( ss[ 3 ] ).startsWith( "Ct" )) { // An effect that travels between two points. + if( this.tokenIDs == undefined || (this.tokenIDs[ this.indexToken || 0]) >= this.tokenIDs.length ) + Earthdawn.errorLog( "Error! bad tokenID in FX", this); + else if (this.targetIDs [ ind ] == undefined) + Earthdawn.errorLog( "Error! targetIDs[] undefined in FX.", this); + else { + let tokObj1 = getObj("graphic", this.tokenIDs[ this.indexToken || 0 ] ), // Caster + tokObj2 = getObj("graphic", Earthdawn.getParam( this.targetIDs [ ind ], 1, ":")); // Target. + if( !tokObj1 ) + Earthdawn.errorLog( "Error! Unable to get Caster Token in FX." + this.edClass.msg.content, this); + else if ( !tokObj2 ) + Earthdawn.errorLog( "Error! Unable to get Target Token " + ind + " (" + this.targetIDs [ ind ] + ") in FX.", this); + else + spawnFxBetweenPoints({x: tokObj1.get( "left" ), y: tokObj1.get( "top" )}, {x: tokObj2.get( "left" ), y: tokObj2.get( "top" )}, typ, tokObj1.get( "_pageid" )); + } + } else { // A single point effect. + let tokObj = getObj("graphic", (Earthdawn.safeString( ss[ 3 ]).startsWith( "CO" )) ? this.tokenIDs[ this.indexToken || 0 ] + : Earthdawn.getParam( this.targetIDs [ ind ], 1, ":")); // Caster or Target. + if( tokObj ) + spawnFx( tokObj.get( "left" ), tokObj.get( "top" ), typ, tokObj.get( "_pageid" )); + else + Earthdawn.errorLog( "Error! Unable to get Token in FX.", this); + } } + } else if( Earthdawn.safeString( ssa[ 0 ] ).toLowerCase() === "fxset" ) { // Being called from Parse(). + // FXset: (1)(code): (2)(rowID): (3)Set/Clear: (4)Attempt/Success: (5)Effect: (6)Color + let to = ""; + if (ssa[ 3 ] === "Set" ) { // If not Set, it is Clear, so just leave the empty string. + if( Earthdawn.safeString( ssa[ 5 ] ).startsWith( "Custom " )) + ssa[ 6 ] = ""; // Custom effects ignore color. + ssa[ 5 ] = Earthdawn.safeString( ssa[ 5 ] ).replace( /}/g, ""); // Due to a system bug, I get an extra closing brace. Just remove it here. + let typ = ssa[ 5 ].toLowerCase(); + if( Earthdawn.safeString( ssa[ 7 ] ).startsWith( "Ct" ) && ( typ !== "beam" && typ !== "breath" && typ !== "splatter" && !typ.startsWith( "custom" ))) + this.chat( "Warning! Only Beam, Breath, Splatter and Custom special effects can travel from the caster to a target. All others must affect only a single point, caster or targets. Try again.", Earthdawn.whoFrom.apiWarning ); + else if( !Earthdawn.safeString( ssa[ 7 ] ).startsWith( "Ct") && ( typ == "beam" || typ == "breath" || typ == "splatter" )) + this.chat( "Warning! Beam, Breath, and Splatter special effects must travel from the caster to a target. Try again.", Earthdawn.whoFrom.apiWarning ); + else + to = ssa.slice( 4 ).toString(); // separate out the special effects entries. + this.chat( "Special Effects for " + Earthdawn.codeToName( Earthdawn.safeString( ssa[ 1 ] )) + " " + + Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( ssa[ 1 ], Earthdawn.safeString( ssa[ 2 ])) + "Name")) + " Set." ); + } else + this.chat( "Special Effects for " + Earthdawn.codeToName( ssa[ 1 ] ) + " " + + Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( ssa[ 1 ], ssa[ 2 ]) + "Name") + " Cleared." ); + this.setWW( Earthdawn.buildPre( ssa[ 1 ], ssa[ 2 ] ) + "FX", to ); + } else + this.chat( "Error! badly formed FX command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiWarning ); + } catch(err) { Earthdawn.errorLog( "ED.FX error caught: " + err, this ); } + } // End ParseObj.FX() + + + + // return the token name of the passed tokenID + this.getTokenName = function( tID ) { + 'use strict'; + try { + let TokenObj = getObj( "graphic", tID); + if (typeof TokenObj === 'undefined' ) + return; + + let TokenName = TokenObj.get( "name" ); + if( TokenName === undefined || TokenName.length < 1 ) { + let CharObj = getObj( "character", TokenObj.get( "represents" )) || ""; + if (typeof CharObj === 'undefined' || CharObj == "") + return; + TokenName = CharObj.get("name"); + } + return TokenName; + } catch(err) { Earthdawn.errorLog( "ED.getTokenName() error caught: " + err, this ); } + } // End ParseObj.getTokenName() + + + + // ParseObj.GetTokenStatus() + // ca: Condition Array [0]: Code, [1] multiplier. + // cID: character ID + // sm status markers. + // + // Return: + this.GetTokenStatus = function( ca, cID, sm ) { + 'use strict'; + let ret = 0; + try { +//log("GetStatusToken ( " + JSON.stringify(ca) + " , " + JSON.stringify(sm) + " ) ") + let mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio["code"] == ca[ 0 ]; }); + if( mi !== undefined ) + ret = Earthdawn.parseInt2( getAttrByName( cID, mi["attrib"], 0 )) * ca[1]; + } catch(err) { Earthdawn.errorLog( "ED.GetTokenStatus() error caught: " + err, this ); } +//log( "ca " + ca + " ret " + ret); + return ret; + } // End ParseObj.GetTokenStatus() + + + + // ParseObj.GetTokenValue() + // We are passed a character attribute and a token ID. + // Call finishTokenValue to process the value we look up. + // Note: this part needs finishing later. + // Lookup the value and modify it by all conditions of the character and status markers of each token. + // Note that the TokenID may not be the current this.tokenInfo ID. It may be a target ID. + // Return fallout +// cdd +// This is not called yet. wait until METracker is done for this to integrate with it. + this.getTokenValue = function( attrib, tokenID ) { + 'use strict'; + let ret = 0; + try { + let TokObj = getObj("graphic", tokenID.trim()); + if (typeof TokObj === 'undefined' ) + this.chat( "Error! Bad Token. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else { + let cID = TokObj.get("represents"); + if ( cID === undefined || cID.length < 3 ) + this.chat( "Error! Token not linked to character. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else { + switch( attrib ) { + case "PD": + case "MD": + // Temporary code until the more complex stuff can be done. + case "SD": + ret = getAttrByName( cID, attrib ); +//log( ret); +// bases = [ attrib + "-Base", "Adjust-Defenses-Misc"]; +// conditionsPlus = [ ["cover", 1], ["defensive", 3] ]; +// if(attrib == "PD") +// conditionsMinus = [ ["harried", 1], ["surprised", 3], ["aggressive", 3], ["blindsided", 2], [ "Shield-Phys" ], ["knocked", 3] ]; +// else +// conditionsMinus = [ ["harried", 1], ["surprised", 3], ["aggressive", 3], ["blindsided", 2], [((attrib == "PD") ? "Shield-Phys" : "Shield-Myst" )], ["knocked", 3] ]; + break; +// case "SD": +// bases = ["SD"]; +// break; + } +/* +log( bases.length); + _.each( bases, function (base) { + ret += Earthdawn.parseInt2( getAttrByName( cID, base )); +log( "t " + t + " raw " + ret); + }) // End ForEach base in bases + + Earthdawn.getStatusMarkerCollection(); + _.each( conditionsPlus, function (condition) { + ret += this.getTokenStatus( condition, cID, sm ) + }) // End ForEach base in bases + + + _.each( conditionsMinus, function (condition) { + ret -= this.getTokenStatus( condition, cID, sm ) + }) // End ForEach base in bases +*/ +// Note don't get final value, get raw value and then look for both character sheet and token mods. If ether appears, apply the moe. but don't do both. +/* +Get these in pairs, char sheet attrib and token status, get them ORed, then figure mod. + var val = getInt( values, "Adjust-Defenses-Misc") + getInt( values, "condition-Cover") + (getInt( values, "combatOption-DefensiveStance") * 3) - + ( getInt( values, "condition-Harried") + (getInt( values, "condition-Surprised") * 3) + (getInt( values, "combatOption-AggressiveAttack") * 3) + + ( getInt( values, "condition-Blindsided") *2) + (getInt( values, "condition-KnockedDown") *3) ); + setAttrsLog({ "Adjust-Defenses-Total": val }); + var val = getInt( values, "PD-Base") + getInt( values, "Adjust-Defenses-Total" ) - + (( values[ "condition-Blindsided"] == "1" ) ? getInt( values, "Shield-Phys" ) : 0); + setAttrsLog({ "PD": val }); +*/ + } // End cID defined + } // End TokObj defined + } catch(err) { Earthdawn.errorLog( "ED.GetTokenValue() error caught: " + err, this ); } + return ret; + } // End ParseObj.GetTokenValue() + + + + // if subfunct is 1, return collection of selected token ids and character ids, grouped by character. + // {"Cid1":[{"token":"Tid1","character":"Cid1"},"token":"Tid2","character":"cid1"],"-O1IHlYDchqYzWPQuJNM":[{"token":"-O1IHoF82AvcOIdVmnUh","character":"-O1IHlYDchqYzWPQuJNM"}]} + // for( let c in lst ) { // For each unique character, do the following. + // for( let t in lst[ c ] ) { // For each unique token found, do the following. + // let TokenObj = getObj("graphic", lst[ c ][ t ][ "token" ]); }} + // if subfunct is 2, return list of selected character tokens, ungrouped. + // This is an easy way to see how many different charcters are selected. + this.getUniqueChars = function( subfunct ) { + 'use strict'; + let po = this; + try { + let arr = []; + _.each( po.edClass.msg.selected, function( sel ) { // Check selected tokens + let TokObj = getObj( "graphic", sel._id ); + if (typeof TokObj === 'undefined' ) + return; + if ( TokObj.get( "_subtype" ) !== "token") + return; + let cID = TokObj.get("represents"); + if( cID ) + arr.push( { token: TokObj.get("_id"), character: cID }); + }); + return (( subfunct === 1) ? _.groupBy( arr, "character" ) : arr ); + } catch(err) { Earthdawn.errorLog( "ED.getUniqueChars() error caught: " + err, po ); } + } // End getUniqueChars() + + + + + // attrib is an attribute to lookup. Get the value with all safety. Return the result. It should never return a null, and will return a zero instead. + // cID (optional) defaults to this.charID + // fSpecial (optional) if 1 then return value as a string, not a number. + // + // Note that this routine now should work with character sheet autocalculated fields. + this.getValue = function( attrib, cID, fSpecial ) { + 'use strict'; + let ret = 0; + try { + if( attrib !== undefined ) { + if( (typeof attrib) == "string" ) + attrib = Earthdawn.safeString( attrib ).trim(); + if( !isNaN( attrib) ) // If it is a number, just use it as a modifier to any result obtained elsewhere. + ret = attrib; + else { // We have a string of some sort, quite possibly a variable name. + if( cID === undefined ) + if( this.charID === undefined ) + this.chat( "Error! charID undefined in getValue() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else + cID = this.charID; + let raw; // Note: While we replaced most getAttrByName with getAttrBN, we leave these here in hope of getting real defaults. + if( attrib.endsWith( "_max" )) + raw = getAttrByName( cID, attrib.slice( 0, -4), "max"); + if( raw === undefined ) + raw = getAttrByName( cID, attrib ); // Treat this as a raw variable name and see what we get. + if( raw === undefined || raw === "") // Bug in getAttrByName() when dealing with repeating values, returns undefined if the value does not exist rather than the default value. This value might (or might not) be valid. Return zero for want of anything better to do. + ret = "0"; + else if( !isNaN( raw ) ) // We have an actual number to use + ret = raw; + else { // we probably have a formula for getting an actual number. + raw = Earthdawn.safeString( raw ); + let processed = raw, + begin = raw.indexOf( "@{" ), + end, + err = false; + while ( !err && begin != -1 ) { + end = processed.indexOf( "}" ); + if( end < begin ) + err = true; + else { + let tst = processed.slice( begin + 2, end); + if( attrib.startsWith( "repeating_" ) && tst.startsWith( Earthdawn.repeatSection( 3, attrib) + "_" )) // if the original attrib has a listed repeating section and rowID, and the derived numbers are from the same section, add the prefix to all the derived numbers. + tst = Earthdawn.buildPre( attrib ) + tst.slice( tst.indexOf( "_" ) +1); + processed = processed.slice(0, begin) + Earthdawn.safeString( this.getValue( tst )) + processed.slice( end + 1); + begin = processed.indexOf( "@{" ); + } } + if( err ) + this.chat( "Error! getValue() failure. '" + attrib + "' = '" + raw + "' Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else + ret = eval( processed ); + } } } + } catch(err) { Earthdawn.errorLog( "ParseObj.getValue() error caught: " + err, this ); } +//if( fSpecial == 2) log( "ret: '" + ret + "'"); // fSpecial == 2 is just an easy way to get logging on some values but not others. +//if( fSpecial == 2) log( "ret: '" + Earthdawn.parseInt2(ret) + "'"); + return ( fSpecial === 1 ) ? ret : Earthdawn.parseInt2( ret, true ); + } // End ParseObj.getValue() + + + + // This one routine should work with both version 2.0 and before, but the two versions work different and take different calls. + // Set the correct karma bonus and Devotion Points into misc.karmaDice, and adjust the karma and DP total. + this.Karma = function( ssa, kcdef, dpdef ) { + 'use strict'; + try { + if( this.tokenInfo === undefined ) { + this.chat( "Error! tokenInfo undefined in Karma() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + let kdice = 0, + ddice = 0, + kAsk = false, + dAsk = false, + attribute; + if( "kask" in this.misc) { + kdice = Earthdawn.parseInt2( this.misc[ "kask" ]); + if( kdice < 0 ) + kdice = 0; + if( kdice < 17 ) // This is a boundry, if kdice > 16, then don't set kAsk to true, so Auto will be used. + kAsk = true; + } + if( "dpask" in this.misc) { + ddice = Earthdawn.parseInt2( this.misc[ "dpask" ]); + if( ddice < 0 ) + ddice = 0; + if( ddice < 17 ) + dAsk = true; + } + + if( state.Earthdawn.sheetVersion < 1.8 || (!Earthdawn.isString( ssa) && !isNaN(ssa[ 1 ] ))) { + // NOTE: KEEP THIS. even with versions greater than 2.0 we still get some calls with a real ssa, mainly "one karma only", and + // action puts a karma command into Dolater. So this code is not dead. + // + // With version < 2.0, you are likely to get something called a "Karma-Control". + // ssa : Karma Control. -1 = Never, 0 or undefined = look to sheetwide karma. >1 = Always use this number of Karma. + // Note: also accepts ssa[0] being "def", "kcdef", or "dpdef" and ssa[1] being a numeric literal value to use as default, with other actual values to follow. + // Note: if kask and/or dpask are set, it just skips looking at karma control, so beware that an ask can overwrite karma control. + if( Earthdawn.isString( ssa )) + ssa = [ "", ssa ]; + let ttmp, kc, dp, + realkcdef = true, // We want to know if we were passed a real default for karma control, or whether we are assuming a default. + realdpdef = true; + if( kcdef === undefined) { kcdef = -1; realkcdef = false; } + if( dpdef === undefined || state.Earthdawn.g1879) { dpdef = -1; realdpdef = false; } + if((ssa !== undefined) && (ssa.length > 0 )) + for( let i = 1; i < ssa.length; i++) { + let skip = false; + kc = kcdef; + dp = dpdef; + ttmp = ssa[ i ]; + if( Earthdawn.isString( ttmp )) { + switch (ttmp.toLowerCase()) { + case "def": + kcdef = Earthdawn.parseInt2( ssa[ ++i ]); + dpdef = Earthdawn.parseInt2( ssa[ i ]); + realkcdef = true; + realdpdef = true; + skip = true; + break; + case "kcdef": + kcdef = Earthdawn.parseInt2( ssa[ ++i ]); + realkcdef = true; + skip = true; + break; + case "dpdef": + dpdef = Earthdawn.parseInt2( ssa[ ++i ]); + realdpdef = true; + skip = true; + break; + } + if( isNaN( ttmp )) { + if( !kAsk) { + let ttmp2 = Earthdawn.getAttrBN( this.charID, ttmp, realkcdef ? kcdef : // Talents and NACs default to karma sometimes. + (ttmp === "Dummy" || ttmp.endsWith( "_T_Karma-Control" ) || ttmp.endsWith( "_NAC_Karma-Control" ) + || (ttmp.startsWith( "SP-" ) && ttmp !== "SP-WilEffect-Karma-Control")) ? "0" : "-1" ); + if( ttmp2 !== undefined && ttmp2 !== "") { + let kc2 = Earthdawn.parseInt2( ttmp2 ); + if( !isNaN( kc2 ) ) + kc = kc2; + } + } + if( state.Earthdawn.gED && !dAsk ) { + let ttmp2 = Earthdawn.getAttrBN( this.charID, ttmp.replace( /Karma-/g, "DP-"), realdpdef ? dpdef : "-1" ); + if( ttmp2 !== undefined && ttmp2 !== "") { + let dp2 = Earthdawn.parseInt2( ttmp2 ); + if( !isNaN( dp2 ) ) + dp = dp2; + } } + } else if( Earthdawn.safeString( ssa[ 0 ] ).toLowerCase().startsWith( "d" )) // If we got a number, and ssa[0] starts with d, then it is DP, otherwise karma. + dp = Earthdawn.parseInt2( ttmp ); + else + kc = Earthdawn.parseInt2( ttmp ); + } + if( !kAsk && !skip ) + if (kc > 0) + kdice += kc; + else if ( kc == 0 ) + kdice += Earthdawn.getAttrBN( this.charID, "Karma-Roll", "0", true ); + if ( !dAsk && !skip ) + if (dp > 0) + ddice += dp; + else if ( dp == 0 && state.Earthdawn.gED) + ddice += Earthdawn.getAttrBN( this.charID, "Devotion-Roll", "0", true ); + } // End for each ssa. + } else { // state.Earthdawn.sheetVersion 2.0 or greater. (except that some v 2.0 and greater calls get processed in the block above this). + // With Version >= 2.0 you will get a string. + // ssa: Dex-Karma + // If this contains the word "karma" we also do the same thing with "DP". + // (this assumes that if passed "Dex-Karma", then there will also be: Dex-Karma-Limit, Dex-Karma-Limit_max, Dex-Karma-Ask, + // Dex-DP, Dex-LP-Limit, Dex-DP-Limit_max, and Dex-DP-Ask). + + // global karma has three modes, off, auto, and Ask. + // Local karma has a max, and a value from 0 through max. Auto + // If global Off, no karma. If Auto, use local. If ask, and the spread of allowed values is greater than one, ask. +// if( !_.String( ssa )) +// Earthdawn.errorLog( "ParseObj.Karma() error, SSA is not a string in version 2.0", this ); + if( !_.isString( ssa )) + if( ssa.length < 2 ) + Earthdawn.errorLog( "ParseObj.Karma() error, SSA is not a string and is of length less than 2 in version 2.0", this ); + else + ssa = ssa[ 1 ] + + if( !kAsk) { + let kgm = Earthdawn.getAttrBN( this.charID, "KarmaGlobalMode", "0" ); // 0: Off, x: Auto, ?:Ask + if( kgm !== "0" ) { // If not in karma off mode + let ttmp = Earthdawn.getAttrBN( this.charID, ssa, "0" ) + if (ttmp && ttmp > 0) + kdice = Earthdawn.parseInt2( ttmp ); + } } + if( !dAsk) { + let dgm = Earthdawn.getAttrBN( this.charID, "DPGlobalMode", "0" ); // 0: Off, 1: Auto, 2:Ask + if( dgm !== "0" ) { // If not in DP off mode + let lastInd = ssa.lastIndexOf( "Karma" ); + if( lastInd !== -1 ) { + let dpName = ssa.slice( 0, lastInd ) + "DP" + ssa.slice( lastInd + 5); // Replace "karma" with "dp" + let ttmp = Earthdawn.getAttrBN( this.charID, dpName, "0" ) + if (ttmp && ttmp > 0) + ddice = Earthdawn.parseInt2( ttmp ); + } } } } // end V2.0 and greater specific code. + + if( kdice > 0 ) { // Are we spending more than zero karma? + let kstep = Earthdawn.parseInt2( Earthdawn.getAttrBN( this.charID, "KarmaStep", "4" )), + currKarma, + bToken = ( this.tokenInfo.type === "token" ), // We have both a token and a character object. + npc = Earthdawn.getAttrBN( this.charID, "NPC", "1" ), + bMook = !((npc == Earthdawn.charType.pc) || (npc == Earthdawn.charType.npc)); + if( bToken && ( bMook || !(this.tokenInfo.tokenObj.get( "bar1_link" )) )) { // If it is a mook, get it from the token. + currKarma = Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar1_value" )); + if( isNaN( currKarma ) ) + currKarma = 0; + } + if( !bMook ) { // if not a mook, get from the character sheet. + attribute = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Karma" }, 0); + currKarma = Earthdawn.parseInt2( attribute.get( "current" )); + } + + if( kdice > currKarma ) { + let bad = true; + if( !bMook && currKarma < 1 && Earthdawn.getAttrBN( this.charID, "Karma", 0) != 0) { // before saying anything about not having enough karma, check that the token is linked properly. + log( Earthdawn.timeStamp() + "Note: Karma() thinks the token is not linked correctly, since the character sheet has karma, but the token does not. Attempting to relink." ) + this.LinkToken( [ "forceLink", this.tokenInfo.tokenObj.get( "_id" ) ] ); + currKarma = Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar1_value" )); + if( kdice <= currKarma ) { // and see if that helped. + bad = false; // fixed, and do have karma to spend. + log( "Linking seems to have fixed it"); + } else + log( "Linking does not seem to have fixed it"); + } + if( bad ) { + this.chat( "Error! " + this.tokenInfo.name + " does not have " + kdice + " karma to spend.", Earthdawn.whoFrom.apiWarning ); + kdice = currKarma; + } } + if( kdice > 0 ) { + currKarma -= kdice; + if( !bMook ) // if not a mook, set it on the character sheet. + Earthdawn.setWithWorker( attribute, "current", currKarma, 0 ); + if( bToken && ( bMook || !(this.tokenInfo.tokenObj.get( "bar1_link" )) )) // If it is a mook, set it on the token. + Earthdawn.set( this.tokenInfo.tokenObj, "bar1_value", currKarma ); + let corruptObj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Creature-CorruptKarma" }, 0), + corruptNum = Earthdawn.parseInt2(corruptObj.get( "current" )), + realdice = Math.max( 0, kdice - corruptNum ), + corrupted = kdice - realdice; + if( corrupted > 0 ) { + this.misc[ "CorruptedKarma" ] = corrupted; + Earthdawn.set( corruptObj, "current", corruptNum - corrupted); + } + + this.misc[ "karmaNum" ] = (("karmaNum" in this.misc) ? this.misc[ "karmaNum" ] : 0 ) + kdice; + let tmp = "", + dc = this.edClass.StepToDice( kstep ); + for( let ind = 1; ind <= realdice; ++ind ) + tmp += "+" + dc; + this.misc[ "karmaDice" ] = (("karmaDice" in this.misc) ? this.misc[ "karmaDice" ] : "" ) + tmp; + this.misc[ "effectiveStep" ] = (("effectiveStep" in this.misc) ? this.misc[ "effectiveStep" ] : 0 ) + (kdice * kstep); +// this.chat( this.tokenInfo.name + " spent " + kdice + " karma." ); + } + } // End we are actually spending karma. + + if( ddice > 0 ) { // Are we spending more than zero Devotion Points? Note: Devotion is a bit simpler than karma, since it in on the sheet only, not in the token bar. + let kstep = Earthdawn.getAttrBN( this.charID, "DevotionStep", "3", true ); + let attribute = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "DP" }, 0); + let currDP = Earthdawn.parseInt2(attribute.get( "current" )); + if( ddice > currDP ) { + this.chat( "Error! " + this.tokenInfo.name + " does not have " + ddice + " Devotion Points to spend.", Earthdawn.whoFrom.apiWarning ); + ddice = currDP; + } + if( ddice > 0 ) { + currDP -= ddice; + Earthdawn.setWithWorker( attribute, "current", currDP, 0 ); + + this.misc[ "DpNum" ] = (("DpNum" in this.misc) ? this.misc[ "DpNum" ] : 0 ) + ddice; + let tmp = "", + dc = this.edClass.StepToDice( kstep ); + for( let ind = 1; ind <= ddice; ++ind ) + tmp += "+" + dc; + this.misc[ "karmaDice" ] = (("karmaDice" in this.misc) ? this.misc[ "karmaDice" ] : "" ) + tmp; + this.misc[ "effectiveStep" ] = (("effectiveStep" in this.misc) ? this.misc[ "effectiveStep" ] : 0 ) + (ddice * kstep); + } + } // End we are actually spending DP. + return; + } catch(err) { Earthdawn.errorLog( "ED.Karma() error caught: " + err, this ); } + } // End ParseObj.Karma() + + + + // keyword( pre, canidate1, canidate2, ...) + // look in (pre + "Special") for a list of keywords this item has. + // Then look for all the other arguments within the keyword list. + // return a bitmap of each keyword found. + // if first and third canidates found, then would return 0x0101; + // + // Note, this routine is not used yet, but cuold be. + this.keyword = function( pre ) { + 'use strict'; + try { + let a = arguments; +log( typeof a); log ("this.keyword"); log( typeof arguments); + return Earthdawn.keywordCheck( Earthdawn.getAttrBN( this.charID, pre + "Special", "None"), a.slice( 1 )); // We cut off the pre, and instead send the keyword list and all the canidates. +// return Earthdawn.keywordCheck( Earthdawn.getAttrBN( this.charID, pre + "Special", "None"), arguments.slice( 1 )); // We cut off the pre, and instead send the keyword list and all the canidates. + } catch(err) { log( Earthdawn.timeStamp() + "ED.keyword error caught: " + err ); } + } // end keyword +/* + let ret = 0; + if( arguments.length > 1 ) { + let special = Earthdawn.getAttrBN( this.charID, pre + "Special", "None"); + if( special ) + for( let i = 1; i < arguments.length; ++i ) + if( special === arguments[ i ] ) // in future versions this can check if in the lst, not just equal. + ret += 1 << (i - 1); + } + return ret; +*/ + + + + // ParseObj.LinkToken () + // Make sure the character associated with CharID is ready to Link. + // Link all selected tokens to this character. + // + // By default the routine both sets the token to some standards, and links it. + // if ssa includes "SetTokenOnly", then set the token only, don't link it. + // ssa including "autoLink" works, but is now depretiated. It was called when the user changed 'represents' in token settings, but the problem was that + // the event fired while the user was still on the token settings screen this routines linking was overwritten if they pressed Save settings on that screen. + // Everything was OK if they pressed cancel instead and then entered the token screen again and saw the new values, but users might miss the message telling them that. + this.LinkToken = function( ssa ) { + 'use strict'; + let edParse = this; + try { + let setTokenOnly = ssa.includes( "SetTokenOnly" ), // standard modifiers are: Standard|SetTokenOnly|LinkOnly of which only setTokenOnly really does anything different. + linkOnly = ssa.includes( "LinkOnly" ), + forceLink = ssa.includes( "forceLink" ), + autoLink = ssa.includes( "autoLink" ), + buttonLink = ssa.includes( "buttonLink" ); // on change represents gives a chat window button. + if( this.charID === undefined ) { + this.chat( "Error! Trying to Link Token when don't have a CharID.", Earthdawn.whoFrom.apiError ); + return; + } + if( !forceLink && !buttonLink && this.tokenAction ) { + this.chat( "Error! Linktoken must be a character sheet action, never a token action.", Earthdawn.whoFrom.apiError); + return; + } + if( !forceLink && !autoLink && !buttonLink && (this.edClass.msg.selected === undefined )) { + this.chat( "Error! You must have exactly one token selected to Link the character sheet to the Token.", Earthdawn.whoFrom.apiWarning); + return; + } + let pc = Earthdawn.getAttrBN( this.charID, "NPC", "1"); + let s = pc ? state.Earthdawn.linking.PC : state.Earthdawn.linking.NPC, + count = 0; + if( !forceLink && !autoLink && !buttonLink && (pc == Earthdawn.charType.mook) && (this.edClass.msg.selected.length > 1 )) + { + this.chat( "Error! You can't link more than one token to a non-mook character!", Earthdawn.whoFrom.apiWarning ); + return; + } + let sName = "", + CharObj = getObj( "character", edParse.charID ), + sels = (forceLink || autoLink || buttonLink) ? [ {_id: ssa[ ssa.length -1 ] } ] : edParse.edClass.msg.selected; + + _.each( sels, function( sel ) { // This is ether the token passed in ssa[ 1 ] or it is messages selected. + 'use strict'; + let TokenObj = getObj("graphic", sel._id); + if (typeof TokenObj === 'undefined' ) + return; + + if( !setTokenOnly && !autoLink ) // if we did not specifically say not to link it, and if we are not called because we detected somebody linking it, link it. + Earthdawn.set( TokenObj, "represents", edParse.charID ); + sName = TokenObj.get( "name"); + if( sName === undefined || sName.length < 1 ) { + if (typeof CharObj !== 'undefined' ) { + sName = CharObj.get( "name" ); + if( !linkOnly ) + Earthdawn.set( TokenObj, "name", sName ); + } } + Earthdawn.set( TokenObj, "bar1_link", ""); + Earthdawn.set( TokenObj, "bar2_link", ""); + Earthdawn.set( TokenObj, "bar3_link", ""); + Earthdawn.set( TokenObj, "bar1_value", Earthdawn.getAttrBN( edParse.charID, "Karma", "0" )); + Earthdawn.set( TokenObj, "bar1_max", Earthdawn.getAttrBN( edParse.charID, "Karma_max", "0" )); + Earthdawn.set( TokenObj, "bar2_value", Earthdawn.getAttrBN( edParse.charID, "Wounds", "0" )); + Earthdawn.set( TokenObj, "bar2_max", Earthdawn.getAttrBN( edParse.charID, "Wounds_max", "8" )); + Earthdawn.set( TokenObj, "bar3_value", Earthdawn.getAttrBN( edParse.charID, "Damage", "0" )); + Earthdawn.set( TokenObj, "bar3_max", Earthdawn.getAttrBN( edParse.charID, "Damage_max", "20" )); + if( !linkOnly ) { + if( s.showname != undefined ) Earthdawn.set( TokenObj, "showname", s.showname, false ); + if( s.showplayers_name != undefined ) Earthdawn.set( TokenObj, "showplayers_name", s.showplayers_name, false ); + if( s.bar_location != undefined ) Earthdawn.set( TokenObj, "bar_location", s.bar_location, null ); + if( s.compact_bar != undefined ) Earthdawn.set( TokenObj, "compact_bar", s.compact_bar, null ); + } + + if( !setTokenOnly && ( pc == Earthdawn.charType.pc || pc == Earthdawn.charType.npc )) { // unique PC or NPC (not a mook). + let kobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: edParse.charID, name: "Karma" }, 0); + let kid = kobj.get("_id"); + if( kid !== undefined ) + Earthdawn.set( TokenObj, "bar1_link", kid ); + kobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: edParse.charID, name: "Wounds" }, 0); + kid = kobj.get("_id"); + if( kid !== undefined ) + Earthdawn.set( TokenObj, "bar2_link", kid ); + kobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: edParse.charID, name: "Damage" }, 0); + kid = kobj.get("_id"); + if( kid !== undefined ) + Earthdawn.set( TokenObj, "bar3_link", kid ); + } // End not mook + + if( typeof CharObj != 'undefined' ) { + ++count; + if( autoLink ) // We are called with autoLink, so only set the character to use this token by default if there is no other token set. + CharObj.get("_defaulttoken", function( dt ) { + 'use strict'; + if( !dt ) { // only if the current default token is empty. + setDefaultTokenForCharacter( CharObj, TokenObj); + if( !CharObj.get( "avatar" )) + Earthdawn.set( CharObj, "avatar", TokenObj.get( "imgsrc" ) ); + } + }); + else if( !setTokenOnly ) { // if linking without autoLinking, then set the character to use this token. + setDefaultTokenForCharacter( CharObj, TokenObj); + if( !CharObj.get( "avatar" )) + Earthdawn.set( CharObj, "avatar", TokenObj.get( "imgsrc" ) ); + } } + }); // End for each selected token + if( count == 0) { + if ( !setTokenOnly ) + this.chat( "Error! No selected token to link.", Earthdawn.whoFrom.apiError | Earthdawn.whoTo.player); + } else { + this.chat( "Token " + sName + " is linked" + (pc == Earthdawn.charType.mook ? " as Mook." : + ( pc == Earthdawn.charType.pc ? " as PC." : ( pc == Earthdawn.charType.npc ? "as NPC." : " as Object."))), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + if( autoLink ) + this.chat( "***Importaint The API has linked the token. Press 'Cancel' in 'Token Settings' instead of 'Save Settings' in order to avoid overwriting the changes. ***", Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + } + this.Debug( [ "Debug", "repSecFix", "silent" ] ); + } catch(err) { Earthdawn.errorLog( "ED.LinkToken error caught: " + err, edParse ); } + } // End ParseObj.LinkToken() + + + + + // ParseObj.Lookup() + // We are passed a character attribute and/or a modifier to an attribute. Lookup the value(s) from the character sheet. + // Note: This is setup like this so that several tags can direct to the same function, and that ether PD or @(PD) will work. + // Note also that the 2nd value is an ssa structure, so in most cases the very first item is ignored. In the example below mod is + // ignored by this routine (it was used to direct the parser to this routine). + // mod : PD : -2 // Find the Physical Defense, subtract 2 from it, and place it in "step" or add it to what is already there. + // + // wherePlace: Where does the result go? 1 = this.misc.step, 2 = this.misc.result, 3 - this.targetNum. + // 4 - The character sheet attribute named in ssa[1]. + // + // Note that if we are told to lookup a value that is an autocalculated field, it passes control to an asynchronous callback function to accomplish it. + // Return: whether Parse should fallout or not. IE: return false unless this thread has launched an asynchronous callback. + this.Lookup = function( wherePlace, ssa ) { + 'use strict'; +// log("this.Lookup " + JSON.stringify(ssa)); + try { + if( ssa.length < 2 ) { + this.chat( "Error! Lookup() not passed a value to lookup. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + return false; + } + let lu = 0, + rcmd = "", + i = 0, + attName; + +//log("this.Lookup " + JSON.stringify(ssa)); + // luResult() + // Put the result that was looked up in the correct variable. + function luResult( what, po ) { + 'use strict'; + if( what !== undefined ) + switch( wherePlace ) { + case 1: po.misc[ "step" ] = ( po.misc[ "step" ] || 0) + what - po.mookWounds(); break; + case 2: po.misc[ "result" ] = ( po.misc[ "result" ] || 0) + what; break; + case 3: if( what ) + po.misc[ "targetNum" ] = ( po.misc[ "targetNum" ] || 0) + what; break; + case 4: +//log( "lookup " + attName + " " + what); + let attribute = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: po.charID, name: attName }); + let old = attribute[ "current" ]; + Earthdawn.setWithWorker( attribute, "current", what); + + switch( attName ) { + case "Misc-StrainPerTurn": + case "Adjust-All-Tests-Misc": + case "Adjust-Attacks-Misc": + case "Adjust-Damage-Misc": + case "Adjust-Defenses-Misc": + case "PD-Buff": + case "MD-Buff": + case "SD-Buff": + case "PA-Buff": + case "MA-Buff": + case "Adjust-Effect-Tests-Misc": + case "Adjust-TN-Misc": + let m = new Map(); + m.set( "name", attName ); + m.set( "current", what ); + m.set( "_characterid", po.charID ); + Earthdawn.attribute( m, { name: attName, current: old, _characterid: po.charID } ); + } } + return; + } // end of luResult() + + + if( wherePlace === 4 ) + attName = ssa[ ++i ]; + + while( ++i < ssa.length ) { + if( !isNaN( ssa[ i ])) // If it is a number, just use it as a modifier to any result obtained elsewhere. + lu += Earthdawn.parseInt2( ssa[ i ]); + else { + if( this.tokenInfo === undefined ) { + this.chat( "Error! tokenInfo undefined in Lookup() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return false; + } + switch ( Earthdawn.safeString( ssa[ i ] ).toLowerCase() ) { + case "defensive": // This action is defensive. If Defensive Stance is turned on, add 3 to compensate for the three that were subtracted previously. + if( Earthdawn.getAttrBN( this.charID, "combatOption-DefensiveStance", "0" ) === "1" ) { + lu += Earthdawn.parseInt2(Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Penalty", "3" )); +//log(" parsing the defensive " + Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Penalty", "0" )); +// Do I need to look at T_Defensive in html for the values? + this.misc[ "Defensive" ] = true; // Since step was modified for being defensive, tell people. + } break; + // note: resistance is depreciated. + case "resistance": // This action is a Resistance Roll. If character is knocked down, add 3 to compensate for the three that were subtracted previously. + if( Earthdawn.getAttrBN( this.charID, "condition-KnockedDown", "0" ) === "1" ) { + lu += 3; + this.misc[ "Resistance" ] = true; // Resistance is depreciated 8/22 + } break; + case "movebased": { // This action is Movement Based. If Movement Penalties are in effect, subtract them from this result. + let tstep = Earthdawn.getAttrBN( this.charID, "condition-ImpairedMovement", "0" ); + if( tstep > 0 ) { + lu -= tstep; + this.misc[ "MoveBased" ] = (tstep == 2) ? "Partial" : Full; + } + } break; + // note: visionbased is depreciated. + case "visionbased": { // This action is Vision Based. If Vision Penalties are in effect, subtract them from this result. + let tstep = Earthdawn.parseInt2(Earthdawn.getAttrBN( this.charID, "condition-Darkness", "0" )); + if( tstep > 0 ) { + lu -= tstep; + this.misc[ "VisionBased" ] = (tstep == 2) ? "Partial" : Full; // NotVisionBased depreciated 8/22 + } + } break; + default: + let raw; + if( ssa[ i ].charAt( 1 ) === "{" && ( ssa[ i ].charAt( 0 ) != "?" && ssa[ i ].charAt( 0 ) != "@" )) // We want to filter out things that come from stuff like modValue : x{Modification|0} + raw = 0; + else { + raw = getAttrByName( this.charID, ssa[ i ] ); +//log("looking for " + ssa[i] + " result " + raw); + if( raw !== undefined ) + if( (typeof raw === "number") || (raw.indexOf( "@{") === -1)) // We have an actual number to use + lu += Earthdawn.parseInt2( raw ); + else // we have a formula for getting an actual number. + rcmd += "+(@{" + Earthdawn.safeString( this.tokenInfo.characterObj.get( "name" )) + "|" + Earthdawn.safeString( ssa[ i ]) + "})"; + } } } +//log("after ssa[i] " + ssa[i] + " lu is "+lu); + + } // end for each ssa item + + if( rcmd === "" ) + luResult( lu, this ); + else { // The main thread is to STOP PROCESSING THIS ITERATION! Control is being passed to callback thread. + let po = this; // We are sending the string to the roll server for it to parse and add up for us. + sendChat( "API", "/r [[" + rcmd.slice( 1 ) + "]]", function( ops ) { + 'use strict'; + // NOTE THAT THIS IS THE START OF A CALLBACK FUNCTION + let RollResult = JSON.parse(ops[0].content); + luResult( lu + RollResult.total, po ); + po.ParseLoop(); // This callback thread is to continue parsing this. + }, {noarchive:true}); // End of callback function + return true; + } + } catch(err) { Earthdawn.errorLog( "ED.Lookup() error caught: " + err, this ); } + return false; + } // End Lookup() + + + + // ParseObj.MarkerSet ( ssa ) + // Set the Status Markers for current tokens + // ssa[ 0 ] This does not matter, except if it is "sheetDirect", then the value has been directly changed on the sheet, and we + // want to set the status marker, but we do NOT want to update the value on the sheet (again). + // ssa[ 1 ] is condition to be set OR name of marker to be set. IE: "aggressive" or "sentry-gun" both set the same marker. + // ssa[ 2 ] level. + // If boolean false, -1 or start with letter the letter U (unset) or O (for off - but not equal ON), remove the marker. + // If zero or not present or is ON or starts with S (set), set the marker without a badge. + // If starts with a "t" than toggle it from set to unset or visa versa, or if more than two valid values, to the next value in the sequence. + // If starts with a "z", expect a numeric value, except in this specific case, a zero means unset. + // If 1 - 9, or A-I set the marker with the number as a badge. + // Note: there is a weird thing in linking a token where it is better to have no digits in the menu. thus A-I substitute for 1-9. + // If ++, --, ++n, or --n then adjust from current level. + // NOTE: if the marker status collection has a submenu, it is important to pass exactly values in the submenu, IE: u for unset, b for 2, etc. + // Example: [ "", "aggressive", "Set"] or [ "", "sentry-gun", "Off"]. + this.MarkerSet = function( ssa ) { + 'use strict'; + let po = this; + try { +//log("this.MarketSet ssa is : " + JSON.stringify(ssa)); + if( this.tokenInfo === undefined ) { + this.chat( "Error! tokenInfo undefined in MarkerSet() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + if( ssa.length < 3 ) { + this.chat( "Error! bad MarkerSet() arguments (" + JSON.stringify( ssa ) + "). Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + if( this.tokenInfo.type === "token" ) { + + // Find the status, icon, or attribute in the StatusMarkerCollection. + function findMenuItem( lookup ) { // This is reused at the bottom of MarkerSet. + 'use strict'; + let lowered = Earthdawn.safeString( lookup ).toLowerCase(), + t = "code"; + let mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "code" ] == lowered; }); // ( karmaauto ) + if( mi === undefined ) { + t = "icon"; + mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "icon" ] == lowered; }); // try it again with icon instead of code ( lightning-helix ). + } + if( mi === undefined ) { + t = "customTag"; + mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "customTag" ] == lookup; }); // ( 001-Karma-On ). Note that customTag has not been lowercased. + } + if( mi === undefined ) { + t = "attrib"; + mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "attrib" ] == lookup; }); // Finally, try looking for an attribute ( KarmaGlobalMode ). + } + if( mi === undefined ) + return; + return { item: mi, type: t }; + } // end function findMenuItem() + + + let tmp = findMenuItem( ssa[ 1 ] ); +//log( ssa[1]); log(tmp); + if( tmp === undefined ) + return; // This is not an icon we are interested in. + let mi = tmp.item, + luType = tmp.type, + sm = mi[ "submenu" ], + code = mi[ "code" ], + attrib = mi[ "attrib" ], + oldAttValue, + newAttValue, + shared = (mi[ "shared" ] === undefined) ? 1 : mi[ "shared" ], + neg = (typeof shared !== 'string') ? false : shared.slice(0, 3).toLowerCase() === "neg", + ss, + level, // -1 is unset, 0 is set, 1-9 is set with badge. + adjust = 0, // if we get an increment or decrement instead. + setObj, + mook = (Earthdawn.getAttrBN( this.charID, "NPC", "1" ) == Earthdawn.charType.mook ), + dupChar = false, + valid = [{ level: -1111, attrib: 0, badge: false, marker: Earthdawn.getIcon( mi ) }], // unset is always valid. + mia = _.filter( Earthdawn.StatusMarkerCollection, function(mio) { return mio[ "attrib" ] == attrib; }); // get an array of menu items with this attribute. + +//log(mia); + if( mia === undefined || mia.length === 0) { + this.chat( "Earthdawn: Markerset error. '" + attrib + "' not be found in StatusMarkerCollection.", Earthdawn.whoFrom.apiWarning ); + return; + } + if( attrib ) + oldAttValue = Earthdawn.getAttrBN( this.charID, attrib, 0); + + // Go through each menuitem that uses the attribute found, to find a list of all valid values. This is used to validate and to toggle to next value. + for( let i = 0; i < mia.length; ++i ) { + let tshared = mia[ i ][ "shared" ], + tsm = mia[ i ][ "submenu" ], + mark = Earthdawn.getIcon( mia[ i ] ); + if(( tshared === undefined ) && ( tsm === undefined )) // neither tsm nor tshared is defined. This is a simple on/off marker without expected badges. + valid.push({ level: 0, attrib: 1, badge: true, marker: mark }); // When attribute is 1, set the icon (no badge). + else if( tshared === undefined ) { // tsm is defined, but tshared is undefined. + // This is a classic submenu such as "?{Impaired Vision|None,[0^u]|Partial,[2^b]|Full,[4^d]}" + let sma = tsm.split( "|" ), // break into menu items. + fnd = 0; + for( let i = 1; i < sma.length; ++i ) { + let itma = sma[ i ].split( "," ); // break into prompt and code. + if( itma.length > 1 ) { // we have a menu that can be broken appart. + ++fnd; + let itm = itma[ 1 ].replace( "\[", "").replace( "\]", "" ); // remove brackets from code. + if( itm !== "0^u" ) { // Unset with value 0 is already in array. + let kernals = itm.split( "^" ), + kb = kernals[ 1 ], + kb2 = kb; + if( kb != undefined ) { + if( kb === "s" ) + kb2 = true; + if( kb === "u") + kb2 = false; + if(( kb >= "a") && ( kb <= "i" )) + kb2 = kb.charCodeAt( 0 ) - "a".charCodeAt( 0 ) + 1; + if(( kb >= "A") && ( kb <= "I" )) + kb2 = kb.charCodeAt( 0 ) - "A".charCodeAt( 0 ) + 1; + if( !isNaN( kernals[ 0 ] )) + if(!kb2) + valid[ 0 ].attrib = kernals[ 0 ]; //If we are in this loop, i.e. there is a submenu, but it is not "0^u", we should update the default unset attrib + else + valid.push({ level: Earthdawn.parseInt2( kernals[ 0 ]), attrib: Earthdawn.parseInt2( kernals[ 0 ]), badge: kb2, marker: mark }); +// log("test " + JSON.stringify(valid)); + } } } } + if( !fnd ) { // A classic submenu was not found, so we seem to have a freeform numeric input. + valid.push({ level: 0, attrib: 0, badge: true, marker: mark }); + for( let i = 1; i < 10; ++i ) + valid.push({ level: i, attrib: i, badge: i, marker: mark }); + } + } else if( tsm === undefined ) { // tshared is defined, but tsm is undefined. This means it is a check-box that has an on-value of something other than 1. + let doit = true; + if( luType === "code" && mia[ i ][ "code" ] != code ) // If somebody is trying to toggle karmaauto, then ignore karmaask as an option. + doit = false; + if( luType === "attrib" && (( attrib === "KarmaGlobalMode" && mia[ i ][ "code" ] === "karmaauto" ) + || ( attrib === "DPGlobalMode" && mia[ i ][ "code" ] === "devpntauto" )) + && Earthdawn.getAttrBN( this.charID, "show_karma_auto", "0") == "0" ) // This is specific to karma auto being hidden, then the karma toggle should skip it. + doit = false; + if( doit ) + valid.push({ level: tshared, attrib: tshared, badge: true, marker: mark }); // when attribute is tshared, set the marker (no badge). + } else // both tsm and tshared are defined. This should be a pos or neg, and a freeform integer value. + if( Earthdawn.safeString( tshared ).slice( 0, 3).toLowerCase() === "neg" ) { // There are a pair of markers, one has negative buffs, and the other has positive buffs. + for( let i = 9; i > 0; --i ) + valid.push({ level: 0 - i, attrib: 0 - i, badge: i, marker: mark }); + } else if( Earthdawn.safeString( tshared ).slice( 0, 3).toLowerCase() === "pos" ) { + for( let i = 1; i < 10; ++i ) + valid.push({ level: i, attrib: i, badge: i, marker: mark }); + } else + this.chat( "Earthdawn: Markerset Warning. Unable to parse " + JSON.stringify( mia[ i ] ), Earthdawn.whoFrom.apiWarning ); + } // end make list of validValues and validBadges + valid = _.sortBy( valid, "level" ); +//log(mi); log( JSON.stringify( valid)); + + if( ssa.length > 1 ) { // This section sets level and adjust. See the declarations above and the comments at the top of the routine. + if( ssa.length > 2 ) { + if( typeof ssa[ 2 ] === "boolean" ) { + if( ssa[ 2 ] === true ) + ss = "s"; + else + ss = "u"; + } else if( typeof ssa[ 2 ] === "number" ) { + ss = ssa[ 2 ]; + if( ss > 9 ) ss = 9; + if( ss < 0 ) ss = 0; + ss = ss.toString(); + } else + ss = Earthdawn.safeString( ssa[ 2 ] ).toLowerCase(); + } else + ss = "s"; // if not passed, default is to set it. + + if( ss.substring( 0, 2) === "z0" ) + level = -1111; + else if ( ss.substring( 0, 1 ) === "z" ) + ss = ss.slice( 1 ); // if it starts with a z, then there should be a numeric value following. + if( ss.substring( 0, 2) === "--" ) // Decrement the current value by this amount. + adjust = -1; + else if ( ss.substring( 0, 2) === "++" || ss.substring( 0, 1) === "t") // toggle is just ++ now, this will do binary or trynary toggles, or just cycle through whatever is there. + adjust = 1; + else if( ss.substring( 0, 1) == "s" || ss.substring( 0, 2) == "on" || ss.substring( 0, 1) == "0" || ss.substring( 0, 1) === "`" ) // On or Set. Last is what you get if somebody does @0 + level = 0; // set with no badge. + else if( ss.substring( 0, 1) == "u" || ss.substring( 0, 1) == "o" || ss.substring( 0, 2) == "-1" ) // If starts with -1 or an O for Off or U for Unset. + level = -1111; // unset + else if ( '0' <= ss[ 0 ] && ss[ 0 ] <= '9' ) // badges + level = ss.charCodeAt( 0 ) - "0".charCodeAt( 0 ); + else if ( 'a' <= ss[ 0 ] && ss[ 0 ] <= 'i' ) // alphabetic badges + level = ss.charCodeAt( 0 ) - "a".charCodeAt( 0 ) + 1; + if( adjust != 0 && ss.length > 2 ) { // This is for adjustments greater than one. ie: ++3 + let t = 1; + if ( '0' <= ss[ 2 ] && ss[ 2 ] <= '9' ) + t = ss.charCodeAt( 2 ) - "0".charCodeAt( 0 ) + 1; // 48 is zero + else if ( 'a' <= ss[ 2 ] && ss[ 2 ] <= 'i' ) + t = ss.charCodeAt( 2 ) - "a".charCodeAt( 0 ) + 1; // 97 is 'a' + adjust = adjust * t; + } + } // end processing ssa + if( level > 0 && neg ) + level *= -1; + // at this point level is -1111 for unset, 0 for set, or a badge between 1 and 9. Adjust might be set, which will modify these these. +//log("level " + level); + + // Given a potential level value, see if it is one of the expected values. If so return that valid item. + // If not, find the closest one. + // Rules: If lower than any option, round up to lowest option. If higher than any option, round down to highest option. + // Else round up to next highest option. + function findMenu( val, attr ) { // val is value to match. attr says whether it is to match attrib or level. + let onValue; + if( attr ) + onValue = _.find( valid, function( item ) { return item.attrib == val; }); + else + onValue = _.find( valid, function( item ) { return item.level == val; }); + if( onValue !== undefined ) + return onValue; + else { + let low, high, rndup, rnddown; + _.each( valid, function( item ) { + if( low === undefined || item.level < low.level ) + low = item; + if( high === undefined || item.level > high.level ) + high = item; + if( item.level > val && (rndup === undefined || item.level < rndup.level )) + rndup = item; + if( item.level < val && (rnddown === undefined || item.level > rnddown.level )) + rnddown = item; + }); + if( rnddown === undefined ) return low; + if( rndup === undefined ) return high; + return rndup; + } + } // end findMenu + + + if( adjust !== 0 ) { // We are incrmenting or decrementing. Find out what the level used to be by looking at the attribute. + let oldLevel; + if( !("charIDsUnique" in this.uncloned.misc)) + this.uncloned.misc.charIDsUnique = []; + let indx = this.uncloned.misc.charIDsUnique.indexOf( this.charID ); + if( indx != -1) { // If we have already processed this character + oldLevel = this.uncloned.misc.charIDsUnique[ indx + 1 ]; + dupChar = true; + } else if( oldAttValue !== undefined ) { + setObj = findMenu( oldAttValue, true ); + oldLevel = setObj.level; + this.uncloned.misc.charIDsUnique.push( this.charID ); + this.uncloned.misc.charIDsUnique.push( oldLevel ); + } + // Now find the index in the array of the item we just found. + let index = valid.indexOf( _.find( valid, function( item ) { return item.level == oldLevel; })); + if( index === -1 ) { + Earthdawn.errorLog( "Earthdawn markerSet serious error finding " + oldLevel + " giving up.", po); + return; + } +//log( "index " + index + " adjust " + adjust); + index += adjust; // The list is sorted, so we can just add or subtract the index by the amount we are adjusting. + if( index < 0 ) // if decremented past the begging, go to the end. + index += valid.length; + else if( index >= valid.length ) // if incremented past the end, go to the beggining + index -= valid.length; + setObj = valid[ index ]; + } // end adjust + + if( !setObj && level !== undefined ) { // make sure new level matches an option. If not pick best one. + if( level !== -1111 && "shared" in mi && mi[ "shared" ].length < 5 ) + setObj = findMenu( mi[ "shared" ], true ); // if level 0, we need to get the RIGHT zero. Which convenently enough is the one in sm. + else + setObj = findMenu( level ); + } + if( !setObj ) { + this.chat( "Earthdawn: Markerset error. level is undefined.", Earthdawn.whoFrom.apiWarning ); + return; + } + // end setting and/or adjusting level. We now know what we want to do. + // Do the actual marker setting. +//log( "level: " + level + " setObj: " + JSON.stringify( setObj )); + if( "marker" in setObj ) + Earthdawn.set( this.tokenInfo.tokenObj, "status_" + setObj[ "marker" ], setObj.badge ); +//log( "have set 'status_" + setObj[ "marker" ] + "' to " + setObj.badge ); + + // If this character has not already been done, also change the character sheet. + if( ssa[ 0 ] !== "sheetDirect" && !dupChar && ( attrib != undefined ) && !(mook && attrib === "condition-Health" )) { +//log("setting att. old is"); + let attribute = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: attrib }, 0); +//log(attribute); + if( attribute[ "current" ] != setObj.attrib ) + Earthdawn.setWithWorker( attribute, "current", setObj.attrib ); + } // End update the attribute. + + // See if any other menu items share this attribute, and if so, unset those. + if( mia !== undefined && mia.length > 1 ) { // This only needs to be done if more than one is found. + for( let i = 0; i < mia.length; ++i ) + if( Earthdawn.getIcon( mia[ i ] ) !== setObj[ "marker" ] ) // obviously we don't want to turn off the marker we just turned on. + Earthdawn.set( this.tokenInfo.tokenObj, "status_" + Earthdawn.getIcon( mia[ i ] ), false ); + } // end turn off all other markers that share this same attribute. + + if( attrib === "condition-Health" ) { // Health has changed + Earthdawn.set( this.tokenInfo.tokenObj, "status_dead", setObj.badge ); // Set the big red X to whatever we just set the unconscious or dead symbol to. + if( !mook) { // If unc or dead, mark blindsided. As a general rule (but this possibly will not be 100% accurate) if waking up, unblindside. + this.MarkerSet( [ "m", "blindsided", setObj.badge ? "s" : "u" ] ); + if( setObj.badge ) // If new status is unc or dead, make sure knocked down is set. + this.MarkerSet( [ "m", "knocked", "s" ] ); + } } + // Aggressive and Defensive stances are mutually exclusive. + if((( ssa[ 0 ] === "markerDirect" ) || ( ssa[ 0 ] === "marker" )) && setObj.badge ) { // This is a direct set (add) of the status marker on the token. + if( code === "aggressive" ) + this.MarkerSet( [ "m", "defensive", "u" ] ); + else if( code === "defensive" ) + this.MarkerSet( [ "m", "aggressive", "u" ] ); + } } // End tokeninfo type is "Token" +//log("end MarkerSet"); + } catch(err) { Earthdawn.errorLog( "ParseObj.MarkerSet() error caught: " + err, po ); } + } // End ParseObj.MarkerSet( ssa ) + + + + // ParseObj.funcMisc() + // This is a collection of several minor functions that don't deserve their own subroutines. + // + // Add Take an attriubte and add a value. + // CorruptKarma + // KarmaBuy (called from when Attribute detects that @{Karma} has changed) + // MacroCreate + // NewDay + // SetAdjust + // State - GM set values into the State. + // toAPI: API / noAPI + this.funcMisc = function( ssa ) { + 'use strict'; + let po = this; + try { + switch ( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() ) { + case "add" : // Add or subtract from an attribute. ssa[2] an attribute, ssa[3] a value. + try { + if( ssa.length > 2 && Earthdawn.parseInt2( ssa[ 3 ] )) { + let attribute2 = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: ssa[ 2 ].trim() }); + Earthdawn.setWithWorker( attribute2, "current", Earthdawn.parseInt2( ssa [ 3 ] ) + Earthdawn.parseInt2( attribute2.get( "current" ))); + } + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.Add error caught: " + err, po ); } + break; + case "corruptkarma": + try { + if( playerIsGM( this.edClass.msg.playerid )) { + let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: this.charID, name: "Creature-CorruptKarmaBank" }, 0), + bobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: this.charID, name: "Creature-CorruptKarma" }, 0), + num = Earthdawn.parseInt2( ssa[ 2 ] ), + bank = Earthdawn.parseInt2( aobj.get( "current" )), + kc = Earthdawn.parseInt2( bobj.get( "current" )), + realnum =( num <= bank ) ? num: bank, + txt = ""; + if( realnum != 0 ) { + Earthdawn.setWithWorker( aobj, "current", bank - realnum ); + Earthdawn.setWithWorker( bobj, "current", kc + realnum ); + txt = "Cursing " + realnum + " karma. "; + } + if( num != realnum ) + txt += "Can't curse " + num + " karma because only " + bank + " in bank."; + if( kc != 0 ) + txt += " New total " + (kc + realnum) + " cursed."; + if( (bank - realnum) < 1 ) + txt += " " + (bank - realnum) + " still in bank."; + this.chat( txt, Earthdawn.whoTo.player ); + } else + this.chat( "Error! Must be GM to Corrupt Karma.", Earthdawn.whoFrom.apiError ); + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.CorruptKarma error caught: " + err, po ); } + break; +/* + case "karmabuy": // We know how many karma were bought, send out an accounting entry. + try { +// Oct 23 obsolete. moved to sheetworker. + if( ssa.length > 1 && Earthdawn.parseInt2( ssa[ 2 ] )) { + let newKarma = Earthdawn.parseInt2( ssa [ 2 ] ), + today = new Date(), + stem = "&{template:chatrecord} {{header=" + getAttrByName( this.charID, "character_name" ) + "}}" + + "{{misclabel=Buy Karma}}{{miscval=" + newKarma + "}}" + + "{{lp=" + (newKarma * 10) + "}}", + slink = "{{button1=[Press here](!Earthdawn~ charID: " + this.charID + + "~ Record: ?{Posting Date|" + today.getFullYear() + "-" + (today.getMonth() +1) + "-" + today.getDate() + + "}: : LP: ?{Action Points to post|" + (newKarma * 10) + + "}: 0: Spend: ?{Reason|Buy " + newKarma + " Karma}"; + this.chat( stem + Earthdawn.colonFix( slink ) + ")}}", Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive ); + } + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.KarmaBuy error caught: " + err, po ); } + break; +*/ + case "macrocreate": + try { + if( !ssa.includes( "firstRun" ) && !(this.edClass && this.edClass.msg && this.edClass.msg.playerid && playerIsGM( this.edClass.msg.playerid ))) + this.chat( "Error! Only GM is allowed to run MacroCreate.", Earthdawn.whoFrom.apiError); + else { + // Check if named Macro Exists, if it does, delete it. then create it again. + // macName: Name of Macro + // macText: Text of Macro + // macTokenAction: true of this is a token action. + macroDetail = function( macName, macText, macTokenAction ) { + 'use strict'; + try { + let macObj = findObjs({ _type: "macro", name: macName }); + if( macObj ) + for( let i = 0; i < macObj.length; ++i ) + macObj[ i ].remove(); + macObj = createObj("macro", { + _playerid: po.edClass.msg.playerid, + name: macName, + action: macText, + visibleto: "all", + istokenaction: macTokenAction }); + } catch(err) { Earthdawn.errorLog( "ED.MacroDetail error caught: " + err, this ); } + } // End MacroDetail() + + + if( ssa.includes( "Delete" ) || ssa.includes( "Refresh" )) { // get rid of all macros that reference Earthdawn. + let macs = findObjs({ _type: "macro" }); + _.each( macs, function ( macObj ) { + let act = macObj.get( "action" ); + if( act.startsWith( "!Earthdawn" ) || act.startsWith( "!edToken" ) || act.startsWith( "!edsdr" ) || act.startsWith( "!edInit" )) + macObj.remove(); + }); + } + if( ssa.includes( "Create" ) || ssa.includes( "Refresh" )) { // create the Earthdawn macros. + macroDetail( "Roll-Public", "!edsdr~ ?{Step|0}~ ?{Bonus or Karma Step|0}~ for ?{reason| no reason}", false ); + macroDetail( "Roll-Player-GM", "!edsdrGM~ ?{Step|0}~ ?{Bonus or Karma Step|0}~ for ?{reason| no reason}", false ); + macroDetail( "Roll-GM-Only", "!edsdrHidden~ ?{Step|0}~ ?{Bonus or Karma Step|0}~ for ?{reason| no reason}", false ); + macroDetail( "NpcReInit", "!Earthdawn~ rerollnpcinit", false ); + macroDetail( "ResetChars", "!Earthdawn~ Misc: ResetChars: ?{(de)buffs Only or Full (including damage and karma) Reset|Mods Only|Full}", false ); + macroDetail( "Token", "!edToken~", false ); + macroDetail( "Dur-Track", "!Earthdawn~ ChatMenu: DurationTracker : ?{Name of the Effect|Effect}: ?{How many rounds ?|1}", false ); //new in v2 - Records duration of an effect in the turn tracker +// macroDetail( "Test", "!edToken~ %{selected|Test}", true ); // Leave Test in here. It can be uncommented when it is actually needed. + + macroDetail( "Attrib", "!Earthdawn~ ChatMenu: Attrib", true ); + macroDetail( "Init", "!edToken~ %{selected|Dex-Initiative-Check}", true ); + macroDetail( "KnockD", "!edToken~ %{selected|Knockdown}", true ); + macroDetail( "Status", "!Earthdawn~ ChatMenu: Status", true ); + macroDetail( "Strain", "!edToken~ !Earthdawn~ setToken: @{target|token_id}~ Damage: ?{Damage)|1}: Strain: NA", true); + macroDetail( Earthdawn.constantIcon( "karma" ) + "Karma-R", "!edToken~ !Earthdawn~ Reason: 1 Karma Only~ ForEach~ Karma: 1~ Roll: 0", true ); + macroDetail( Earthdawn.constantIcon( "karma" ) + "Karma-T", "!edToken~ !Earthdawn~ ForEach~ marker: KarmaGlobalMode :t", true ); + macroDetail( Earthdawn.constantIcon( "T" ) + "-Talents", "!Earthdawn~ ChatMenu: Talents", true ); + macroDetail( Earthdawn.constantIcon( "SK" ) + "-Skills", "!Earthdawn~ ChatMenu: Skills", true ); + macroDetail( Earthdawn.constantIcon( "WPN" ) + "-Damage", "!Earthdawn~ ChatMenu: Damage", true ); +// macroDetail( "•Status", "!edToken~ !Earthdawn~ ForEach~ Marker: ?{@{selected|bar2|max}}", true ); +// macroDetail( "⚡Cast", "!edToken~ %{selected|SP-Spellcasting}", true ); /* high voltage: 9889; +// macroDetail( Earthdawn.constantIcon( "Target" ) + "Clear-Targets", "!Earthdawn~ charID: @{selected|character_id}~ ForEach~ TargetsClear", true); +// macroDetail( Earthdawn.constantIcon( "Target" ) + "Set-Targets", "!Earthdawn~ charID: @{selected|character_id}~ Target: Set", true); + this.chat( "Macros created (look in collections tab).", Earthdawn.whoTo.gm | Earthdawn.whoFrom.api ); + } } + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.MacroCreate error caught: " + err, po ); } + break; + case "newday": // Recovery tests and Karma reset. Some systems karma must be bought. + try { + let recov = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Recovery-Tests" }); + let rt = (Earthdawn.parseInt2(recov.get( "max" )) || 2) + Earthdawn.getAttrBN( this.charID, "Misc-NewDayRecoveryOffset", "0", true); +//log(rt); + Earthdawn.setWithWorker( recov, "current", rt.toString()); // set recovery tests available today to max. + let karmaObj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Karma" }, 0), + kparam = ( ssa.length > 2 ) ? ssa[ 2 ] : Earthdawn.getAttrBN( this.charID, "Misc-KarmaRitual", "-1"), // V3.19 and later karmaritual is passed. + dpparam = ( ssa.length > 3 ) ? ssa[ 3 ] : Earthdawn.getAttrBN( this.charID, "Misc-DPRitual", "-1"), + add; + + switch ( Earthdawn.parseInt2( kparam )) { + case -1: // set karma to it's max value + add = karmaObj.get( "max" ); + break; + case -2: // Karma_ritual refills Circle per day + add = Earthdawn.getAttrBN( this.charID, "working-Circle", "0" ); + break; + case -3: // Karma_ritual refills racial karma modifier per day + add = Earthdawn.getAttrBN( this.charID, "Karma-Modifier", "0" ); + break; + case -4: { // There is a Karma Ritual Talent. Find it and use it's rank. + // go through all attributes for this character and look for ones we are interested in. + let po = this, + attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + _.each( attributes, function (att) { + if( att.get( "name" ).endsWith( "_Special" )) + if( Earthdawn.keywordCheck( att.get( "current" ), "Karma Ritual" )) { + if( add === undefined ) { + let nm = att.get( "name" ); + add = Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( Earthdawn.repeatSection( 3, nm), Earthdawn.repeatSection( 2, nm)) + "Effective-Rank", "0" ); + } else + po.chat( "Warning, found more than one Talent/Knack/Skill with special Karma Ritual. Using the first found.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.apiWarning) + } + }); // End for each attribute. + if( add === undefined ) { + po.chat( "Warning, no Talent/Knack/Skill with special Karma Ritual. Using the Karma Modifier.", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.apiWarning) + add = Earthdawn.getAttrBN( this.charID, "Karma-Modifier", "0" ); + } + } break; + default : // We were passed the number of karma to add. + if( isNaN( kparam ) || ( kparam < 0)) + this.chat( "ED.funcMisc.NewDay Warning, invalid karma parameter " + kparam + ".", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.apiWarning) + else + add = kparam; + }; + let newKarma = Math.min( Earthdawn.parseInt2( karmaObj.get( "max" )), Earthdawn.parseInt2(karmaObj.get( "current" )) + Earthdawn.parseInt2( add )) + + Earthdawn.getAttrBN( this.charID, "Misc-NewDayKarmaOffset", "0", true); + Earthdawn.setWithWorker( karmaObj, "current", newKarma.toString()); + + add = 0; + switch ( Earthdawn.parseInt2( dpparam )) { + case -1: // set Add questor tier to their current devotion pool + add = Earthdawn.getAttrBN( this.charID, "IsQuestor", "0", true); + break; + default : // We were passed the number of karma to add. + if( isNaN( dpparam ) || ( dpparam < 0)) + this.chat( "ED.funcMisc.NewDay Warning, invalid DP parameter " + dpparam + ".", Earthdawn.whoTo.player | Earthdawn.whoFrom.api | Earthdawn.whoFrom.apiWarning) + else + add = dpparam; + }; + let added = ""; + if( add > 0 ) { + let newDP = Math.min( Earthdawn.getAttrBN( this.charID, "DP_max", "0", true), Earthdawn.getAttrBN( this.charID, "DP", "0", true) + Earthdawn.parseInt2( add )); + Earthdawn.setWithWorker( karmaObj, "current", newDP.toString()); + added = " " + add + " added to DP."; + } + this.chat( "New Day: Karma and Recovery tests reset." + added, this.WhoSendTo() | Earthdawn.whoFrom.character ); + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.NewDay error caught: " + err, po ); } + break; + case "resetchars": // reset all selected tokens to full health, karma, Recovery Tests, and no modifications or status markers. + try { // Note, when this routine enters, we will not have this.charID or TokenInfo set. So be careful what other functions get called. + let lst = this.getUniqueChars( 1 ), // All selected Token IDs and Character IDs, grouped by character. + full = ssa.includes( "Full" ), + nlist = ""; + if( this.charID && _.isEmpty( lst )) { // No tokens selected, but this is done from the Special Functions menu, so do all tokens for this character. + let tkns = findObjs({ _type: "graphic", _subtype: "token", represents: this.charID }), + cid = this.charID, + arr = []; + _.each( tkns, function( TokObj ) { + arr.push( { token: TokObj.get( "_id" ), character: cid}); + }) // End ForEach Token + lst = _.groupBy( arr, "character" ); + } // end no tokens selected + for( let c in lst ) { // For each unique character, do the following. + this.charID = c; + let kObj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Karma" }); + let karma = kObj.get( "max" ), + unc = Earthdawn.getAttrBN( this.charID, "Damage-Unc-Rating", "2" ), + markers = ""; + if( full ) { // karma and recovery tests reset to max, no wounds or damage. + Earthdawn.setWithWorker( kObj, "current", karma ); + this.setWW( "Damage", 0); + this.setWW( "Damage-Stun", 0); + this.setWW( "Wounds", 0); + this.setWW( "Recovery-Tests", Earthdawn.getAttrBN( this.charID, "Recovery-Tests_max", "2" )); + } + this.setWW( "Dex-Mods", 0); // reset all modifications (buffs and debuffs) to zero. + this.setWW( "Str-Mods", 0); + this.setWW( "Tou-Mods", 0); + this.setWW( "Per-Mods", 0); + this.setWW( "Wil-Mods", 0); + this.setWW( "Cha-Mods", 0); + this.setWW( "Initiative-Mods", 0); + this.setWW( "Misc_Wound_Threshold-Buff", 0); + this.setWW( "PD-Buff", 0); + this.setWW( "MD-Buff", 0); + this.setWW( "SD-Buff", 0); + this.setWW( "PA-Buff", 0); + this.setWW( "MA-Buff", 0); + this.setWW( "Adjust-All-Tests-Misc", 0); + this.setWW( "Adjust-Effect-Tests-Misc", 0); + this.setWW( "Adjust-Defenses-Misc", 0); + this.setWW( "Adjust-Attacks-Misc", 0); + this.setWW( "Adjust-Damage-Misc", 0); + this.setWW( "Adjust-TN-Misc", 0); + this.setWW( "Adjust-Attacks-Bonus", 0); + this.setWW( "Adjust-Damage-Bonus", 0); + this.setWW( "Movement-Buff", 0); + this.setWW( "Shield-Phys-Buff", 0); + this.setWW( "Shield-Myst-Buff", 0); + this.setWW( "Attack-Step-Mod", 0); + if( Earthdawn.getAttrBN( this.charID, "Creature-Ambushing_max", "0", true ) > 0 ) { // If the creature can ambush, reset is that it is. + let mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "code" ] == "ambushing"; }); + if( mi ) + markers += Earthdawn.getIcon( mi ) + ","; + } + if( Earthdawn.getAttrBN( this.charID, "Creature-DivingCharging_max", "0", true ) > 0 ) { // If the creature can charge, reset is that it is. + let mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "code" ] == "divingcharging"; }); + if( mi ) + markers += Earthdawn.getIcon( mi ) + ","; + } + + for( let t in lst[ c ] ) { // For each unique token found, do the following. + let TokenObj = getObj("graphic", lst[ c ][ t ][ "token" ]); + if( TokenObj ) { + this.tokenInfo = { type: "token", name: TokenObj.get( "name" ), tokenObj: TokenObj, characterObj: getObj("character", c ) }; + let tMarkers = markers, + oldMarkers = TokenObj.get( "statusmarkers" ); + nlist += TokenObj.get( "name" ) + ", "; + + function keepthese( checkfor ) { // There are certain markers that we don't want to clear away. + let mi = _.find( Earthdawn.StatusMarkerCollection, function(mio){ return mio[ "code" ] == checkfor; }); + if( mi ) { + if( oldMarkers.indexOf( mi[ "customTag" ] ) !== -1 ) + tMarkers += mi[ "customTag" ] + ","; + else if( oldMarkers.indexOf( mi[ "icon" ] ) !== -1 ) + tMarkers += mi[ "icon" ] + ","; + } } + keepthese( "karmaauto" ); + keepthese( "karmaask" ); + keepthese( "devpntauto" ); + keepthese( "devpntask" ); + keepthese( "flying" ); + + let removed = _.difference( _.without( oldMarkers.split( "," ), ""), _.without( tMarkers.split( "," ), "") ); + if( removed.length ) { + for( let i = 0; i < removed.length; ++i ) // unset everything with these markers. + this.MarkerSet( [ "marker", removed[ i ].replace( /\@\d*/g, ""), "u"] ); + } + Earthdawn.set( TokenObj, "statusmarkers", tMarkers.slice(0, -1) ); // set each tokens status markers to empty (except maybe ambush and charge, which default to on for those who have it) Note that this should be unnessicary as far as the tokens go, but it also removes all hits and other things stored in the status markers. + + if( full ) { + let npc = Earthdawn.getAttrBN( this.charID, "NPC", "1" ), + bMook = !((npc == Earthdawn.charType.pc) || (npc == Earthdawn.charType.npc )); // We only need to process tokens if they are mooks or not linked correctly. Otherwise everything is linked to the character. + if( bMook || !(this.tokenInfo.tokenObj.get( "bar1_link" ))) Earthdawn.set( TokenObj, "bar1_value", karma); // karma + if( bMook || !(this.tokenInfo.tokenObj.get( "bar2_link" ))) Earthdawn.set( TokenObj, "bar2_value", 0); // wounds + if( bMook || !(this.tokenInfo.tokenObj.get( "bar3_link" ))) { + Earthdawn.set( TokenObj, "bar3_value", 0 ); // damage + Earthdawn.set( TokenObj, "bar3_max", unc ); // Unconscious rating + } } } // end tokenObj + } } + this.chat( "Reset characters: " + ( nlist ? nlist.slice( 0, -2 ) : "None (no selected tokens)" ) + ".", Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive ); + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.ResetChars error caught: " + err, po ); } + break; // end resetchars + case "setadjust": + // For the current character, copy all the buffs from the Combat Tab as adjustments on the Adjustments tab. + // This makes it easier for the GM to enter monsters. enter Buffs on the Combat tab until the values are right, then use this option to copy + // them all to the Adjustments tab. + try { + let po = this; + + function copyCombatToAdjust( from, to, noClear ) { + 'use strict'; + try { + let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: po.charID, name: from }, 0), + bobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: po.charID, name: to }, 0); + Earthdawn.setWithWorker( bobj, "current", Earthdawn.parseInt2( aobj.get( "current" ))+ Earthdawn.parseInt2( bobj.get( "current" ))); + if( !noClear ) + aobj.setWithWorker( "current", 0 ); + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.SetAdjust.copyCombatToAdjst error caught: " + err + " On " + from + " to " + to, po ); } + } // End copyCombatToAdjust() + + copyCombatToAdjust( "PD-Buff", "Defense-Phys-Adjust" ); + copyCombatToAdjust( "MD-Buff", "Defense-Myst-Adjust" ); + copyCombatToAdjust( "SD-Buff", "Defense-Soc-Adjust" ); + copyCombatToAdjust( "PD-ShieldBuff", "Shield-Phys" ); + copyCombatToAdjust( "MD-ShieldBuff", "Shield-Myst" ); + copyCombatToAdjust( "PA-Buff", "Armor-Phys-Adjust" ); + copyCombatToAdjust( "MA-Buff", "Armor-Myst-Adjust" ); + copyCombatToAdjust( "Movement-Buff", "Misc-Movement-Adjust" ); + copyCombatToAdjust( "Initiative-Mods", (( state.Earthdawn.sheetVersion < 3.1 ) ? "Misc-Initiative-Adjust" : "Initiative-Adjust")); + this.chat( "setAdjust: Temporary buffs have been set to permanent adjustments and cleared.", this.WhoSendTo() | Earthdawn.whoFrom.character ); + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.SetAdjust error caught: " + err, po ); } + break; + case "state": + // Set various state.Earthdawn values. + // ssa is an array that holds the parameters. + // Earthdawn~ State~ (one of the options below)~ (parameters). + try { + if( !playerIsGM( this.edClass.msg.playerid ) ) + this.chat( "Error! Only GM can set state variables.", Earthdawn.whoFrom.apiWarning ); + else { + let logging = false, bitfield; + switch( Earthdawn.safeString( ssa[ 2 ] ).toLowerCase() ) { + case "cursedlucksilent": { + let t = parseInt( ssa[ 3 ], 2 ); // This is a binary value, so use the supplied parseInt instead of Earthdawn.parseInt2() + state.Earthdawn.CursedLuckSilent = ( isNaN( t ) || t <= 0 ) ? undefined: t; + this.chat( "Campaign now set so that CursedLuckSilent is " + state.Earthdawn.CursedLuckSilent, Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + } break; + case "edition": { + state.Earthdawn.edition = Earthdawn.parseInt2( Earthdawn.getParam( ssa[ 3 ], 1, " ")); + if( ssa[ 3 ].slice( -4 ) === "1879") { + state.Earthdawn.game = "1879"; + state.Earthdawn.g1879 = true; + state.Earthdawn.gED = undefined; + } else if ( ssa[ 3 ].slice( -2 ) === "ED" ) { + state.Earthdawn.game = "ED"; + state.Earthdawn.g1879 = undefined; + state.Earthdawn.gED = true; + } else + this.chat( "Error! state parameters not correctly formed: " + ssa.toString(), Earthdawn.whoFrom.apiError ); + this.chat( "Campaign now set to use " + state.Earthdawn.game + " Edition " + Math.abs(state.Earthdawn.edition) + + " rules. IMPORTAINT NOTE! Make sure you also change the Default Sheet Settings of the Campaign Settings page." ); + let count = 0, + chars = findObjs({ _type: "character" }); + _.each( chars, function (charObj) { + Earthdawn.setWW( "edition", state.Earthdawn.edition, charObj.get( "_id" )); + ++count; + }) // End ForEach character + this.chat( count + " character sheets updated." ); + } break; + case "effectisaction": { +// Dead code as of changes to JSON. + state.Earthdawn.effectIsAction = Earthdawn.parseInt2( ssa[ 3 ] ); + this.chat( "Campaign now set so that Action tests " + (state.Earthdawn.effectIsAction ? "are" : "are NOT") + " Effect tests." ); + let count = 0, + chars = findObjs({ _type: "character" }); + _.each( chars, function (charObj) { + Earthdawn.setWW( "effectIsAction", state.Earthdawn.effectIsAction, charObj.get( "_id" ) ); + ++count; + if( state.Earthdawn.effectIsAction == "1" ) + for( let ind = 0; ind < 6; ++ind ) { + Earthdawn.setWW( [ "Dex", "Str", "Tou", "Per", "Wil", "Cha" ][ ind ] + "-ActnEfct", "1", charObj.get( "_id" ) ); + } + }) // End ForEach character + this.chat( count + " character sheets updated." ); + } break; + case "karmaritual": { +// Dead code as of changes to JSON. + state.Earthdawn.karmaRitual = ssa[ 3 ]; + let s; + switch(state.Earthdawn.karmaRitual) { + case "-1": s = "Max"; break; + case "-2": s = "Circle"; break; + case "-3": s = "Racial Mod"; break; + case "-4": s = "Talent"; break; + default: s = state.Earthdawn.karmaRitual; + } + this.chat( "Campaign now set so that default Karma Ritual is " + s, Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive ); + let count = 0, + chars = findObjs({ _type: "character" }); + _.each( chars, function (charObj) { + Earthdawn.setWW( "Misc-KarmaRitual", state.Earthdawn.karmaRitual, charObj.get( "_id" ) ); + ++count; + }) // End ForEach character + this.chat( count + " character sheets updated." ); + } break + case "linking": + this.chat( "Token Linking options for " + Earthdawn.makeButton("PCs", "!Earthdawn~ Misc: State: LinkingDetail: PC: " + + "?{Show nameplate to GM and controler|undefined|true,1|false,0}: ?{Show nameplate to all players|undefined|true,1|false,0}" + + ": ?{bar location|undefined|above,null|overlap_top|overlap_bottom|below}" + + ": ?{compact_bar|undefined|Standard,null|compact}" + , "Change the linking options for PCs (for example whether or not nameplates are automatically shown)", "param") + + " showname=" + state.Earthdawn.linking.PC.showname + " showplayers_name=" + state.Earthdawn.linking.PC.showplayers_name + + " bar_location=" + state.Earthdawn.linking.PC.bar_location + " compact_bar=" + state.Earthdawn.linking.PC.compact_bar + + " Token Linking Options for " + Earthdawn.makeButton("NPCs", "!Earthdawn~ Misc: State: LinkingDetail: NPC: " + + "?{Show nameplate to GM and controler|undefined|true,1|false,0}: ?{Show nameplate to all players|undefined|true,1|false,0}" + + ": ?{bar location|undefined|above,null|overlap_top|overlap_bottom|below}" + + ": ?{compact_bar|undefined|Standard,null|compact}" + , "Change the linking options for NPCs (for example whether or not nameplates are automatically shown)", "param") + + " showname=" + state.Earthdawn.linking.NPC.showname + " showplayers_name=" + state.Earthdawn.linking.NPC.showplayers_name + + " bar_location=" + state.Earthdawn.linking.NPC.bar_location + " compact_bar=" + state.Earthdawn.linking.NPC.compact_bar + + " Values that are undefined are unchanged during linking." + , Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + break; + break; + case "linkingdetail": + if( ssa.length > 7 ) { + let s; + if( ssa[ 3 ] === "PC" ) + s = state.Earthdawn.linking.PC; + else + s = state.Earthdawn.linking.NPC; + s.showname = (ssa[ 4 ] == "undefined") ? undefined : ( Earthdawn.parseInt2( ssa[ 4 ] ) ? true: false); + s.showplayers_name = (ssa[ 5 ] == "undefined") ? undefined : ( Earthdawn.parseInt2( ssa[ 5 ] ) ? true: false); + s.bar_location = (ssa[ 6 ] == "undefined") ? undefined : (( ssa[ 6 ] == "null") ? null: ssa[ 6 ]); + s.compact_bar = (ssa[ 7 ] == "undefined") ? undefined : (( ssa[ 7 ] == "null") ? null: ssa[ 7 ]); + this.chat( "New values for " + ssa[ 3 ] + + " showname=" + s.showname + " showplayers_name=" + s.showplayers_name + " bar_location=" + s.bar_location + " compact_bar=" + s.compact_bar + , Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + } else { + Earthdawn.errorLog( "ED.funcMisc.State.linkingDetail() invalid arguments: ", po ); + log( ssa ); + } + break; + case "logstartup": + state.Earthdawn.logStartup = Earthdawn.parseInt2( ssa[ 3 ] ) ? true: false; + logging = true; + break; + case "logcommandline": + state.Earthdawn.logCommandline = Earthdawn.parseInt2( ssa[ 3 ] ) ? true: undefined; + logging = true; + break; + case "logmsg": + state.Earthdawn.logMsg = Earthdawn.parseInt2( ssa[ 3 ] ) ? true: undefined; + logging = true; + break; + case "nopileondice": { + let t = Earthdawn.parseInt2( ssa[ 3 ] ); + state.Earthdawn.noPileonDice = ( isNaN( t ) || t < 0 ) ? undefined: t; + this.chat( "Campaign now set so that noPileOnDice is " + state.Earthdawn.noPileonDice, Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + } break; + case "nopileonstep": { + let t = parseFloat( ssa[ 3 ] ); + state.Earthdawn.noPileonStep = ( isNaN( t ) || t <= 0 ) ? undefined: t; + this.chat( "Campaign now set so that noPileOnStep is " + state.Earthdawn.noPileonStep, Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive); + } break; + case "mook": + bitfield = 0x02; + break; + case "npc": + bitfield = 0x01; + break; + case "pc": + bitfield = 0x04; + break; + case "style": { + state.Earthdawn.style = Earthdawn.parseInt2( ssa[ 3 ] ); + let style; + switch (state.Earthdawn.style) { + case Earthdawn.style.VagueSuccess: style = " - Vague Successes."; break; + case Earthdawn.style.VagueRoll: style = " - Vague Roll."; break; + case Earthdawn.style.Full: + default: style = " - Full."; break; + } + this.chat( "Campaign now set to use result style: " + state.Earthdawn.style + style ); + } break; + case "showdice": + state.Earthdawn.showDice = Earthdawn.parseInt2( ssa[ 3 ] ) ? true: false; + this.chat( "Campaign now set to showDice: " + state.Earthdawn.showDice ); + break; +// case "version": +// case "api": +// state.Earthdawn.version = Number( ssa[ 3 ] ); +// this.chat( "Campaign now set to API version: " + state.Earthdawn.version ); +// break; +// case "html": +// state.Earthdawn.sheetVersion = Number( ssa[ 3 ] ); +// this.chat( "Campaign now set to HTML version: " + state.Earthdawn.sheetVersion ); +// break; + default: + this.chat( "funcMisc:State bad command: " + ssa.toString( ssa ), Earthdawn.whoFrom.apiWarning ); + } + if( bitfield ) { + state.Earthdawn.defRolltype = Earthdawn.parseInt2( ssa[ 3 ] ) ? (state.Earthdawn.defRolltype | bitfield) : (~state.Earthdawn.defRolltype & bitfield); + this.chat( "New character default RollType - NPC: " + ((state.Earthdawn.defRolltype & 0x01) ? "GM Only" : "Public") + + " Mook: " + ((state.Earthdawn.defRolltype & 0x02) ? "GM Only" : "Public") + " PC: " + ((state.Earthdawn.defRolltype & 0x04) ? "GM Only" : "Public")); + } + if( logging ) + this.chat( "Campaign now set to " + state.Earthdawn.game + + " - logging Startup: " + state.Earthdawn.logStartup + " - " + + "Commandline: " + state.Earthdawn.logCommandline + " - " + + "Msg: " + state.Earthdawn.logMsg + ".", Earthdawn.whoTo.player ); + } + } catch(err) { Earthdawn.errorLog( "ED.funcMisc.State() error caught: " + err, po ); } + break; + case "toapi": { // API, noAPI, Set, or Never Mind If have not been using the API, then things are not setup correctly for API use. This sets it up. Can also be run as last thing before removing API to clean stuff up. + if( ssa[ 2 ] === "Never Mind" ) + return; + if( playerIsGM( this.edClass.msg.playerid )) { + if( ssa[ 2 ] !== "Set" ) { + let macs = findObjs({ _type: "macro" }); // to start with, remove all macros that reference earthdawn. + _.each( macs, function (macObj) { + let act = macObj.get( "action" ); + if( act.startsWith( "!Earthdawn" ) !== -1 || act.startsWith( "!edToken" ) !== -1 || act.startsWith( "!edsdr" ) !== -1 || act.startsWith( "!edInit" ) !== -1 ) + macObj.remove(); + }); + let abs = findObjs({ _type: "ability" }); // then do the same for all abilities. + _.each( abs, function (abObj) { + let act = abObj.get( "action" ); + if( act.startsWith( "!Earthdawn" ) || act.startsWith( "!edToken" ) || act.startsWith( "!edsdr" ) || act.startsWith( "!edInit" ) ) + abObj.remove(); + }); + } + + let attributes = findObjs({ _type: "attribute" }), + noApi = ssa[ 2 ] === "noAPI"; + _.each( attributes, function (att) { + let nm = att.get("name"); + if( nm === "API" ) + att.set( "current", noApi ? "0" : "1" ); + else if ( nm.startsWith( "repeating_")) { + if( nm.endsWith( "_Name" )) // No matter what, make sure we get the RowID set for this section. + Earthdawn.setWW( nm.slice( 0, -5) + "_RowID", Earthdawn.repeatSection( 2, nm), att.get( "_characterid" )); + else if ( nm.endsWith( "_CombatSlot" ) || (nm.endsWith( "_Contains" ) && (Earthdawn.repeatSection( 3, nm) === "SPM" ))) { + let nmn, + rowID = Earthdawn.repeatSection( 2, nm), + code = Earthdawn.repeatSection( 3, nm), + cID = att.get( "_characterid" ); + symbol = Earthdawn.constantIcon( code ), + cbs = att.get( "current" ), + lu = "Name"; + if( code === "SPM" ) { + cbs = "1"; + lu = "Contains"; + } + if ( code !== "SP" ) { // skip if it is SP, we don't do those token actions. + nmn = Earthdawn.getAttrBN( cID, Earthdawn.buildPre( code, rowID ) + lu, "" ); + if( !noApi && cbs == "1" ) // If we are going toAPI, and combatslot is one, make a token action. + Earthdawn.abilityAdd( cID, symbol + nmn, "!edToken~ %{selected|" + Earthdawn.buildPre( code, rowID ) + "Roll}" ); + } + } // End Token Action maint. + } + }); // End for each attribute. + if( noApi ) + state.Earthdawn = undefined; + this.chat( "Done.", this.WhoSendTo() | Earthdawn.whoFrom.api ); + } + } break; // end toAPI + } // end switch + } catch(err) { Earthdawn.errorLog( "ED.funcMisc error caught: " + err, po ); } + } // End ParseObj.funcMisc() + + + + // If this is being processed for a single token, and that token is a mook, then + // return the number of wounds that mook has, as recorded on the token bar2_value + this.mookWounds = function() { + 'use strict'; + try { + if( this.charID === undefined ) { + this.chat( "Error! charID undefined in mookWounds() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return 0; + } + if( Earthdawn.getAttrBN( this.charID, "NPC", "1") != Earthdawn.charType.mook ) + return 0; + if( this.tokenInfo === undefined || this.tokenInfo.type !== "token" ) + return 0; + + return Earthdawn.parseInt2( this.tokenInfo.tokenObj.get( "bar2_value" ) ); + } catch(err) { Earthdawn.errorLog( "ED.mookWounds error caught: " + err, this ); } + } // End ParseObj.mookWounds() + + + + // For chat messages created with htmlBuilder. You need a section and a body. Use this.newSect and Earthdawn.newBody to get them. + this.newSect = function ( size ) { + 'use strict'; + try { + let is1879 = false; + if( "charID" in this ) { + if( Earthdawn.getAttrBN( this.charID, "edition", "4" ) === "-1" ) + is1879 = true; + } else { + log( Earthdawn.timeStamp() + "Earthdawn.newSect warning. newSect without charID: " + size ); + if( !state.Earthdawn.logCommandline ) log( this.msg ); + log( this ); + } + + return new HtmlBuilder( "", "", { + class: (( size && size.toLowerCase().startsWith( "s" )) ? "sheet-rolltemplate-sectSmall" : "sheet-rolltemplate-sect") + + (is1879 ? " sheet-rolltemplate-1879" : " sheet-rolltemplate-Earthdawn")}) + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn.newSect error caught: " + err ); } + }; + + + + // ParseObj.Record () + // Post an accounting journal entry for SP, LP, or Dev points gained or spent. + // ssa[ 1] Real Date + // ssa[ 2] Throalic Date + // ssa[ 3] Item: SPLP, SP, LP, Dev, or Other + // ssa[ 4] Amount LP + // ssa[ 5] Amount SP + // ssa[ 6] Type: Gain, Spend, Decrease (ungain), or Refund (unspend). + // ssa[ 7] Reason - Text. +// This whole routine Obsolete Oct 23. Can be removed. + this.Record = function( ssa, clear ) { // If clear is TRUE then clear out all the data entry fields on the record tab to prepare for next entry. + 'use strict'; + try { +log("Record Obsolete code. If you see this except on an 1879 sheet, please report to the API developer."); + if( this.charID === undefined ) { + this.chat( "Error! Trying Record() when don't have a CharID.", Earthdawn.whoFrom.apiError); + return; + } + + let nchar = 0, + iyear, + oldReal = "", + oldThroalic = "", + res, + reason = "", + kobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "record-journal" }, ""), + oldStr = kobj.get( "current" ); + do { + res = oldStr.slice( nchar, 28 + nchar).match( /\b\d{4}[\-\#\\\/\s]\d{1,2}[\-\#\\\/\s]\d{1,2}\b/g ); // find things that looks like dates near the start of lines. + if( res === null ) { + nchar = oldStr.length; + continue; + } + + for( let i = 0; i < res.length; i++) { // Find out what dates were on the last entry posted. + iyear = Earthdawn.parseInt2( res[ i ], true); + if( iyear > 2000 ) { + if ( oldReal === "" ) + oldReal = res[ i ]; + } else if ( iyear > 1000 ) { + if( oldThroalic === "" ) + oldThroalic = res[ i ]; + } + } + nchar = oldStr.indexOf( "\n", nchar + 1 ); + if( nchar === -1 ) + nchar = oldStr.length; // this now holds location of the end of the current line. + } while( (oldReal === "" || oldThroalic === "") && nchar < oldStr.length ); + + for( let i = 7; i < ssa.length; i++ ) // If there was a colon in the reason, put it back in. + if( ssa[i] ) reason += ":" + ssa[ i ]; + let Item = ssa[ 3 ], + post = (( ssa[ 1 ] === oldReal.trim()) ? "" : ( ssa[ 1 ] + " ")) + + (( ssa[ 2 ] === oldThroalic.trim()) ? "" : ( ssa[ 2 ] + " ")) + String.fromCharCode( 0x25B6 ) + reason.slice(1) + " "; + + if( Item != "LPSP" && Item != "SP" && Item != "LP" && Item != "Other" ) + Item = "LPSP"; // Just in case a weird value. + if( Item !== "Other" ) { + let iAmountLP = Earthdawn.parseInt2( ssa[ 4 ]) * (( ssa[ 6 ] === "Spend" || ssa[ 6 ] === "Decrease" ) ? -1 : 1); + if( Item.indexOf( "LP" ) != -1 && iAmountLP != 0) { // LP + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "LP-Current" }, 0); + let newTotal = iAmountLP + Earthdawn.parseInt2( aobj.get( "current" )); + if( isNaN( newTotal )) + newTotal = iAmountLP; + Earthdawn.setWithWorker( aobj, "current", newTotal ); + post += ssa[ 6 ] + " " + Math.abs( iAmountLP ) + " " + (state.Earthdawn.g1879 ? "AP" : "LP") + + " (new total " + newTotal + ") "; + if(ssa[ 6 ] === "Gain" || ssa[ 6 ] === "Decrease") { + let newTotal2 = iAmountLP, + aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "LP-Total" }, 0); + newTotal2 += Earthdawn.parseInt2( aobj.get( "current" )); + if( isNaN( newTotal2 )) + newTotal2 = iAmountLP; + post += "(new career total " + newTotal2 + ") "; + Earthdawn.setWithWorker( aobj, "current", newTotal2 ); + } } + if( Item.indexOf( "SP" ) != -1 ) { + if( state.Earthdawn.gED ) { // Earthdawn always just uses SP, we are going to figure it in copper. upgrade would be to make change in GP if needed. + let rawSP = ssa[ 5 ], borrow = 0, amount = 0; + if( rawSP.trim().indexOf( "-" ) > 0 ) { +// rawSP = rawSP.slice( 0, rawSP.indexOf( "-" )); // The price is a range, such as 100 - 175. Just lop off the 2nd half and assume they got the cheap price. + let spa = rawSP.split( "-" ); // Rather than use cheapest, figure the average amount. + for( let i = 0; i < spa.length; ++i ) + amount += parseFloat( spa[ i ] ); + amount = amount / spa.length; + } + else + amount = parseFloat( rawSP ); + if( rawSP.toLowerCase().indexOf( "gp" ) !== -1 ) // If it does say it is gold, then convert gold to copper + amount *= 100; + else if( rawSP.toLowerCase().indexOf( "cp" ) === -1 ) // cIf it does not say it is already copper, convert silver to copper. + amount *= 10; + amount = Math.round( amount ); // Round to nearest CP. + amount *= (( ssa[ 6 ] === "Spend" || ssa[ 6 ] === "Decrease" ) ? -1 : 1); + if( amount != 0) { // we have a non-zero amount + let cobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Wealth_Copper" }, 0), + sobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Wealth_Silver" }, 100), + gobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Wealth_Gold" }, 0); + let oldCopper = Earthdawn.parseInt2( cobj.get( "current" )), + oldSilver = Earthdawn.parseInt2( sobj.get( "current" )), + oldGold = Earthdawn.parseInt2( gobj.get( "current" )); + let newCopper = oldCopper + amount; + while( newCopper < 0 ) { + ++borrow; + newCopper += 10; + } + if( newCopper != oldCopper ) + Earthdawn.setWithWorker( cobj, "current", newCopper ); + let newSilver = oldSilver - borrow; + borrow = 0; + while (newSilver < 0 ) { + ++borrow; + newSilver += 10; + } + if( newSilver != oldSilver ) + Earthdawn.setWithWorker( sobj, "current", newSilver ); + let newGold = oldGold - borrow; + if( newGold != oldGold ) + Earthdawn.setWithWorker( gobj, "current", newGold ); + post += ssa[ 6 ] + " " + rawSP + " " + (( rawSP.toLowerCase().indexOf( "p" ) == -1) ? "SP" : "") + " (new total " + ( newGold ? ( newGold + " gold, ") : "") + + ( newSilver + " silver") + ( newCopper ? ( ", " + newCopper + " copper") : "") + ") "; + } // end Earthdawn money + } else { // 1879: £1/2/3 or £1/2/3 &4f or 2s/3, 2s/- or 3d & 4f. If two, they are shillings and pence 5/2 = 5s/2d. If three they are pounds/shillings/pence. + let mult = ( ssa[ 6 ] === "Spend" || ssa[ 6 ] === "Decrease" ) ? -1 : 1; + let t = Earthdawn.safeString( ssa[ 5 ] ).replace( /-/g, "0").toLowerCase(); + if( t.trim().length > 0 && /\d/.test(t) ) { // string is not blank, and contains a digit. + + function formatLSDF( gold, silver, copper, bronze) { + 'use strict'; + let c = 0, v = ""; + if( gold ) { v = "£" + gold + "/"; c++; } + if( silver || c ) v += (silver ? silver : "-") + (c++ ? "" : "s") + "/"; + v += (copper ? copper : "-") + (c++ ? "" : "d"); + if( bronze ) v += " & " + bronze + "f"; + return v; + } + + let a = ssa[ 5 ].toLowerCase().replace( /l/g, "£").split( "/" ); // Allow L's for £. + if( a[ a.length -1 ].indexOf( "&" ) != -1 ) { // Last is pence and Farthing. split them as well. + let b = a.pop(); + a.push( Earthdawn.getParam( b, 1, "&" )); + a.push( Earthdawn.getParam( b, 2, "&" )); + while( a.length < 4 ) + a.unshift( 0 ); + } + for( let i = 0; i < a.length; ++i ) + for( let j = i; j < 4; ++j ) + if ( a[ i ].indexOf( [ "£", "s", "d", "f" ][ j ] ) !== -1 ) { // we found a coin symbol + while( i++ < j ) + a.unshift( "0" ); + while( a.length < 4 ) + a.push( "0" ); + i = j = 99; // break out of both loops. + } + while( a.length < 2 ) // We never found anything at all, so assume that what we found is shillings. + a.unshift( "0" ); + while( a.length < 4 ) + a.push( "0" ); + for( let i = 0; i < a.length; i++) + a[ i ] = Earthdawn.parseInt2( a[ i ].replace(/[^0-9]/g, "")); // a should not be an array of length 4 integers that contans lsdf. + + let b = [0, 0, 0, 0], changed = [ false, false, false, false ], loop, + objarr = []; + for( let i = 0; i < a.length; ++i ) { + objarr[ i ] = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, + name: [ "Wealth_Gold", "Wealth_Silver", "Wealth_Copper", "Wealth_Bronze" ][i] }, 0); + b[ i ] = (a[ i ] * mult) + Earthdawn.parseInt2( objarr[ i ].get( "current" )) + if( isNaN( b[ i ] )) + b[i] = a[ i ] * mult; + } + do { + loop = false; // If a coin is negative, try making change until everything is positive. + if( b[0] > 0 && b[1] < 0 ) { --b[0]; b[1] += 20; changed[0] = changed[1] = loop = true;} + if( (b[0] > 0 || b[1] > 0) && b[2] < 0 ) { --b[1]; b[2] += 12; changed[1] = changed[2] = loop = true;} + if( (b[0] > 0 || b[1] > 0 || b[2] > 0) && b[3] < 0 ) { --b[2]; b[3] += 4; changed[2] = changed[3] = loop = true;} + if( b[1] > 19 && b[0] < 0 ) { ++b[0]; b[1] -= 20; changed[0] = changed[1] = loop = true;} + if( b[2] > 11 && b[1] < 0 ) { ++b[1]; b[2] -= 12; changed[1] = changed[2] = loop = true;} + if( b[3] > 3 && b[2] < 0 ) { ++b[2]; b[3] -= 4; changed[2] = changed[3] = loop = true;} + } while ( loop ); + + for( let i = 0; i < a.length; ++i ) + Earthdawn.setWithWorker( objarr[ i ], "current", b[i] ); + post += ssa[ 6 ] + " " + formatLSDF( a[0], a[1], a[2], a[3] ) + + " (new total " + formatLSDF( b[0], b[1], b[2], b[3] ) + ") "; + } // End we don't have an empty string for 1879 money. + } // End 1879 SP + } // End SP + } // End not Other +// log( post); + if ( ( ssa[ 1 ] !== oldReal.trim()) || ( ssa[ 2 ] !== oldThroalic.trim()) ) + Earthdawn.setWithWorker( kobj, "current", post.trim() + ".\n" + oldStr ); // Post at top of page. + else { // This is the same date as the last post, so add this to the end of the entry for this day. + nchar = oldStr.indexOf( "\n" ); + if( nchar === -1 ) + nchar = oldStr.length; + Earthdawn.setWithWorker( kobj, "current", oldStr.slice( 0, nchar) + " " + post.trim() + "." + oldStr.slice( nchar ) ); // Post as a continuation of the top line. + } + if( post ) + this.chat( Earthdawn.getAttrBN(this.charID, "character_name") + " : " + post + ".", Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive, null, this.charID); + if( clear ) { + this.setWW( "record-reason", "" ); + this.setWW( "record-amount-LP", 0 ); + if( state.Earthdawn.gED ) + this.setWW( "record-record-SP", 0 ); + else { // 1879 + this.setWW( "record-amount-pound", 0 ); + this.setWW( "record-amount-shilling", 0 ); + this.setWW( "record-amount-pence", 0 ); + this.setWW( "record-amount-farthing", 0 ); + } } + } catch(err) { Earthdawn.errorLog( "ED.Record() error caught: " + err, this ); } + } // End parseObj.Record() + + + + // ParseObj.RerollNpcInit() + // Go through all the initiatives currently existing, empty the list, but issue a command to have all conscious NPC's re-roll initiative. + this.RerollNpcInit = function() { + 'use strict'; + let po = this; + try { + let turnorder = ( Campaign().get("turnorder") == "") ? [] : JSON.parse( Campaign().get("turnorder") ), + newTO = [], + chatMsg = ""; + _.each( turnorder, function( sel ) { + if( "custom" in sel ) // Don't clear out any custom (non character) entries that have been added. + newTO.push( sel ); + let TokObj = getObj("graphic", sel.id); + if (typeof TokObj === 'undefined' ) + return; + let cID = TokObj.get("represents"); + let CharObj = getObj("character", cID) || ""; + if (typeof CharObj === 'undefined') + return; + + if ((Earthdawn.parseInt2( TokObj.get( "bar3_value" )) < (Earthdawn.getAttrBN( cID, "Damage_max", "20", true ))) && + ( Earthdawn.getAttrBN( cID, "NPC", "1", true) > 0 )) // Not Items, not PCs, and nobody unconscious. + chatMsg += "~ SetToken : " + sel.id + "~ value : Initiative~ Init~ SetStep: 0~ SetResult: 0"; + }); // End for each selected token + Campaign().set( "turnorder", JSON.stringify( newTO )); + if( chatMsg.length > 0 ) + this.chat( "!Earthdawn" + chatMsg); + } catch(err) { Earthdawn.errorLog( "ED.RerollNpcInit() error caught: " + err, po ); } + } // End ParseObj.RerollNpcInit() + + + + // ParseObj.rollPre ( ssa ) + // prepair a Roll for selected token. + // ssa[] - Step modifiers. + this.rollPre = function( ssa ) { + 'use strict'; + try { + let init = Earthdawn.safeString( ssa[ 0 ] ).toLowerCase() === "init"; + this.misc[ "init" ] = init; + if( this.tokenInfo === undefined || (init && this.tokenInfo.tokenObj === undefined)) { + if( init ) this.chat( "Initiative requires a token be selected. Do you have a token selected?", Earthdawn.whoFrom.apiWarning ); + else this.chat( "Error! tokenInfo undefined in Roll() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + if( init && ("step" in this.misc === false)) { + this.chat( "Error! Step value undefined in Roll() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + this.misc[ "cButtons" ] = []; + if( "step" in this.misc ) ssa.push( this.misc[ "step" ] ); + this.misc[ "recipients" ] = this.WhoSendTo(); + let dice, + step = this.ssaMods( ssa ); // Add any mods to roll. + if( this.indexTarget !== undefined ) { + let tokChar = Earthdawn.tokToChar( Earthdawn.getParam( this.targetIDs[ this.indexTarget ], 1, ":")); + this.misc[ "targetChar" ] = tokChar; + if ( this.uncloned.bFlags & Earthdawn.flags.HitsFound ) { + let extra = Earthdawn.parseInt2( Earthdawn.getParam( this.targetIDs[ this.indexTarget ], 2, ":")); + if( extra > 0 ) { + step += extra * ((Earthdawn.getAttrBN( tokChar, "Creature-HardenedArmor", "0") == "1") ? 1: 2); + this.misc[ "stepExtra" ] = extra; + } } } + if( init && Earthdawn.getAttrBN( this.charID, "Misc-StrainPerTurn", "0" ) > 0 ) { + this.misc[ "StrainPerTurn" ] = Earthdawn.getAttrBN( this.charID, "Misc-StrainPerTurn", "0" ); + this.doLater += "~StrainSilent:" + this.misc[ "StrainPerTurn" ]; + } + + this.doNow(); + if( step == 0 && "reason" in this.misc && this.misc[ "reason" ].toLowerCase().indexOf( " only" ) !== -1 ) { // One karma only and one devpoint only are allowed to make step zero rolls. + // empty statement to allow an else if. + } else if ( step < 1 ) { + if( init ) + this.misc[ "warnMsg" ] = "If anything other than armor and wounds are causing " + this.tokenInfo.tokenObj.get( "name" ) + + " an initiative step of " + step + " it is probably illegal."; + else if (this.misc[ "reason" ] && !this.misc[ "reason" ].startsWith( "1 " ) && !this.misc[ "reason" ].endsWith( " Only" )) + this.misc[ "warnMsg" ] = "Warning!!! Step number " + step; + step = 1; + } + dice = this.edClass.StepToDice( step ); + if( "moreSteps" in this.misc ) + for( let i = this.misc[ "moreSteps" ].reps; i > 1; --i ) + dice += "+" + this.edClass.StepToDice( this.misc[ "moreSteps" ].step ); + this.misc[ "finalStep" ] = (("moreSteps" in this.misc) && ("step" in this.misc)) ? this.misc[ "step" ] : step; + this.misc[ "effectiveStep" ] = (("effectiveStep" in this.misc) ? this.misc[ "effectiveStep" ] : 0 ) + step; + if( "bonusDice" in this.misc ) + dice += this.misc[ "bonusDice" ]; + if( "karmaDice" in this.misc ) + dice += this.misc[ "karmaDice" ]; + if( "result" in this.misc && this.misc[ "result" ] ) { + if( Earthdawn.parseInt2( this.misc[ "result" ]) < 0 ) + dice = "{{" + dice + "+" + this.misc[ "result" ] + "}+d1}kh1"; + else + dice += "+" + this.misc[ "result" ]; + this.misc[ "resultMod" ] = this.misc[ "result" ]; + this.misc[ "effectiveStep" ] += this.misc[ "result" ]; + } + if( dice.startsWith( "+" )) + dice = dice.slice( 1 ); + + let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: this.charID, name: "Creature-CursedLuck" }, 0), + cluck = Earthdawn.parseInt2( aobj.get( "current" )); + if( cluck > 0 ) { + this.misc[ "CursedLuck" ] = cluck; + aobj.setWithWorker( "current", 0); + } + if ((cluck > 0) || (Earthdawn.getAttrBN( this.charID, "NPC", "1") > 0 && ((state.Earthdawn.noPileonDice != undefined && state.Earthdawn.noPileonDice >= 0) + || (state.Earthdawn.noPileonStep != undefined && state.Earthdawn.noPileonStep > 0)))) + this.misc[ "DiceFunnyStuff" ] = true; + if( "FX" in this.misc && (this.misc[ "FX" ].startsWith( "Attempt" ) || this.misc[ "FX" ].startsWith( "Effect" ))) + this.FX( this.misc[ "FX" ] ); + if( dice === "" ) // If for some reason you are rolling a step zero without karma, just forget the whole thing. + return; + this.misc[ "dice" ] = dice; + + this.misc[ "natural" ] = ((this.misc[ "targetNum" ] || 0) == 0) // The roll does not have a target number, and does not have several other conditions (such as being a recovery test) that need special processing. + && !(this.misc[ "recipients" ] & Earthdawn.whoTo.player ) + && !( this.uncloned.bFlags & Earthdawn.flags.HitsFound ) + && !( this.bFlags & Earthdawn.flagsArmor.Mask ) + && !( this.bFlags & Earthdawn.flags.Recovery ) + && !( this.bFlags & Earthdawn.flagsTarget.Each ) // for vs Each, targetNum is not set yet. + && !( "DiceFunnyStuff" in this.misc ) + && !( "AfterRoll" in this.misc ) + && !state.Earthdawn.showDice // need the dice passed to rollFormat for this to work. + && !init; + if( !( "queueRolls" in this.misc )) // We might be queueing several rolls up, or we might want to just roll it right now. + this.rollPost(); + } catch(err) { Earthdawn.errorLog( "ED.rollPre() error caught: " + err, this ); } + } // End ParseObj.rollPre(ssa) + + + + // ParseObj.rollPost () + // Perform a Roll for selected token and interprite the results. + this.rollPost = function( ssa ) { + 'use strict'; + try { +// if( !( "dice" in this.misc ) || !( "recipents" in this.misc)) { +// } + if ( this.misc[ "natural" ] ) { // A natural roll is one without a callback. It just goes out, and the raw results are returned. Done for rolls without target numbers, and rolls that are sent to both player and gm, but not public. + this.misc[ "roll" ] = "[[" + this.misc[ "dice" ] + "]]"; // The actual roll is done as an inline roll when this is evaluated inside of rollformat. + this.rollFormat( this.misc[ "recipients" ] ); + } else { // Not a "natural" roll. We are going to send it to the dice roller now, but capture the result for interpretation. + let po = this; + po.edClass.rollCount++; + sendChat( "player|" + this.edClass.msg.playerid, "/r " + this.misc[ "dice" ], function( ops ) { + 'use strict'; + // NOTE THAT THIS IS THE START OF A CALLBACK FUNCTION + if( ops.length !== 1 ) + Earthdawn.errorLog( "Earthdawn.js program warning! ops returned length of " + ops.length, this); + let con = JSON.parse( ops[ 0 ].content ); + if ( "DiceFunnyStuff" in po.misc ) { + po.misc[ "DiceOrigional" ] = con; + con = po.CursedLuck( po.misc[ "CursedLuck" ], con ); + } + po.misc[ "result" ] = con.total; + po.misc[ "showResult" ] = (( "StyleOverride" in po.misc ? po.misc[ "StyleOverride" ] : state.Earthdawn.style) != Earthdawn.style.VagueRoll) + || ( po.bFlags & Earthdawn.flags.VerboseRoll ) + || ((po.misc[ "targetNum" ] || 0) == 0) || po.misc[ "init" ] + || ( po.misc[ "recipients" ] == Earthdawn.whoTo.gm); // going to gm only. + + // We have a target number. Count successes. + if( "targetNum" in po.misc && po.misc[ "targetNum" ] !== undefined ) { + po.misc[ "targetNum" ] = Math.max( po.misc[ "targetNum" ], 2 ); // target number may not be less than 2. + let res = po.misc[ "result" ] - po.misc[ "targetNum" ]; + if( res < 0 ) { // Failed + po.misc[ "failBy" ] = Math.abs( res ); + if ( "SpellBuffSuccess" in po.misc ) // Did not actually succeed, but spell is a buff, so pretend we did. + po.edClass.countSuccess++; + else + po.edClass.countFail++; + if(( "Special" in po.misc) && Earthdawn.keywordCheck( po.misc[ "Special" ], "Knockdown" )) { + po.MarkerSet( ["r", "knocked", "s"] ); + po.misc[ "endNoteFail" ] = "Character Knocked Down"; + } + } else { // Have target number and succeeded. + po.misc[ "succBy" ] = Math.abs( res ); + po.misc[ "extraSucc" ] = Math.floor(res / 5) + (("grimCast" in po.misc) ? 1 : 0); + po.edClass.countSuccess++; + if( "FX" in po.misc && po.misc[ "FX" ].startsWith( "Success" )) + po.FX( po.misc[ "FX" ] ); + if( (po.bFlags & Earthdawn.flagsTarget.Riposte) && po.misc[ "extraSucc" ] > 0 && ("targetNum2" in po.misc)) // Special situation, Since the test succeeded against the first target number, compare it against a 2nd target number to count the successes! + po.misc[ "secondaryResult" ] = po.misc[ "result" ] - po.misc[ "targetNum2" ]; + if((( "Special" in po.misc ) && Earthdawn.keywordCheck( po.misc[ "Special" ], "CursedLuck", "CorruptKarma", "SPL-Spellcasting" ) + && ( "targetName" in po.misc )) // We need rollESbutton to memorize hits on spellcasting tests + || (( po.bFlags & Earthdawn.flagsTarget.Mask) && (( "ModType" in po.misc ) && (po.misc[ "ModType" ].search( /Attack/ ) != -1)) //Attack rolls + && (po.targetIDs[ po.indexTarget] !== undefined ))) // The last bit of this keeps riposte tests from messing it up. + po.rollESbuttons(); + } // End have successes + } // End we have a target number + + // Apply Damage if appropriate. IE: if this was a damage roll, and hits were found on any of the selected tokens. + let dcode, dtext; + if ( po.bFlags & Earthdawn.flagsArmor.PA ) { dcode = ["PA"]; dtext = ["Physical"]; } + else if ( po.bFlags & Earthdawn.flagsArmor.MA ) { dcode = ["MA"]; dtext = ["Mystic"]; } + else if ( po.bFlags & Earthdawn.flagsArmor.None ) { dcode = ["NA"]; dtext = ["No"]; } + else if ( po.bFlags & Earthdawn.flagsArmor.Unknown) { dcode = ["PA", "MA", "NA"]; dtext = ["Physical","Mystic","No"]; } + + if( dcode !== undefined ) { // This is a damage roll. + if( po.uncloned.bFlags & Earthdawn.flags.HitsFound ) { + let targs; + if ( po.bFlags & Earthdawn.flags.WillEffect ) + targs = po.targetIDs; + else + targs = [ po.targetIDs[ po.indexTarget ]] + for( let ind = 0; ind < targs.length; ++ind ) { + let tID = Earthdawn.getParam( targs[ ind ], 1, ":"); + for( let i = 0; i < dcode.length; ++i ) { + po.misc[ "cButtons" ].push({ + link: "!Earthdawn~ setToken: " + tID + "~ Damage: " +dcode[i] + ": " + po.misc[ "result" ] + + ": ?{Damage Mod (" + ((po.bFlags & Earthdawn.flagsArmor.Natural) ? " Natural" : "") + + dtext[i] + " Armor applies)|0}", + text: "Apply Dmg " + dcode[i] + (( po.bFlags & Earthdawn.flagsArmor.Natural) ? "-Nat" : "") + + " - " + po.getTokenName( tID ) }); + } } } + for( let i = 0; i < dcode.length; ++i ) + po.misc[ "cButtons" ].push({ + link: "!Earthdawn~ setToken: " + Earthdawn.constantButton( "at" ) + "{target|Apply damage to which token|token_id}~ Damage: " +dcode[ i ] + + ": " + po.misc[ "result" ] + ": ?{Damage Mod (" + + ((po.bFlags & Earthdawn.flagsArmor.Natural) ? " Natural" : "") + dtext[i] + " Armor applies)|0}", + text: "Apply Dmg " + dcode[i] + (( po.bFlags & Earthdawn.flagsArmor.Natural) ? "-Nat" : "") + " - Targeted", + tip: "Apply this damage to the token you click upon." }); + } // end damage roll + if( "AfterRoll" in po.misc ) { // After the Roll, we might do something else with the result. + let sub = po.misc[ "AfterRoll" ].split( ":" ); // We have a command stored for additional processing of the result. + if( sub[ 1 ].trim() === "buttonDamageBoth" || sub[ 1 ].trim() === "buttonDamageTargeted" ) // AfterRoll: buttonDamageBoth: (type of damage PA/NA) + po.misc[ "cButtons" ].push({ + link: "!Earthdawn~ setToken: " + Earthdawn.constantButton( "at" ) + "{target|Apply damage to which token|token_id}~ " + + "Damage: " + sub[ 2 ].trim() + ": " + po.misc[ "result" ] + ": ?{Damage Mod (" + sub[ 2 ].trim() + " Armor applies)|0}", + text: "Apply " + po.misc[ "result" ] + " Dmg (" + sub[ 2 ].trim() + ") to Targeted", + tip: "Apply this damage to the token you click upon."}); + if( sub[ 1 ].trim() === "buttonDamageBoth" || sub[ 1 ].trim() === "buttonDamageSelected" ) + po.misc[ "cButtons" ].push({ + link: "!Earthdawn~ foreach: st~ Damage: " + sub[ 2 ].trim() + ": " + po.misc[ "result" ] + ": ?{Damage Mod (" + sub[ 2 ].trim() + " Armor applies)|0}", + text: "Apply " + po.misc[ "result" ] + " Dmg (" + sub[ 2 ].trim() + ") to all Selected", + tip: "Apply this damage to all selected tokens."}); + } // end AfterRoll + + if( po.misc[ "init" ] ) { // This is an initiative roll. + let TokenObj = po.tokenInfo.tokenObj, + tID = TokenObj.get( "id" ), + tt = Campaign().get( "turnorder" ), + turnorder = (tt == "") ? [] : ( JSON.parse( tt )); + state.Earthdawn.actionCount[ tID ] = 0; // New initiative, so reset the count of Standard Actions taken. + turnorder = _.reject(turnorder, function( toremove ) { return toremove.id === tID }); + turnorder.push({ id: tID, _pageid: TokenObj.get( "pageid" ), pr: po.misc[ "result" ], secondary: ("000" + po.misc[ "result" ]).slice(-3) + + ((Earthdawn.getAttrBN( po.charID, "NPC", "1" ) == Earthdawn.charType.pc) ? "1" : "0" ) + + ("000" + Earthdawn.getAttrBN( po.charID, "Attrib-Dex-Curr", "5" )).slice(-3) + ("000" + step).slice( -3)}); + turnorder.sort( function( a, b ) { + 'use strict'; + if( "secondary" in a && "secondary" in b ) { + if( a.secondary < b.secondary ) return 1; + else if ( a.secondary > b.secondary ) return -1; + else return 0; + } else + return (b.pr - a.pr) + }); // End turnorder.sort(); + Campaign().set("turnorder", JSON.stringify( turnorder )); + } // End Initiative + + // Send the results to the user. + po.rollFormat( po.misc[ "recipients" ], con ); + + if(( --po.edClass.rollCount === 0) && (po.edClass.countFail + po.edClass.countSuccess) > 1) + po.chat( "Group " + po.misc[ "reason" ] + " results. " + po.edClass.countSuccess.toString() + " succeeded and " + po.edClass.countFail.toString() + " failed." + , po.misc[ "recipients" ]); + + if( po.bFlags & Earthdawn.flags.Recovery ) + po.Damage( [ "Recovery", "NA", "-" + po.misc[ "result" ] ]); + if( po.misc[ "init" ] ) { + po.ParseLoop(); // This callback thread is to continue parsing this. + return false; + } + }, {noarchive:true}); // End of sendChat callback function + if( po.misc[ "init" ] ) + return true; // If this is an init roll, then parseLoop should fallout here. + } // end not a natural roll + } catch(err) { Earthdawn.errorLog( "ED.rollPre() error caught: " + err, this ); } + } // End ParseObj.rollPre() + + + + + + // ParseObj.rollESbuttons() + // create chat window buttons for the user to spend extra successes upon. + // Also do the special coding required for powers such as Cursed Luck or Corrupt Karma here (instead of buttons). + this.rollESbuttons = function() { + 'use strict'; + let po = this; + try { + let suc = this.misc[ "extraSucc" ], + tName, + lstart, lend; + + function makeButtonLocal( nm, txt, lnk, tip ) { + po.misc[ "cButtons" ].push( { name: nm, text: txt, link: lnk, tip: tip }); + }; + + if (this.bFlags & Earthdawn.flagsTarget.Highest) { + lstart = 0; + lend = this.targetIDs.length; + } else { + lstart = this.indexTarget; + lend = this.indexTarget + 1; + } + let charStr1 = "!Earthdawn~ " + (( this.tokenInfo !== undefined && this.tokenInfo.tokenObj !== undefined) + ? "setToken: " + this.tokenInfo.tokenObj.get( "id" ) : "charID: " + this.charID ); + let numTargets = lend - lstart; + for( let i = lstart; i < lend; ++i ) { // do this for each target + if(( "Special" in this.misc) && Earthdawn.keywordCheck( this.misc[ "Special" ], "CorruptKarma" )) { + if( playerIsGM( this.edClass.msg.playerid ) ) { + let targetChar = Earthdawn.tokToChar( this.targetIDs[ i ] ); + if( targetChar ) { + let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: targetChar, name: "Creature-CorruptKarmaBank" }, 0), + val = Earthdawn.parseInt2(this.misc[ "extraSucc" ]) + 1 + Earthdawn.parseInt2( aobj.get( "current" )); + Earthdawn.setWithWorker( aobj, "current", val ); + } + } else + this.chat( "Error! Only GM is allowed to do run CorruptKarma powers!", Earthdawn.whoFrom.apiError); + } else if(( "Special" in this.misc) && Earthdawn.keywordCheck( this.misc[ "Special" ], "CursedLuck" )) { + if( playerIsGM( this.edClass.msg.playerid ) ) { + let targetChar = Earthdawn.tokToChar( this.targetIDs[ i ] ); + if( targetChar ) + Earthdawn.setWW( "Creature-CursedLuck", Earthdawn.parseInt2( this.misc[ "extraSucc" ]) + 1, targetChar ); + } else + this.chat( "Error! Only GM is allowed to do Cursed Luck powers!", Earthdawn.whoFrom.apiError); + } else { // It is a more generic hit, without any special coding. + this.TokenSet( "add", "Hit", this.targetIDs[ i ], "0"); // Make note that this token has hit that token. + if( numTargets > 1 ) + tName = this.getTokenName( this.targetIDs[ i ] ); + if( !(this.bFlags & Earthdawn.flags.NoOppMnvr )) { // We don't record these extra successes as something that affects damage rolls. + makeButtonLocal( tName, "Bonus Dmg", charStr1 + "~ StoreInToken: Hit: " + this.targetIDs[ i ] + + ":?{How many of the extra successes are to be devoted to bonus damage to " + this.getTokenName( this.targetIDs[ i ] ) + "|" + suc + "}", + "How many of the " + suc + " extra successes are to be devoted to bonus damage to " + this.getTokenName( this.targetIDs[ i ] )); + makeButtonLocal( tName, "Opp Mnvr", charStr1 + "~ ChatMenu: OppMnvr: " + this.targetIDs[ i ], "Choose Opponent Maneuvers that might or might not be applicable to this Target." ); + + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }), + cc = (( "ModType" in po.misc ) && (po.misc[ "ModType" ].endsWith( "CC" )) ? 1 : -1); + _.each( attributes, function (att) { + if (att.get("name").endsWith( "_WPN_Name" )) + if(( Earthdawn.getAttrBN(po.charID, att.get( "name" ).slice( 0, -5) + "_CloseCombat", "1", true) < 0) == ( cc < 0 )) // We want to see if they have the same sign. So test (x<0 == y<0). If both are true or both is false, then true. + makeButtonLocal( tName, att.get( "current" ), + "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + po.charID + "|" + att.get( "name" ).slice(0, -4) + "Roll}", + "Roll a Weapon Damage."); + + if( att.get("name").endsWith( "_Mod-Type" ) && ( att.get( "current" ) == "Damage Poison" + || (cc < 0 && att.get("current")=="Damage") || (cc > 0 && att.get("current")=="Damage CC") )){ // We want to list the damages corresponding to the Attack + Poisons + let pre = Earthdawn.buildPre( att.get( "name")), + name = Earthdawn.getAttrBN( po.charID, pre + "Name", "" ) + makeButtonLocal( tName, name, "!edToken~ " + Earthdawn.constantButton( "percent" ) + "{" + po.charID + "|" + pre + "Roll}", "Roll an Ability Damage."); + } + }); // End for each attribute. + + let cflags = Earthdawn.getAttrBN( this.charID, "CreatureFlags", 0 ); + if( cflags & Earthdawn.flagsCreature.CreatureMask ) { + if( cflags & Earthdawn.flagsCreature.GrabAndBite ) + makeButtonLocal( tName, "Grab & Bite", charStr1 + "~ CreaturePower: " + "GrabAndBite: " + this.targetIDs[ i ], + "The creature may spend an additional success from an Attack test to automatically grapple an opponent. Grappled opponents automatically take bite damage each round until the grapple is broken." ); + if( cflags & Earthdawn.flagsCreature.Hamstring ) + makeButtonLocal( tName, "Hamstring", charStr1 + "~ CreaturePower: " + "Hamstring: " + this.targetIDs[ i ], + "The creature may spend an additional success from an Attack test to halve the opponent’s Movement until the end of the next round. If the attack causes a Wound, the penalty lasts until the Wound is healed." ); + if( cflags & Earthdawn.flagsCreature.Overrun ) + makeButtonLocal( tName, "Overrun", charStr1 + "~ CreaturePower: " + "Overrun: " + this.targetIDs[ i ], + "The creature may spend an additional success from an Attack test to force an opponent with a lower Strength Step to make a Knockdown test against a DN equal to the Attack test result." ); + if( cflags & Earthdawn.flagsCreature.Pounce ) + makeButtonLocal( tName, "Pounce", charStr1 + "~ CreaturePower: " + "Pounce: " + this.targetIDs[ i ], + "If the creature reaches its opponent with a leap and the opponent isn’t too much larger, the creature may spend an additional success from the Attack test to force the opponent to make a Knockdown test against a DN equal to the Attack test result." ); + if( cflags & Earthdawn.flagsCreature.SqueezeTheLife ) + makeButtonLocal( tName, "Squeeze the Life", charStr1 + "~ CreaturePower: " + "SqueezeTheLife: " + this.targetIDs[ i ], + "The creature may spend two additional successes from an Attack test to automatically grapple an opponent. Grappled opponents automatically take claw damage each round until the grapple is broken." ); + } + + let lst = Earthdawn.getAttrBN( po.charID, "ManRowIdList", "bad" ); + if( lst !== "bad" && lst.length > 1 ) { + let arr = lst.split( ";" ); + for( let i = 0; i < arr.length; ++i ) { + let pre = Earthdawn.buildPre( "MAN", arr[ i ] ); + if( Earthdawn.getAttrBN( po.charID, pre + "Type", "1") == "1" ) { // type 1 is Creature + let n = Earthdawn.getAttrBN( po.charID, pre + "Name", ""), + d = Earthdawn.getAttrBN( po.charID, pre + "Desc", "").replace(/\n/g, " "); + if( n.length > 0 || d.length > 0 ) + makeButtonLocal( tName, Earthdawn.getAttrBN( po.charID, pre + "Name", "" ), + charStr1 + "~ CreaturePower: " + "Custom" + arr[ i ] + ": " + po.charID, d ); + } } } } } } // End it's a hit, and end foreach target + } catch(err) { Earthdawn.errorLog( "ED.RollESbuttons() error caught: " + err, po ); } + } // End ParseObj.RollESbuttons() + + + + // ParseObj.rollFormat() + // create HTML to nicely format the roll results. + // + // sectMain: The main card can go to public, GM only, or player and GM. + // GM might get a message with target numbers and buttons. + // playerCard: Player might get message with buttons that public did not get. + this.rollFormat = function( recipients, rolls ) { + 'use strict'; + let po = this; + try { + let pIsGM = playerIsGM( this.edClass.msg.playerid ), + playerCard = !pIsGM && (recipients & Earthdawn.whoTo.gm) && !(recipients & Earthdawn.whoTo.player), // Need a msg to player saying roll was sent to gm only. This has modifiers, but no roll results. + playerCardNix = [], // This is an array of index numbers of the child elements that should be removed from the card shown to the player when the roll is GM only. + whichMsgs = recipients & ( pIsGM ? ~Earthdawn.whoTo.player : ~0x00), // This is recipients, but if pIsGM, then strips out the to player bit. + sectMain = po.newSect(), + bpc = (Earthdawn.getAttrBN( this.charID, "NPC", "1") == Earthdawn.charType.pc), + buttonLine = "", + linenum = 0; + // Do a body append. If an icon is in the line, Add line-i to classes + // Pass body, and the attributes for append. +// newBody and 4 bAppends. +// {"_tag":".body","_attrs":{"class":"sheet-rolltemplate-body"},"_css":{},"_children": +// ["" +// ,{"_tag":".odd","_attrs":{},"_css":{},"_children":["Step: 8."]} +// ,{"_tag":".even","_attrs":{"class":"sheet-rolltemplate-ResultLine"},"_css":{},"_children":["Result: 13"]} +// ,{"_tag":".odd","_attrs":{},"_css":{},"_children":["Dice: 364"]} +// ,{"_tag":".even","_attrs":{"class":"sheet-rolltemplate-line-i"},"_css":{},"_children":["Action: "]} +// ]} + function bAppend( b, tag, content, attrs) { + 'use strict'; + try { + if( !b || !content ) { + Earthdawn.errorLog( "ED.rollFormat invalid call: bAppend( " + b + ", " + tag + ", " + content + ", " + attrs + " )" ); + return; + } + if( content.indexOf( "sheet-rolltemplate-icons-" ) == -1 ) + b.append( tag, content, attrs ); // There is no icon, so just post what is passed with no modification. + else { // icon detected. + if( !attrs ) // No existing attras, so just supply one. + b.append( tag, content, { class: "sheet-rolltemplate-line-i" }); + else { + if( "class" in attrs ) { + if( attrs[ "class" ].indexOf( "rolltemplate-line-i" ) == -1 ) // If not already have this class for some reason. + attrs[ "class" ] += " sheet-rolltemplate-line-i"; + } else attrs[ "class" ] = "sheet-rolltemplate-line-i"; + b.append( tag, content, attrs); + } } + } catch(err) { Earthdawn.errorLog( "ED.rollFormat bAppend() error caught: " + err, po ); } + } // end bAppend + + // Message destination, overrides and exceptions section. + // In the header we have an button to allow control of overrides. This floats at the right top of the header, so needs to be the first html. + let whoString; + switch( recipients & Earthdawn.whoTo.mask ) { + case 0: whoString = "public"; break; + case 1: whoString = "player"; break; + case 2: whoString = "GM"; break; + case 3: whoString = "PGM"; break; + default: + whoString = "public"; + Earthdawn.errorLog( "ED.rollFormat invalid recipents value: " + recipients, po ); + } + let k, bis = 0, lt; + if(( "skillExceptionIs" in this.misc) || ( "skillExceptionWouldBe" in this.misc )) { // This is a knowledge or Artisan skill, which have special rolltype catagories. Send user to a special menu to ask which he wants. to set/edit. + bis |= 0x04; + lt = "!Earthdawn~ ChatMenu: RolltypeMulti: state.Earthdawn.Rolltype." + (bpc ? "PC" : "NPC" ); + if( "skillExceptionIs" in this.misc ) + lt += ": skillExceptionIs: " + this.misc[ "skillExceptionIs" ]; + if( "skillExceptionWouldBe" in this.misc ) + lt += ": skillExceptionWouldBe: " + this.misc[ "skillExceptionWouldBe" ]; + if( "exceptionIs" in this.misc ) + lt += ": exceptionIs: " + this.misc[ "exceptionIs" ]; + if( "exceptionWouldBe" in this.misc ) + lt += ": exceptionWouldBe: " + this.misc[ "exceptionWouldBe" ]; + } else { // only one possible exception (talent name), not two (skill class). + lt = "!Earthdawn~ ChatMenu: RolltypeEdit: state.Earthdawn.Rolltype." + (bpc ? "PC" : "NPC" ) + ": "; + if( "exceptionIs" in this.misc ) { + k = this.misc[ "exceptionIs" ]; + let e = bpc ? state.Earthdawn.Rolltype.PC.Exceptions : state.Earthdawn.Rolltype.NPC.Exceptions; + if( k in e ) { + bis |= 0x01; + lt += k + ": " + +"?{Edit display exception for '" + k + + "'|Change to Public, exceptionEdit: Public|Change to GM Only, exceptionEdit: GM Only" + + "| Change to Player and GM, exceptionEdit: Player and GM|Delete exception, exceptionDelete}" + } else + edParse.chat( "Warning! Earthdawn internal data mismatch in rollFormat. key '" + k+ "' not found in exceptions.", Earthdawn.whoFrom.apiError); + } else if ( "exceptionWouldBe" in this.misc ) { + bis |= 0x02; + lt += this.misc[ "exceptionWouldBe" ] + ":?{Create a display exception for '" + this.misc[ "exceptionWouldBe" ] + + "'|Public, exceptionAdd: Public|GM Only, exceptionAdd: GM Only|Player and GM, exceptionAdd: Player and GM}"; + } } + if( bis ) + sectMain.append( "span", Earthdawn.makeButton( + Earthdawn.addIcon( whoString, "small" ), lt, + "This message is being sent to '" + whoString + (( "whoReason" in this.misc) ? "' due to " + this.misc[ "whoReason" ] : ""), "icon" ), + { class: "sheet-rolltemplate-floatRight sheet-rolltemplate-RollTypeButton" }); + else if( state.Earthdawn.Rolltype.Override ) + sectMain.append( "span", Earthdawn.makeButton( " ! ", "!Earthdawn~ ChatMenu: RolltypeEdit: state.Earthdawn.Rolltype: : Display", + "Override is on and being sent to '" + state.Earthdawn.Rolltype.Override + "'" ), + { class: "sheet-rolltemplate-floatRight sheet-rolltemplate-overrideOn" }); + + + // Roll Header & subheader. + // In general, information provided is header information, subheader information, and strain information. + // Try to fit them into a few lines as possible. Subheader info might be included with header, and strain might be included with subheader, or each be their own seperate line. + // So the first thing we do is build the subheader, so we know how long it is. + let subhead = "vs."; // vs. target name, TN#, and Strain x. + if( "targetName" in this.misc ) + subhead += " " + this.misc[ "targetName" ] + (( "targettype" in this.misc) ? "'s " : ""); + if( "targettype" in this.misc ) { + let e = this.misc[ "targettype" ].endsWith( "-each" ); + subhead += " " + (e ? "each " : "") + this.misc[ "targettype" ].replace( "p1p", " +1p").replace( "-each", "" ); + } + if(( "targetNum" in this.misc) && this.misc[ "targetNum" ] != undefined ) + if ((this.bFlags & (Earthdawn.flagsTarget.Ask | Earthdawn.flagsTarget.Riposte | Earthdawn.flags.VerboseRoll)) + || (( "StyleOverride" in this.misc ? this.misc[ "StyleOverride" ] : state.Earthdawn.style) === Earthdawn.style.Full)) + subhead += " Target # " + this.misc[ "targetNum" ]; + else + this.misc[ "gmTN" ] = this.misc[ "targetNum" ]; + if( subhead.length > 4 ) + subhead += ". "; + else + subhead = ""; // we had neither targetName nor targettype above. Forget the whole section. Except that it might be used for strain below. + + let sc = this.strainCalc(), + header = ("reason" in this.misc) ? this.misc[ "reason" ] : "", + shLen = subhead.length, strainButton, strainLine, strainLen = 20 + sc[ "special" ].join().length, + bsub = ( subhead && header && ((header.length + shLen) > 28)); + if( sc && ((sc[ "strain" ] > 0 ) || sc[ "special" ] )) { + if( sc[ "strain" ] > 0 ) + this.Damage( [ "strainsilent", "NA", sc[ "strain" ]]); // strain, No Armor + strainButton = Earthdawn.makeButton( sc[ "strain" ] + Earthdawn.addIcon( "strain", "s") + (sc[ "special" ] ? " " + sc[ "special" ].join() : ""), + "!Earthdawn~ " + (( this.tokenInfo !== undefined && this.tokenInfo.tokenObj !== undefined) + ? "setToken: " + this.tokenInfo.tokenObj.get( "id" ) : "charID: " + this.charID ) + + "~ ChatMenu: strainChange: " + sc[ "strain" ] + ": ?{How much strain should this action have cost|" + sc[ "strain" ] + "}", + sc[ "tooltip" ], (( "special" in sc) && (sc[ "special" ].length > 0)) ? "damage" : "dflt" ); // This button allows people to change the amount of strain that was charged. + + if(( shLen + strainLen ) < 65) { // If strain fits on the subheader line, add it there. + subhead += strainButton; + shLen += strainLen; // new length of the visible portion of the subheader (displayed text vs html code). + } else + strainLine = "Strain: " + strainButton; + } + + sectMain.append( "", (("headcolor" in this.misc) ? Earthdawn.addIcon( Earthdawn.safeString( this.misc[ "headcolor" ] ).toLowerCase(), "l", this.misc[ "headcolor" ]) : "") + + header + ((bsub || !subhead ) ? "" : " -- " + new HtmlBuilder( "span", subhead, { class: "sheet-rolltemplate-subheadertext" })), // Main Header + {class : ("sheet-rolltemplate-header" + (("headcolor" in this.misc) ? + " sheet-rolltemplate-header-" + Earthdawn.safeString( this.misc[ "headcolor" ] ).toLowerCase() : "" )) }); // The headers give the right colored thick line at bottom. + if( bsub ) // This is a subheader. If is long enough it needs its own line as opposed to be appended to the main header. + sectMain.append("", subhead, { class: "sheet-rolltemplate-subheadertext"}); + + // end of header and subheader. Start of body of message. + let modBit = 0, x, x2, + bodyMain = Earthdawn.newBody( sectMain ), + txt = ""; + if( strainLine ) // there was no room for strain in the subheader, so we are putting it on a seperate line. + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), strainLine); + if( "warnMsg" in this.misc ) + bodyMain.append( "", this.misc[ "warnMsg" ], { class: "sheet-rolltemplate-warnMsg" }); + + if( "SP-Step" in this.misc ) + this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; // The spell-casting talents are talents too, make them look a bit more like the other talents. + if( "ModType" in this.misc ) { // Conditions and options that affected the step. + let modtype = this.misc[ "ModType" ], + extraMod = 0; + + // 0x10 range are all action tests. 0x11 is general action test. 0x12 is Attack test, 0x14 Attack CC. 0x18 is jumpup. + // 0x20 range are all effect tests. 0x21 is general effect test. 0x22 is damage test. 0x24 Damage CC. 0x28 Init. 0x40 No modifier and Damage Poison. + if ( modtype == "Action" ) + modBit = 0x11; + else if (modtype.search( /Attack/ ) != -1) { + if (modtype == "Attack CC" ) + modBit = 0x14; + else + modBit = 0x12; + } else if( modtype == "JumpUp" + || modtype.search( /Armor/ ) != -1 // v3.1 Armor obsolete + ) + modBit = 0x18; + else if (modtype == "Effect" ) + modBit = 0x21; + else if (modtype.search( /Damage/ ) != -1) { + if (modtype == "Damage CC" ) + modBit = 0x24; + else if (modtype == "Damage Poison" ) // Poison has no modifier + modBit = 0x40; + else + modBit = 0x22; + } else if (modtype == "Init" ) + modBit = 0x28; + else if (modtype === "0" ) + modBit = 0x40; + + if( modBit === 0x18 && (x = this.getValue( "IP" )) != 0) + txt += Earthdawn.texttip( " IP: -" + x, "Penalty from Armor and shield." ); + else if( modBit === 0x28 && ((x = this.getValue( "Initiative-Mods" ) + this.getValue( "Initiative-Mod-Auto" ) - this.getValue( "Adjust-Effect-Tests-Total" )) != 0)) + txt += Earthdawn.texttip( " Init Mods: " + x, "Initiative Modifiers, such as Ambush for a Creature." ); + else if( modBit === 0x40 ) + txt += Earthdawn.texttip( " Never. ", "This test never has any modifiers." ); + + if((( modBit & 0x10 ) || ( modBit & 0x20 && Earthdawn.getAttrBN( this.charID, "effectIsAction", "0", true ))) + && (x = this.getValue( "Adjust-All-Tests-Misc" )) != 0) + txt += Earthdawn.addIcon( "action", "s", "Action Misc: Misc modifiers that apply to Action Tests.", undefined, x ); + if(( modBit & 0x20 ) && !Earthdawn.getAttrBN( this.charID, "effectIsAction", "0", true ) && (x = this.getValue( "Adjust-Effect-Tests-Misc" )) != 0) + txt += Earthdawn.addIcon( "effect", "s", "Effect Misc: Misc modifiers that apply to Effect Tests.", undefined, x ); + if(( modBit == 0x12 || modBit == 0x14 ) && (x = this.getValue( "Adjust-Attacks-Misc" )) != 0) + txt += Earthdawn.addIcon( "attack", "s", "Attack Misc: Misc modifiers that apply to Attack Tests.", undefined, x ); + if(( modBit == 0x22 || modBit == 0x24 ) && (x = this.getValue( "Adjust-Damage-Misc" )) != 0) + txt += Earthdawn.addIcon( "damage", "s", "Damage Misc: Misc modifiers that apply to Damage Tests.", undefined, x ); + if(( modBit & 0x04 ) && (this.getValue( "combatOption-AggressiveAttack" ) == "1" )) // & 0x04 is -CC + txt += Earthdawn.addIcon( "aggressive", "s", "Aggressive Stance: + " + Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Bonus", "3" ) + + " bonus to Close Combat attack and damage tests. " + Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Penalty", "-3") + + " penalty to PD and MD. " + Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Strain", "1") + " Strain per Attack." + , undefined, Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Bonus", "3" )); + +// if( "rsPrefix" in this.misc || "SP-Step" in this.misc ) { // rsPrefix means came from "Action". SP-Step means spellcasting command line. +// Note, I don't think the above test was needed. But maybe it was. + if(( modBit & 0x10 || modBit == 0x22 || modBit == 0x24 )) { + if( "Defensive" in this.misc ) { + if( [ 1, 2, 3, 4, 5, 6, 7 ].includes( this.misc[ "Defensive" ] )) + extraMod -= Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Penalty", "-3", true ); // extraMod is adding back in stuff that already subtracted from the base modifications if that modification does not apply to this test. + if( [ 1, 2, 4, 5 ].includes( this.misc[ "Defensive" ] )) { + extraMod += Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Bonus", "3", true ); // And these ones get a bonus. + txt += Earthdawn.addIcon( "defensive", "s", "Defensive Stance: +" + Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Bonus", "3" ) + + " bonus to Active Defenses. " ) + Earthdawn.getAttrBN( this.charID, "Misc-DefStance-Bonus", "3" ); + } } + if( "Aggressive" in this.misc && [ 1, 2, 3 ].includes( this.misc[ "Aggressive" ] )) { + extraMod += Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Penalty", "-3", true ); // Active defenses get penalty for being aggressive. + txt += Earthdawn.addIcon( "aggressive", "s", "Aggressive Stance: +" + Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Penalty", "-3" ) + + " penalty to Active Defenses. " ) + Earthdawn.getAttrBN( this.charID, "Misc-AggStance-Penalty", "-3" ); + } + } + if(( modBit & 0x10 ) && this.getValue( "condition-KnockedDown" ) == "1" && modtype != "JumpUp" ) //Need to skip the text with JumpUp tests. + if ("Resistance" in this.misc ) // Resistance is depreciated 8/22. + extraMod += 3; + else + txt += Earthdawn.addIcon( "knockdown", "s", "Knocked Down: -3 penalty on all defenses and tests. Movement 2. May not use Combat Option other than Jump-Up.", undefined, -3 ); + if( "MoveBased" in this.misc ) { + extraMod -= this.getValue( "condition-ImpairedMovement" ); + txt += Earthdawn.addIcon(this.misc[ "MoveBased" ] == "Full" ? "impaired4" : "impaired", "s", + "Impaired Movement: -5/-10 to Movement Rate (1 min). -2/-4 Penalty to movement based tests due to footing, cramping, or vision impairments. Dex test to avoid tripping or halting.", + undefined, this.misc[ "MoveBased" ] == "Full" ? -4 : -2 ); + } + if( "VisionBased" in this.misc ) { + extraMod -= this.getValue( "condition-Darkness" ); // NotVisionBased depreciated 8/22 + txt += Earthdawn.addIcon(this.misc[ "VisionBased" ] == "Full" ? "vision4" : "vision", "s", + "Impaired Vision: -2/-4 Penalty to all sight based tests due to Darkness, Blindness or Dazzling.", undefined, this.misc[ "VisionBased" ] == "Full" ? -4: -2 ); + } +// } // from Action() or spell. + + if(( modBit == 0x12 || modBit == 0x14) && this.getValue( "combatOption-CalledShot" ) == "1" && !("SP-Step" in this.misc)) + txt += Earthdawn.addIcon( "calledshot", "s", "Called Shot: -3. Take one Strain; –3 penalty to Attack test; if successful, attack hits designated area. One automatic extra success for maneuver being attempted, other extra successes spent that way count twice.", undefined, -3 ); + if( modBit & 0x10 && this.getValue( "combatOption-SplitMovement" ) == "1" ) + txt += Earthdawn.addIcon( "split", "s", "Split Movement: Take 1 Strain and be Harried to attack during movement." ); + if( modBit & 0x10 && this.getValue( "combatOption-TailAttack" ) == "1" ) + txt += Earthdawn.addIcon( "tail", "s", "Tail Attack: T'Skrang only: Make extra Attack, Damage = STR. -2 to all Tests.", undefined, -2 ); + if(( modBit == 0x12 || modBit == 0x22 || modBit == 0x14 || modBit == 0x24) // Note, -CC does not make any sense, but if they DID do it, let them, especially since the updateAdjustAttacks() routine lets them and it is too much of a pain to fix. + && !("SP-Step" in this.misc) && this.getValue( "condition-RangeLong" ) == "1" ) + txt += Earthdawn.addIcon( "rangelong", "s", "The attack is being made at long range. -2 to attack and damage tests.", undefined, -2 ); + if( this.getValue( "condition-Surprised" ) == "1" ) + txt += Earthdawn.addIcon( "surprised", "s", "Surprised! Acting character is surprised. Can it do this action?", undefined, "?" ); + + if( modBit & 0x10 && (x = this.getValue( "condition-Harried" )) != "0" ) + txt += Earthdawn.addIcon( ((x > 2 && x < 6 ) ? "harried" + x.toString() : "harried"), "s", + "Harried: Penalty to all Tests and to PD and MD.", undefined, x); + if(( modBit & 0x02 || modBit & 0x04 || modBit == 28 )&& this.getValue( "Creature-Ambushing" ) != "0" ) + txt += Earthdawn.addIcon( "ambush", "s", "Ambush: Added to Initiative, Attack, and Damage.", undefined, this.getValue( "Creature-Ambushing_max" )); + if(( /* modBit & 0x02 || */ modBit & 0x04 )&& this.getValue( "Creature-DivingCharging" ) != "0" ) + txt += Earthdawn.addIcon( "charging", "s", "Diving or Charging: Added to Attack, and Damage", undefined, this.getValue( "Creature-DivingCharging_max" ) ); + if(( modBit & 0x10 || modBit & 0x20 ) && ((x = this.getValue( "Wounds" )) > 0)) + if( this.getValue( "Creature-Fury" ) >= x ) + txt += Earthdawn.texttip( " Fury.", "Instead of suffering penalties, Wounds grant this creature a bonus to tests." ); + else if ((x2 = this.getValue( "Total-ResistPain" )) < x) + txt += Earthdawn.addIcon( "wounds", "s", "Wounds: -1 penalty to all action and effect tests per wound.", undefined, 0-(x2 - x)); + let modvalue = this.misc[ "ModValue" ] + extraMod; + if( txt.length > 0 || modvalue ) + bAppend( bodyMain, (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.texttip( "Mods: " + ((modvalue) ? modvalue.toString() + " " : " " ), + "Total of all bonuses and Penalties." ) + txt.trim()); + + txt = ""; // Stuff under here flows down into target. + if(( modBit == 0x12 || modBit == 0x14 || ("SP-Step" in this.misc)) && this.getValue( "condition-Blindsiding" ) == "1") + txt += Earthdawn.addIcon( "blindsiding", "s", "Blindsiding: The acting character is blindsiding the targeted character, who takes -2 penalty to PD and MD. Can't use shield. No active defenses.", undefined, -2 ); + if(( modBit == 0x12 || modBit == 0x14 || "SP-Step" in this.misc) && this.getValue( "condition-TargetPartialCover" ) == "1") + txt += Earthdawn.addIcon( "partialcover", "s", "Tgt Cover: The targeted character has cover from the acting character. +2 bonus to PD and MD.", undefined, "+2" ); + if( modBit & 0x10 && this.getValue( "combatOption-Reserved" ) == "1" ) + txt += Earthdawn.addIcon( "reserved", "s", "Reserved Action: +2 to all Target Numbers. Specify an event to interrupt.", undefined, "+2 TN" ); + } // end modType + + if( "stepExtra" in this.misc ) + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), "Extra Successes: " + this.misc[ "stepExtra" ]); + + if( this.indexTarget !== undefined && modBit & 0x10 ) { + let targetChar = Earthdawn.tokToChar( Earthdawn.getParam( this.targetIDs[ this.indexTarget ], 1, ":")); + if ( targetChar ) { // These are modifiers from the TARGET character. + if((x = Earthdawn.getAttrBN( targetChar, "Adjust-Defenses-Misc", "0" )) != 0 + && ( "targettype" in this.misc && this.misc[ "targettype" ].indexOf( "SD") == -1)) + txt += Earthdawn.addIcon( "defenses", "s", "Defenses Misc: Total of all bonuses and penalties to targets PD and MD.", undefined, x ); + if( Earthdawn.getAttrBN( targetChar, "combatOption-AggressiveAttack", "0" ) == "1" ) + txt += Earthdawn.addIcon( "aggressive", "s", "Aggressive: Target is in an Aggressive Stance, giving bonuses to Attack and Damage, but penalty to PD and MD." + , undefined, Earthdawn.getAttrBN( targetChar, "Misc-AggStance-Penalty", "-3" )); + if( Earthdawn.getAttrBN( targetChar, "combatOption-DefensiveStance", "0" ) == "1" ) + txt += Earthdawn.addIcon( "defensive", "s", "Defensive: Target is in a Defensive Stance, giving a bonus to PD and MD." + , undefined, Earthdawn.getAttrBN( targetChar, "Misc-DefStance-Bonus", "3" )); + if( Earthdawn.getAttrBN( targetChar, "condition-Blindsided", "0" ) == "1" ) + txt += Earthdawn.addIcon( "blindsided", "s", + "Blindsided: Target is blindsided and takes -2 penalty to physical and mystic defenses. Can't use shield. No active defenses.", undefined, -2 ); + if( Earthdawn.getAttrBN( targetChar, "condition-KnockedDown", "0" ) == "1" ) + txt += Earthdawn.addIcon( "knockdown", "s", "Knocked Down: Target is Knocked Down and suffers a -3 penalty to all defenses.", undefined, -3 ); + if( Earthdawn.getAttrBN( targetChar, "condition-Surprised", "0" ) == "1" ) + txt += Earthdawn.addIcon( "surprised", "s", "Surprised: Target is surprised and suffers a -3 penalty to all defenses.", undefined, -3 ); + if((x = Earthdawn.getAttrBN( targetChar, "condition-Cover", "0" )) != "0" ) + txt += Earthdawn.addIcon( "cover", "s", "Cover Target has cover and gains bonus to PD and MD." + , undefined, ((x == "2") ? "Partial: -2" : "Full: Can't hit.")); + if((x = Earthdawn.getAttrBN( targetChar, "condition-Harried", "0", true )) != "0" ) + txt += Earthdawn.addIcon( ((x > 2 && x < 6 ) ? "harried" + x.toString() : "harried"), "s", + "Harried: Target is Harried and suffers a penalty to PD and MD as well as action tests.", undefined, 0-x ); + } } + if( txt.length > 0 ) + bAppend(bodyMain, (( ++linenum % 2) ? ".odd" : ".even"), "Target: " + txt.trim()); + + let gmResult = "", + karmanum = ( "karmaNum" in this.misc ) ? Earthdawn.parseInt2( this.misc[ "karmaNum" ] ) : undefined, + dpnum = ( "DpNum" in this.misc ) ? Earthdawn.parseInt2( this.misc[ "DpNum" ] ) : undefined; + if( this.misc[ "step" ] || this.misc[ "finalStep" ] || this.misc[ "dice" ] ) { // if all three of these are undefined or zero, then skip this (this should probably only be the case for NoRoll). + let tip = (( "dice" in this.misc) ? this.misc[ "dice" ].replace( /!/g, "") + ". " : "" ) + + (( "step" in this.misc) ? "Base step " + this.misc[ "step" ] : "" ) + + (( "stepExtra" in this.misc) ? " plus " + this.misc[ "stepExtra" ] + " extra successes." : ""); + txt = (( "bonusStep" in this.misc ) ? " + step " + this.misc[ "bonusStep" ] + " bonus" : "") + + (!karmanum ? "" : (" + " + Earthdawn.addIcon((karmanum == 2) ? "karma2" : ((karmanum == 3) ? "karma3" : "karma" ), "s", "karma", undefined, karmanum ))) + + (!dpnum ? "" : (" + " + Earthdawn.addIcon((dpnum == 2) ? "devotion2" : ((dpnum == 3) ? "devotion3" : "devotion" ), "s", "Devotion Points", undefined, dpnum ))) + + (( "resultMod" in this.misc ) ? ((this.misc[ "resultMod" ] < 0) ? " - " : " + ") + Math.abs(this.misc[ "resultMod" ]) : "" ) + + (( "roll" in this.misc ) ? ". = " + this.misc[ "roll" ] : "." ); // the actual roll is done when this.misc[ "roll" ] gets evaluated as an inline roll. + bAppend( bodyMain, ( ++linenum % 2) ? ".odd" : ".even", + new HtmlBuilder( "span", "Step: " + (( "finalStep" in this.misc ) ? this.misc[ "finalStep" ] : (( "step" in this.misc ) ? this.misc[ "step" ] : "")) ,{ + class: "sheet-rolltemplate-emphasis", + title: Earthdawn.encode( Earthdawn.encode( tip )) }) + txt ); + } + + if( "CorruptedKarma" in this.misc ) + bodyMain.append( "", new HtmlBuilder( "span", this.misc[ "CorruptedKarma" ] + " karma corrupted.", { + class: "sheet-rolltemplate-warning", + title: Earthdawn.encode( Earthdawn.encode( "This dice roll was affected by a Horror Power that caused the karma to be spent, but not rolled." )) })); + + // Standard. Tell what rolled, vague about how much missed by. + // Full: Tell exact roll result and how much made or missed by. + // Vague. Do not tell roll result, but tell how much made or missed by. + if( "FunnyStuffDone" in this.misc ) { + txt = ""; + + function silentOrNot( test, t ) { + if( state.Earthdawn.CursedLuckSilent && (state.Earthdawn.CursedLuckSilent & test)) + gmResult += (gmResult.length > 0 ? " " : "") + t; + else + txt += (txt.length > 0 ? " " : "") + t; + } + + if( "CursedLuck" in this.misc ) + silentOrNot( 0x04, "Cursed Luck: " + this.misc[ "CursedLuck" ]); + if( this.misc[ "FunnyStuffDone" ] & 0x02 ) + silentOrNot( 0x02, "No-Pile-On-Dice: " + state.Earthdawn.noPileonDice ); + if( this.misc[ "FunnyStuffDone" ] & 0x01 ) + silentOrNot( 0x01, "No-Pile-On-Step: " + state.Earthdawn.noPileonStep ); + + if( txt.length > 0 ) { + bodyMain.append( "", txt + ". Roll was " + this.BuildRoll( "showResult" in this.misc && this.misc[ "showResult" ], + this.misc[ "DiceOrigional" ].total, this.misc[ "DiceOrigional" ] ), { class: "sheet-rolltemplate-warning" }); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } + if(gmResult.length > 0 && (txt.length == 0 || !( "showResult" in this.misc && this.misc[ "showResult" ] ))) + gmResult += (gmResult.length > 0 ? ". " : "") + "Orig Rslt was: " + this.BuildRoll( true, this.misc[ "DiceOrigional" ].total, this.misc[ "DiceOrigional" ] ); + } // end funnystuff + + txt = ""; + if( "showResult" in this.misc && this.misc[ "showResult" ] ) + txt += "Result: " + this.BuildRoll( true, this.misc[ "result" ], rolls ); + else if ( rolls ) + gmResult += " Result: " + this.BuildRoll( true, this.misc[ "result" ], rolls ); + if( "failBy" in this.misc ) { + if ( "SpellBuffSuccess" in this.misc ) // Did not actually succeed, but spell is a buff, so pretend we did. + txt += ((txt.length > 0) ? " " : "" ) + "Buff Spell: Auto succeed. " + + this.BuildRoll( true, this.misc[ "result" ], rolls ) + "."; + else + txt += ((txt.length > 0) ? " " : "" ) + "Failure " + + ((( "StyleOverride" in this.misc ? this.misc[ "StyleOverride" ] : state.Earthdawn.style) != Earthdawn.style.VagueSuccess ) + ? " by " + this.BuildRoll( this.misc[ "showResult" ], this.misc[ "failBy" ], rolls ) + "." : "!" ); + } + if( "succBy" in this.misc ) { + let es = Earthdawn.parseInt2( "extraSucc" in this.misc ? this.misc[ "extraSucc" ] : 0 ); + txt += ((txt.length > 0) ? " " : "" ) + "Success " + + ((( "StyleOverride" in this.misc ? this.misc[ "StyleOverride" ] : state.Earthdawn.style) != Earthdawn.style.VagueSuccess ) + ? " by " + this.BuildRoll( this.misc[ "showResult" ], this.misc[ "succBy" ], rolls ) : "!" ) + + (("sayTotalSuccess" in this.misc && this.misc[ "sayTotalSuccess" ] ) + ? new HtmlBuilder( "span", " (" + (es +1) + " Total)", { class: "sheet-rolltemplate-good", + title: Earthdawn.encode( Earthdawn.encode( es + " extra success" + ((es != 1) ? "es!" : "!") + + (("grimCast" in this.misc) ? " One added assuming you are casting from your own Grimoire." : ""))) }) + : (( es > 0) ? new HtmlBuilder( "span", " (" + es + " Extra)", { class: "sheet-rolltemplate-emphasis", + title: Earthdawn.encode( Earthdawn.encode((es +1) + " total success" + ((es != 0) ? "es!" : "!") + + (("grimCast" in this.misc) ? " One added assuming you are casting from your own Grimoire." : ""))) }) + : "." )) + if (this.misc[ "reason" ] === "Jumpup Test") { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), "No longer Knocked Down"); + this.MarkerSet( [ "rf", "knocked", "u" ] ); + } } + if( "Willful" in this.misc ) + txt += " Note: Target is Willful"; + if( txt.length > 0 ) { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), txt, { class: "sheet-rolltemplate-ResultLine"}); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } + if( state.Earthdawn.showDice && rolls ) { + txt = "Dice: " + this.buildDice( this.misc[ "showResult" ], rolls ); + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), txt); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } + if( "RuleOfOnes" in this.misc ) { + bodyMain.append( "", "Rule of Ones", { class: "sheet-rolltemplate-bad" }); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + delete this.misc[ "RuleOfOnes" ]; // Need to delete it, or will show up for other rolls in this group. + } + + let TNall = (this.bFlags & Earthdawn.flags.VerboseRoll) + || (( "StyleOverride" in this.misc ? this.misc[ "StyleOverride" ] : state.Earthdawn.style) === Earthdawn.style.Full); + if( "secondaryResult" in this.misc ) { + let es = Math.max(0, Math.floor(this.misc[ "secondaryResult" ] / 5 -1)); + txt = "Cntr-Atk:" + + ( TNall ? " TN " + this.misc[ "targetNum2" ] : "" ) + + ((this.misc[ "secondaryResult" ] < 0) + ? " Failed by " + : " Succeeded by ") + + Math.abs( this.misc[ "secondaryResult" ]) + + (("sayTotalSuccess" in this.misc && this.misc[ "sayTotalSuccess" ] ) + ? new HtmlBuilder( "span", " (" + (es +1) + " TOTAL Success" + ((es != 0) ? "es)" : ")") + " (After subtracting success required to Riposte)", { + class: "sheet-rolltemplate-good", + title: Earthdawn.encode( Earthdawn.encode( es + " extra success" + ((es != 1) ? "es!" : "!"))) }) + : (( es > 0) ? new HtmlBuilder( "span", " (" + es + " EXTRA Success" + ((es != 1) ? "es)" : ")"), { + class: "sheet-rolltemplate-emphasis", + title: Earthdawn.encode( Earthdawn.encode((es +1) + " total success" + ((es != 0) ? "es!" : "!") + " (After subtracting success required to Riposte)")) }) + : "." )) + if( txt.length > 0 ) { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), txt); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } } + if( "Spell" in this.misc ) { + let splines = this.Spell( this.misc[ "Spell" ] ); // Make a callback to Roll: Cast2, which will return additional lines to be added to the roll. + if( Array.isArray( splines )) + for( let i = 0; i < splines.length; ++i ){ +//log(splines[i]); + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), splines[ i ] ); + } } + if( "actType" in this.misc ) { + let t = ""; + if(( this.misc[ "actType" ] === "Standard" ) && (Campaign().get("initiativepage"))) { // Only count the standard actions if the initiative page is being displayed. + if( this.tokenInfo && this.tokenInfo.tokenObj ) { + let nbr = Earthdawn.getAttrBN( this.charID, "Actions", "1", true ), + tID = this.tokenInfo.tokenObj.get( "id" ), + count = 0; + if( tID && nbr ) { + if( tID in state.Earthdawn.actionCount ) + count = state.Earthdawn.actionCount[ tID ]; + state.Earthdawn.actionCount[ tID ] = ++count; + if( nbr > 1 || count > nbr ) + t = " " + count + " of " + nbr; + } } } + bAppend( bodyMain, (( ++linenum % 2) ? ".odd" : ".even"), "Action: " + (( this.misc[ "actType" ] !== "NA") ? + Earthdawn.addIcon( "action-" + this.misc[ "actType" ].toLowerCase(), "s", this.misc[ "actType" ]) : "" ) + t ); + } + if( "StrainPerTurn" in this.misc && ( x = Earthdawn.parseInt2( this.misc[ "StrainPerTurn" ] ))) + bAppend( bodyMain, (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.addIcon( "strain" + ((x < 4) ? x.toString() : ""), "s", + "On the Combat tab, it says to spend this much Strain every time initiative is rolled. This is probably from Blood Charms, Thread Items, Spells or some-such.", + "Strain Per Turn: ", x)); + if( "eachTargets" in this ) // We compare the roll result to each target number in the list. + for( let i = 0; i < this.eachTargets.length; ++i ) { + let res = po.misc[ "result" ] - this.eachTargets[ i ][ "tNum" ], + t; + if( res < 0 ) t = "Failed"; + else if (res < 5) t = "Succeeded"; + else t = (1 + Math.floor(res / 5)) + " Successes"; + bAppend( bodyMain, (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.constantIcon( "Target" ) + "" + this.eachTargets[ i ][ "tName" ] + " " + + (( state.Earthdawn.style === Earthdawn.style.Full || state.Earthdawn.style === Earthdawn.style.VagueSuccess) + ? Earthdawn.addIcon( "TN", "s") + "TN# " + this.eachTargets[ i ][ "tNum" ] + ":" : "" ) + " " + t ); + } + + function procMsg( msg, otherTests, playernix ) { // Anything before the first Tildi gets displayed to the user. + if( otherTests && (msg in po.misc )) { + let m = po.misc[ msg ]; + if( m.length > 1) { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.getParam( m, 1, "~" )); + if( !playernix ) + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + let i = m.indexOf( "~" ); + if( i !== -1 ) + po.doLater += m.slice( i ); // Anything after the first Tildi gets treated as a command an run though the API parser. + } } } + procMsg( "displayMsg", true, true ); + procMsg( "successMsg", ( "succBy" in this.misc ) || !( "targetNum" in this.misc)); + procMsg( "failMsg", "failBy" in this.misc); + + if( ("endNote" in this.misc) && (this.misc[ "endNote" ].length > 1)) + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), this.misc[ "endNote" ]); + if( ("endNoteSucc" in this.misc) && (this.misc[ "endNoteSucc" ].length > 1) && (( "succBy" in this.misc ) || !( "targetNum" in this.misc))) { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), this.misc[ "endNoteSucc" ]); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } + if( ("endNoteFail" in this.misc) && (this.misc[ "endNoteFail" ].length > 1) && (( "failBy" in this.misc ) || !( "targetNum" in this.misc))) { + bodyMain.append( (( ++linenum % 2) ? ".odd" : ".even"), this.misc[ "endNoteFail" ]); + if( playerCard ) playerCardNix.push( bodyMain._children.length ); + } + if(("cButtons" in this.misc) && this.misc[ "cButtons" ].length > 0) { + let tName; + for( let i = 0; i < this.misc[ "cButtons" ].length; ++i ) { + if( tName != this.misc[ "cButtons" ][ i ].name ) { // If this button has a different target name than the last button, display it. + tName = this.misc[ "cButtons" ][ i ].name; + buttonLine += " " + tName + " "; + } + buttonLine += Earthdawn.makeButton( this.misc[ "cButtons" ][ i ].text, this.misc[ "cButtons" ][ i ].link, + ("tip" in (this.misc[ "cButtons" ][ i ])) ? this.misc[ "cButtons" ][ i ].tip.replace( /(\r|\n)/g , " "): "", "effect"); + } } + +/* findthis */ + /* + ed3 211013.css + mix-blend-mode experiments + + vISIBILITY ASK NOT WORKING. + */ + function addGmInfo( b ) { + 'use strict'; + try { + if( !b ) { + Earthdawn.errorLog( "ED.rollFormat invalid call: addGmInfo( " + b + " )" ); + return; + } + if ("gmTN" in po.misc) { + bAppend( b, (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.addIcon( "TN", "s") + "TN " + po.misc[ "gmTN" ] ); + if( playerCard ) playerCardNix.push( b._children.length ); + } + if ( gmResult.length > 0 ) { + bAppend( b, (( ++linenum % 2) ? ".odd" : ".even"), gmResult.trim() ); + if( playerCard ) playerCardNix.push( b._children.length ); + } + if(( "secondaryResult" in po.misc ) && !TNall) { + bAppend( b, (( ++linenum % 2) ? ".odd" : ".even"), Earthdawn.addIcon( "TN", "s") + "Cntr-Atk: TN " + po.misc[ "targetNum2" ] ); + if( playerCard ) playerCardNix.push( b._children.length ); + } + if( buttonLine ) { + b.append( (( ++linenum % 2) ? ".odd" : ".even"), buttonLine); + if( playerCard ) playerCardNix.push( b._children.length ); + } + } catch(err) { Earthdawn.errorLog( "ED.rollFormat addGmInfo() error caught: " + err, po ); } + } // end addGmInfo + + // Here we figure out what messages get sent where. + if( whichMsgs === Earthdawn.whoTo.public ) { // public: Main message to everybody. Maybe supplementary messages to player and/or gm. + let gmline = ""; + this.chat( sectMain.toString().replace( /(\r|\n)/g , " "), Earthdawn.whoTo.public | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character ); + // But we also need player and GM cards. + if( ("gmTN" in this.misc) || gmResult.length > 0 || (( "secondaryResult" in this.misc ) && !TNall) || buttonLine) { + let sectGM = po.newSect( "s" ), + bodyGM = Earthdawn.newBody( sectGM ); + addGmInfo( bodyGM ); + gmline = sectGM.toString().replace( /(\r|\n)/g , " "); + } + setTimeout(function() { + try { // After public message, supplementary message to GM with TN and buttons. + if( gmline ) + po.chat( gmline, Earthdawn.whoTo.gm | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character ); + } catch(err) {Earthdawn.errorLog( "ED.rollFormat setTimeout1() error caught: " + err, po );} + }, 50); + if (buttonLine && !pIsGM ) + setTimeout(function() { // After public message, if player is not GM, supplementary message to player with buttons. + try { + let sectPlayer = po.newSect( "s" ); + let bodyPlayer = Earthdawn.newBody( sectPlayer ); + bodyPlayer.append( (( ++linenum % 2) ? ".odd" : ".even"), buttonLine); + po.chat( sectPlayer.toString().replace( /(\r|\n)/g , " "), Earthdawn.whoTo.player | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character | Earthdawn.whoFrom.noArchive ); + } catch(err) {Earthdawn.errorLog( "ED.rollFormat setTimeout2() error caught: " + err, po );} + }, 70); // end public message and GM and player info cards. + } else { // Send GM message, and then Player message. + addGmInfo( bodyMain, true ); + + this.chat( sectMain.toString().replace( /(\r|\n)/g , " "), Earthdawn.whoTo.gm | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character, " sent Roll to GM" ); + if( !pIsGM ) { // If the roll to GM was requested by a GM, don't send a duplicate to them as player. + while( playerCardNix.length > 0 ) + bodyMain._children.splice( playerCardNix.pop() -1, 1); // If identical messages are being sent to player and gm, this will do nothing. But if we are just sending a playercard to the player, this will remove the roll information. + this.chat( sectMain.toString().replace( /(\r|\n)/g , " "), Earthdawn.whoTo.player | Earthdawn.whoFrom.player | Earthdawn.whoFrom.character ); + } } + this.doNow(); + if( this.rollQueue.length > 0 ) { + setTimeout(function() { + try { + po.indexTarget++; + po.misc = JSON.parse( po.rollQueue.shift()); // make the next roll in the queue active. + po.rollPost(); + } catch(err) {Earthdawn.errorLog( "ED.rollFormat setTimeout3() error caught: " + err, po );} + }, 100); + } + } catch(err) { Earthdawn.errorLog( "ED.rollFormat() error caught: " + err, po ); } + } // End ParseObj.rollFormat() + + + + // Build html to display a tool tip to show a roll result. + // If howMuchDetail is true, then display all components of the roll. If false then only show 1's, maxes, and placeholders. + // Main is what should appear in main span. + // rolls is passed from dice roller and contains roll result. + // + // Note: I do something weird to handle very small step numbers, such as step 2 which has a roll like + // {{1d4!-1}+d1}kh1 Which means roll 1d4 (exploding) then subtract one, then roll 1d1 (which will give a 1), and keep the highest of the two. ie: keep the higher of 1d4-1, or 1. + // The {'s show up as type G Groups. + // As long as the type is type V or G, I keep going deeper into it. When I find the first non V or G, I start processing the roll results. + // This will result in processing the 1d4!-1 but not the rest, which is simpler and OK. This should work on all current rolls I expect to see right now, + // but if somebody messes with the groupings on the dice in order to achieve some other effect, this might need to be rethought. See also CursedLuck which uses this same logic. + // {"type":"V","rolls":[{"type":"G","rolls":[[{"type":"G","rolls":[[{"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]},{"type":"M","expr":"-1"}]],"mods":{},"resultType":"sum","results":[{"v":6}]},{"type":"M","expr":"+"},{"type":"R","dice":1,"sides":1,"mods":{},"results":[{"v":1,"d":true}]}]],"mods":{"keep":{"end":"h","count":1}},"resultType":"sum","results":[{"v":6}]}],"resultType":"sum","total":6} + this.BuildRoll = function( howMuchDetail, main, rolls ) { + 'use strict'; + try { + let po = this, + tip1 = 'Rolling ', + tip2 = " = ", + f, level = 0, levelmax = 0, dice = 0, ones = 0; +//log(JSON.stringify(rolls)); + function walk( item ) { // Walk through the roll structure, extracting what we need. + 'use strict'; + switch ( item[ "type" ] ) { + case "V": // This is the outermost container. It should have ,"resultType":"sum","total":6 at the end. + for( let k1 = 0; k1 < item[ "rolls" ].length; ++k1) + walk( item[ "rolls" ][ k1 ] ); + break; + case "G": // This is a group delimited by brackets. {{1d4!-1}+d1}kh1 is two nested groups. "1d4!-1" and that and d1, with a keep highest 1. + tip1 += "{"; + if (( level === 0) && ( "results" in item )) { // If we are at the lowest level of a group, and have results, take them here. This should take care of {{d4-1}+d1}kh1 with the results of the whole thing. + // Note however that as written this group total will not be updated if Cursed Luck curses a dice in the group. This is a known bug that is considered not important and too much trouble to fix. + // If we did have to fix it, we would need to interpret inside the group, and figure out "keep high one" and any other strange stuff ourselves. + // But as written, the totals are right, it is just the details of which dice got cursed by cursed luck that are not accurate. + if( item[ "results" ].length == 1 ) + tip2 += item[ "results" ][ 0 ][ "v" ]; + else { + tip2 += "("; + for( let k5 = 0; k5 < item[ "results" ].length; ++k5 ) + tip2 += item[ "results" ][ k5 ][ "v" ]; + tip2 += ")"; + } } + if( ++level > levelmax) levelmax = level; // When level was zero, we were not in a group. Now we are. + for( let k2 = 0; k2 < item[ "rolls" ].length; ++k2) + for( let k3 = 0; k3 < item[ "rolls" ][ k2 ].length; ++k3 ) + walk( item[ "rolls" ][k2][ k3 ] ); + tip1 += "}"; +// if(( "mods" in item ) && ( "keep" in item[ "mods" ] )) +// tip1 += "k" + item[ "mods" ][ "keep" ][ "end" ] + item[ "mods" ][ "keep" ][ "count" ]; + --level; + break; + case "R": // This is a sub-roll result. {"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]} + if( level === levelmax ) // only do tip1 this if it is the first R of this level. This will strip out the +d1 kh1. + tip1 += ((( "dice" in item) && item[ "dice" ] > 1) ? item[ "dice" ] : "") + + "d" + + (( "sides" in item) ? item[ "sides" ] : "") + + (( "mods" in item) ? (( "exploding" in item[ "mods" ]) ? "!" : "") : ""); + if(( level === 0 ) &&( "results" in item )) { + tip2 += "("; + for( let j = 0; j < item[ "results" ].length; ++j ) + if( "v" in item[ "results" ][ j ] ) { + ++dice; + let v = item[ "results" ][ j ][ "v" ]; + if( v === item[ "sides" ] ) + f = v; + else if( v == 1) { + f = v; + ++ones; + } else + f = howMuchDetail ? v : "?"; + tip2 += (( j === 0) ? "" : "+") + f; + } + tip2 += ")"; + } + break; + case "M": // This is an expression, such as "+" between two items, or "-1" as a modifier to a roll. + if( level === 0 || item[ "expr" ].startsWith( "-" )) + tip1 += item[ "expr" ] + if( level === 0 ) + tip2 += item[ "expr" ] + break; + default: + Earthdawn.errorLog( "Error in ED.BuildRoll(). Unknown type '" + item[ "type" ] + "' in rolls " + item + ". Complete roll is ...", po ); + log( JSON.stringify( rolls )); + } + } // end walk() + + walk( rolls ); + if( dice > 1 && dice === ones ) + this.misc[ "RuleOfOnes" ] = true; + + return new HtmlBuilder( "span", main.toString(), { class: "sheet-rolltemplate-chatbox2", title: Earthdawn.encode( Earthdawn.encode( tip1 + tip2 )) }); + } catch(err) { Earthdawn.errorLog( "ED.BuildRoll() error caught: " + err, this ); } + } // End ParseObj.BuildRoll() + + + + // Build html to display a detailed roll result showing all the dice. + this.buildDice = function( howMuchDetail, rolls ) { + 'use strict'; + try { + let po = this, + txt = ' ', + rov = [], + groupIndex = 0; +//log(JSON.stringify(rolls)); + function walk( item ) { // Walk through the roll structure, extracting what we need. + 'use strict'; +//log(item); + switch ( item[ "type" ] ) { + case "V": // This is the outermost container. It should have ,"resultType":"sum","total":6 at the end. + for( let k1 = 0; k1 < item[ "rolls" ].length; ++k1) + walk( item[ "rolls" ][ k1 ] ); + break; + case "G": // This is a group delimited by brackets. {{1d4!-1}+d1}kh1 is two nested groups. "1d4!-1" and that and d1, with a keep highest 1. + // Do special parsing for this whole group. + // Note that this probably only works if the roll results are of the exact form expected. {{1d4!-1}+d1}kh1. +/* +//"{\"type\":\"V\",\"rolls\":[{\"type\":\"G\",\"rolls\":[[{\"type\":\"G\",\"rolls\":[[{\"type\":\"R\",\"dice\":1,\"sides\":4,\"mods\":{\"exploding\":\"\"},\"results\":[{\"v\":2}]},{\"type\":\"M\",\"expr\":\"-2\"}]],\"mods\":{},\"resultType\":\"sum\",\"results\":[{\"v\":0}],\"d\":true},{\"type\":\"M\",\"expr\":\"+\"},{\"type\":\"R\",\"dice\":1,\"sides\":1,\"mods\":{},\"results\":[{\"v\":1}]}]],\"mods\":{\"keep\":{\"end\":\"h\",\"count\":1}},\"resultType\":\"sum\",\"results\":[{\"v\":1}]}],\"resultType\":\"sum\",\"total\":1}" +{\"type\":\"V\",\"rolls\":[ + {\"type\":\"G\",\"rolls\": + [[ + {\"type\":\"G\",\"rolls\": + [[ + {\"type\":\"R\",\"dice\":1,\"sides\":4,\"mods\":{\"exploding\":\"\"},\"results\":[ + {\"v\":2}] + }, + {\"type\":\"M\",\"expr\":\"-2\"} + ]] + ,\"mods\":{},\"resultType\":\"sum\",\"results\": + [{\"v\":0}],\"d\":true + }, + {\"type\":\"M\",\"expr\":\"+\"}, + {\"type\":\"R\",\"dice\":1,\"sides\":1,\"mods\":{},\"results\":[{\"v\":1}]} + ]], + \"mods\":{\"keep\":{\"end\":\"h\",\"count\":1}},\"resultType\":\"sum\",\"results\":[{\"v\":1}] + }],\"resultType\":\"sum\",\"total\":1 +} +*/ + groupIndex = rov.length; // Save the place in rov where we entered this group. That is where we need to make adjustments. + for( let k2 = 0; k2 < item[ "rolls" ].length; ++k2) + for( let k3 = 0; k3 < item[ "rolls" ][ k2 ].length; ++k3 ) + walk( item[ "rolls" ][k2][ k3 ] ); + break; + case "R": // This is a sub-roll result. {"type":"R","dice":1,"sides":4,"mods":{"exploding":""},"results":[{"v":4},{"v":3}]} + let bonus = false; + if(( "results" in item ) && ( "sides" in item)) { + if( item[ "sides" ] == 1) { // skip results where side equals 1. Those are in keep highest one. + if( rov.length > 0 && rov[ rov.length - 1 ] === "+" ) + rov.pop(); // Throw away the last + sign saved. + } else { + for( let j = 0; j < item[ "results" ].length; ++j ) // For each dice + if( "v" in item[ "results" ][ j ] ) { + let obj = {}; + if( "sides" in item ) + obj[ "sides" ] = item[ "sides" ]; + if( bonus ) + obj[ "bonus" ] = true; + let v = item[ "results" ][ j ][ "v" ], + f; + if(( v == item[ "sides" ])) { + f = v; + obj[ "max" ] = true; + if( "exploding" in item[ "mods" ]) + bonus = true; + } else if( v == 1) { + f = v; + obj[ "min" ] = true; + bonus = false; + } else { + f = howMuchDetail ? v : "?"; + bonus = false; + } + obj[ "disp" ] = f; + obj[ "v" ] = v; + rov.push( obj ); + } } } + break; + case "M": // This is an expression, such as "+" between two items, or "-1" as a modifier to a roll. + if(( "expr" in item) && item[ "expr" ].startsWith( "-" )) { + let m = Earthdawn.parseInt2( item[ "expr" ] ), + old = Earthdawn.parseInt2( rov[ groupIndex ][ "v" ] ); + let n = old + m; // There is a modifier, probably -1 or -2. + if( n < 1 ) + n = 1; + if( n == 1 || rov[ groupIndex ][ "disp" ] !== "?" ) + rov[ groupIndex ][ "disp" ] = n.toString(); + rov[ groupIndex ][ "v" ] = n.toString(); + } else + rov.push( item[ "expr" ] ); + break; + default: + Earthdawn.errorLog( "Error in ED.Builddice(). Unknown type '" + item[ "type" ] + "' in rolls " + item + ". Complete roll is ...", po ); + log( JSON.stringify( rolls )); + } + } // end walk() + walk( rolls ); + rov.forEach( function( item ) { + 'use strict'; + if( typeof item === "string" ) + txt += item; + else if (typeof item === "object" ) { + txt += "" + item[ "disp" ] + ""; // single quote is end of class section, ending > is end of span opening. v is test within the span. + } + }); + return "" + txt + ""; +// return "" + txt + ""; + } catch(err) { Earthdawn.errorLog( "ED.buildDice() error caught: " + err, this ); } + } // End ParseObj.buildDice() + + + + // ParseObj.SetStatusToToken () + // Set the Token Status markers to match the Character sheet. Mostly just done when a new token is dropped on the VTT. + this.SetStatusToToken = function() { + 'use strict'; + let po = this; + try { + if( this.tokenInfo === undefined || this.tokenInfo.tokenObj === undefined ) { + this.chat( "Error! tokenInfo undefined in SetStatusToToken() command.", Earthdawn.whoFrom.apiError); + return; + } + let markers = ""; + + _.each( Earthdawn.StatusMarkerCollection, function( item ) { + let attName = item[ "attrib" ]; + if( attName === undefined ) + return; + let attValue = Earthdawn.getAttrBN( po.charID, attName, 0 ); + if( "shared" in item ) { + let shared = Earthdawn.safeString( item[ "shared" ] ); + if( shared.toLowerCase().startsWith( "pos" )) { + if( Earthdawn.parseInt2( attValue ) > 0 ) + markers += "," + Earthdawn.getIcon( item ) + "@" + Earthdawn.parseInt2( attValue ); + } else if( shared.toLowerCase().startsWith( "neg" )) { + if( Earthdawn.parseInt2( attValue ) < 0 ) + markers += "," + Earthdawn.getIcon( item ) + "@" + Math.abs( Earthdawn.parseInt2( attValue )); + } else if( attValue == shared ) // This is an OnValue. + markers += "," + Earthdawn.getIcon( item ); + } // if there is a shared, and none of the above is true, then this marker is definitely unset. + else if( !("submenu" in item )) { + if( attValue == "1" ) // If there is no submenu, than anything equal to 1 is set. + markers += "," + Earthdawn.getIcon( item ); + } else // There is no shared, and there is a submenu. Just set the badge to the value. + if( Earthdawn.parseInt2( attValue ) > 0 ) + markers += "," + Earthdawn.getIcon( item ) + "@" + attValue; + }); + Earthdawn.set( this.tokenInfo.tokenObj, "statusmarkers", markers.slice(1)); + } catch(err) { Earthdawn.errorLog( "ED.SetStatusToToken() error caught: " + err, po ); } + } // End ParseObj.SetStatusToToken() + + + + + // ParseObj.SetToken () + // We have been passed a tokenID to act upon. Set TokinInfo and CharID. + // ssa[1] Token ID + this.SetToken = function( ssa ) { + 'use strict'; + try { + if( ssa.length > 1 ) { + let TokObj = getObj("graphic", ssa[ 1 ]); + if (typeof TokObj != 'undefined' ) { + this.charID = TokObj.get("represents"); + let CharObj = getObj("character", this.charID) || ""; + if (typeof CharObj != 'undefined') { + let TokenName = TokObj.get("name"); + if( TokenName.length < 1 ) + TokenName = CharObj.get("name"); + this.tokenInfo = { type: "token", name: TokenName, tokenObj: TokObj, characterObj: CharObj }; + } // End CharObj defined + } // end TokenObj undefined + } + } catch(err) { Earthdawn.errorLog( "ED.SetToken() error caught: " + err, this ); } + } // End ParseObj.SetToken() + + + + + // setWW (note that there is also an EARTHDAWN.setWW that accepts cID) + // helper routine that sets a value into an attribute and nothing else. + // is part of parseObj so that have access to parseObj.char.ID + // All that is required is name and val. To set max only, use name, undefine, undefined, maxVal. + this.setWW = function( attName, val, dflt, maxVal, maxDflt ) { + 'use strict'; + try { + Earthdawn.setWW( attName, val, this.charID, dflt, maxVal, maxDflt ); + } catch(err) { Earthdawn.errorLog( "ParseObj.SetWW() error caught: " + err, this ); } + } // End ParseObj.SetWW() + + + + // ParseObj.ssaMods ( ssa ) + // + // ssa is an array of zero or more numbers. Sum them and return the result. + // start is where to start processing the array if other than ssa[1] + // fSpecial: if 1, then numbers are not additive. If a non-zero number does not explicitly start with plus or minus sign, it replaces all previous numbers instead of modifying them. + // cID is an optional character ID, otherwise defaults to this.charID + // + // Note that you CAN pass this variable names so long as those variables contain simple numbers, but not character sheet auto-calculated fields. + this.ssaMods = function( ssa, start, fSpecial, cID ) { + 'use strict'; + let ret = 0; + try { + if( start === undefined ) + start = 1; + for( let i = start; i < ssa.length; i++ ) { + let nomatch = true; + if( typeof ssa[ i ] === "string" ) { + let fnd = ssa[ i ].match( /\d+\s*\(\s*\d+\s*\)/g ); // Falling damage table uses a format nothing else does. step (reps). Look for it. + if( fnd ) { + let item = fnd[ 0 ]; + let fnd2 = item.match( /\(\s*\d+\s*\)/g ); // find number of repetitions inside the (). + if( fnd2 ) { + let rep = Earthdawn.parseInt2( fnd2[ 0 ].slice( 1, -1 )), // slice off the paren and get the reps. + stp = Earthdawn.parseInt2( item.replace( fnd2[ 0 ], "" )); + if( rep > 0 && stp > 0 ) { + ret += stp; + this.misc[ "moreSteps" ] = { step: stp, reps: rep }; + nomatch = false; + } else Earthdawn.errorLog( "ParseObj.ssaMods failed to parse " + ssa[ i ] + " got " + rep + " and " + stp ); + } else Earthdawn.errorLog( "ParseObj.ssaMods failed to parse " + ssa[ i ] ); + } } + if( nomatch ) { + let j = this.getValue( ssa[ i ], cID, fSpecial); + if( fSpecial != 1 ) + ret += j; + else { + let k = Earthdawn.parseInt2( j ); + if( i === start || j === "0" || j.startsWith( "-" ) || j.startsWith( "+" )) + ret += k; + else + ret = k; + } } } + } catch(err) { Earthdawn.errorLog( "ParseObj.ssaMods() error caught: " + err, this ); } + return ret; + } // End ParseObj.ssaMods + + + + // Spell-casting Token action. + // + // Earthdawn.abilityAdd( this.charID, Earthdawn.constantIcon( "Spell" ) + t, + // "!edToken~ %{selected|" + preTo + "Roll}" ); + // ssa: (0) Spell (1) SPM RowId (2) what {options} (n-1) G/M + // what: Info : Outputs spell information + // Sequence: displays the spell sequence menu + // New: Extra Threads Menu (called from the sequence menu) + // Extra: Adds Extra Thread (only called via the new command) + // Weave: T_RowID - Makes a Weaving test (called from the sequence menu, or offered after choosing extra thread) + // Cast: T_RowID -MAkes a Spellcasting test (called from the sequence menu or offered after weaving the last thread) + // Effect: T_RowID - Makes an Effect test (called from the sequence menu or offered after a successful test) + // Reset: Resets the whole sequence (offered from the sequence menu or after failing a spellcasting, or making an effect test) + // + // TuneGrimoire/TuneGrimoire2: Attunes the Grimoire (appears on the Sequence menu when it is a starting sequence from SP) + // TuneOnFly: Triggers a Weaving test. (offered if starting a new Grimoire cast) If successful will offer to chose the Matrix, if failed offer to wipe Matrix + // Reset: Clear the current casting section. + // WarpTest: Makes a Warping Test. Only appears after a spellcasting test detected as raw + // HorrorMark: Makes a Horror Mark test. See WarpTest + // + // seq: (0) Cast, (1) G/M (Will be GA if casting from a Grimoire successfuly attuned and GR if casting Raw), (2) (spell or matrix row id), (3) SequenceStep, (4) Number threads pulled (including already in matrix), (5) Effect bonus from spellcasting (6+) extra threads. + // (3) SequenceStep = 0 new, 1 pulling threads, 2 pulled all threads, 3 spellcasting failed, 4 spellcasting succeeded, 5 an effect has been rolled. + + // 3.19 - Rewrote the sequence. Instead of reading the data from the SP or SPM Attributes, the Sequence now looks for the data in a seqdata JSON that stores sequence and spell data. + // This allows the spelldata to be dynamically overwritten if some Spell Knacks / Binding Secrets do change the sequence. + // spellseq { SeqStep, Type , RowId: (spell or matrix row id) , CurThreads, TotThreads, numExtraThreads, ExtraThreads, numExtraSuccesses spelldata:{Name, SP_RowID, Circle, SThreads:Spell Threads base number }} + // SequenceStep = 0 new, 1 pulling threads, 2 pulled all threads, 3 spellcasting failed, 4 spellcasting succeeded, 5 an effect has been rolled. + // Type: G/M (Will be GA if casting from a Grimoire successfuly attuned and GR if casting Raw) + // RowId: (spell or matrix row id) depending on Type + // TotThreads : Total number of Threads to be woven (including Extras), CurThreads: Currently Woven threads (including held in Matrix) + // numExtraThreads : Number of Extra Threads added to the Spell, ExtraThreads : Comma separated list of Extra Threads added to the spell + // numExtraSuccesses : Number of Extra Successes on the Spellcasting + // ESEffect : Added WIL Effect due to extra Successes, numExtraSuccesses: number of spellcasting extra Successes + // bGrim : Grimoire or Raw Casting, bGrimAtt: Grimoire Attuned + // inMatrix: SPM_RowID of the holding Matrix MatrixType: code of the Matrix Orifin + this.Spell = function( ssa ) { + 'use strict'; + let po = this; + try { +//log("this.Spell ssa is " + JSON.stringify(ssa)); + //Interpreting the SSA Information coming directly from the Spell passed in the SSA + let bGrim = Earthdawn.safeString( ssa[ ssa.length -1 ] ).toLowerCase().startsWith( "g" ), // false = Matrix(SPM). true = Spell (SP). + pre = Earthdawn.buildPre( bGrim ? "SP" : "SPM", ssa[ 1 ] ), // The prefix for the spell, SPM or SP depending where the call was made from + presp = (bGrim ? pre : Earthdawn.buildPre( "SP", Earthdawn.getAttrBN( this.charID, pre + "spRowID" ))), //The prefix for the SP, even if call was done from SPM + inMatrix = Earthdawn.getAttrBN( this.charID, presp + "spmRowID", "0" ); // If the spell is in a matrix this is (at least one of) the rowID of the matrix it is in (or zero). + if( bGrim && inMatrix != "0" ) { // They pressed a grimoire button, but this spell is in a matrix, so just pretend they pressed the matrix button. + bGrim = false; + ssa[ ssa.length -1 ] = "M"; + ssa[ 1 ] = inMatrix; + pre = Earthdawn.buildPre( "SPM", inMatrix ); + } + + // Retrieving the info of the last active sequence (note that at this stage it could be a different spell from the SSA) + let spellseqsaved = JSON.parse(Earthdawn.getAttrBN(this.charID, "SS-spellseq", "{}" ) || "{}" ); // JSON.parse( this.TokenGet( "spellseq", true )||"{}"); +//log("this.Spell ssa interpreted is " + JSON.stringify(ssa)); +//log("this.Spell spellseqsaved is " + JSON.stringify(spellseqsaved)); + + // We now compare the SSA info with the saved spellseq. Very detailed booleans to be able to actually well understand the situation in the whole code + // As this stage it is possible that the spell in the SSA and the one in spellseq are different, so we should be careful where we recover the data + let other = false, // Is the current sequence from another spell than the one from the last this.Spell call + noseqactive = (!spellseqsaved || spellseqsaved.SeqStep == undefined), // The sequence has been called from a clean start + seqended = false, // Respectively is there an active sequence, and if there is, is it still active (i.e. didn't reach its normal end, like spellcasting failure) + seqcontinue = false // This indicates an active sequence, that is not finished, and not from another spell, so this is actually the "Normal" case where we are in sequence + // Analysis of where we are in the sequence + if( !noseqactive) { // We know that we are in a sequence we have to determine if the sequence was a continuation of the previous one + seqended = ( spellseqsaved.SeqStep == 3 || spellseqsaved.SeqStep == 5); //Even if there is a sequence, it is actually ended + other = (spellseqsaved.spelldata.RowId !== (bGrim ? ssa[1] : Earthdawn.getAttrBN( po.charID, pre + "spRowID", ""))) && !seqended; //This indicates that we were called on a different spell, that didn't finished its sequence + seqcontinue= !seqended && !other; // Only here are we sure that we are continuing an existing sequence and can trust the data in the spellseq + } + + // Initializing spell data, either from the saved one or from the one retrieved from SSA data + // We need a set of spell data from the spell called in the ssa, which may be the same, or not + let spellseq = {}; + let fieldarray = {"sThreads":"0","Casting":"MD1: @{target|Target|token_id}","Duration":"","Range":"","WilSelect":"None","Discipline":"","WilEffect":"0","Effect":"","Numbers":"","ExtraThreads":"None","SuccessLevels":"None","AoE":"","EffectArmor":"N/A","FailText":"","SuccessText":"","DisplayText":"","sayTotalSuccess":"0","FX":"","Circle":"1"}; + + function initSpell() { //Function to reinit a spell sequence. Need to get back to the original data + 'use strict'; +// log('this.Spell : initspell'); + spellseq.SeqStep = 0; + spellseq.RowId = ssa[ 1 ]; + spellseq.bGrim = bGrim; + spellseq.bGrimAtt = false; + spellseq.Type = bGrim ? "G" :"M"; + spellseq.ExtraThreads = []; + if(!bGrim) { + spellseq.inMatrix = inMatrix; + spellseq.MatrixType = Earthdawn.getAttrBN( po.charID, pre + "Type", "-10") + } + spellseq.spelldata = {}; + spellseq.spelldata.Name = Earthdawn.getAttrBN( po.charID, presp + "Name", ""); + spellseq.spelldata.RowId = bGrim ? ssa[ 1 ] : Earthdawn.getAttrBN( po.charID, pre + "spRowID", ""); + spellseq.spelldata.StrainSave = po.misc[ "strainSave" ]; + + for(let key in fieldarray) + spellseq.spelldata[ key ] = Earthdawn.getAttrBN( po.charID, presp + key, fieldarray[ key ]); + spellseq.spelldata.Notes = Earthdawn.getAttrBN( po.charID, presp + "Notes", ""); + + if(!bGrim && Earthdawn.safeString(Earthdawn.getAttrBN( po.charID, pre + "EnhThread", "")).length > 2) { + spellseq.numExtraThreads = 1; + spellseq.ExtraThreads = [ Earthdawn.getAttrBN( po.charID, pre + "EnhThread", "")]; + } + spellseq.TotThreads = Earthdawn.parseInt2( spellseq.spelldata[ "sThreads" ] ) + Earthdawn.parseInt2( spellseq.numExtraThreads ); //Total Number of Threads to be woven, Base + maybe one if there is an extra + spellseq.CurThreads = (!bGrim && Earthdawn.parseInt2( Earthdawn.getAttrBN( po.charID, pre + "Type", "0" )) > 0 ) ? 1 : 0; //Threads already woven, i.e. if we have an enhanced Matrix or similar + spellseq.RemThreads = Earthdawn.parseInt2( spellseq.TotThreads) - Earthdawn.parseInt2( spellseq.CurThreads ); + + spellseq.spelldata.KnackNames = []; + spellseq.spelldata.KnacksId = []; + let lpv = spellseq.spelldata.LinksProvideValue = Earthdawn.getAttrBN( po.charID, presp + "LinksProvideValue", "").split( "," ), + lpvm = spellseq.spelldata.LinksProvideValueList = Earthdawn.getAttrBN( po.charID, presp + "LinksProvideValueList", "").split( "," ); //LPVM is SP;RowID +//log(lpv); +//log(lpvm); + for( let i = 0; i < lpvm.length; i++) + if( lpvm[ i ] && lpvm[ i ].includes( ";" ) && [ "SpellKnack", "BindingSecret" ].includes( Earthdawn.getAttrBN( po.charID, + Earthdawn.buildPre( Earthdawn.getParam( lpvm[ i ], 1, ";"), Earthdawn.getParam( lpvm[ i ], 2, ";")) + "Type", ""))) { + spellseq.spelldata.KnackNames.push( lpv[ i ] ); // KnackNames and KnacksId are knacks available, not knacks chosen. + spellseq.spelldata.KnacksId.push( Earthdawn.getParam( lpvm[ i ], 2, ";")); + } + }; //end InitSpell + + function SaveSeq() { //Function to save the spell Sequence in the Token and Character Sheet + 'use strict'; + po.setWW( "SS-Type", spellseq.Type ); //SS-Type stores info M/G/GA/GR + po.setWW( "SS-SeqStep", spellseq.SeqStep ); //SS-SeqStep stores the current step of spellcasting + po.setWW( "SS-Name", spellseq.spelldata.Name); + po.setWW( "SS-CurThreads", spellseq.CurThreads + "/" + spellseq.TotThreads ); + po.setWW( "SS-WilSelect", spellseq.spelldata.WilSelect); + po.setWW( "SS-WilEffect", spellseq.spelldata.WilEffect); + po.setWW( "SS-Effect", spellseq.spelldata.Effect); + po.setWW( "SS-Notes", spellseq.spelldata.Notes); + po.setWW( "SS-Range", spellseq.spelldata.Range); + po.setWW( "SS-Duration", spellseq.spelldata.Duration); + po.setWW( "SS-EnhThread", spellseq.ExtraThreads ? spellseq.ExtraThreads.toString() : ""); + po.setWW( "SS-Knacks", spellseq.KnacksChosenName ? spellseq.KnacksChosenName.toString() : "x"); + po.setWW( "SS-StrainAdvanced", po.misc[ "strainSave" ] || ""); // As far as I can tell, this is saved, but never used. + po.setWW( "SS-Numbers", spellseq.spelldata.Numbers); + po.setWW( "SS-Active", spellseq.RowId); //SS-Active stores the info about the SP_RowID/SPM_Row ID + po.setWW( "SS-Type", spellseq.Type); //SS-Type stores info M/G/GA/GR + po.setWW( "SS-SeqStep", spellseq.SeqStep); //SS-SeqStep stores the current step of spellcasting + po.setWW( "SS-spellseq", JSON.stringify(spellseq)); + //po.TokenSet( "clear", "spellseq", JSON.stringify(spellseq)); +//log("seq saved with spellseq " + JSON.stringify( spellseq )); + }; //End SaveSeq + + // We need some buttons made up. We should be able to inheirit ssa from the main routine. + function buttonOnDemand( what, sp ) { + 'use strict'; + let txt, t = "", sp2 = ((sp === undefined || sp) ? "" : " NOTE : This is not the recommended next step of the Sequence"); + + function getKask( pre3 ) { // KarmaGlobalMode 0: Off, x: Auto, ?:Ask when the ? is in front of the { the chat window ask triggers. + let charname = getAttrByName( po.charID, "character_name" ); + return "modValue : ?{Modification|0}~ K-ask: @{" + charname + "|KarmaGlobalMode}@{" + charname + "|" + + pre3 + "Karma-Ask}: @{" + charname + "|DPGlobalMode}@{" + charname + "|" + pre3 + "DP-Ask}~"; + } + + switch( what ) { + case "extra": + let opt = _.without( Earthdawn.safeString(spellseq.spelldata.ExtraThreads).split( "," ), ""), + et = ""; + for( let i = 0; i < opt.length; ++i ) // Add extra threads to the tooltip. + et += opt[ i ] + (i < (opt.length -1) ? ", " : ""); + t = Earthdawn.makeButton( "Extra Threads", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": New: " + ssa[ ssa.length -1 ], + "Start a new casting and chose extra threads: " + et, "param" ); + break; + case "cast": + if( !txt ) txt = txtcst; + case "effect": // effect + if( !txt ) txt = txtfx; + case "onfly": // thread weaving to tune on fly. + if( !txt ) txt = txtonfly; + case "pattern": // patterncraft + if( !txt ) txt = txtpatt; + case "weave": + if( !txt ) txt = txtwv; +//log("BoDa " + txt); + // All the above falls into here. + let bi3 = txt.split( "$" ); + for( let i = 0; i < bi3.length; ++i ) { + if( i > 0 ) t += " "; + if( bi3[ i ].includes( "missing!" )) + t = bi3[ i ]; + else { + let ci = bi3[ i ].split( "," ), // typically of the form: "Cast, pre" or "Cast, code, Rid" + code, rid, pre; + if( ci.length > 1 ) { + if( Earthdawn.safeString( ci[ 1 ] ).trim().startsWith( "repeating_" ) ) { // we have a pre + pre = ci[ 1 ]; + rid = Earthdawn.repeatSection( 2, ci [ 1 ]); + code = Earthdawn.repeatSection( 3, ci[ 1 ]); + } else if( ci[ 1 ] === "noRid" ) { // This is just to keep it from going into the else. + pre = code = rid = undefined; + } else if( Earthdawn.codeToName( ci[ 1 ], true ) && ci.length > 2 ) { // we have a code, and a rid + code = ci[ 1 ]; + rid = ci[ 2 ]; + pre = Earthdawn.buildPre( code, rid); + } else { // we probably just have a rid so assume "T" + code = "T"; + rid = ci[ 1 ]; + pre = Earthdawn.buildPre( code, rid); + } } + let sendon = (code === "T") ? rid : pre; // if it is a talent, just send the rowID, otherwise send on the entire pre from which the code and rid can be derived. + switch( Earthdawn.safeString( ci[ 0 ] ).trim() ) { + case "Cast": + t += Earthdawn.makeButton( Earthdawn.getAttrBN( po.charID, pre + "Name", "") + , "!Earthdawn~ charID: " + po.charID + "~ Target:" + spellseq.spelldata.Casting + "~ ForEach: inmt~ " + getKask( pre ) + + " Spell: " + ssa[ 1 ] + ": Cast: " + sendon + ": " + ssa[ ssa.length -1 ] + , "Cast using this Talent. " + sp2, "param"); + break; + case "CircleNG": // Circle Namegiver: CircleNG, repeating_discipline_xxx_DSP_, Elementalism. + t += Earthdawn.makeButton( ci[ ci.length -1 ] + " Circle" + , "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Effect: " + sendon + ": " + ssa[ ssa.length -1 ] + , "Effect Test based on Creature Circle/SR." + sp2, "param"); + break; + case "CircleSR": // Circle Creature or spirit: CircleSR, noRid, the spirit rating value. + t += Earthdawn.makeButton( ci[ 2 ] + " Circle" + , "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Effect: " + ci[ 1 ] + ": " + ssa[ ssa.length -1 ] + , "Effect Test based on this Discipline Circle." + sp2, "param"); + break; + case "CircleMissing": + t += "No Corresponding Discipline!"; + break; + case "onfly": + t += Earthdawn.makeButton( "Attune on the Fly", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ " + + getKask( pre ) + " Spell: " + ssa[ 1 ] + " : TuneOnFly: " + sendon + ": " + ssa[ ssa.length -1 ] + , "Attune Matrix on the Fly using " + ci[ 3 ] , "param2") + " "; + break; + case "Pattern": + t = Earthdawn.makeButton( "Attune Grimoire", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ " + + getKask( pre ) + "Spell: " + ssa[ 1 ] + " : TuneGrimoire : " + sendon + ": " + ssa[ ssa.length -1 ] + , "Attune Grimoire using the Patterncraft Talent." , "param"); + break; + case "Rank": // rid of talent from which rank can be gotten. + t += Earthdawn.makeButton( Earthdawn.getAttrBN( po.charID, pre + "Name", "") + " Rk" + , "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Effect: " + sendon + ": " + ssa[ ssa.length -1 ] + , "Effect Test based on this Talent Rk." + sp2, "param"); + break; + case "RankMissing": + t += "Spellcasting Talent Missing!"; + break; + case "reset": // We might get a reset inside a txt. + t += Earthdawn.makeButton( "Reset", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Reset: " + ssa[ ssa.length -1 ], + "Press this button to Reset the Spell Sequence.", "param2"); + break; + case "Weave": + t += Earthdawn.makeButton( Earthdawn.getAttrBN( po.charID, pre + "Name", ""), "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ " + getKask( pre ) + + " Spell: " + ssa[ 1 ] + ": Weave: " + sendon + ": "+ssa[ ssa.length -1 ], "Weave using this Talent." + sp2, "param"); + break; + case "Wil": + t += Earthdawn.makeButton( "WIL" + , "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Effect : Wil" +":" + ssa[ ssa.length -1 ] + , "Effect Test using Willpower Attribute." + sp2, "param"); + break; + case "Willtalent": + t += Earthdawn.makeButton( Earthdawn.getAttrBN( po.charID, pre + "Name", ""), "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ " + getKask( pre ) + + "Spell: " + ssa[ 1 ] + ": Effect: " + sendon + ": " + ssa[ ssa.length -1 ], "Effect Test using this Talent, or directly with WIL." + sp2, "param"); + break; + default: + t += Earthdawn.safeString( ci[ 0 ] ).trim() + ": "; + } } } + break; // end wv, cst, fx. + // continuing switch (what) + case "info": + t = Earthdawn.makeButton( "Info", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Info: " + ssa[ ssa.length -1 ], + "Press this button to get the details of the spell." , "param2"); + break; + case "knack": { + let ttn = ""; + for( let i = 0; i < spellseq.spelldata.KnackNames.length; ++i ) + ttn += spellseq.spelldata.KnackNames[ i ] + ", "; + t = Earthdawn.makeButton( "Spell Knacks " + spellseq.spelldata.KnacksId.length, "!Earthdawn~ charID: " + po.charID + "~ Spell: " + ssa[ 1 ] + " : Knack " + , "Choose Knacks to add to Casting: " + ttn.slice(0, -2).trim(), "param"); + } break; + case "mark": + t = Earthdawn.makeButton( "Horror Mark", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": HorrorMark: ?{ What is the region type? |Safe|Open|Tainted|Corrupt}:" +ssa[ ssa.length -1 ], + "When Casting Raw Magic, The unmasked magic might attract unwanted attention from astral entities. Test Against MD to see if character was Horror Marked." , "param2"); + break; + case "reset": + t = Earthdawn.makeButton( "Reset", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": Reset: " + ssa[ ssa.length -1 ], + "Press this button to Reset the Spell Sequence.", "param2"); + break; + case "tunematrix": + t = Earthdawn.makeButton( "Attune Matrix", "!Earthdawn~ %{" + getAttrByName( po.charID, "character_name" ) + "|" + pre + "AttuneButton}", + "Press this button to ReAttune this spell in a Matrix (using the 10 minute ritual)." , "param"); + break; + case "warp": + t = Earthdawn.makeButton( "Warp Test", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": WarpTest: ?{ What is the region type? |Safe|Open|Tainted|Corrupt} :" + ssa[ ssa.length -1 ], + "When Casting Raw Magic, the Astral Space can damage you. Make this test according to the Region Type. Test Against MD to know if character suffers Damage" , "param2"); + break; + case "wipe": + t = Earthdawn.makeButton( "Wipe All Matrix", "!Earthdawn~ charID: " + po.charID + "~ ForEach: inmt~ TuneMatrix: WipeMatrix", + "When re-attuning on the fly, Spellcaster must remain concentrated (Wil/Willforce chack against any damage), and continue until success, or all his matrix are wiped." , "param2"); + break; + default: + t = ""; + log( Earthdawn.timeStamp() + "Earthdawn.Spell.buttonOnDemand invalid value of 'what': " + what ); + } // end switch( what ) +//log( "BoD " + what + " " + t); + return " " + t.trim() + " "; + } // end buttonOnDemand + + + function line( txt, tip, addtxt) { + bodySpell.append( (( ++linenum % 2) ? ".odd" : ".even"), (( !tip || tip == "" ) ? txt: Earthdawn.texttip( txt, tip )) + ( addtxt ? addtxt : "")); + } + + if( seqcontinue && !(Earthdawn.safeString(spellseqsaved.Type).startsWith("G") && inMatrix != "0" )) //This indicates the spell has been put in Matrix since it was saved, reInit + spellseq = spellseqsaved; + else + initSpell(); + + // Some basic data from the spell as declared in the SSA that will be useful to consider next steps + // let sthrd = Earthdawn.parseInt2( spellseq.spelldata["sThreads"] ), //Spell Base Thread Number + // thrd = Earthdawn.getAttrBN( this.charID, pre + (bGrim ? "sThreads" : "Threads"), "", true ); //Starting Threads, ignores any extra thread + let nowil = (spellseq.spelldata[ "WilSelect" ] !== "Wil" ), //Spell has no Will Effect. + bGrimAtt = Earthdawn.safeString( spellseq.Type ).toLowerCase().endsWith( "a" ), //If a Grimoire is attuned we stored GA + nxtextra =(spellseq.SeqStep == 0), // We can only weave at the beginning of the sequence + nxtwv = ((spellseq.SeqStep == 0 && spellseq.RemThreads > 0) || spellseq.SeqStep == 1 ), // We begin weaving after chosing extra spells, or if we are in the middle of weaving and didn't pull them all + nxtcst = (spellseq.SeqStep == 2 ||(spellseq.SeqStep <= 1 && spellseq.RemThreads <= 0)), // We only recommend casting on a newly started sequence if there are no Threads + nxtfx = (spellseq.SeqStep == 4), // FX recommended if spellcasting was successful + linenum = 0, rid, txtwv = "", txtcst = "", txtfx = "", txtpatt = "", txtonfly = "", //This are the buttons for each of the main stuff + isng = (Earthdawn.getAttrBN( this.charID, "isNamegiver", "0") == "1" ), // non namegivers use their spellcasting instead of thread weaving and don't matrix cast + disp = !isng ? "Spellcasting" : Earthdawn.dispToName( spellseq.spelldata.Discipline, "weaving" ), + wilselect = spellseq.spelldata.WilSelect, + sectSpell = po.newSect(); // HTML functions for the menu. Initializes the sect for the future display if needed + sectSpell.append( "", spellseq.spelldata.Name, { class: "sheet-rolltemplate-spelldetail" }); + let bodySpell = Earthdawn.newBody( sectSpell ); + + if( Earthdawn.safeString( spellseq.spelldata.Casting ).startsWith( "MD1" ) && spellseq.ExtraThreads + && (spellseq.ExtraThreads.filter( str=>str.includes( "Target" )).length > 0 + || spellseq.ExtraThreads.filter( str=>str.includes( "Tgt" )).length > 0 )) { //Look for an Extra Thread that adds Targets + spellseq.spelldata.Casting = "MDh"; // If we had an Extra Thread that adds Targets, change MD1 to MDh + } + + // If there was already something in it, give origional, plus a tildi. Then each additional argument followed by a comma. + // sample return: cast, rid1 $ cast, label, value + function addit() { + let t = ""; + if( arguments.length > 0 && arguments[ 0 ] ) + t = arguments[ 0 ] + "$ "; + if( arguments.length > 1 ) + t += arguments[ 1 ]; + for( let i = 2; i < arguments.length; ++i ) + t += ", " + arguments[ i ]; + return t; + } // end addit + + let attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + _.each( attributes, function (att) { + if (att.get( "name" ).endsWith( "_Special" ) && Earthdawn.keywordCheck( att.get( "current" ), true, "SPL-" )) { // If any of the SPL- Talents. + let pre2 = Earthdawn.buildPre( att.get("name")); + rid = Earthdawn.repeatSection( 2, pre2 ); // if this rid is not a "T", then send pre2 instead, and the downstream code should accept that instead. + + if( att.get( "current" ).includes( "-" + disp )) { // Thread Weaving. + txtwv = addit( txtwv, "Weave", rid ); + if( Earthdawn.parseInt2( getAttrByName( po.charID, pre2 + "Strain" )) == 0) // We are using Strain == 0 as a signal that this is the base thread weaving talent for this discipline. + txtonfly = addit( txtonfly, "onfly", rid, Earthdawn.getAttrBN( po.charID, pre2 + "Name","") ); + } + if( att.get( "current" ).includes( "-Spellcasting" )) { // Spellcasting + txtcst = addit( txtcst, "Cast", rid ); + if( wilselect == "Rank" ) //Button for Effect for Rank based spell + txtfx = addit( txtfx, "Rank", rid ); + } else if(( wilselect == "Wil" ) && att.get( "current" ).includes( "-Willforce" )) // Wil and Willforce + txtfx = addit( txtfx, "Willtalent", rid ); + else if( att.get( "current" ).includes( "-Patterncraft" )) // Patterncraft + txtpatt = addit( txtpatt, "Pattern", rid ); + } // end T_Special SPL- + if( wilselect == "Circle" && isng && att.get( "name" ).endsWith( "_DSP_Name" ) && ( disp == Earthdawn.dispToName( att.get( "current" ), "weaving" ))) { // Effect based on Circle Namegiver + txtfx = addit( txtfx, "CircleNG", Earthdawn.buildPre( att.get( "name" )), disp); + } + + if( wilselect == "Circle" && !isng && att.get( "name" ).endsWith( "SrRating" )) // Effect based on Circle SR + txtfx = addit( txtfx, "CircleSR", "noRid", att.get( "current" )); + }); // End for each attribute. + + if( txtwv == "") txtwv = "Weaving Talent missing! Possible no Discipline defined in spell."; + if( txtcst == "") txtcst = "Spellcasting Talent missing!"; + if( txtpatt == "") txtpatt = "Patterncraft Talent missing!" + if( txtfx == "") + switch( wilselect ) { + case "Circle": txtfx = addit( txtfx, "CircleMissing" ); break; + case "Rank": txtfx = addit( txtfx, "RankMissing" ); break; + case "Wil": txtfx = addit( txtfx, wilselect ); break; + default: txtfx = addit( txtfx, Earthdawn.getAttrBN( po.charID, presp + "Effect", "None") ); + txtfx = addit( txtfx, "reset" ); break; + } + + // Main Spell() switch statement. + switch( ssa[ 2 ] ) { + case "Sequence": { //This is the master sequence... Where the magic happens + if( other ) { //Another Sequence is in progress, but for now we didn't take any action to abort it, this may still be a mistake... But any further action will re-initiate the sequence + line( "Other Sequence Active:", + "A different spellcasting sequence was in progress, continue with the old action or start a new spellcasting sequence with this new action?", + Earthdawn.makeButton( "Resume-" + spellseqsaved.spelldata.Name, + "!Earthdawn~ charID: " + this.charID + "~ ForEach: inmt~ Spell: " + spellseqsaved.RowId + " : Sequence", "Resume Sequence." , "param")); + line( "Start new Sequence for: " + spellseq.spelldata.Name + "" ); + } + //Header Logics when Grimoire/Raw + if( isng && spellseq.bGrim ) { //isng because creatures cast raw, like if it was matrix + if( spellseq.bGrimAtt ) // A Grimoire is attuned, this is definitely Grimoire Casting + line("Grimoire Casting, Grimoire Attuned" ,"You successfully Attuned to a Grimoire and are casting from the Grimoire. This will grant you one extra spellcasting success"); + else if( nxtextra ) { // We are at the beginning of the Grimoire/Raw, and we didn't + line("Spell not in Matrix. ","You are currently casting a spell that you selected in your spellbook, so you have either to Attune to your Grimoire to cast it from your Grimoire (takes 10 min), to Attune your matrix (takes 10 min), or to Attune on the fly (takes 1 round, 1 strain, and you risk wiping all your Matrix). If yo select none of these options, this will be supposed to be raw casting, with risks of warping and attracting Horror attention"); + line("", "", buttonOnDemand( "pattern" ) + buttonOnDemand( "tunematrix" ) + buttonOnDemand( "onfly" )); + } else // Grimoire Casting, with no Grimoire Attuned and sequence is started: This is Raw magic + line( " Raw Magic ! ", " You started to cast a spell not from Matrix and without attuning the Grimoire... Proceed at your own risks. Side Effects include Warping, Horror Marks, and potentially Death... or worse..."); + } + //Sequence header + if( !other ) + switch(spellseq.SeqStep) { + case 0 : line( "New Sequence.", "To start the Sequence, choose extra Thread, or start Weaving or Casting"); break; + case 1 : line( "Weaving Threads" ); break; + case 2 : line( "All Threads Woven, Cast" ); break; + case 3 : line( "Spellcasting Failed, press Reset", "You can hit reset, but can also proceed with the sequence if it was a mistake.You can also just restart the sequence from any step."); break; + case 4 : line( "Spellcasting Successful, Effect", "Proceed with the Effect"); break; + case 5 : line( "Effect Rolled, press Reset", "You can hit reset, but can also proceed with rerolling the Effect if multi-round. You can also just restart the sequence from any step."); break; + } + line( buttonOnDemand( "info" ) + buttonOnDemand( "reset" )); + //Body Main Action buttons + if(( spellseq.spelldata.KnacksId && spellseq.spelldata.KnacksId.length > 0 && spellseq.SeqStep == 0) || spellseq.KnacksChosenName) + line(" Spell Knacks: " + (spellseq.KnacksChosenName ? spellseq.KnacksChosenName.join() : "") + + ((spellseq.SeqStep == 0 && ( !spellseq.KnacksChosenName || spellseq.KnacksChosenName.length !== spellseq.spelldata.KnackNames.length)) + ? buttonOnDemand( "knack" ): ""), "Choose Spell Knacks" ); + line(" Extra Threads: " + (spellseq.ExtraThreads ? spellseq.ExtraThreads.toString() : + (spellseq.SeqStep==0 ? "" : "None")) + " " + (spellseq.SeqStep == 0 ? buttonOnDemand( "extra" ) : ""), ""); + line(" Weave (" + spellseq.CurThreads + " / " + spellseq.TotThreads +"): ", + "Choose Talent to weave Threads. " + spellseq.CurThreads + " woven (including held in Matrix) out of " + + spellseq.TotThreads + (spellseq.numExtraThreads > 0 ? "including " + spellseq.numExtraThreads + " Extra" : "") + , spellseq.SeqStep >= 2 ? "Done" : buttonOnDemand( "weave" )); + line(" Cast : ", "", spellseq.SeqStep == 3 + ? "Spellcasting Failed!" + : spellseq.SeqStep == 4 + ? ((spellseq.spelldata.SuccessLevels && spellseq.numExtraSuccesses ) + ? "Done - " + spellseq.spelldata.SuccessLevels + " x " + spellseq.numExtraSuccesses + " ES" + : "Spellcasting Successful" + ((spellseq.numExtraSuccesses && spellseq.numExtraSuccesses > 0) + ? " (" + spellseq.numExtraSuccesses + "Extra Success)" + : "" )) + : buttonOnDemand( "cast" )); + line(" Effect : ", "", buttonOnDemand( "effect" )); + this.chat( sectSpell.toString(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; //End Sequence + case "InfoPub": + case "Info": { //v3.19 No change because we want to stay with the unchanged data, i.e. read it directly from the sheet + let infoPub = (ssa[ 2 ] === "InfoPub"); + let castdiff = Earthdawn.getAttrBN( this.charID, presp + "Casting", "MD1"); + if( castdiff && castdiff.toString().indexOf( ":" ) > -1) + castdiff = castdiff.split( ":" )[ 0 ]; + let mattype = "Std "; + if( !bGrim ) { + switch( Earthdawn.getAttrBN( this.charID, pre + "Type", "-10") ) { + case "15": mattype = "Enhanced";break; + case "25": mattype = "Armored"; break; + case "-20": mattype = "Shared"; break; + } + line( "Matrix: Rank " + Earthdawn.getAttrBN( this.charID, pre + "Rank", "0") + " - " + mattype + " - " + + Earthdawn.getAttrBN( this.charID, pre + "Origin", "Free").replace("-link",""), "Rank of matrix, it's Type and Origin."); + } else line( "Matrix: Not in Matrix.",""); + line( "Min Threads: " + Earthdawn.getAttrBN( this.charID, presp + "sThreads", "0"), + "Number of spell threads that must be woven in order to cast the base version of the spell."); + line( "Weave Diff: " + Earthdawn.getAttrBN( this.charID, presp + "Numbers", ""), + "Weaving difficulty / Reattunment difficulty / Dispeling difficulty / Sensing difficulty."); + line( "Range: " + Earthdawn.getAttrBN( this.charID, presp + "Range", "") + " Cast Diff: " + castdiff, + "Range spell can be cast.
    Cast Difficulty, if coded is the appropriate Defense, if third character is 'h' then it is the highest among all targets. If 'p1p' is present it stands for 'plus one per additional target'."); + if( Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, presp + "AoE", "")).trim().length > 1 ) + line( "AoE: " + Earthdawn.getAttrBN( this.charID, presp + "AoE", ""), "Area of Effect."); + line( "Duration: " + Earthdawn.getAttrBN( this.charID, presp + "Duration", ""), "Duration of Effect."); + { let x1 = "Effect:"; + let efctArmor = Earthdawn.getAttrBN( this.charID, presp + "EffectArmor", "N/A"); + if( efctArmor != "N/A" ) + x1 += " " + Earthdawn.getAttrBN( this.charID, presp + "WilSelect", "None") + " +" + + Earthdawn.getAttrBN( this.charID, presp + "WilEffect", "0") + " " + efctArmor + "."; + if( Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, presp + "Effect", "")).trim().length > 1) + x1 += " " + Earthdawn.getAttrBN( this.charID, presp + "Effect", "") + if( x1.length > 16 ) + line( x1, "Spell Effect: Some spells have Willpower effects. Some of these WIL effects get modified by the targets armor. Many spells also have none WIL effects." ); + } + line( "Success Levels: " + Earthdawn.getAttrBN( this.charID, presp + "SuccessLevels", "None"), + "Getting more than one success upon the spellcasting test often provides a bonus effect."); + line( "Extra Threads: " + Earthdawn.getAttrBN( this.charID, presp + "ExtraThreads", ""), + "Extra Threads indicates what enhanced effects the caster can add to their spell by weaving additional threads into the spell pattern."); + { let enhThread = bGrim ? "x" : Earthdawn.getAttrBN( this.charID, pre + "EnhThread", ""); + if( enhThread && enhThread !== "x" ) + line( "Pre-woven Thread: " + enhThread.toString(), "EnhThread: This spell is in a matrix that can hold a pre-woven thread, and this is the extra thread option pre-woven into this matrix." ); + } + if( Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, presp + "DisplayText", "")).trim().length > 1 ) + line( "Display Text: " + Earthdawn.getAttrBN( this.charID, presp + "DisplayText", ""), + "This text is displayed when spellcasting is rolled. It can optionally be used to remind players of what the spell does and how it works. If there is a Tildi (~) then anything after the first Tildi is processed by the API"); + if( Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, presp + "SuccessText", "")).trim().length > 1 ) + line( "Success Text: " + Earthdawn.getAttrBN( this.charID, presp + "SuccessText", ""), + "This text appears if the spellcasting test is successful. It can optionally be used to remind players of what the spell does and how it works. If there is a Tildi (~) then anything after the first Tildi is processed by the API"); + if( Earthdawn.safeString( Earthdawn.getAttrBN( this.charID, presp + "FailText", "")).trim().length > 1 ) + line( "Fail Text: " + Earthdawn.getAttrBN( this.charID, presp + "SuccessText", ""), + "This text appears if the spellcasting test is failed. If there is a Tildi (~) then anything after the first Tildi is processed by the API"); + line( "Description (hover)", Earthdawn.getAttrBN( this.charID, presp + "Notes", "").replace( /\n/g, Earthdawn.constantIcon( "cr" ))); + if( !infoPub ) + line( Earthdawn.makeButton( "Post to all", "!Earthdawn~ charID: " + this.charID + "~ ForEach: inmt~ Spell: " + ssa[ 1 ] + ": InfoPub: " +ssa[ ssa.length -1 ], + "Press this button to send the details of the spell to all players." , "param2" )); + this.chat( sectSpell.toString(), (infoPub ? Earthdawn.whoTo.public : Earthdawn.whoTo.player) | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; // end info + case "TuneGrimoire": { + //Spell Sequence + if( other ) { + this.misc[ "warnMsg" ] = "Cancelling Sequence for " + spellseqsaved.spelldata.Name + ". Sequence Restarted ."; + } + else if( !nxtextra ) { + this.misc[ "warnMsg" ]= "Attuning Grimoire: " + spellseq.spelldata.Name //Reset Sequence instead + + " but the Spell was already in sequence. Sequence Restarted ."; + } + //Tune Grimoire is in any case a new sequence + initSpell(); + + let disp, tmp, pre2; + if( bGrim ) + tmp = spellseq.spelldata.Discipline; + else{ + this.chat( "Earthdawn Error : trying to Attune Grimoire with a Spell in a Matrix.", Earthdawn.whoFrom.apiWarning ); + return; + } + disp = Earthdawn.dispToName( tmp, "weaving" ); // Name of weaving talent + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); + this.strainAdd( pre2 ); + this.Karma( pre2 + "Karma", 0 ); + this.misc[ "rollName" ] = "Attuning"; + this.misc[ "reason" ] = "Attuning Grimoire " + Earthdawn.getAttrBN( this.charID, pre2 + "Name", "0") + " Test : " + spellseq.spelldata.Name; + this.misc[ "headcolor" ] = "weave"; + this.bFlags |= Earthdawn.flags.VerboseRoll; + this.Lookup( 1, [ "value", pre2 + "Step" ]); + this.misc[ "targetNum" ] = Earthdawn.getParam( spellseq.spelldata.Numbers, 2, "/" ); + this.misc[ "sayTotalSuccess" ] = true; + this.misc[ "endNoteSucc" ] = "Grimoire Successfully Attuned." + buttonOnDemand( "extra" ) + buttonOnDemand( "weave" ) + buttonOnDemand( "cast" ); + this.misc[ "endNoteFail" ] = "Grimoire Attuning Failed, Retry (takes 10 min) or cast Raw at your own risks"; + SaveSeq(); + ssa[ 2 ] = "TuneGrimoire2"; + this.misc[ "Spell" ] = ssa; + this.rollPre( [ "Roll" ] ); + } break; //End TuneGrimoire + case "TuneGrimoire2": { // Roll calls back to here. + let lines = []; + if("succBy" in this.misc) { + spellseq.Type= "GA"; //Grimoire is attuned + spellseq.bGrimAtt=true; + } + SaveSeq(); + return lines; + } //End TuneGrimoire2 + case "TuneOnFly": { // " Spell: " + SPM rowID + " : TuneOnFly: " + (rid or pre of tuning talent) + ": " + ssa[ ssa.length -1 ] + + let disp = Earthdawn.dispToName( spellseq.spelldata.Discipline, "weaving" ), + pre2; + if( !bGrim ){ + this.chat( "Earthdawn Error : trying to Attune Matrix with a Spell in a Matrix.", Earthdawn.whoFrom.apiWarning ); + return; + } + + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); + this.strainAdd( pre2 ); + this.strainAdd( "Attune on fly", 1 ); + this.Karma( pre2 + "Karma", 0 ); + + this.misc[ "rollName" ] = "Attuning"; + this.misc[ "reason" ] = "Attuning on the Fly " + Earthdawn.getAttrBN( this.charID, pre2 + "Name", "0") + " Test : " + spellseq.spelldata.Name; + this.misc[ "headcolor" ] = "weave"; + this.bFlags |= Earthdawn.flags.VerboseRoll; + this.Lookup( 1, [ "value", pre2 + "Step" ]); + this.misc[ "targetNum" ] = Earthdawn.getParam( spellseq.spelldata.Numbers, 2, "/" ); + this.misc[ "sayTotalSuccess" ] = true; + this.misc[ "endNoteSucc" ] = "Choose Matrix : " + buttonOnDemand( "tunematrix" ); + this.misc[ "endNoteFail" ] = "Attuning on the fly failure. Retry until success or " + buttonOnDemand( "wipe" ) ; + if( other ) { + this.misc[ "warnMsg" ]= "Cancelling Sequence for "+ spellseqsaved.spelldata.Name + ". Sequence Restarted ."; + initSpell(); + } + else if( !nxtextra ) { + this.misc[ "warnMsg" ]= "Attuning Matrix on the Fly: " + spellseq.spelldata.Name + " but the Spell was already in sequence. Sequence Restarted .";//Reset Sequence instead + initSpell(); + } + ssa[ 2 ] = "TuneOnFly2"; + this.misc[ "Spell" ] = ssa; + this.rollPre( [ "Roll" ] ); + } break; //End TuneOnFly + case "TuneOnFly2": { // Roll calls back to here. + let lines = []; + SaveSeq(); + return lines; + } + case "Knack": { // user has said want to pick a spell knack. Give them a list of knacks that may be chosen. + //Sequence Control. Normal Case is (noseqactive || seqended || (step==0 && !other)), if not go through different cases to issue warnings. + //At this stage, no action was taken, so sequence must not be initialized yet + if( !(noseqactive || seqended || (spellseq.SeqStep==0 && !other))) { + if ( other ) { + line( "Other Sequence Active : ", "", + Earthdawn.makeButton( "Resume-" + spellseqsaved.spelldata.Name, + "!Earthdawn~ charID: " + this.charID + "~ ForEach: inmt~ Spell: " + spellseqsaved.RowId + " : Sequence" , + "Resume Sequence." , "param")); + line("Choose Spell Knack to start new sequence " + spellseq.spelldata.Name + "" ); + } else if ( spellseq.SeqStep !== 0 ) { + line( "Warning! Trying to add Knack for: " + Earthdawn.getAttrBN( this.charID, pre + (bGrim ? "Name" : "Contains"), "" ) + + "When sequence was already started. Choosing a Knack will restart the sequence","Knacks Can only be selected when starting a sequence. "); + } + } else //Normal Case + line("Choosing Spell Knacks & Binding Secrets"); + let t = ""; + //end sequence control + + for( let i = 0; i < spellseq.spelldata.KnacksId.length; ++i ) { + if(!spellseq.KnacksChosenName || !spellseq.KnacksChosenName.includes(spellseq.spelldata.KnackNames[ i ])) // don't do button for any knack already chosen + t += Earthdawn.makeButton( spellseq.spelldata.KnackNames[ i ], "!Earthdawn~ charID: " + this.charID + "~ Spell: " + ssa[ 1 ] + ": " + "Knack2: " + spellseq.spelldata.KnacksId[ i ], + "Add this optional spell knack to this casting." , "action"); + else + t += "" + spellseq.KnacksChosenName[ i ] + ""; + } + line( "Spell Knacks: " + t ); + line( buttonOnDemand( "extra" ) + buttonOnDemand( "weave" ) + buttonOnDemand( "cast" )); + this.chat( sectSpell.toString(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; // End Knack + case "Knack2": { // Spell: (SPM ID): Knack2: (knackID) They have chosen a knack to add to the spell + //Sequence Control. Normal Case is (noseqactive || seqended || (step==0&!other)), if not go through different cases to issue warnings. + //At this stage, Action was taken, so we need to update the base spell information with the information from the new knack. + if( !(noseqactive || seqended || (spellseq.SeqStep == 0 && !other))) { + line( "Previous Sequence reset. Sequence restarted and Knack added for : " + spellseq.spelldata.Name +"", + "Knacks were selected while another spell was in sequence. Sequence restarted with current spell"); + initSpell(); //Action was taken initialize sequence + } + let prenac = Earthdawn.buildPre( "SP", ssa[ 3 ] ), + nacnm = Earthdawn.getAttrBN( this.charID, prenac + "Name", ""); + line( "Knack/BSecret added: " + nacnm + "" ); + if(spellseq.KnacksChosenName) + spellseq.KnacksChosenName.push( nacnm ); + else + spellseq.KnacksChosenName = [ nacnm ]; + + if(spellseq.KnacksChosenID) + spellseq.KnacksChosenID.push( ssa[ 3 ] ); + else + spellseq.KnacksChosenID = [ ssa[ 3 ]]; + + // Update the spelldata. This will override anything in the origional spell with matching entreis from the knack. + // Note that this does not update strain. + let castSave = spellseq.spelldata.Casting; + for( let key in fieldarray ) { + let x = Earthdawn.getAttrBN( po.charID, prenac + key, ""); + if( x !== "" ) + spellseq.spelldata[ key ] = x; + } + if(spellseq.bGrim && !spellseq.bGrimAtt) { + line( "No Attunement Raw Magic" ); + spellseq.Type = "GR"; + } + SaveSeq(); + if( castSave == spellseq.spelldata.Casting ) + line( buttonOnDemand( "extra" ) + buttonOnDemand( "weave" ) + buttonOnDemand( "cast" )); + else { + line( buttonOnDemand( "extra" ) + buttonOnDemand( "weave" )); + line( "Spell targeting has changed. Do not use a previous Spellcasting button. " + buttonOnDemand( "cast" )); + } + this.chat( sectSpell.toString(), Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; //End Knacks2 + case "New": { + //Sequence Control. Normal Case is (noseqactive || seqended || (step==0 && !other)), if not go through different cases to issue warnings. + //At this stage, no action was taken, so sequence must not be initialized yet + if( !(noseqactive || seqended || (spellseq.SeqStep==0 && !other))) { + if ( other ) { + line( "Other Sequence Active : " + Earthdawn.makeButton( "Resume-" + spellseqsaved.spelldata.Name + , "!Earthdawn~ charID: " + this.charID + "~ ForEach: inmt~ Spell: " + spellseqsaved.RowId + " : Sequence" + , "Resume Sequence." , "param")); + line( "Choose Extra Thread to start new sequence " + spellseq.spelldata.Name +"" ); + } else if ( spellseq.SeqStep!==0 ) { + line( "Warning! Trying to add threads for: " + Earthdawn.getAttrBN( this.charID, pre + (bGrim ? "Name" : "Contains"), "" ) + + "When sequence was already started. Choosing an Extra Thread will restart the sequence", "Extra Threads Can only be selected when starting a sequence." ); + } + } //else //Normal Case + line( "Choosing extra Threads" ); + //end sequence control + + let opt = _.without( Earthdawn.safeString(spellseq.spelldata.ExtraThreads).split( "," ), ""); + for( let i = 0; i < opt.length; ++i ) + line( Earthdawn.makeButton( opt[ i ], "!Earthdawn~ charID: " + this.charID + "~ Spell: " + ssa[ 1 ] + ": " + "Extra: " + opt[ i ], + "Press this button to add an optional extra thread to this casting." , "action")); + line( buttonOnDemand( "weave" ) ); + this.chat( sectSpell.toString(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; + case "Extra": { + // Sequence Control. Normal Case is (noseqactive || seqended || (step==0&!other)), if not go through different cases to issue warnings. + // At this stage, Action was taken, so we actually + if( !(noseqactive || seqended || (spellseq.SeqStep==0 && !other))) { + line( "Previous Sequence reset. Sequence restarted and extra thread added for : " + spellseq.spelldata.Name +"" + , "Extra Threads were selected while another spell was in sequence. Sequence restarted with current spell" ); + initSpell(); //Action was taken initialize sequence + } //else //Normal Case + // if(noseqactive || seqended) + // initSpell(); //Action was taken initialize sequence + // //end sequence control + spellseq.numExtraThreads=Earthdawn.parseInt2(spellseq.numExtraThreads)+1; + spellseq.TotThreads=Earthdawn.parseInt2(spellseq.TotThreads)+1; + spellseq.RemThreads=Earthdawn.parseInt2(spellseq.RemThreads)+1; + line( "Extra Thread added - Threads: " + spellseq.spelldata.sThreads + " + " + spellseq.numExtraThreads +" extra" ); + + //v3.19 is it Necessary ?? + this.misc[ "esGoal" ] = spellseq.numExtraThreads; + this.misc[ "esStart" ] = spellseq.TotThreads; + if(spellseq.ExtraThreads) spellseq.ExtraThreads.push(ssa[3]); else spellseq.ExtraThreads=[ssa[3]]; + if(spellseq.bGrim && !spellseq.bGrimAtt) { + line( "No Attunement Raw Magic" ); + spellseq.Type = "GR"; + } + SaveSeq(); + line( "Updated " + ssa[ 3 ] ); + line( buttonOnDemand( "weave" )); + this.chat( sectSpell.toString(), Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.noArchive | Earthdawn.whoFrom.character ); + } break; //End Extra + case "Weave": { // " Spell: " + ssa[ 1 ] + ": Weave: " + sendon + ": "+ssa[ ssa.length -1 ] + //Sequence Control. + //At this stage, Action was taken, so we actually reset the sequence + if( other && !seqended ) { //Another spell was in middle of an unfinished sequence + this.misc[ "warnMsg" ]= "Cancelling Sequence for " + spellseqsaved.spelldata.Name + ". Sequence Restarted ."; + //initSpell(); + } else if( spellseq.SeqStep == 2 ) // We are not on a correct step + this.misc[ "warnMsg" ] = "Note: Haven't you already pulled all the threads for this casting?"; + else if( spellseq.SeqStep == 4 ) { // We are not on a correct step + this.misc[ "warnMsg" ]= "Weaving threads for: " + spellseq.spelldata.Name + " but the spell just got cast. Sequence Restarted .";//Reset Sequence instead + initSpell(); + } + // else { //Normal Case + // if( noseqactive || seqended) initSpell(); //Action was taken initialize sequence + // } + //end sequence control + + let disp = Earthdawn.dispToName( spellseq.spelldata.Discipline, "weaving" ), + pre2; + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); + this.strainAdd( pre2 ); + this.Karma( pre2 + "Karma", 0 ); + this.misc[ "rollName" ] = "Weaving"; + this.misc[ "reason" ] = "Weaving " + Earthdawn.getAttrBN( this.charID, pre2 + "Name", "0") + " : " + spellseq.spelldata.Name; + this.misc[ "headcolor" ] = "weave"; + this.bFlags |= Earthdawn.flags.VerboseRoll; + this.Lookup( 1, [ "value", pre2 + "Step" ]); + this.misc[ "targetNum" ] = Earthdawn.getParam( spellseq.spelldata.Numbers, 1, "/" ); + this.misc[ "sayTotalSuccess" ] = true; + + //v3.19 is is tuseful? + this.misc[ "esGoal" ] = spellseq.TotThreads; + this.misc[ "esStart" ] = spellseq.CurThreads; + this.misc[ "woveTip" ] = spellseq.spelldata.sThreads + " base spell threads, plus " + spellseq.numExtraThreads + " Extra threads." ; + if( spellseq.bGrim && !spellseq.bGrimAtt ) { + this.misc[ "endNote" ] = "No Attunement Raw Magic"; + spellseq.Type = "GR"; + } + SaveSeq(); + ssa[ 2 ] = "Weave2"; + this.misc[ "Spell" ] = ssa; + this.rollPre( [ "Roll" ] ); + } break; //End Weave + case "Weave2": { // Roll calls back to here. + let lines = []; + let esDone = this.misc[ "esStart" ] + (( "succBy" in this.misc ) ? 1 : 0 ) + (( "extraSucc" in this.misc ) ? this.misc[ "extraSucc" ] : 0 ); + lines.push( "Wove: " + esDone + " of " + Earthdawn.texttip( this.misc[ "esGoal" ] + " threads.", this.misc[ "woveTip" ] )); + spellseq.CurThreads = esDone; + spellseq.RemThreads=Math.max(0, spellseq.TotThreads-spellseq.CurThreads) ; + spellseq.SeqStep = ( spellseq.RemThreads <=0) ? "2" : "1"; + if ( spellseq.RemThreads <= 0 ) lines.push( buttonOnDemand( "cast" ) ); //If Weaving finished, offer to cast + SaveSeq(); + return lines; + } + case "Cast": { // Spellcasting. (0) Spell, (1) SPM rowID, (2) Cast, (3) Spellcasting (or other) rowID. (4) + //Sequence Control. + //At this stage, Action was taken, so we actually reset the sequence + if( other && !seqended ) { //Another spell was in middle of an unfinished sequence + this.misc[ "warnMsg" ] = "Canceling Sequence for "+ spellseqsaved.spelldata.Name + ". Sequence Restarted ."; + spellseq.SeqStep = 2; + spellseq.CurThreads = 99; //Skipping Weaving altogether + spellseq.RemThreads = 0; + } else if( spellseq.SeqStep == 4 ) { + this.misc[ "warnMsg" ] = "Note: You have already attempted to cast this spell. Resetting the sequence"; + spellseq.SeqStep = 2; + spellseq.CurThreads = 99; //Skipping Weaving altogether + spellseq.RemThreads = 0; + } else if(spellseq.CurThreads < spellseq.TotThreads) // && ( spellseq.SeqStep == 1 || spellseq.SeqStep == 0 ) ?? + this.misc[ "warnMsg" ] = "Note: Have you pulled all the threads you need? Only " + spellseq.CurThreads + " of " + spellseq.TotThreads + " done."; + //Normal Case + // if(noseqactive || seqended) + // initSpell(this); //Action was taken initialize sequence + //end sequence control + + let pre2; + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); + this.strainAdd( pre2 ); // This is the strain for the spellcasting talent + this.strainAdd( presp ); // This is the strain for the base Spell + if( spellseq.KnacksChosenID ) + for( let i = 0; i < spellseq.KnacksChosenID.length; i++) + this.strainAdd( Earthdawn.buildPre( "SP", spellseq.KnacksChosenID[ i ] )); // This is the strain for the Knacks. + this.Karma( pre2 + "Karma", 0 ); + this.misc[ "rollName" ] = "Spell"; + this.misc[ "reason" ] = Earthdawn.getAttrBN( this.charID, pre2 + "Name", "0") + " : " + spellseq.spelldata.Name; + this.misc[ "headcolor" ] = "action"; + this.misc[ "Special" ] = "SPL-Spellcasting"; + this.bFlags |= Earthdawn.flags.NoOppMnvr; + this.Lookup( 1, [ "value", pre2 + "Step" ]); + let tType = spellseq.spelldata.Casting; + if( tType && tType !== "None" ) + if( tType.startsWith( "Ask" )) + this.misc[ "targettype" ] = tType.substring( 0, tType.lastIndexOf( ":" )); + else if( tType.slice( 1, 3) === "D1") // PD1, MD1, and SD1, go to just the first two characters. + this.misc[ "targettype" ] = tType.slice( 0, 2); + else if( tType.startsWith( "Riposte" )) + this.misc[ "targettype" ] = "Riposte"; + else + this.misc[ "targettype" ] = tType; + if( spellseq.bGrim && spellseq.bGrimAtt) + this.misc[ "grimCast" ] = true; // This is a spell being cast from a grimoire. It counts as one extra success. + + let fx = spellseq.spelldata.FX; + if( fx && ( fx.startsWith( "Attempt" ) || fx.startsWith( "Success"))) + this.misc[ "FX" ] = fx; + this.misc[ "displayMsg" ] = spellseq.spelldata.DisplayText||""; + this.misc[ "successMsg" ] = spellseq.spelldata.SuccessText||""; + this.misc[ "failMsg" ] = spellseq.spelldata.FailText||""; + let tt = spellseq.spelldata.sayTotalSuccess; + this.misc[ "sayTotalSuccess" ] = (tt % 2) == 1; + if( tt > 1 ) this.misc[ "SpellBuffSuccess" ] = true; + ssa[ 2 ] = "Cast2"; + this.misc[ "Spell" ] = ssa; + // this.rollPre( [ "Roll" ] ); + this.ForEachHit( [ "Roll" ] ); // This makes a call to Roll and RollFormat. Cast2 below will be called, and then back to RollFormat. + } break; //end Cast + case "Cast2": { // RollFormat calls back to here. We return additional formatted lines. + let lines = [], pre2; + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); + spellseq.SeqStep = ( "failBy" in this.misc ) ? "3" : (nowil ? "5":"4"); //If there is no will effect, we skip the step 4 + let fail = ("failBy" in this.misc) && !("SpellBuffSuccess" in this.misc); // Will allow to skip unnecessary information in the roll template + let es = ( "extraSucc" in this.misc ) ? Earthdawn.parseInt2(this.misc[ "extraSucc" ]) : 0; //Number of extra successes + if( bGrim && bGrimAtt ) + es += 1; // Player Guide : Grimoire casting with own Grimoire automatically adds one success. + let cntdwn = 0; //This is the duration in round for the countdown + if( es > 0 ) + spellseq.numExtraSuccesses=es; + let esbonus = (es>=1) ? [spellseq.spelldata.SuccessLevels] : []; //Extra Success Bonuses + let esdone = (es==0); // This will track if the extra successes were processed (or if it is ever needed) + let etbonus = spellseq.ExtraThreads || []; //Extra Thread Bonus + let allbonus = esbonus.concat( etbonus ); + let bbonus = etbonus; // Will progressively be removed of the + let rank = Earthdawn.getAttrBN( this.charID, pre2 + "Effective-Rank", "0" ); + + //log("allbonus " + JSON.stringify(allbonus)); + + function extra( lbl, txtvar, lookup, def ) { // Write out spell information, possibly modified by extra successes. + 'use strict'; + let incDur = false, // Will record if we are dealing with duration changed to minutes + inc = 0, + saved = "", + t3 = ""; + //let txt = Earthdawn.getAttrBN( po.charID, presp + txtvar, def ).toString(); + let txt = Earthdawn.safeString(spellseq.spelldata[txtvar]).length > 0 ? Earthdawn.safeString(spellseq.spelldata[txtvar]) : def; + for( let i = (es ? 0 : 1); i < allbonus.length; ++i ) { // If we don't have any extra successes, then we don't process the first value. + if( allbonus[ i ].match( lookup ) ) { //allbonus lists all bonus from Extra Success and Extra Threads + if ( i == 0 && es > 0 ) + esdone = true; // We are processing the ES, don't display them at the end + if( allbonus[ i ].match( /Inc.*Dur.*min.*/ig )) { //Duration switched from Round to Min + incDur = true; + bbonus = _.without( bbonus, allbonus[ i ] ); // We found a line, we remove it + continue; + } + let i1 = allbonus[ i ].indexOf( "(" ); // This looks for what is between brackets + if( i1 != -1 ) { + let i2 = allbonus[ i ].indexOf( ")", i1 +1 ); + if( i2 == -1) + i2 = allbonus[i].length; + let i3 = i1+1; + let number = new RegExp( /[\s\d+-]/ ); + while ( number.test(allbonus[i].charAt( i3 )) ) + ++i3; + inc += Earthdawn.parseInt2(allbonus[ i ].slice( i1+1, i3)) * (( i == 0 && es > 0 ) ? es : 1 ); // We get the increment, and for the extra success, we multiply by the number of es + saved = allbonus[ i ].slice( i3,i2 ); + } + t3 += " " + (( i == 0 && es > 1 ) ? "+ " + es.toString() + " x " : "+ " ) + allbonus[i]; + bbonus = _.without( bbonus, allbonus[ i ] ); + } } + if( txt.length > 0 || t3.length > 1) { + txt = txt.replace( /R[an]{0,2}k/gi, "Rk( " + rank + " )"); //Rank, rank, Rnk, Rk rnk ... + t3 = t3.replace( /R[an]{0,2}k/gi, "Rk( " + rank + " )"); + if( incDur ) { + txt = txt.replace( /R[oun]{0,3}d/gi, "Min" ); //Round, Rd, Rnd, ... + t3 = t3.replace( /R[oun]{0,3}d/gi , "Min" ) + " Inc Dur (min)"; + } + lines.push( "" + lbl + " " + (inc ? Earthdawn.texttip( txt + " + " + inc.toString() + (saved ? " " + saved : ""), t3) : txt + " " + t3)); + } + //Duration calculation + if( lbl == "Duration" && txt.match(/R[oun]{0,3}d/gi)) { + let txta = txt.split( "+" ); + for(let i=0; i < txta.length; i++) + cntdwn += Earthdawn.parseInt2( txta[i].replace( /[^\d]/gi, "")); //Purge any non number + cntdwn += Earthdawn.parseInt2( inc ); + } + return inc; + } // End function extra() + if( fail ) + lines.push("End Sequence " + buttonOnDemand( "reset" )); //If failed, we don't display all the details of the results + else { // is successful + extra( "Range", "Range", /R[a]{0,1}ng/gi, "" ); //Range, Rng + let x3 = spellseq.spelldata.AoE; + if( x3 && x3 != "x" && x3 != " " ) + extra( "AoE", "AoE", /Area/gi, " " ); //Area + let nmbrs = spellseq.spelldata.Numbers; + if( spellseq.spelldata.Discipline === "Illusionism" ) + lines.push( "Dispel / Sense Diff: " + Earthdawn.texttip( Earthdawn.getParam( nmbrs, 3, "/" ), "Target number to Dispel this spell.") + " / " + + Earthdawn.texttip( Earthdawn.getParam( nmbrs, 4, "/" ), "Target number to Sense an Illusion spell.")); + else + lines.push( "Dispel Diff: " + Earthdawn.texttip( Earthdawn.getParam( nmbrs, 3, "/" ), "Target number to Dispel this spell.")); + extra( "Duration", "Duration", /Dur/gi, "" ); + let x4 = spellseq.spelldata.WilSelect; + if( x4 && x4 !== "None") { + spellseq.ESEffect = extra( "Effect :" + x4 + " +", "WilEffect", /Ef[fe]{0,2}ct.*Step/gi, "0" ); + let x2 = spellseq.spelldata.EffectArmor; + if( x2 && x2 != "N/A" ) + lines[ lines.length - 1 ] += " " + x2; + lines.push( "Effect :" + buttonOnDemand( "effect", true )); + } + extra( "Effect" , "Effect", /Ef[fe]{0,2}ct.*Other.*/gi, "" ); + + if( !esdone ) + lines.push( "" + "Extra Success" + " " + esbonus.toString() + (( es >= 1) ? " x " + es : "")); //ES was not processed, display at the end + if( bbonus.length > 0 ) + lines.push( "" + "Extra Threads" + " " + bbonus.toString()); //Some ET not processed, display at the end + if( spellseq.KnacksChosenName ) + for(let i=0 ; i < spellseq.KnacksChosenName.length ; i++ ) + lines.push( " Spell Knack : " + Earthdawn.texttip(spellseq.KnacksChosenName[ i ], Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre("SP",spellseq.KnacksChosenID[ i ]) + "Notes", ""))); + lines.push( "Description: "+ Earthdawn.texttip("(Hover)",Earthdawn.safeString(spellseq.spelldata.Notes) .replace( /\n/g, Earthdawn.constantIcon( "cr" )))); + if( !x4 || x4 == "None") + lines.push("No Will Effect - End Sequence :" + buttonOnDemand( "reset" )); //If no Will Effect, Sequence is finished + //This sends multi-round spells to the Turn Tracker + if( cntdwn >= 2 ) { + let tt = Campaign().get( 'turnorder' ); + let tracker = (tt == "") ? [] : JSON.parse( tt ); + let name = "CntDwn: " + spellseq.spelldata.Name; + let custom = {id: "-1" , pr: "-" + cntdwn ,custom: name, formula: "1" }; + tracker.push( custom ); + Campaign().set('turnorder', JSON.stringify( tracker )); + } + } //End if successful + if(spellseq.bGrim && !spellseq.bGrimAtt) { //Raw Casting only + lines.push(" Raw Magic " + buttonOnDemand( "warp" ) + buttonOnDemand( "mark" )); + spellseq.Type="GR"; + } + SaveSeq(); + //log(JSON.stringify(lines)); + return lines; + } //End Cast2 + case "Effect": { // Spell Effect test. // (0) Spell, (1) SPM RowID, (2) Effect, (3) T (or other) RowID, (4), SP RowID + let skipseq = false; + if( other ) { + this.misc[ "warnMsg" ] = "Rolling Effect for: " + spellseq.spelldata.Name + " but the spell in Sequence is: " + + spellseqsaved.spelldata.Name + ". Sequence will not be reset (in case it is a multiround effect)"; + skipseq = true; + } else if( spellseq.SeqStep == "5" ) + this.misc[ "warnMsg" ] = "Warning: An Effect Test has already been rolled."; + else if( spellseq.SeqStep != "4" ) + this.misc[ "warnMsg" ] = "Warning: The last thing you did was not to successfully cast the spell."; + + let pre2, wilattr; + if( ssa[ 3 ].startsWith( "repeating_" )) // we have a pre + pre2 = ssa[ 3 ]; + else if( ssa[ 3 ] == "Wil" ) { + wilattr = true; + pre2 = "Wil-"; + } else // we have a rid + pre2 = Earthdawn.buildPre( "T", ssa[ 3 ]); +// see if it works without the line below this. +// if( Earthdawn.repeatSection( pre, 3 ) === "T" ) { + this.strainAdd( pre2 ); + this.Karma( pre2 + "Karma", 0 ); + + this.misc[ "headcolor" ] = "effect"; + this.misc[ "rollName" ] = "Spell Effect"; + this.misc[ "reason" ] = spellseq.spelldata.Name + " Effect : "; + if( wilselect == "Circle" ) + this.misc[ "reason" ] += isng ? (disp + " Circle") : "Circle/SR"; + else if ( wilattr ) + this.misc[ "reason" ] += "Will Effect"; + else + this.misc[ "reason" ] +=(Earthdawn.getAttrBN( this.charID, pre2 + "Name", "0")) + (( wilselect == "Rank" ) ? " Rank" : ""); + let fx = spellseq.spelldata.FX; + if( fx && fx.startsWith( "Effect" )) + this.misc[ "FX" ] = fx; + this.Parse( "armortype: " + spellseq.spelldata.EffectArmor ); + this.bFlags |= Earthdawn.flags.WillEffect; + + let aname; + if( !isng && wilselect == "Circle" ) aname = "SrRating"; + else if( wilattr ) aname = "Wil"; + else if( wilselect == "Circle" ) aname = pre2 + "Circle"; + else if( wilselect == "Rank" ) aname = pre2 + "Effective-Rank"; + else aname = pre2 + "Step"; + + let t2 = Earthdawn.parseInt2( Earthdawn.getAttrBN( this.charID, aname, 5)); + this.Lookup( 1, [ "value", spellseq.spelldata.WilEffect, t2.toString(), spellseq.ESEffect||0 ] ); + if( !skipseq ) { + spellseq.SeqStep = 5; + SaveSeq(); + this.misc[ "endNote" ] = "Apply Dmg and then " + buttonOnDemand( "reset" ); + } + this.ForEachHit( [ "Roll" ] ); + } break; // end effect + case "WarpTest": { + let circle = Earthdawn.parseInt2( spellseq.spelldata.Circle ); + let region = ssa[ 3 ]; + let step, dmg; + switch( region ){ + case "Open": step = circle + 5; dmg = circle + 8; break; + case "Tainted": step = circle +10; dmg = circle +12; break; + case "Corrupt": step = circle +15; dmg = circle +16; break; + default: step = circle; dmg = circle + 4; //Region Type Safe + } + this.misc[ "rollName" ] = "Warp"; + this.misc[ "reason" ] = "Warp Test " + region + " Astral Space"; + this.misc[ "headcolor" ] = "action"; + this.bFlags |= Earthdawn.flags.VerboseRoll; + this.misc[ "step" ] = step; + this.misc[ "targetChar" ] = this.charID; + this.misc[ "targetName" ] = Earthdawn.getAttrBN( this.charID, "character_name", "0"); + this.misc[ "targettype" ] = "MD-Nat"; + this.misc[ "targetNum" ] = Earthdawn.getAttrBN( this.charID, "MD-Nat", "0"); + this.misc[ "sayTotalSuccess" ] = true; + this.misc[ "endNoteFail" ] = "No Warping Damage"; + this.misc[ "endNoteSucc" ] = Earthdawn.makeButton( "Warp Damage", + "!Earthdawn~ charID:"+ this.charID + "~ armortype: MA-Nat~ Reason:Warp Damage ~ foreach~ Roll : " + dmg + ": ?{Modification|0}" , + // "!Earthdawn~ charID: " + this.charID + "~ Damage : MA-Nat : " + dmg, + "Warping Damage done to the character" , "damage"); + ssa[ 2 ] = "WarpTest2"; + this.misc[ "Spell" ] = ssa; + this.rollPre( [ "Roll"] ); + } break; //End WarpTest + case "WarpTest2": { // Roll calls back to here. + let lines = []; + return lines; + } //End WarpTest2 + case "HorrorMark": { + let circle = Earthdawn.parseInt2( spellseq.spelldata.Circle ); + let region = ssa[ 3 ]; + let step; + switch( region ) { + case "Open": step = circle + 2; break; + case "Tainted": step = circle + 5; break; + case "Corrupt": step = circle +10; break; + default : this.chat(" Region is Safe, No Corruption!" ); return; //Region Type Safe + } + this.misc[ "RollType" ] = "w gm"; + this.misc[ "rollName" ] = "Horror Mark"; + this.misc[ "reason" ] = "Horror Mark Test " + region + " Astral Space"; + this.misc[ "headcolor" ] = "action"; + this.bFlags |= Earthdawn.flags.VerboseRoll; + this.misc[ "step" ] = step; + this.misc[ "targetChar" ] = this.charID; + this.misc[ "targetName" ] = Earthdawn.getAttrBN( this.charID, "character_name", "0"); + this.misc[ "targettype" ] = "MD-Nat"; + this.misc[ "targetNum" ] = Earthdawn.getAttrBN( this.charID, "MD-Nat", "0"); + this.misc[ "sayTotalSuccess" ] = true; + this.misc[ "endNoteFail" ] = "Nothing Happens"; + this.misc[ "endNoteSucc" ] = "A Horror Marked the character"; + ssa[ 2 ] = "HorrorMark2"; + this.misc[ "Spell" ] = ssa; + this.rollPre( [ "Roll"] ); + } break; //End HorrorMark + case "HorrorMark2": { // Roll calls back to here. + let lines = []; + return lines; + } //End WarpTest2 + case "Reset": { + this.setWW( "SS-Active", "0"); + //this.TokenSet( "clear", "SustainedSequence", "" ); + this.setWW( "SS-spellseq", "{}"); +// this.TokenSet( "clear", "spellseq", "{}" ); + this.chat("Spell Sequence Reset. Start a new one", Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + } break; // End Reset + } // end main switch. + } catch(err) { Earthdawn.errorLog( "ED.Spell() error caught: " + err, po ); } + } // End ParseObj.Spell( ssa ) + + + + + // ParseObj.strainCalc() + // Note that this is meant to be used in conjunction with strainAdd and calculates the strain commands that have been saved. + // strainSave is a $ delimited list that contains one of the following: + // Code, rowID Lookup the strain variables (below) and calculate the strain. + // repeating_(section)_(rowID)_(code)_ Use the code and rowID to do as above. + // A label, a strain value. Add the strain to the total. + /* + T_Strain is still the input for the numeric part + T_StrainAdvanced is a drop-down that right now is either "" (fixed strain) or "Ask". This will become much more complex in the future, especially for 1879. + T_StrainAdvanced_max is any numeric part from the drop down list. + + Example, 4 + 2 per success will have "Strain": 4, "StrainAdvanced": "per Success", "StrainAdvanced_max": 2. + Example Strains from ED and 1879: (ones marked with * are currently supported. All others are not yet supported but will be in the future. + *Strain: 6 + Strain: 1 + 1 per additional target. + Strain: 2 + target count + Strain: 4 + TMD + Strain: 4 + 1 per Spellcasting Test success + Strain: 4 + 2 per success + Strain: 5 + Rank + Strain: 9 + 1 per target + Strain: 1 + Casting Difficulty + Strain: 5 + Force Rating of spirit + Number of threads (this would be ask). + Note: Deeper secets uses format "Strain: 1+ (see text)" with in the text: Each additional target costs an additional 1 Strain. + Note: This routine will get called once per target, but that is OK as only the last one counts. + */ + this.strainCalc = function() { + 'use strict'; + try { + let po = this, str = 0, tt = "", retSpecial = [], pre, save = ("strainSave" in this.misc) ? this.misc[ "strainSave" ] : ""; + let m = save.split( "$" ); // Major part is a $ delimited list. Split into seperate entries. + + for( let i = 0; i < m.length; ++i ) { + let n = m[ i ].split( "," ); + if( Earthdawn.safeString( n[ 0 ] ).trim().startsWith( "repeating_" )) // we have a pre + pre = Earthdawn.safeString( n[ 0 ] ).trim(); + else if( Earthdawn.codeToName( n[ 0 ], true ) && n.length > 1 ) // we have a code, and a rid + pre = Earthdawn.buildPre( n[ 0 ], n[ 1 ]); + else + pre = undefined; + + if( pre ) { + let s = Earthdawn.getAttrBN( this.charID, pre + "Strain", 0, true ), + as = Earthdawn.getAttrBN( this.charID, pre + "StrainAdvanced", "", 2 ).trim(), + asm = Earthdawn.getAttrBN( this.charID, pre + "StrainAdvanced_max", 1, true ), + name = Earthdawn.getAttrBN( this.charID, pre + "Name", "" ); + if( s != 0 ) { + str += s; + tt += s; + } + + // generic error message formating + function err( tag, tip, logmsg ) { + 'use strict'; + if( tag ) + retSpecial.push( tag ); + if( tip ) + tt += Earthdawn.constantIcon( "warning" ) + "(" + tip + "). "; + if( logmsg !== null ) // if we explicitly pass a null, then don't do an errorlog. Otherwise, use tip plus anthing that might be in logmsg (if undefined then just log tip). + Earthdawn.errorLog( "ED.strainCalc() " + ( tip ? tip : "") + (logmsg ? " " + logmsg + " " : ""), po ); + } + + // most of these do the same basic thing with different things found in this.misc. + // if special 0x01 then DO NOT log an error message if the parameter is not found. + // 0x02 means value is passed, not a this.misc. + function strainStandard( att, special ) { + 'use strict'; + let tmp = (special & 0x02) ? att : ((("misc" in po) & (att in po.misc)) ? po.misc[ att ] : 0); // Usually value is in this.misc, but sometimes it is just passed. + if ( tmp !== undefined ) { + let x = tmp * asm; + if( x != 0 ) { + str += x; + tt += "+" + asm + as + "(" + tmp + ")=" + x + (name ? " (" + name + ")" : ""); + } + } else if ( !(special & 0x01 )) + err( "Error", "Could not find " + as, " : " + att + " : " + save ); + } // end strainStandard processing + switch( as ) { + case "Fixed": case "": + if( s != 0 ) + tt += " Fixed" + (name ? " (" + name + ")" : ""); + break; + case "per additional Target": // used where any book says "per target, per additional target, or target count". + strainStandard( this.targetIDs.length -1, 0x02 ); + break; + case "per Target": // used where any book says "per target, per additional target, or target count". + strainStandard( this.targetIDs.length, 0x02 ); + break; + case "per Success": // per success on the test being rolled. + strainStandard( "extraSucc", 0x01); + if( "misc" in this && ( !("succBy" in this.misc) && !("failBy" in this.misc))) + err( "Error", "Number of success' undetermined", null ); + break; + case "x Rank": // Rank of talent used. + strainStandard( "effRank" ); + break; + case "x Step": // Effective step of the talent being used. + strainStandard( "finalStep" ); + break; + case "x TMD": // Targets Mystic Defense is the same as the casting difficulty. + if(( "targettype" in po.misc ) && ( po.misc[ "targettype" ].indexOf( "MD") != -1 )) + {} else + err( "Error", "targettype is not MD", ("targettype" in po.misc) ? ". targettype is " + po.misc[ "targettype" ] : "" ); // note, falls through to x Casting Difficulty on purpose. + case "x Casting Difficulty": + if(( "targetNum" in this.misc ) && this.misc[ "targetNum" ] != undefined ) + strainStandard( this.misc[ "targetNum" ], 0x02 ); + else + err( "Error", "targetNum is undefined", null ); + break; + case "x Force Rating": // Force rating of the spirit being dealt with. + strainStandard( "highSR" ); + if( !("highSR" in this.misc)) + err( "Error", "Spirt or Challenge Rating undefined", null ); + break; + case "x Number of Threads": // Number of threads woven to the spell. +// CDD do we want to test if this is really part of a spellcasting sequence? + let x = Earthdawn.getAttrBN( this.charID, "SS-CurThreads", "0/0" ); + strainStandard( Math.max( Earthdawn.getParam( x, 1, "/" ), Earthdawn.getParam( x, 2, "/" )), 0x02); + break; + case "Ask": // unused. + case "Variable": + err( as, "uncalculated value " + (name ? " (" + name + ")" : "") + " Click this to change strain to the correct value.", null ); + break; + default: + Earthdawn.errorLog( "ED.strainCalc() error unknown StrainAdvanced entry: " + as, this ); + err( "Error", "Unknown strain code '" + as + "'", this.misc[ "strainSave" ] ); + } + } else { // otherwise it is a text description and strain. + let x = Earthdawn.parseInt2( n[ 1 ] ); + if( x != 0 ) { + str += x; + tt += Earthdawn.safeString( n[ 0 ] ).trim() + ": " + Earthdawn.safeString( n[ 1 ] ).trim(); + } } + let x2 = Earthdawn.getAttrBN( this.charID, pre + "Strain_max", "" ); // strain_max can contain a special strain explanation, usually used with Variable. + if( x2 && isNaN( x2 )) // Due to histtorical reasons, there might be a simple number in Strain_max, in which case ignore it. + tt += x2; + tt += ", "; + } // end outer loop. +// cdd check for battle rites and modify. if so, modify retSpecial + return { strain: str, tooltip: tt.replace( /^[\s|\,]+/g, "").replace( /\,\s*\,+/g, ",").replace( /[\s|\,]+$/g, ''), special: retSpecial }; // trim off trailing spaces and the trailing comma. + } catch(err) { Earthdawn.errorLog( "ED.strainCalc() error caught: " + err, this ); } + } // End ParseObj.strainCalc + + + + // strainAdd: save a source of strain to be calculated later. + // arguments will ether be a valid prefix, or it will be a text, label. + // In ether case just save them to be evaluated later. + // Format of this.misc[ "strainSave" ] is what1, rid1 $ what2, rid2 etc. IE: major separator $, minor comma. + this.strainAdd = function() { + 'use strict'; + let t = ""; + if( arguments.length > 0 ) t = arguments[ 0 ]; + for( let i = 1; i < arguments.length; ++i ) + t += ", " + arguments[ i ]; + this.misc[ "strainSave" ] = (( "strainSave" in this.misc) ? this.misc[ "strainSave" ] + "$ " : "") + t; + } // End ParseObj.strainAdd + + + + // ParseObj.TargetCalc() + // get or calculate the target number for an upcoming roll. + this.TargetCalc = function( targetID, flags ) { + 'use strict'; + let ret, val = 0, sr, po = this; + try { + let TokObj = getObj("graphic", targetID.trim() ); + if (typeof TokObj != 'undefined' ) { + let cID = TokObj.get("represents"); // cID is TARGET character id. + + function getDefs( what, what2 ) { + val = po.getValue( ((flags & Earthdawn.flagsTarget.Natural) ? what + "-Nat" : what), cID); + // If the target is NOT blindsided (in which case we already have the blindsided value), + // but the current sheet IS blindsiding, need to figure out the targets blindsided value. + if ((Earthdawn.getAttrBN( cID, "condition-Blindsided", "0") != "1" ) && (Earthdawn.getAttrBN( po.charID, "condition-Blindsiding", "0") == "1" )) { + val -= 2; // basic blindsided penalty to target number. +// cdd Aug 2024, do not automatically lose defensive stance. +// if (Earthdawn.getAttrBN( cID, "combatOption-DefensiveStance", "0", true) == "1" ) +// val -= Earthdawn.getAttrBN( cID, "Misc-DefStance-Bonus", 3); // remove the Defensive stance bonus target had when blindsided. + if ( !(flags & Earthdawn.flagsTarget.Natural) // remove any bonus attached to shield target had when blindsided. + && (state.Earthdawn.g1879 || (state.Earthdawn.gED && state.Earthdawn.edition == 4)) + && Earthdawn.getAttrBN( cID, "condition-NoShield", "0") != "1" ) + val -= Earthdawn.parseInt2( Earthdawn.getAttrBN( cID, "Shield-" + what2, 0)) + Earthdawn.parseInt2( Earthdawn.getAttrBN( cID, what + "-ShieldBuff", 0)) + } } + + if( cID ) { + let tsr = Earthdawn.getAttrBN( cID, "SrRating", "-9" ); // strainCalc wants to know about any spirit ratings the target might have. + if( tsr > 0 ) + sr = tsr; + if( flags & Earthdawn.flagsTarget.PD ) + getDefs( "PD", "Phys" ); + else if( flags & Earthdawn.flagsTarget.MD ) + getDefs( "MD", "Myst" ); + else if( flags & Earthdawn.flagsTarget.SD ) { + val = this.getValue( "SD", cID); + let x = this.getValue( "Creature-Willful", cID); + if( x ) + this.misc[ "Willful" ] = x; + } + if( flags & Earthdawn.flagsTarget.P1pt) + val += this.targetIDs.length - 1; + if( this.charID && Earthdawn.getAttrBN( this.charID, "condition-TargetPartialCover", "0") == "1" && Earthdawn.getAttrBN( cID, "condition-Cover", "0") == "0") + val += 2; // Get bonus for target token being marked as having Cover, or attacking token being marked as target having Cover, not both. + val += Earthdawn.getAttrBN( this.charID, "Adjust-TN-Total", "0", true ); // If this character has a condition which causes it's target numbers to be adjusted. + ret = { val: val, name: TokObj.get( "name"), spiritRating: sr}; + } + } // end TokenObj defined + } catch(err) { Earthdawn.errorLog( "ED.TargetCalc() error caught: " + err, po ); } + return ret; + } // End ParseObj.TargetCalc() + + + + // Note: This is starting to look like spaghetti code. Here is a key. + // + // A command of the form + // !Earthdawn~ TargetSet : -KLOmKZ3zS2jc8XDKu3r + // will cause a note to be attached to a token (TokenSet( "TargetList")) that all actions should use this target. + // + // When a command comes in of the form + // !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: PD~ foreach~ Action: T: -KEW6yCLJjtZiAxEfE4n: 0 + // If a target has been set for the token, it will use that target. + // Note that Target just sets bFlags, and ForEachTarget reads and sets targetID. + // Otherwise it will generate a button which will generate a command of the form + // !Earthdawn~ TargetSet : -KLOmKTy5HOb6qHm-SXK~ TargetType: PD~ TokenList: -KMgp4hXFfF86s6ZcJJe~ Action: T: -KEW6yCLJjtZiAxEfE4n: 0 + // + // Also: + // The above procedure allows user to specify the number of targets at runtime. However it makes it a two button process: + // First: press the "use talent" token action, then press the "one target" button, and then finally press the target. + // If the talent ALWAYS has one target, then a step can be saved by specifying it in target type. Then when + // the user presses the "use talent" token action, it immediately asks what the (singular) target is) + // Unfortunately, some weird stuff has to go on behind the scenes to allow this to work. + // When we get a command of the form + // !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: PD1: (targetID)~ foreach~ Action: T: -KEW6yCLJjtZiAxEfE4n: 0 + // (Note that instead of PD, we got PD1, and we also got a target ID) + // then instead of processing as above, we use the information here to make two other commands and insert them into the + // command queue at appropriate places. These new commands will be processed as normal. The revised command would look like this. + // !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: PD1: (targetID)~ TargetType: PD~ TargetSet: (targetID)~ Action: T: -KEW6yCLJjtZiAxEfE4n: 0 + // Note also that when this happens, due to a design flaw in Roll20, we probably will not know which token actually was the current token. + +// Normal action command. +// !edToken~ !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: Ask: 11~ foreach~ Action: T: 1: 0 +// Normal PD command. Part 1. +// !edToken~ !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: PD~ foreach~ Action: T: -KEW6yCLJjtZiAxEfE4n: 0 +// Normal PD command. Part 2. +// ["!Earthdawn","TargetType: PD","TokenList: -KMgp4hXFfF86s6ZcJJe","TargetSet : -KLOmKTy5HOb6qHm-SXK","Action: T: -KEW6yCLJjtZiAxEfE4n: 0"] + +// Set target command. Part 1 +// !Earthdawn~ charID: -JvAdIYpXgVyt2yd07hv~ Target: Set +// Set target command. Part 2, after resort. +// ["!Earthdawn","TargetType: Set","TokenList: -KMgp4hXFfF86s6ZcJJe","TargetSet : -KLOmKTy5HOb6qHm-SXK"] + + + // ParseObj.TargetT() + // get or calculate the target number for an upcoming roll. + // This is the first threads routine. It will generate a chat command which will call TokenList() + // ssa: + // None + // Ask: (number) // Note: Ask is optional. If ssa[1] is a number it will just use that. + // PD: (or MD or SD) + // Ask: PD (or MD or SD): +/-(number) // This is not a combo of the other two. PD is ACTING tokens PD, not a TARGET token. if a normal non-zero integer is entered after it, it replaces and overwrites the PD based target number, the modifier number only modifies the target number if it starts with a plus or a minus sign. + // Note: if this turns out to be a problem later, can easily change this to something other than "Ask". + this.TargetT = function( ssa ) { + 'use strict'; + try { + let flg, oneTarg, tType; + if( ssa.length > 1) { + if( !isNaN( ssa[ 1 ])) + this.misc[ "targetNum" ] = this.ssaMods( ssa ); + else { + tType = Earthdawn.safeString( ssa[ 1 ] ); + switch( tType.slice(0,2).toLowerCase() ) { + case "no": // none + break; + case "as": // ask + this.misc[ "targetNum" ] = this.ssaMods( ssa, 2, 1); + this.bFlags |= Earthdawn.flagsTarget.Ask; + break; + case "ri": // Riposte + this.bFlags |= this.TargetTypeToFlags( ssa[ 3 ] ) | Earthdawn.flagsTarget.Riposte; + this.misc[ "targetNum" ] = this.ssaMods( ssa, 3, 1); + let tar2 = this.TargetCalc( ssa[ 2 ], this.bFlags ); + this.misc[ "targetNum2" ] = tar2[ "val" ]; // The 2nd target number, of the PD of the counterattack target. + break; + case "pd": + case "md": + case "sd": + flg = 1; + if( tType.length === 3 && tType.endsWith( "1" ) && ssa.length > 2 && ssa[ 2 ].length > 0 ) + oneTarg = true; + break; + case "se": // Set + flg = 2; + break; + default: + this.chat( "Error! ED.TargetT() unknown target type '"+ tType +"'. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + } } } + if( oneTarg === true ) { // we are not going to process this command as is. Instead we are going to make new commands and insert them into the command line. + let i; + // See Notes at the top of the routine. + this.edClass.msgArray.splice( this.indexMsg + 1, 0, "TargetType: " + ssa[ 1 ].slice(0, -1) ); // TargetType is what was passed to us, with the trailing "1" stripped off. + let r = this.indexMsg + 3; // default is three steps from current. + if( ssa.length > 3 && ssa[ 3 ] ) { + ++r; + this.edClass.msgArray.splice( this.indexMsg + 3, 0, "Tokenlist: " + ssa[ 3 ] ); + } + for( i = this.indexMsg + 2; i < this.edClass.msgArray.length; ++i ) + if( Earthdawn.safeString( this.edClass.msgArray[ i ] ).slice(0,10).toLowerCase().indexOf( "foreach" ) != -1 ) { // If there was a foreach, replace it. + this.edClass.msgArray.splice( i, 1, "TargetSet: " + ssa[ 2 ] ); // Target ID was also passed. + i = 99; + } + if( i < 99 ) + this.edClass.msgArray.splice( r, 0, "Tokenlist: " + ssa[ 3 ], "TargetSet: " + ssa[ 2 ] ); // Target ID was also passed. + } else if( flg !== undefined ) { + let s = this.ForEachToken( ["ForEach", "Status", "TargetList"]); + if( s === undefined) + this.bFlags |= this.TargetTypeToFlags( tType ); + else { + if( s && flg === 1 ) // There are already targets assigned to at least one of these tokens. + this.bFlags |= this.TargetTypeToFlags( tType ); + else { + let v, + t = ""; + if( this.tokenIDs.length === 0 ) + v = Earthdawn.colonFix( "!Earthdawn~ TargetType: " + tType + "~ charID: " + this.charID ); // There are no selected tokens, just pass the charID. + else + v = Earthdawn.colonFix( "!Earthdawn~ TargetType: " + tType + "~ TokenList: " + this.tokenIDs.join( ":" ) ); + while ( ++this.indexMsg < this.edClass.msgArray.length ) // Note that this will cause this thread to end with this routine. + if( !( Earthdawn.safeString( this.edClass.msgArray[ this.indexMsg ]).trim().toLowerCase().startsWith( "foreach" ))) + t += Earthdawn.colonFix( "~ " + this.edClass.msgArray[ this.indexMsg ].trim() ); + s = "&{template:default} {{name=How many targets? "; + let a = [ "", "First", "Second", "Third", "Forth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth", "Tenth" ]; + for( let j = 1; j < 11; ++j ) { + s += "[" + j.toString() + "](" + v + "~ TargetSet"; + for( let k = 1; k <= j; ++k ) + s += Earthdawn.constantAlt( "ColonAlt" ) + Earthdawn.constantButton( "at" ) + Earthdawn.constantButton( "braceOpen" ) + "target" + + Earthdawn.constantButton( "pipe" ) + a[k] + " Target" + Earthdawn.constantButton( "pipe" ) + "token_id" + Earthdawn.constantButton( "braceClose" ); +// s += Earthdawn.colonFix( ": " + Earthdawn.constantButton( "at" ) + "{target|" + a[k] + " Target|token_id}"); + s += t + ")"; + } + s += "}}"; + this.chat( s, Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ); + } + this.misc[ "targetNum" ] += this.ssaMods( ssa, 2) || 0; + } } + } catch(err) { Earthdawn.errorLog( "ED.TargetT() error caught: " + err, this ); } + return; + } // End ParseObj.TargetT() + + + + // ParseObj.TargetTypeToFlags() + // + // Passed a Target Type (PD, PDh, PDHp1p, PD-each, PD-Nat, etc) + // Return the bFlags values corresponding to this target type. + this.TargetTypeToFlags = function( tType ) { + 'use strict'; + let ret = 0; + try { + let tmp = Earthdawn.safeString( tType ).trim().toLowerCase(); + switch ( tmp.slice( 0, 2)) { + case "no": break; // None + case "se": ret |= Earthdawn.flagsTarget.Set; break; + case "pd": ret |= Earthdawn.flagsTarget.PD; break; + case "md": ret |= Earthdawn.flagsTarget.MD; break; + case "sd": ret |= Earthdawn.flagsTarget.SD; break; + default: this.chat( "Failed to parse TargetType: '" + tmp + "' in msg: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + } + if( ret & Earthdawn.flagsTarget.Mask ) { // Do this only if found a valid string above. + if( tmp.slice( 2, 3) === "h" ) + ret |= Earthdawn.flagsTarget.Highest; + if( tmp.endsWith( "each")) + ret |= Earthdawn.flagsTarget.Each; + if( tmp.endsWith( "p1p")) + ret |= Earthdawn.flagsTarget.P1pt; + if( tmp.indexOf( "-nat" ) > -1) + ret |= Earthdawn.flagsTarget.Natural; + } + } catch(err) { Earthdawn.errorLog( "ED.TargetTypeToFlags() error caught: " + err, this ); } + return ret; + } // End ParseObj.TargetTypeToFlags() + + + + // ParseObj.TokenActionToggle () + // + // We have a token action that can be ether set to turn on, turn off, or not appear at all. Set the correct token action name. + // lu: karma or SP-Willforce-Use (lowercased) + // show: true or false to show button + this.TokenActionToggle = function( lu, show ) { + 'use strict'; + try { + let name; + let actn = "!edToken~ !Earthdawn~ ForEach~ marker: " + lu + ":t"; + if (lu === "willforce") + name = Earthdawn.constantIcon( "karma" ) + "WilFrc-T" + if( name != undefined ) { + if( !show ) + Earthdawn.abilityRemove( this.charID, name ); + if( show ) + Earthdawn.abilityAdd( this.charID, name, actn) + } + } catch(err) { Earthdawn.errorLog( "ParseObj.TokenActionToggle() error caught: " + err, this ); } + } // End ParseObj.TokenActionToggle() + + + + // TokenFind() + // For some reason (almost certainly because there was a @{target} in the command macro - which caused the system to stupidly clear all selected tokens) + // we don't have tokenInfo for a routine that needs it. + // See if we can figure it out. + // 1) If there is only one token for the charID on the current page, use that token. + this.TokenFind = function() { + 'use strict'; + try { + if( this.charID === undefined ) { + this.chat( "Error! TokenFind() when don't have a charID.", Earthdawn.whoFrom.apiError ); + return; + } + if( this.edClass.msg === undefined ) + return; + let tl = Earthdawn.safeArray( this.ForEachToken( [ "ForEach", "list", "c", "ust" ] )); + if( tl.length === 1 ) { + this.tokenInfo = tl[ 0 ]; + return true; + } else + return false; + } catch(err) { Earthdawn.errorLog( "ParseObj.TokenFind() error caught: " + err, this ); } + return; + } // End ParseObj.TokenFind() + + + + // TokenSet() + // store a value among a tokens statusmarkers. + // Stores it as a 'statusmarker' whose name is key:secondary:tertiary. + // what: clear: clear all entries with this key. Afterwards add key:secondary:tertiary if they were passed. + // replace: remove all key/secondary combinations. Replace with new values if passed. + // add: clear nothing - add this key/secondary/tertiary combo. + // Usage: + // key is a keyword that indicates the category. Such as "Hits". + // secondary is a value associated with the key, such as the token ID that was hit. + // tertiary is a value (or sequence of values separated by anything but commas) that is stored and accessed via the key/secondary combo. + // Note that if you don't clear old entries out, you can have multiple entries with the same keys. + // Note that comma's and colons are used in these structures. if secondary or tertiary contain any commas or colons, they are turned into a less common symbol and TokenGet restores them. + this.TokenSet = function( what, key, secondary, tertiary) { + 'use strict'; +//log( "TokenSet " + what + " " + key + " " + secondary + " " + tertiary); +//log( this.tokenInfo); + try { + if( what === undefined ) { + this.chat( "Error! TokenSet() 'what' undefined. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + if( key === undefined ) { + this.chat( "Error! TokenSet() 'key' undefined. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + let ch = getObj( "character", this.charID) + if( !ch ) { + log( "Error! TokenSet() for charID " + this.charID + " the character for this token is no longer valid." ); + return; + } + if( this.tokenInfo === undefined || this.tokenInfo.tokenObj === undefined ) + this.TokenFind(); + + let bToken = (this.tokenInfo != undefined && this.tokenInfo.tokenObj != undefined); + if( secondary !== undefined ) + secondary = secondary.replace( /,/g, Earthdawn.constantAlt( "CommaAlt" )).replace( /:/g, Earthdawn.constantAlt( "ColonAlt" )); + if( tertiary !== undefined ) + tertiary = tertiary.replace( /,/g, Earthdawn.constantAlt( "CommaAlt" ) ).replace( /:/g, Earthdawn.constantAlt( "ColonAlt" )); + key += ":"; // Key should end in colon to make it more readable when debugging. + let changed = false, + changedMark = false; + let clr; + switch ( Earthdawn.safeString( what ).toLowerCase().trim()) { + case "clear": clr = -1; break; // This means clear all old secondaries out, before adding new value if it exists. + case "add": clr = 0; break; + case "remove": + case "replace": clr = 1; break; + default: + this.chat( "Error! TokenSet() unknown 'what' value of " + what +". Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + return; + } + let markers = "," + ( bToken ? this.tokenInfo.tokenObj.get( "statusmarkers" ) : ""); + if (markers.length > 3) + markers += ","; + + let att = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "pseudoToken" }, ""); + let pt = "," + att.get( "current" ); + if( pt.length > 3 ) + pt += ","; // It is easier to assume that every marker has a comma before it and after it. + + // First, remove any status markers that may already be set that start with this key (unless this is add only operation). + function remove( marks ) { + let k = "," + ((clr < 0 || secondary === undefined) ? key : key + secondary ); + let i = marks.indexOf( k ); + while ( clr !== 0 && i > -1 ) { + let e = marks.indexOf( ",", i + 1); + marks = marks.slice( 0, i) + marks.slice( e); + changed = true; + i = marks.indexOf( k ); + } + return marks; + } // end function remove + + markers = remove( markers); + changedMark = changed; changed = false; + pt = remove( pt); + + if( secondary !== undefined ) { // Now set any new secondary that was passed. + if( bToken ) { + markers += key + secondary + ((tertiary !== undefined) ? ":" + tertiary : "" ) + ","; + changedMark = true; + } else { + pt += key + secondary + ((tertiary !== undefined) ? ":" + tertiary : "" ) + ","; + changed = true; + } + } + if( changedMark && bToken ) + this.tokenInfo.tokenObj.set( "statusmarkers", markers.slice(1, -1)); + if( changed ) + Earthdawn.setWithWorker( att, "current", pt.slice(1, -1)); + } catch(err) { Earthdawn.errorLog( "ParseObj.TokenSet() error caught: " + err, this ); } + return; + } // End ParseObj.TokenSet() + + + + // TokenGet() + // return all values stored among a tokens statusmarkers for a specific key. + // if retString is true, then return first thing found as a string. Otherwise return an array of every key found. + // Also return anything stored in character attribute "pseudoToken", where we might have stashed something when we could not figure out what token is being used. + this.TokenGet = function( key, retString ) { + 'use strict'; + let ret = []; + try { + if( this.tokenInfo === undefined || this.tokenInfo.tokenObj === undefined ) + this.TokenFind(); + key += ":"; // Key should end in colon to make it more readable when debugging. + + let markers = "," + ( (this.tokenInfo && this.tokenInfo.tokenObj) ? this.tokenInfo.tokenObj.get( "statusmarkers" ) : ""); + if (markers.length > 3) + markers += ","; + let att = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "pseudoToken" }, ""), + pt = att.get( "current" ); + + if( pt.length > 2 ) + markers = "," + pt + markers; // It is easier to assume that every marker has a comma before it and after it. + + let i = markers.indexOf( "," + key ); + while ( i > -1 ) { + let e = markers.indexOf( ",", i + 1); + ret.push( markers.slice( i + key.length + 1, e).replace(new RegExp( Earthdawn.constantAlt( "ColonAlt" ), "g"), ":").replace(new RegExp( Earthdawn.constantAlt( "CommaAlt" ), "g"), ",") ); + i = markers.indexOf( "," + key, e-1 ); + } + } catch(err) { Earthdawn.errorLog( "ParseObj.TokenGet() error caught: " + err, this ); } + return retString ? (ret.length == 0 ? "" : ret[ 0 ]) : ret; + } // End ParseObj.TokenGet() + + + + // return all values stored among a tokens statusmarkers for a specific key. + // This version is passed a tokenID instead of already having the information in tokenObj. + this.TokenGetWithID = function( key, tokenID ) { + 'use strict'; + let ret = []; + try { + let TokObj = getObj("graphic", tokenID); + if (typeof TokObj === 'undefined' ) + return; + key += ":"; // Key should end in colon to make it more readable when debugging. + let markers = "," + TokObj.get( "statusmarkers" ) + ","; // It is easier to assume that every marker has a comma before it and after it. + let i = markers.indexOf( "," + key ); + while ( i > -1 ) { + let e = markers.indexOf( ",", i + 1); + ret.push( markers.slice( i + key.length + 1, e)); + i = markers.indexOf( "," + key, e-1 ); + } + } catch(err) { Earthdawn.errorLog( "ParseObj.TokenGetWithID() error caught: " + err, this ); } + return ret; + } // End ParseObj.TokenGetWithID() + + + + // ParseObj.TuneMatrix() + // Move a spell into a matrix. + // This routine is called twice. The character sheet calls this with ssa[1] = "Spell". + // This will generate a chat window button that when pressed calls this routine with ssa[1] = "Matrix" + // ssa = Spell, spell row ID. + // ssa = Matrix, spell row ID, Matrix row ID. + // ssa = share or pseudo, spell row ID, (Std, Enh, Arm, Sha) + // ssa = destroy" + this.TuneMatrix = function( ssa ) { + 'use strict'; + let po = this; + try { + let pseudoType,pnm,prid; + + function WipeMatrix() { + let attributes = findObjs({ _type: "attribute", _characterid: po.charID }), + attflt= attributes.filter( function (att) { return att.get( "name" ).endsWith( "_SPM_Contains" )}); + _.each( attflt, function (att) { + if( att.get( "name" ).endsWith( "_SPM_Contains" )) { + Earthdawn.abilityRemove( po.charID, Earthdawn.constantIcon( "Spell" ) + att.get( "current" )); + let pre3=Earthdawn.buildPre( att.get("name")); + if(Earthdawn.getAttrBN( po.charID, pre3 + "Origin" , "") == "Pseudo") { + let attflt2= attributes.filter( function (att) { return att.get( "name" ).startsWith( pre3 )}); + _.each( attflt2, function (att2) {att2.remove();}); + } else { + po.setWW(pre3 + "Contains", "Empty"); + po.setWW(pre3 + "Threads", "x"); + po.setWW(pre3 + "EnhThread", "x"); + po.setWW(pre3 + "Notes", ""); + po.setWW(pre3 + "spRowID", ""); + po.setWW(pre3 + "ChainCast", "0"); + } + } + }); //End each Attribute + + attflt= attributes.filter( function (att) { return att.get( "name" ).endsWith( "_SP_Name" )}); + _.each( attflt, function (att) { po.setWW( Earthdawn.buildPre( att.get( "name" )) + "spmRowID", "0" ); }); //End each Attribute + + po.setWW( "SS-Active", "0"); + po.setWW( "SS-spellseq", "{}"); + //this.TokenSet( "clear", "spellseq", "{}" ); + po.chat( "All Matrix have been wiped, and Spell Sequence reset" , Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + } // End WipeMatrix + + + switch ( Earthdawn.safeString( ssa[ 1 ] ).toLowerCase() ) { + case "spell": { // The user has chosen "Attune" from the button in the spell list. Send a message to the chat window asking what matrix to put this spell into. + if( ssa === undefined || ssa.length < 3 || ssa[2] === "") { + this.chat("ED.TuneMatrix() error - There was an error, Could not read RowID for the spell. Go to the spell you tried to tune, and change the name, then change it back. That should force the system to save the RowID.", Earthdawn.whoFrom.apiError ); + } else { // go through all attributes for this character and look for ones that end in "_SPM_RowID". + let circle = Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SP", ssa[ 2 ] ) + "Circle", "0", true ), + attributes = findObjs({ _type: "attribute", _characterid: this.charID }), + matrices = []; + _.each( attributes, function ( indexAttributes ) { + if( indexAttributes.get( "name" ).endsWith( "_SPM_RowID" )) { + let row = indexAttributes.get( "current" ); + if ( ( Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SPM", row ) + "Rank", 0, true) >= circle || Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SPM", row ) + "Origin", "") == "Pseudo" ) && !matrices.includes( row )) // Only list the matrices of a rank ot hold this spell and are not already listed. Pseudo are now Rank 0, but listed anyway + matrices.push( row ); + } + }); // End for each attribute. + if( matrices.length > 0) { // For each matrix found, make a chat button. + let t2; + let s = Earthdawn.makeButton( "Pseudo matrix", "!Earthdawn~ charID: " + po.charID + "~ TuneMatrix: Share: " + + "?{What type of Pseudo Matrix|Standard,Std|Enhanced,Enh|Armored,Arm|Shared,Sha}: " + ssa[ 2 ], "Create a Pseudo Matrix & Attune", "param" ); + + _.each( matrices, function (indexMatrix) { + let prespm = Earthdawn.buildPre( "SPM", indexMatrix ); + switch( Earthdawn.getAttrBN( po.charID, prespm + "Type", "-10") ) { + case "15": t2 = "Enh"; break; + case "25": t2 = "Armd"; break; + case "-20": t2 = "Shrd"; break; + case "-10": + default: t2 = "Std"; + } + let pseu = ( Earthdawn.getAttrBN( po.charID, prespm + "Origin", "") == "Pseudo" ) ? "p" : ""; + s += Earthdawn.makeButton( pseu + t2 + (pseu ? "" : "-" + Earthdawn.getAttrBN( po.charID, prespm + "Rank", "0")) + " " + + Earthdawn.getAttrBN( po.charID, prespm + "Contains", ""), + "!Earthdawn~ charID: " + po.charID + "~ TuneMatrix: Matrix: " + ssa[2] + ": " + indexMatrix, + undefined , "param" ); + if( pseu ) + s += " " + Earthdawn.makeButton( Earthdawn.constantIcon( "pointLeft" ) + "X", "!Earthdawn~ charID: " + po.charID + "~ TuneMatrix: Destroy: " + indexMatrix + + ": ?{Confirm Destroy this pseudo matrix|No|Yes}", + "The preceding is a pseudo-matrix and only exists when two spells can share a matrix, or in a shared matrix, or in a Spellstore. " + + "Use this button to delete the Pseudo matrix.", "param2" ); + }); /// End each spell matrix + this.chat( "Load " + Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SP", ssa[2] ) + "Name", "") + + " into which Matrix? " + s.trim(), Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive); // Chat buttons don't like colons. Change them to something else. They will be changed back later. +// this.chat( "&{template:default} {{name=Load " + Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SP", ssa[2] ) + "Name", "") +// + " into which Matrix? " + Earthdawn.colonFix( s ) + "}}", Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive); // Chat buttons don't like colons. Change them to something else. They will be changed back later. + } else + this.chat("ED.TuneMatrix() Warning - You don't have any matrices of high enough rank to attune this spell."); + } + } break; + case "pseudo": // TuneMatrix: share: (Std, Enh, Arm, Sha): (spellFrom ID) + case "share": // Create a matrix and move a spell into it. + if( ssa[ ssa.length -2 ] === "Enh" ) pseudoType = 2; // v2.04 changed the number of parameters. + else if( ssa[ ssa.length -2 ] === "Arm" ) pseudoType = 3; + else if( ssa[ ssa.length -2 ] === "Sha" ) pseudoType = 4; + else pseudoType = 1; + ssa[ ssa.length ] = Earthdawn.generateRowID(); + // Note: this does NOT break; it falls down. + case "matrix": { // The user has told us to swap a spell into a certain matrix. + // TuneMatrix: matrix: (spellFrom ID): (spellTo ID) + let spellFrom = ssa[ ssa.length -2 ], // rowID of repeating_spell + matrixTo = ssa[ ssa.length -1 ], // rowID of repeating_matrix + preFrom = Earthdawn.buildPre( "SP", spellFrom ), + preTo = Earthdawn.buildPre( "SPM", matrixTo ), + preFromOld= Earthdawn.buildPre( "SP", Earthdawn.getAttrBN( this.charID, preTo + "spRowID", "") ), //Prefix of the Spell that was in the Matrix before + t = "", + lnks = Earthdawn.getAttrBN( this.charID, preFrom + "LinksProvideValueList", "").split( "," ), //Variants of the spell that is currently Attuning in order to create the pseudo + lnksold = Earthdawn.getAttrBN( this.charID, preFromOld + "LinksProvideValueList", "").split( "," ), //Variants of the spell that was previously Attuned in order to destroy the pseudos + mattype = Earthdawn.getAttrBN( this.charID, preTo + "Type", "-10"); + + function toMatrix( base, val ) { + 'use strict'; + po.setWW( preTo + base, (val === undefined || val === null) ? "" : val ); + // don't just test val ? val : "" because that changes zero's to "", which is bad. + } // End ToMatrix() + // This local function looks up the correct value (if it can find it) and copies it to the matrix + function copySpell( base, dflt ) { + 'use strict'; + let val = Earthdawn.getAttrBN( po.charID, preFrom + base, dflt); + if( base === "AoE" && (val === "x" || val === " ")) + val = dflt; + toMatrix( base, (val === undefined || val === null) ? dflt : val ); + } // End CopySpell() + + if ( pseudoType ) { + toMatrix( "RowID", ssa[ ssa.length -1 ] ); + toMatrix( "Origin", "Pseudo" ); + //toMatrix( "Rank", 15 ); No Rank for Pseudo Matrix + switch( pseudoType ) { + case 1: + toMatrix( "DR", "10"); + toMatrix( "Type", "-10"); + break; + case 2: + toMatrix( "DR", "15"); + toMatrix( "Type", "15"); + break; + case 3: + toMatrix( "DR", "25"); + toMatrix( "Type", "25"); + break; + case 4: + toMatrix( "DR", "20"); + toMatrix( "Type", "-20"); + break; + } } else{ //For non-Pseudo Matrix, we have to take care of the pseudos of the linked spells + _.each(lnksold, function(sp) { //Destroy all the pseudo Matrices + let x=sp.replace("SP;",""), + spm= Earthdawn.getAttrBN(po.charID,Earthdawn.buildPre("SP",x)+"spmRowID",""), + spmt=spm.length>0 ? Earthdawn.getAttrBN(po.charID,Earthdawn.buildPre("SPM",spm)+"Origin",""): ""; + if(x && x.length>0 && spmt=="Pseudo") + po.TuneMatrix(["TuneMatrix","Destroy",spm,"Yes"]); + }); + _.each(lnks, function(sp) { + let x=sp.replace("SP;",""), + y; + switch(mattype){ + case "-20": y="Sha";break; + case "25" : y="Arm";break; + case "15" : y="Enh";break; + case "-10": + default: y="Std"; + } + if(x && x.length>0 && Earthdawn.getAttrBN(po.charID,Earthdawn.buildPre("SP",x)+"Type","Spell")=="Spell") + po.TuneMatrix(["TuneMatrix","Pseudo",y,x]) + }); + } + + t = Earthdawn.dispToName(Earthdawn.getAttrBN( po.charID, preFrom + "Discipline", "0"), "short") + "-" + + Earthdawn.getAttrBN( po.charID, preFrom + "Circle", "0") + " - " + + Earthdawn.getAttrBN( po.charID, preFrom + "Name", ""); + // + Earthdawn.getAttrBN( po.charID, preFrom + "Circle", "0") + " '" + // + Earthdawn.getAttrBN( po.charID, preFrom + "Name", "") + "'"; + + + let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: po.charID, name: preTo + "Contains" }); + Earthdawn.abilityRemove( this.charID, Earthdawn.constantIcon( "Spell" ) + aobj.get( "current" ) ); + Earthdawn.setWithWorker( aobj, "current", t ); + if( Earthdawn.getAttrBN( po.charID, preFrom + "CombatSlot", 1, true)) + Earthdawn.abilityAdd( this.charID, Earthdawn.constantIcon( "Spell" ) + t, "!edToken~ %{selected|" + preTo + "Roll}" ); + + let mThreads = (Earthdawn.getAttrBN( po.charID, preTo + "Type", 0) > 0) ? 1 : 0, // Uses SPM_Type to calculate how many threads the matrix can hold. + sThreads = Earthdawn.parseInt2(Earthdawn.getAttrBN( po.charID, preFrom + "sThreads", "0" )); + toMatrix( "spRowID", Earthdawn.getAttrBN( this.charID, preFrom + "RowID" )); + toMatrix( "Threads", Math.max( sThreads - mThreads, 0)); + copySpell( "CombatSlot", "1"); +// let aobj = Earthdawn.findOrMakeObj({ _type: "attribute", _characterid: po.charID, name: preFrom + "spmRowID" }); +// Earthdawn.setWithWorker( aobj, "current", matrixTo ); + toMatrix( "EnhThread", "x"); + toMatrix( "ChainCast", "0"); + + let txt = t + " loaded into Matrix."; + + if( mThreads > 0 && sThreads < 1 ) { + txt += "
    What Extra Thread do you wish to tie into the matrix? "; + let opt = Earthdawn.getAttrBN( this.charID, preFrom + "ExtraThreads", "").split( "," ); + for( let i = 0; i < opt.length; ++i ) + txt += " " + Earthdawn.makeButton( opt[ i ], "!Earthdawn~ charID: " + this.charID + "~ TuneMatrix: thread: " + matrixTo + ":" + opt[ i ], + "Press this button to tie this extra thread option into the matrix.", "param" ); + } + this.chat( txt, Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive); + } // This falls through on purpose. The below is done after every attune. + case "inmatrix": { // This alternate entry point is for when it detects a matrix has been destroyed. + let spellsInMatrix = [], // Get a list of every matrix that has a spell, and of every spell. + spells = [], + matrixes = [], + attributes = findObjs({ _type: "attribute", _characterid: po.charID }); + _.each( attributes, function (att) { + let nm = att.get("name"); + if (nm.endsWith( "_SP_RowID" )) + spells.push( att.get( "current" )); + else if (nm.endsWith( "_SPM_spRowID" )) { + spellsInMatrix.push( att.get( "current" )); + matrixes.push( Earthdawn.repeatSection( 2, att.get( "name" ))); + } + }); // End for each attribute. + _.each( spells, function (rID ) { // Set InMatrix depending upon if spellsInMatrix includes the spell. + let ind = spellsInMatrix.indexOf( rID ); + Earthdawn.setWW( Earthdawn.buildPre( "SP", rID ) + "spmRowID", ((ind === -1) ? "0" : matrixes[ ind ]), po.charID ); + }); + } break; + case "thread": { // Set this extra thread option to be the enhanced thread. + this.setWW( Earthdawn.buildPre( "SPM", ssa[ 2 ] ) + "EnhThread", ssa[ 3 ] ); + this.chat( "Updated to " + ssa[ 3 ], Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive); + } break; + case "destroy": { // Destroy a pseudo matrix. format Destroy: RowID + if( Earthdawn.safeString( ssa[ 3 ] ).toUpperCase().startsWith( "Y" ) ) { + let pre = Earthdawn.buildPre( "SPM", ssa[ 2 ] ), + attributes = findObjs({ _type: "attribute", _characterid: this.charID }); + if(Earthdawn.getAttrBN( this.charID, pre + "spmRowID", "").length>0) + this.setWW( pre + "spmRowID",""); + _.each( attributes, function (att) { + if( att.get( "name" ).startsWith( pre ) ) + att.remove(); + }); // End for each attribute. + } + } break; + case "presetnew": //Create a New Preset Line and Save + pnm = "Preset"; + prid = Earthdawn.generateRowID(); + //Will flow in case Save + case "save": { //Saves a Preset of Spell Matrices + let pst=[], + pstm=[], + attributes = findObjs({ _type: "attribute", _characterid: this.charID }), + attflt = attributes.filter( function (att) { return att.get( "name" ).endsWith( "_SPM_RowID" )}), + attarray = ["CombatSlot","RowID","spRowID","EnhThread","Contains","Threads","ChainCast"], + attarray2 = ["Type","Origin","Rank","DR"]; + + if( !prid ) prid = ssa[ 2 ]; + if( !pnm ) pnm = Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SPP", ssa[ 2 ] ) + "Name", ""); + let prespp = Earthdawn.buildPre( "SPP" , prid ); + + + _.each( attflt, function (att) { + let pre=Earthdawn.buildPre( att.get( "name" ) ), + js={}, + t=""; + _.each( attarray, function (obj) { if(Earthdawn.getAttrBN( po.charID, pre + obj, "").length>0) js[obj]=Earthdawn.getAttrBN( po.charID, pre + obj, ""); }); + if(Earthdawn.getAttrBN( po.charID, pre + "Origin", "")=="Pseudo"){ + _.each( attarray2, function (obj) { if(Earthdawn.getAttrBN( po.charID, pre + obj, "").length>0) js[obj]=Earthdawn.getAttrBN( po.charID, pre + obj, ""); }); + t+="Pseudo:"; + } + pstm.push(js); + switch(Earthdawn.getAttrBN( po.charID, pre + "Type", "")){case "15" :t+="Enh:";break; case "25" : t+="Arm:";break; case "-20": t+="Sha:"; break; default:t+="Std:";} + t+=Earthdawn.getAttrBN( po.charID, pre + "Contains", ""); + t+=Earthdawn.getAttrBN( po.charID, pre + "EnhThread", "x").length>1 ? ":"+Earthdawn.getAttrBN( po.charID, pre + "EnhThread", ""):""; + pst.push(t); + }); // End for each attribute. + this.setWW( prespp + "RowID", prid); + this.setWW( prespp + "Name", pnm); + this.setWW( prespp + "Preset", pst.join( "\n" ), undefined, JSON.stringify( pstm )); // current, default undefined, max + this.chat( "Preset " + pnm + " Saved" , Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + break; + } //End Save + case "load":{ //Loads a Preset of Spell Matrices + let prespp = Earthdawn.buildPre("SPP", ssa[ 2 ] ), + js=JSON.parse(Earthdawn.getAttrBN( po.charID, prespp + "Preset_max","[]")), + ok=true; + WipeMatrix(); + setTimeout(function(){ + _.each( js, function (att) { //Each item saved in the Preset_max + let pseudo = (att.Origin && att.Origin=="Pseudo"), //pseudo matrix will create a new one + rid = pseudo ? Earthdawn.generateRowID() : att.RowID, + prespm = Earthdawn.buildPre( "SPM" , rid); + + if(!pseudo && rid !== Earthdawn.getAttrBN( po.charID, prespm + "RowID","")) { + ok=false; + log("Earthdawn.TuneMatrix() failed to load preset for Matrix "+ att.Contains) + } else if( !att.Contains || !(att.Contains.includes(Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SP" , att.spRowID ) + "Name","")))) { + ok=false; + log("Earthdawn.TuneMatrix() Failed to load preset, spell doesn't exist "+ att.Contains) + } else { + po.setWW(Earthdawn.buildPre( "SP", att.spRowID ) + "spmRowID" , rid) + for(let key in att) + po.setWW( prespm + key , att[ key ] ); + } + if( Earthdawn.getAttrBN( po.charID, Earthdawn.buildPre( "SP", att.spRowID ) + "CombatSlot", 1, true)) + Earthdawn.abilityAdd( po.charID, Earthdawn.constantIcon( "Spell" ) + att.Contains, "!edToken~ %{selected|" + prespm + "Roll}" ); + + + }); // End for each attribute. + if(ok) + po.chat( "Preset " + Earthdawn.getAttrBN( po.charID, prespp + "Name", "") + " Successfully Loaded" , Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + else + po.chat( "Preset " + Earthdawn.getAttrBN( po.charID, prespp + "Name", "") + " Loaded with errors, recommended to save the preset back" , Earthdawn.whoTo.player | Earthdawn.whoTo.gm | Earthdawn.whoFrom.character); + },500); + + break; + } //End Load + case "wipematrix":{ //Wipes all Matrices, Resets the Sequence and deletes Pseudo Matrices + WipeMatrix(); + break; + } //end wipematrix + default: + Earthdawn.errorLog( "ED.TuneMatrix() error - badly formed command.", po ); + log( ssa ); + } // end switch + } catch(err) { Earthdawn.errorLog( "ED.TuneMatrix() error caught: " + err, po ); } + } // End ParseObj.TuneMatrix() + + + + // ParseObj.UpdateDates () + // Paste dates into the current character sheet. + // Real date is real date. Throalic date is whatever is in Party Sheet. + // Note - For this to work there must be a PARTY sheet - and the GM must keep the throalic date current. This just copies whatever he last set to this sheet. + this.UpdateDates = function( ssa ) { + 'use strict'; + try { + if( this.charID === undefined ) { + this.chat( "Error! Trying updateDates() when don't have a CharID.", Earthdawn.whoFrom.apiError ); + return; + } + if( ssa[ 1 ].indexOf( "Party" ) === -1 ) { + this.setWW( "record-date-throalic", ssa[ 1 ] ); + } + let date = new Date(); + let ds = date.getFullYear() + "-" + (date.getMonth() +1) + "-" + date.getDate(); + this.setWW( "record-date-real", ds ); + } catch(err) { Earthdawn.errorLog( "ED.UpdateDates() error caught: " + err, this ); } + } // End ParseObj.UpdateDates() + + + + // ParseObj.WhoSendTo () + // Look up how this character is to report activity (public, gm, or hidden) + this.WhoSendTo = function() { + 'use strict'; + let ret = 0; // Default is public. + try { + if( this.charID === undefined ) + this.chat( "Error! charID undefined in WhoSendTo() command. Msg is: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError); + else { + let rt; + if( state.Earthdawn.Rolltype.Override === false ) { // no override, check default and exceptions. + if ( this.tokenInfo && ("tokenObj" in this.tokenInfo) && this.tokenInfo.tokenObj.get( "layer" ) === "gmlayer" ) { // If the token is on the GM layer, it is always gm only! + rt = "w gm"; + this.misc[ "whoReason" ] = "Token is on the GM layer."; + } else { // Token is not on gm layer. Check for exceptions, and then default. + let bpc = (Earthdawn.getAttrBN( this.charID, "NPC", "1") == Earthdawn.charType.pc); + let except = bpc ? state.Earthdawn.Rolltype.PC.Exceptions : state.Earthdawn.Rolltype.NPC.Exceptions; + let rn; // first, lets see if we can find a rolltype or a reason to check. + if( "skillClass" in this.misc ) { // check skill class first, but if there is a named exception, this class exception will be overriden. + let rnc = Earthdawn.matchString( this.misc[ "skillClass" ] ); // striped and lowercased + if( rnc in except ) { + rt = except[ rnc ][ "display" ]; + this.misc[ "skillExceptionIs" ] = rnc; + this.misc[ "whoReason" ] = "an exception for " + this.misc[ "skillClass" ] + " skills."; + } else + this.misc[ "skillExceptionWouldBe" ] = this.misc[ "skillClass" ]; + } + if( "rollName" in this.misc ) + rn = this.misc[ "rollName" ]; + else if( "reason" in this.misc ) // Note that right now we are testing all reasons. We might have to narrow it down. + rn = this.misc[ "reason" ]; + if( rn ) { + let rnc = Earthdawn.matchString( rn ); // striped and lowercased + if( rnc in except ) { + rt = except[ rnc ][ "display" ]; + this.misc[ "exceptionIs" ] = rnc; + this.misc[ "whoReason" ] = "an exception for this name."; + } else // no exception, use the default. + this.misc[ "exceptionWouldBe" ] = rn; + } + if( rt === undefined ) { + rt = bpc ? state.Earthdawn.Rolltype.PC.Default : state.Earthdawn.Rolltype.NPC.Default + this.misc[ "whoReason" ] = "Default for " + (bpc ? "PCs." : "NPCs."); + } + } // end exceptions or default. + } else if( state.Earthdawn.Rolltype.Override === "Sheet" ) { // use the old system where things are controled on a per talent basis by the player. + if( "RollType" in this.misc ) // Option was "Ask" and we got an rolltype that way. + rt = this.misc[ "RollType"]; // ?{Who should be able to see the results|Public, |Player & GM,pgm|GM Only,/w gm} + else if ( this.tokenInfo && ("tokenObj" in this.tokenInfo) && this.tokenInfo.tokenObj.get( "layer" ) === "gmlayer" ) // If the token is on the GM layer, it is always gm only! + rt = "w gm"; + else if( "rollWhoSee" in this.misc ) // This will be something like (xxx)_T_Rolltype or RollType-Dex + rt = Earthdawn.getAttrBN( this.charID, this.misc[ "rollWhoSee" ], "@{RollType}" ); + + if( rt === "default" || rt === "@{RollType}" ) + rt = undefined; + if( !rt ) + rt = Earthdawn.getAttrBN( this.charID, "RollType", "" ); // This is the default for the whole sheet. + this.misc[ "whoReason" ] = "sheet settings."; + } else { // An override is in effect. + rt = state.Earthdawn.Rolltype.Override; + this.misc[ "whoReason" ] = "an active Override."; + } + + if ( rt != undefined ) { + let r = rt.trim().toLowerCase(); + if(( r === "player and gm") || r.endsWith( "pgm")) + ret = Earthdawn.whoTo.gm | Earthdawn.whoTo.player; + else if(( r === "gm only" ) || r.endsWith( "w gm" )) + ret = Earthdawn.whoTo.gm; + else if(( r === "controlling only") || r.endsWith("plr" )) + ret = Earthdawn.whoTo.player; + } } + } catch(err) { Earthdawn.errorLog( "ED.WhoSendTo() error caught: " + err, this ); } + return ret; + } // End ParseObj.WhoSendTo() + + + + // ParseObj.Parse - A message segment that needs to be parsed. It could do any of several functions. + // The basic form is that a parse message is tilde delimited (~). + // Many message segments have subsegments that are colon (:) delimited. + // + // This routine parses a message segment and it's subsegments. + this.Parse = function( cmdSegment ) { + 'use strict'; + let falloutParse = false; + try { + let subsegmentArray = cmdSegment.split( ":" ); // Split out any subsegments into an array by colon : delimiter. + for( let i = 0; i < subsegmentArray.length; ++i ) + subsegmentArray[ i ] = subsegmentArray[ i ].trim(); + let loopvar; // This is just a utility variable is mostly used for setting things depending upon which specific command is being called. + +//log( cmdSegment); + switch ( Earthdawn.safeString( subsegmentArray[ 0 ] ).toLowerCase() ) { + case "!edinit": // We need to skip to the old parsing method. + this.edClass.msgArray.splice( 0, this.indexMsg); + this.edClass.msgArray[0] = this.edClass.msgArray[0].trim(); + this.edClass.Initiative(); + falloutParse = true; + break; + case "!earthdawn": // Just skip this. There will be an extra one of these on Token Actions. + case "!edcustom": + case "": // Also just skip these. The RollType and Karma routines sometimes put extra fields that can be skipped. + case "@{RollType}": + case "/w gm": + case "pgm": + case "plr": + case "-1": + case "0": + case "1": + case "2": + case "3": + break; + case "!edsdr": + case "!edsdrgm": + case "!edsdrhidden": + if( subsegmentArray[ 0 ].charAt( 0 ) === "!" ) { + this.edClass.msgArray.splice( 0, this.indexMsg ); + this.edClass.msgArray[ 0 ] = this.edClass.msgArray[ 0 ].trim(); + if ( this.edClass.msgArray.length > 1 ) + this.edClass.StepDiceRoller(); + falloutParse = true; + } + break; + case "abilityrebuild": + this.abilityRebuild ( subsegmentArray ); + break; + case "action": + this.Action( subsegmentArray ); + break; + case "apiping": + if( Earthdawn.getAttrBN( this.charID, "API", 1 ) == 1) { + this.setWW( "API", 3 ); // We want the on change sheetworkers to trigger, so FIRST change to 3, and then change back to 1, so that there is actually a change. + let po = this; + setTimeout(function() { try { + po.setWW( "API", 1 ); + } catch(err) {Earthdawn.errorLog( "ED.APIping setTimeout() pingpong error caught: " + err, po );} }, 200); + } else // API is not 1, so set it to 1. + this.setWW( "API", 1 ); + break; + case "armortype": + if( subsegmentArray.length > 1 ) { + switch( Earthdawn.safeString( subsegmentArray[ 1 ] ).toLowerCase()) { + case "n/a": this.bFlags |= Earthdawn.flagsArmor.na; break; // Not Applicable. + case "pa": this.bFlags |= Earthdawn.flagsArmor.PA; break; + case "ma": this.bFlags |= Earthdawn.flagsArmor.MA; break; + case "pa-nat": this.bFlags |= Earthdawn.flagsArmor.PA | Earthdawn.flagsArmor.Natural; break; + case "ma-nat": this.bFlags |= Earthdawn.flagsArmor.MA | Earthdawn.flagsArmor.Natural; break; + case "na": // No Armor. + case "noarmor": + case "none": this.bFlags |= Earthdawn.flagsArmor.None; break; + case "unknown": + case "unk": this.bFlags |= Earthdawn.flagsArmor.Unknown; break; + } + } + break; + case "bonus": // Need to have called SetToken or Foreach before this. + this.Bonus( subsegmentArray ); // There is a bonus die to set. + break; + case "calcstep": + case "calculatestep": + case "calcvalue": + case "calculatevalue": + switch ( Earthdawn.safeString( subsegmentArray[ 1 ] ).toLowerCase() ) { + case "jumpup": + this.Karma( "Dex-Karma" ); + this.misc[ "rollName" ] = "Jumpup"; + this.misc[ "reason" ] = "Jumpup Test"; + this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; + this.misc[ "StyleOverride" ] = Earthdawn.style.Full; + this.misc[ "headcolor" ] = "knockdown" ; + break; + } + falloutParse = this.calculateStep( subsegmentArray ); + break; + case "charid": // "CharID: (xx)": This command came with character ID attached (attached by the macro). Store this character ID. + if( 1 < subsegmentArray.length ) + this.charID = subsegmentArray[ 1 ]; + break; + case "chatmenu": + this.ChatMenu( subsegmentArray ); + break; + case "creaturepower": + case "opponentmaneuver": + this.CreaturePower( subsegmentArray ); + break; + case "display": // This just causes everything after the first colon to display in the chat window. + case "chat": + this.chat( subsegmentArray.slice( 1 ).join( ": " ), Earthdawn.whoFrom.character ); + break; + case "strain": // This falls through into Damage, but with an extra parameter (no armor) inserted between "strain" and the value. + case "strainsilent": + subsegmentArray.splice( 1, 0, "NA" ); + case "damage": // Apply damage to selected tokens. Must be preceded by ForEach or SetToken + case "stun": + case "recovery": + case "woodskin": // Obsolete Nov 2023. + case "notwoodskin": // Obsolete Nov 2023. + this.Damage( subsegmentArray ); + break; + case "debug": + this.Debug( subsegmentArray ); + break; + case "debugecho": + this.chat( "echo got: " + subsegmentArray.toString(), Earthdawn.whoFrom.api | Earthdawn.whoFrom.noArchive ); + break; + case "endnote": + this.misc[ "endNote" ] = subsegmentArray.slice( 1 ).join( ":" ); + break; + case "endnotesucc": + this.misc[ "endNoteSucc" ] = subsegmentArray.slice( 1 ).join( ":" ); + break; + case "endnotefail": + this.misc[ "endNoteFail" ] = subsegmentArray.slice( 1 ).join( ":" ); + break; + case "foreach": // "ForEach : CB1 : Wpn1": For Each selected token, perform the following macros. + case "foreachtoken": + this.ForEachToken( subsegmentArray ); + break; + case "foreachtokenlist": // Note that this is the list of tokens that we are to do the command FOR (not the list of targets). It is not unusual that the list will be one token that we are to do the command for. + case "tokenlist": + case "fet": + case "fetl": + this.ForEachTokenList( subsegmentArray ); + break; + case "fxset": + this.FX( subsegmentArray ); + break; + case "dec": // dec: Wounds or dec: Wounds: 2 + case "decrement": + loopvar = true; + case "inc": + case "increment": + if( subsegmentArray.length < 3 ) subsegmentArray[ 2 ] = 1; // default to incrementing 1. + if( loopvar ) // Decrement + subsegmentArray[ 2 ] = parseInt( subsegmentArray[ 2 ]) * -1; + subsegmentArray.splice( 1, 0, subsegmentArray[ 1 ] ); // We need the name doubled since the first tells it where to place it, and the 2nd what the original value was. + falloutParse = this.Lookup( 4, subsegmentArray ); // 4 sets the character sheet attribute in the first ssa parameter. + break; + case "init": + falloutParse = this.rollPre( subsegmentArray ); + break; + case "karma": + case "kc": + case "dev pnt": + case "dev pnts": + case "dev": + case "dp": + this.Karma( subsegmentArray ); + break; + case "k-ask": + if( subsegmentArray.length > 1 && !isNaN( subsegmentArray[ 1 ] ) && subsegmentArray[ 1 ] != "") + this.misc[ "kask" ] = subsegmentArray[ 1 ]; + if( subsegmentArray.length > 2 && !isNaN( subsegmentArray[ 2 ] ) && subsegmentArray[ 2 ] != "") + this.misc[ "dpask" ] = subsegmentArray[ 2 ]; + break; + case "dp-ask": + if( !isNaN( subsegmentArray[ 1 ] ) && subsegmentArray[ 1 ] != "") + this.misc[ "dpask" ] = subsegmentArray[ 1 ]; + break; + case "buttonlink": + case "linktoken": // Link selected token(s) to CharId (that has been previously parsed) + this.LinkToken( subsegmentArray ); + break; + case "marker": // Set the status marker for selected tokens. + this.MarkerSet( subsegmentArray ); + break; + case "max": // max: variable: constant. // If variable is is greater than constant, set it to equal constant. + case "setmax": { + let m = Earthdawn.parseInt2( Earthdawn.getAttrBN( this.charID, subsegmentArray[ 1 ], "0")); + if( m > Earthdawn.parseInt2( subsegmentArray[ 2 ] )) + this.setWW( subsegmentArray[ 1 ], subsegmentArray[ 2 ] ); + } break; + case "min": + case "setmin": { + let m = Earthdawn.parseInt2( Earthdawn.getAttrBN( this.charID, subsegmentArray[ 1 ], "0")); + if( m < Earthdawn.parseInt2( subsegmentArray[ 2 ] )) + this.setWW( subsegmentArray[ 1 ], subsegmentArray[ 2 ] ); + } break; + case "misc": + this.funcMisc( subsegmentArray ); + break; + case "quick": // This is just a way to quickly and easily insert some commands into the processing, but unlike 'value' it does not attempt to do any lookups. + switch ( Earthdawn.safeString( subsegmentArray[ 1 ] ).toLowerCase()) { + case "fire": // quick: fire: (size): (step) + this.misc[ "step" ] = subsegmentArray[ 3 ]; + this.misc[ "reason" ] = subsegmentArray[ 2 ] + " damage"; + this.misc[ "rollName" ] = "Fire Damage"; + this.misc[ "AfterRoll" ] = "AfterRoll: buttonDamageBoth: PA: Fire damage"; + this.misc[ "headcolor" ] = "damage"; + break; + case "fall": // quick: fall: (size): (step) + this.misc[ "step" ] = subsegmentArray[ 3 ]; + this.misc[ "rollName" ] = "Falling Damage"; + this.misc[ "reason" ] = subsegmentArray[ 2 ] + " Yrd falling damage"; + this.misc[ "AfterRoll" ] = "AfterRoll: buttonDamageBoth: NA: Falling damage"; + this.misc[ "headcolor" ] = "damage"; + break; + } // end quick switch + break; + case "mod": + case "adjust": + case "modadjust": + case "adjustmod": + case "adjustresult": + case "resultadjust": + falloutParse = this.Lookup( 2, subsegmentArray ); // 2 is this.misc.result. + // this.misc[ "result" ] = (this.misc[ "result" ] || 0) + Earthdawn.parseInt2( subsegmentArray[ 1 ] ); // Mod : x - Adds X to any result obtained. + break; + case "reason": + if( subsegmentArray.length > 1 ) { + this.misc[ "reason" ] = cmdSegment.slice( cmdSegment.indexOf( ":" ) + 1).trim(); // Use the raw cmdSegment to allow colons in reason. +// We might want to put this explicitly here. +// this.misc[ "rollName" ] = this.misc[ "reason" ]; + } + break; + case "recalc": // Recalc function is done by sheetworker, but after they do the work, need to reload combat slots. + this.chat( "Note: Sheetworker Recalc is triggered by setting the dropdown TO recalc, not by pressing the button.", Earthdawn.whoTo.player ); + break; +// record and record2 obsolete Oct 23. Remove. + case "record2": + this.Record( subsegmentArray, true ); + break; + case "record": + this.Record( subsegmentArray ); + break; + case "rerollnpcinit": + this.RerollNpcInit(); + break; + // dead code, after I wrote it I decided I did not need it for that purpose (used quick instead). Keep it for now to see if it does come in handy. + case "replacethis": { // We need to make a substitution in a following command. ReplaceThis, string to replace, Where is the Source of the string to replace with, index number within source to replace with. + let source; + if( subsegmentArray[ 2 ] === "AfterRoll" && "AfterRoll" in this.misc ) // source of replacement string + source = this.misc[ "AfterRoll" ]; + else Earthdawn.errorLog( "ED.parse() Badly formed ReplaceThis command: " + cmdSegment, this ); + if( source ) { + let tmp = source.split( ":" ); + if( tmp && tmp.length > subsegmentArray[ 3 ]) { + let t = Earthdawn.safeString( tmp[ Earthdawn.parseInt2( subsegmentArray[ 3 ])]).trim(); + for( let ind = this.indexMsg + 1; ind < this.edClass.msgArray.length; ++ind ) + this.edClass.msgArray[ ind ] = this.edClass.msgArray[ ind ].replace( subsegmentArray[ 1 ], t ); + } else Earthdawn.errorLog( "ED.parse() Could not perform ReplaceThis command: " + cmdSegment, this ); + } + } break; + case "afterroll": // We have some special processing to be done after the roll. Save the command. + this.misc[ "AfterRoll" ] = cmdSegment; + break; + case "roll": + this.ForEachHit( subsegmentArray ); + break; + case "rolltype": + this.misc[ "RollType" ] = subsegmentArray[ 1 ]; + break; + case "set": // Set: ConditionJumpup: "1" // Don't do anything fancy, just set attribute to argument. except weirdness (? can't be in a button). + this.setWW( subsegmentArray[ 1 ], subsegmentArray[ 2 ].replace( "weirdness", "?")); + break; + case "setattrib": + falloutParse = this.Lookup( 4, subsegmentArray ); // 4 sets the character sheet attribute in the first ssa parameter. + break; + case "setresult": + this.misc[ "result" ] = this.ssaMods( subsegmentArray ); + break; + case "setstep": + this.misc[ "step" ] = this.ssaMods( subsegmentArray ); + break; + case "settoken": // We are being passed a token ID. Set it into tokenInfo + this.SetToken( subsegmentArray ); + break; + case "spell": + case "grim": + this.Spell( subsegmentArray ); + break; + case "statustotoken": + this.SetStatusToToken(); + break; + case "step": // Note also that THIS routine (unlike other routines such as ssaMods()) allows use of asynchronous process to interpret a calculated value. + case "attribute": // Note that ssa holds ether a numerical number, or an attribute that needs to be looked up (hopefully giving a numerical step number). + case "value": + switch ( Earthdawn.safeString( subsegmentArray[ 1 ] ).toLowerCase() ) { + case "dex": this.Karma( "Dex-Karma" ); this.misc[ "reason" ] = "Dexterity Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Dex"; this.misc[ "headcolor" ] = "action"; break; + case "str": this.Karma( "Str-Karma" ); this.misc[ "reason" ] = "Strength Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Str"; this.misc[ "headcolor" ] = "action"; break; + case "tou": this.Karma( "Tou-Karma" ); this.misc[ "reason" ] = "Toughness Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Tou"; this.misc[ "headcolor" ] = "action"; break; + case "per": this.Karma( "Per-Karma" ); this.misc[ "reason" ] = "Perception Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Per"; this.misc[ "headcolor" ] = "action"; break; + case "wil": this.Karma( "Wil-Karma" ); this.misc[ "reason" ] = "Willpower Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Wil"; this.misc[ "headcolor" ] = "action"; break; + case "cha": this.Karma( "Cha-Karma" ); this.misc[ "reason" ] = "Charisma Action Test"; this.misc[ "ModType" ] = "@{Adjust-All-Tests-Total}"; this.misc[ "rollWhoSee" ] = "RollType-Cha"; this.misc[ "headcolor" ] = "action"; break; + case "dex-effect": this.Karma( "Dex-Karma" ); this.misc[ "reason" ] = "Dexterity Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Dex"; this.misc[ "headcolor" ] = "effect"; break; + case "str-effect": this.Karma( "Str-Karma" ); this.misc[ "reason" ] = "Strength Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Str"; this.misc[ "headcolor" ] = "effect"; break; + case "tou-effect": this.Karma( "Tou-Karma" ); this.misc[ "reason" ] = "Toughness Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Tou"; this.misc[ "headcolor" ] = "effect"; break; + case "per-effect": this.Karma( "Per-Karma" ); this.misc[ "reason" ] = "Perception Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Per"; this.misc[ "headcolor" ] = "effect"; break; + case "wil-effect": this.Karma( "Wil-Karma" ); this.misc[ "reason" ] = "Willpower Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Wil"; this.misc[ "headcolor" ] = "effect"; break; + case "cha-effect": this.Karma( "Cha-Karma" ); this.misc[ "reason" ] = "Charisma Effect Test"; this.misc[ "rollWhoSee" ] = "RollType-Cha"; this.misc[ "headcolor" ] = "effect"; break; + case "str-step": this.Karma( "Str-Karma" ); this.misc[ "reason" ] = "Knockdown Test"; this.misc[ "headcolor" ] = "knockdown"; this.misc[ "Special" ] = "Knockdown"; this.misc[ "ModType" ] = "Effect"; break; + case "knockdown": this.Karma( "Str-Karma" ); this.misc[ "reason" ] = "Knockdown Test"; this.misc[ "headcolor" ] = "knockdown"; this.misc[ "Special" ] = "Knockdown"; this.misc[ "ModType" ] = "Effect"; break; + case "initiative": this.Karma( "Initiative-Karma" ); this.misc[ "reason" ] = "Initiative"; this.misc[ "headcolor" ] = "init"; break; + case "ls-speak-rank": this.Lookup( 1, [ "", "Per" ]); this.misc[ "reason" ] = "Speak Language Test"; this.misc[ "headcolor" ] = "action"; if (state.Earthdawn.g1879) this.Damage( ["Strain", "NA", 1 ] ); break; // 1879 and ED Talents have strain. ED Skill does not. This line is not for ED Talents. + case "ls-readwrite-rank": this.Lookup( 1, [ "", "Per" ]); this.misc[ "reason" ] = "R/W Language Test"; this.misc[ "headcolor" ] = "action"; break; // Skill does not have strain. In ED, Talent Does. + case "recovery-step": { + this.Karma( "Recovery-Karma", -1 ); + if( subsegmentArray.indexOf( "Wil" ) > 0 ) { + this.bFlags |= Earthdawn.flags.Recovery | Earthdawn.flags.RecoveryStun; + this.misc[ "reason" ] = "Stun Recovery Test"; + } else { + this.bFlags |= Earthdawn.flags.Recovery; + this.misc[ "reason" ] = "Recovery Test"; + } + this.misc[ "headcolor" ] = "recovery"; + if (Earthdawn.getAttrBN( this.charID, "NPC", "1") != Earthdawn.charType.mook ) { + let aobj = Earthdawn.findOrMakeObj({ _type: 'attribute', _characterid: this.charID, name: "Recovery-Tests" }, 0, 2); + if( (aobj.get( "current" ) || 0) <= 0) { + this.chat( this.tokenInfo.name + " does not have a Recovery Test to spend.", Earthdawn.whoFrom.apiWarning ); + falloutParse = true; + } else + Earthdawn.setWithWorker( aobj, "current", Earthdawn.parseInt2( aobj.get( "current" )) -1 ); + } + } break; + case "casting": { +log("I think this is dead code (casting). Please report to ChrisD what you were doing when this appeared."); +log(cmdSegment); + let base = Earthdawn.buildPre( "SPM", subsegmentArray[2] ); + this.misc[ "headcolor" ] = "action"; + if( Earthdawn.getAttrBN( this.charID, base + "SuccessLevels", "None") !== "Effect +2 Inc") + this.bFlags |= Earthdawn.flags.NoOppMnvr; + this.doLater += "~Karma: kcdef: 0: SP-Spellcasting-Karma-Control"; + // redo karma control. Dead code message July 24. + this.misc[ "reason" ] = Earthdawn.getAttrBN( this.charID, base + "Contains", "") + " Spellcasting Test"; + subsegmentArray = [ "value", "SP-Spellcasting-Step" ]; + } break; + case "will-effect": // Generic casting will effect button. +log("I think this is dead code (will-effect). Please report to ChrisD what you were doing when this appeared."); +log(cmdSegment); + this.misc[ "headcolor" ] = "effect"; + if (Earthdawn.getAttrBN( this.charID, "SP-Willforce-Use", "0") == "1") { + this.doLater += "~Karma: kcdef: -1: SP-WilEffect-Karma-Control: kcdef: 0: SP-Willforce-Karma-Control" + "~Strain: 1"; + this.misc[ "reason" ] = "WillForce Effect"; + } else { + this.doLater += "~Karma: kcdef: -1: SP-WilEffect-Karma-Control"; + this.misc[ "reason" ] = "Will Effect"; + } + this.bFlags |= (Earthdawn.flagsArmor.Unknown & Earthdawn.flags.WillEffect ); + break; + default: + this.chat( "Failed to parse 'value' in msg segment: '" + cmdSegment + "' in msg: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + } // End Value, Step, or Attribute. Note that this falls into the Lookup below. + + case "stepmod": + case "modstep": + case "modvalue": + if( !falloutParse ) + falloutParse = this.Lookup( 1, subsegmentArray ); // 1 = this.misc.step + break; + case "strainsave": + this.misc[ "strainSave" ] = (( "strainSave" in this.misc) ? this.misc[ "strainSave" ] + "$ " : "") + cmdSegment.replace(/^\s*strainSave\s*[\~\$\:]\s*/i, "" ); // get rid the key to get us here, but save the rest. +// cdd. Note, this might be unnessisary for spells with changes to spellinit. +//log( "strainsave " + cmdSegment); +//log( this.misc[ "strainSave" ] ); + case "storeintoken": + this.TokenSet( "replace", subsegmentArray[ 1 ], subsegmentArray[2], subsegmentArray[ 3 ]); + this.chat( subsegmentArray[ 3 ] + " extra successes to go to next damage upon " + this.getTokenName( subsegmentArray[ 2 ] ), Earthdawn.whoTo.player | Earthdawn.whoTo.gm); + break; + case "target": + case "targetnum": + this.TargetT( subsegmentArray ); + break; + case "targetspell": { + let tt = Earthdawn.getAttrBN( this.charID, Earthdawn.buildPre( "SPM", subsegmentArray[1] ) + "Casting", "MDh"); + let sa = ( "targetspell:" + tt ).split( ":" ); + this.TargetT( sa ); + } break; + case "targettype": // PD: (or MD or SD) with optional h = highest, p1p = plus one per person, -Nat. This marks that this is the secondary chat message. + if( subsegmentArray.length > 1) + this.bFlags |= this.TargetTypeToFlags( subsegmentArray[ 1 ] ); + break; + case "targetclear": + case "targetsclear": + this.TokenSet( "clear", "TargetList"); + break; + case "targetid": + case "targetset": + if( this.bFlags & Earthdawn.flagsTarget.Set ) { + this.TokenSet( "clear", "TargetList"); + for( let i = 1; i < subsegmentArray.length; i++ ) + this.TokenSet( "add", "TargetList", subsegmentArray[ i ]); + } else + this.forEachTarget( subsegmentArray ); + break; + case "targetmod": + case "modtarget": + case "targetvalue": + falloutParse = this.Lookup( 3, subsegmentArray ); // 1 = this.misc[ "targetNum" ] + break; + // Toggle option is obsolete and can be removed. + case "toggle": // toggle the token action for karma or willforce on or off. + this.MarkerSet( [ "toggle", subsegmentArray[ 1 ], "t"] ); + break; + case "token": // When called from a token action, "!edToken~ " is inserted ahead of another valid !Earthdawn~ command. + case "!edtoken": + case "edtoken": + this.tokenAction = true; // This lets us know we were called from a token action. Without this. we were called directly from a character sheet. + break; // This is done by creating a macro named Token, and inserting #Token in front of any other action you want a token action to preform. + case "tunematrix": + this.TuneMatrix( subsegmentArray ); + break; + case "updatedates": + this.UpdateDates( subsegmentArray ); + break; + default: + this.chat( "Failed to parse msg segment: '" + cmdSegment + "' in msg: " + this.edClass.msg.content, Earthdawn.whoFrom.apiError ); + } // End Switch + } catch( err ) { Earthdawn.errorLog( "ED.Parse() error caught: " + err, this ); } + return falloutParse; + }; // End ParseObj.Parse(); + + + + // ParseObj.ParseLoop - Loop through as long as there are message segments left to process. + // + // Note that with asynchronous code, ParseLoop() is usually the place to reenter the loop. + // IE: the old thread is told to fallout, and the new thread takes over processing and continues with + // a call to ParseLoop() to process any remaining tokens. + this.ParseLoop = function() { + 'use strict'; + let falloutLoop = false; + while ( !falloutLoop && (++this.indexMsg < this.edClass.msgArray.length )) { + falloutLoop = this.Parse( this.edClass.msgArray[ this.indexMsg ].trim() ); + } + }; // End ParseObj.ParseLoop(); + + + + }; // End of ParseObj; + + + + // ED.ParseCmd () + // This routine is the control routine that sets up the loop. The real work is done by routines called by this one. + this.ParseCmd = function() { + 'use strict'; + let edParse = new this.ParseObj( this ); // This object is used to parse the message into segments delimited by tilde (~) characters and to process each individual segment. + if ( this.msgArray[ 0 ].trim() === "!edToken" ) + edParse.tokenAction = true; // This lets us know we were called from a token action. Without this. we were called directly from a character sheet. + edParse.ParseLoop(); + edParse.doNow(); + }; // End ED.ParseCmd(); + + + +// +// NOTE: Everything between this point and the similar note above is used with the PARSE command and interacts with the character sheet. +// +// So if you are just using the stepdice and initiative rollers, and are not using my Earthdawn character sheets, you can cut everything between this point and the note above. +// + + + + // NOTE: This is the continuation of the main CREATE thread for this object. It makes use of functions declared above + if( origMsg !== undefined ) { + origMsg.content = origMsg.content.replace( new RegExp( Earthdawn.constantAlt( "ColonAlt" ), "g"), ":"); // Buttons don't like colons, so any colon has been changed to this weird character. Change them back. + this.msg = this.ReconstituteInlineRolls( origMsg ); + this.msgArray = origMsg.content.split( "~" ); + if ( this.msgArray.length < 2) + this.chat( "Error! Earthdawn.js was unable to parse string. msg was: " + this.msg.content, Earthdawn.whoFrom.apiError ); + if( state.Earthdawn.logCommandline ) + log( this.msg ); + } + + +}; // End of EDclass; + + + + + + +on("ready", function() { + 'use strict'; +// log("ready"); +// +// on("change:attribute", function (attr,prev) { +// 'use strict'; +// let sa = attr.get( "name" ); +// log("Value has change " + sa); +// +// if(sa==="testAPIbug" && (attr.get("current") !== attr.get("max"))){ +// log("Pinged by the value " + attr.get("current")); +// attr.setWithWorker( "max", attr.get("current")); +// log("Ponged wth the value " + attr.get("max")); +// } +// }); // end on("change:attribute" + + + on("add:character", function( obj ) { // Brand new character. Make sure that certain important attributes fully exist. + 'use strict'; + let ED = new Earthdawn.EDclass(); + ED.newCharacter( obj.get( "_id" ) ); + }); + + + on("add:graphic", function( obj ) { // New Graphic. Set it's statusmarkers to character sheet conditions and options. + 'use strict'; + Earthdawn.tokenRefresh( obj ); + }); + + + + on("add:attribute", function (attr) { + 'use strict'; + Earthdawn.attribute( attr ); + }); + + + // change attribute. See if it needs some special processing. + on("change:attribute", function (attr, prev) { + 'use strict'; + Earthdawn.attribute( attr, prev ); + }); // end on("change:attribute" + + + + // An attribute is being destroyed. + on("destroy:attribute", function (attr ) { + 'use strict'; + try { + let nm = attr.get( "name" ); + function testDeletion() { // return true if character is still there and we should proceed. false if character is not there. + if( !getObj( 'character', attr.get( 'characterid' ))) { +// log( "Earthdawn on destroy attribute() character does not exist: probably character deletion: " + nm ); + return false; + } + let aobj = findObjs({ _type: 'attribute', _characterid: attr.get( "_characterid" ), name: "edition" }); + if( aobj === undefined || aobj.length == 0 ) { +// log( Earthdawn.timeStamp() + "Earthdawn on destroy attribute() edition attribute not found, probably character deletion: + nm"); + return false; + } + return true; + } + +// log("Earthdawn - Attribute deleted " + nm ); + // If it is a link attribute, it is probably because the row has been deleted. + // If it is a link, go through the links, to / from the linked item and remove the other half of the links. + if( nm.endsWith( "_LinksGetValue" ) || nm.endsWith( "_LinksProvideValue" )) { // One half of a link has been deleted. remove the link from the other half. +//log("destroying"); log( attr); + setTimeout( function() { + try { + if( !testDeletion() ) return; + let arr = attr.get( "max" ).split( "," ); + if( arr.length > 0 ) { + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = attr.get( "_characterid" ); + for( let i = 0; i < arr.length; ++i ) + if( nm.endsWith( "_LinksGetValue" )) { // LinksGetValue are of form comma delimited list of fully qualified attributes, maybe more than one seperated by plus signs. + let a2 = arr[ i ].split( "+" ), + cnt = 0; + for( let j = 0; j < a2.length; ++j ) { + if( a2[ j ].startsWith( "repeating_") && cnt < 1 ) { // if it is not linking to a repeating section, then it is not bidirectional, and we don't have anything to do for this link. + edParse.ChatMenu( [ "ChatMenu", "LinkRemoveHalf", + Earthdawn.buildPre( Earthdawn.repeatSection( 3, a2[ j ] ), Earthdawn.repeatSection( 2, a2[ j ] )) + + "LinksProvideValue", Earthdawn.repeatSection( 2, nm )] ); + ++cnt; + } } + } else // LinksProviceValue are comma delimited lists of form (code);(rowID). + edParse.ChatMenu( [ "ChatMenu", "LinkRemoveHalf", + Earthdawn.buildPre( Earthdawn.getParam( arr[ i ], 1, ";" ), Earthdawn.getParam( arr[ i ], 2, ";" )) + + "LinksGetValue", Earthdawn.repeatSection( 2, nm )] ); + } // This should remove this rowID (last parameter) from the referenced lists. + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn on destroy attribute() Links error caught: " + err ); } + }, 2000 ); // end delay 2 seconds. + } + else if( nm.endsWith( "_SPM_spRowID" )) { // Spell matrix has been deleted. + setTimeout( function() { + try { + if( !testDeletion() ) return; + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = attr.get( "_characterid" ); + edParse.TuneMatrix( [ "Tune", "inMatrix" ]); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn on destroy attribute() spell Matrix error caught: " + err ); } + }, 2000 ); // end delay 10 seconds. + } + else if( nm.endsWith( "_RowID" )) { // A row has been deleted. If the row has a Token action, delete it. + setTimeout( function() { + try { + if( !testDeletion() ) return; + let rowID = Earthdawn.repeatSection( 2, nm ); + let ab = findObjs({ _type: "ability", _characterid: attr.get( "_characterid" )}); + _.each( ab, function (abObj) { + if( abObj.get( "action" ).indexOf( rowID ) !== -1) + abObj.remove(); + }); + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn on destroy attribute() token action error caught: " + err ); } + }, 2000 ); // end delay 10 seconds. + } + } catch(err) { log( Earthdawn.timeStamp() + "Earthdawn on destroy attribute() error caught: " + err ); } + }); // end on("destroy:attribute") + + + + // damage changed on token. See if it need to set token unc or dead. + // what is "value" or "max". + // Note: This routine is a bit round about due to a change in the data structure. I just modified the old code to the new data structure instead of redesigning from scratch. + function onChangeDamage(what, attr, prev) { + 'use strict'; + try { + if( attr.get("_subtype") !== "token") + return; + let rep = attr.get("represents") + if( !rep ) return; + + let death = Earthdawn.getAttrBN( rep, "Damage-Death-Rating", "25" ), + unc = ( what === "value" ) ? Earthdawn.getAttrBN( rep, "Damage_max", "20" ) : attr.get( "bar3_max" ), + dam = ( what === "max" ) ? Earthdawn.getAttrBN( rep, "Damage", "0" ) : attr.get( "bar3_value" ); + + function health() { + if( dam < unc) + return "u"; // Set Healthy. + else if( dam < death) + return "s"; // Set unconscious + else + return "a"; // Set to special value dead. + } + + let whatcurr = health(); + let whatprev = whatcurr; + if( prev ) { + if( what === "value" ) + dam = prev[ "bar3_value"]; + else + unc = prev[ "bar3_max" ]; + whatprev = health(); + } + if( !prev || whatcurr !== whatprev) { // Health status has changed. + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = rep; + edParse.tokenInfo = { type: "token", tokenObj: attr } + edParse.MarkerSet( [ "ocd", (whatcurr === "a") ? "healthdead" : "healthunconscious", (whatcurr === "u") ? "u" : "s" ] ); // This will set healthdead or healthunconscious to set, or healthunconscious to unset which will also unset healthdead. + } + } catch(err) { log( Earthdawn.timeStamp() + "on Change Damage() error caught: " + err ); } + }; // end onChangeDamage(); + + + on("change:graphic:bar3_value", function (attr, prev) { + onChangeDamage( "value", attr, prev ); + }); + + + on("change:graphic:bar3_max", function (attr, prev) { + onChangeDamage( "max", attr, prev ); + }); + + +/* functionality moved to sheetworker. + on("change:character:name", function (attr, prev) { + let nm = attr.get("name") + if( nm ) + Earthdawn.setWW( "character_name", nm, attr.get( "_id" )); + }); // end on change character name +*/ + + + // The GM has moved the players to a new map. + on("change:campaign:playerpageid", function (attr, prev) { + 'use strict'; + try { +//log( "on change campaign playerpageid"); log(attr); log(prev); + let newpage = attr.get("playerpageid"); +//let pobj = getObj( "page", newpage); +//log( "page " + pobj.get( "name" )); + let tkns = findObjs({ _pageid: newpage, _type: "graphic", _subtype: "token" }); // All tokens on the new page. + _.each( tkns, function (TokObj) { + Earthdawn.tokenRefresh( TokObj ); + }) // End ForEach Token + } catch(err) { Earthdawn.errorLog( "ED.on change campaign playerpageid() error caught: " + err, po ); } + }); // end on change campaign playerpageid + + + + // The GM has moved a specific player to a new map. + // If a player was not on a specific page, or is now on a different page, process that page for that player. + // If a player was on a specific page, and now is not, process the all players page for that player. + on("change:campaign:playerspecificpages", function (attr, prev) { + 'use strict'; + try { +//log( "on change campaign playerspecificpages"); log(attr); log(prev); + function refreshpage( page ) { + 'use strict'; +//log( "refreshing page " + page); + let tkns = findObjs({ _pageid: page, _type: "graphic", _subtype: "token" }); // All tokens on the new page. + _.each( tkns, function (TokObj) { + Earthdawn.tokenRefresh( TokObj ); + }) // End ForEach Token + } + + let added, + unique = [ attr.get( "playerpageid" )]; // any player removed from the specific pages list went back to the all players page, so refresh that. + if( !prev[ "playerspecificpages" ] ) // if prev is empty, then everything in attr just got added. + added = attr.get( "playerspecificpages" ); + else + added = _.difference( attr.get( "playerspecificpages" ), prev[ "playerspecificpages" ] ); // current minus old. + for( let key in added ) + if( unique.indexOf( added[ key ] ) == -1 ) + unique.push( added[ key ] ); + for( let i = 0; i < unique.length; ++i ) { + refreshpage( unique[ i ] ); // refresh all the tokens on any page a player just got sent to. + } + } catch(err) { Earthdawn.errorLog( "ED.on change campaign playerspecificpages() error caught: " + err, po ); } + }); // end on change campaign playerspecificpages + + + on("change:graphic:represents", function( attr, prev ) { + 'use strict'; + try { +//log( "change graphic represents"); log( prev); log(attr); + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.charID = attr.get( "represents" ); + edParse.chat( "***You are attempting to link a token to character '" + Earthdawn.getAttrBN( edParse.charID, "character_name", "" ) + + "' using the roll20 interface.*** The Earthdawn API has its own link token button (in Parameters / Sheet / Special Functions) " + + "that will automatically link the token and set the token bars to Damage, Wounds, and Karma. Do you want to " + + Earthdawn.makeButton("Link token", "!Earthdawn~ charID: " + edParse.charID + "~ buttonLink: " + attr.get( "_id" ) + , "Link the token and the character and set the token bars.", "param" ) + + "? ***Note:*** please press the link button only after having saved the token parameters on the roll20 popup.", Earthdawn.whoTo.player | Earthdawn.whoFrom.noArchive ) +// edParse.LinkToken( [ "autoLink", attr.get( "_id" ) ] ); // pass tokenID + } catch(err) { log( Earthdawn.timeStamp() + "on change represents() error caught: " + err ); } + }); // end on change represents + + // a tokens statusmarkers have changed. + // If it is not a mook, see if it is a status marker that has meaning to this sheet, and set the appropriate condition. + on("change:graphic:statusmarkers", function( attr, prev ) { + 'use strict'; + try { + let rep = attr.get( "represents" ); + if( rep && rep != "" ) { +//log("at change marker"); + let npc = Earthdawn.getAttrBN( rep, "NPC", "1" ); + if( npc !== Earthdawn.charType.pc && npc !== Earthdawn.charType.npc ) // Don't mess with the statusmarkers of anything except PCs and NPC. + return; + let newSM = _.without( attr.get( "statusmarkers" ).split( "," ), ""), // split( "" ) will return an array of [""], so filter those out. + oldSM = _.without( prev[ "statusmarkers" ].split( "," ), ""); + let added = _.difference( newSM, oldSM ), + removed = _.difference( oldSM, newSM ); + if( removed.length || added.length ) { + let ED = new Earthdawn.EDclass(); + let edParse = new ED.ParseObj( ED ); + edParse.tokenInfo = { type: "token", name: attr.name, tokenObj: attr, characterObj: getObj("character", rep ) }; + edParse.charID = rep; + for( let i = 0; i < removed.length; ++i ) // unset everything with these markers. + edParse.MarkerSet( [ "ocsm", removed[ i ].replace( /\@\d*/g, ""), "u"] ); + for( let i = 0; i < added.length; ++i ) { +//log("change:graphic:statusmarkers added :" + added[i]); + let fnd = added[ i ].match( /\@\d/ ); // does it look like foo@3? (this does not match "bar::3" but does match the end of "foo::3@3); + if( fnd ) // strip the @n numbers out of the marker names, but convert them to "a", "b", etc. to be set. Changes foo@3 to MarkerSet( "foo", c); + edParse.MarkerSet( [ "markerDirect", added[ i ].replace( /\@\d*/g, ""), String.fromCharCode(Earthdawn.parseInt2( fnd[0].charAt(1)) + 96) ] ); + else + edParse.MarkerSet( [ "markerDirect", added[ i ].replace( /\@\d*/g, ""), "s"] ); + } } } + } catch(err) { log( Earthdawn.timeStamp() + "on change statusmarkers() error caught: " + err ); } + }); // end on change statusmarkers + + + let ED = new Earthdawn.EDclass(); + ED.Ready(); +}); // End on("ready") + + + +on("chat:message", function(msg) { + 'use strict'; +//log(msg); + if(msg.type === "api" ) { +//log(msg); + // Earthdawn or Token - Earthdawn Message to be sent to the parser to handle. Could be any of several commands. + if ( msg.content.startsWith( "!Earthdawn" ) || msg.content.startsWith( "!edToken" ) || msg.content.startsWith( "!edCustom" )) { + let ED = new Earthdawn.EDclass( msg ); + if ( ED.msgArray.length > 1 ) + ED.ParseCmd(); + } else if( msg.content.startsWith( "!edsdr" )) { // edsdr - Earthdawn Step Dice Roller + let ED = new Earthdawn.EDclass( msg ); + if ( ED.msgArray.length > 1 ) + ED.StepDiceRoller(); + } else if( msg.content.startsWith( "!edInit" )) { // edInit - Earthdawn Initiative. Rolls individual initiatives for all selected tokens + let ED = new Earthdawn.EDclass( msg ); + if ( ED.msgArray.length > 1 ) + ED.Initiative(); + } + } // End if msgtype is api +}); // End ON Chat:message. + +// cdd +// get spellcasting working without a token selected! + diff --git a/Earthdawn (FASA Official) character sheet companion/script.json b/Earthdawn (FASA Official) character sheet companion/script.json index 6948514dd7..29a8fa9425 100644 --- a/Earthdawn (FASA Official) character sheet companion/script.json +++ b/Earthdawn (FASA Official) character sheet companion/script.json @@ -1,8 +1,8 @@ { "name": "Earthdawn by FASA character sheet companion", "script": "Earthdawn.js", - "version": "03.330", - "previousversions": ["03.301","03.30","03.19","03.15","03.023","03.022","03.021","03.020","03.001","03.000","02.042","02.041","02.040","02.000","01.002","01.001","01.000"], + "version": "03.400", + "previousversions": ["03.330","03.301","03.30","03.19","03.15","03.023","03.022","03.021","03.020","03.001","03.000","02.042","02.041","02.040","02.000","01.002","01.001","01.000"], "description": "The Roll20 \"Earthdawn by FASA character sheet companion\" API script is made to work with the \"Earthdawn by FASA\" character sheet and \"Earthdawn 4th Edition\" Compendium. It provides many features that automate and assist in many tasks. It is optimized for Earthdawn 4th edition and 1879 1st Edition, but currently has limited support for other editions.\r\r For more information please see [Earthdawn Sheet wiki page](https://wiki.roll20.net/Earthdawn_-_FASA_Official_V2).", "authors": "Chris Dickey", "roll20userid": "633707", From 884868326651115286aacd06538737f32e0880d0 Mon Sep 17 00:00:00 2001 From: boli32 Date: Tue, 14 Jan 2025 14:59:49 +0000 Subject: [PATCH 41/42] Allowed for users to add their own custom calanders --- QuestTracker/1.0/QuestTracker.js | 52 +++++-- QuestTracker/README.md | 230 ++++++++++++++++++++++++++++++- 2 files changed, 267 insertions(+), 15 deletions(-) diff --git a/QuestTracker/1.0/QuestTracker.js b/QuestTracker/1.0/QuestTracker.js index 18d3d67c76..eb50dfdc0d 100644 --- a/QuestTracker/1.0/QuestTracker.js +++ b/QuestTracker/1.0/QuestTracker.js @@ -13,6 +13,7 @@ var QuestTracker = QuestTracker || (function () { if (state.CalenderData.CALENDARS) CALENDARS = state.CalenderData.CALENDARS; if (state.CalenderData.WEATHER) WEATHER = state.CalenderData.WEATHER; } + Object.assign(CALENDARS, state.QUEST_TRACKER.calendar); return { CALENDARS, WEATHER }; }; const { CALENDARS, WEATHER } = getCalendarAndWeatherData(); @@ -39,10 +40,12 @@ var QuestTracker = QuestTracker || (function () { let QUEST_TRACKER_globalQuestArray = []; let QUEST_TRACKER_globalRumours = {}; let QUEST_TRACKER_Events = {}; + let QUEST_TRACKER_Calendar = {}; let QUEST_TRACKER_QuestHandoutName = "QuestTracker Quests"; let QUEST_TRACKER_RumourHandoutName = "QuestTracker Rumours"; let QUEST_TRACKER_EventHandoutName = "QuestTracker Events"; let QUEST_TRACKER_WeatherHandoutName = "QuestTracker Weather"; + let QUEST_TRACKER_CalendarHandoutName = "QuestTracker Calendar"; let QUEST_TRACKER_rumoursByLocation = {}; let QUEST_TRACKER_readableJSON = true; let QUEST_TRACKER_pageName = "Quest Tree Page"; @@ -102,6 +105,7 @@ var QuestTracker = QuestTracker || (function () { QUEST_TRACKER_questGrid = state.QUEST_TRACKER.questGrid || []; QUEST_TRACKER_jumpGate = state.QUEST_TRACKER.jumpGate || true; QUEST_TRACKER_Events = state.QUEST_TRACKER.events || {}; + QUEST_TRACKER_Calendar = state.QUEST_TRACKER.calendar || {}; QUEST_TRACKER_calenderType = state.QUEST_TRACKER.calenderType || 'gregorian'; QUEST_TRACKER_currentDate = state.QUEST_TRACKER.currentDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate QUEST_TRACKER_defaultDate = state.QUEST_TRACKER.defaultDate || CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate @@ -150,6 +154,7 @@ var QuestTracker = QuestTracker || (function () { state.QUEST_TRACKER.questGrid = QUEST_TRACKER_questGrid; state.QUEST_TRACKER.jumpGate = QUEST_TRACKER_jumpGate; state.QUEST_TRACKER.events = QUEST_TRACKER_Events; + state.QUEST_TRACKER.calendar = QUEST_TRACKER_Calendar; state.QUEST_TRACKER.currentDate = QUEST_TRACKER_currentDate; state.QUEST_TRACKER.defaultDate = QUEST_TRACKER_defaultDate; state.QUEST_TRACKER.calenderType = QUEST_TRACKER_calenderType; @@ -179,6 +184,7 @@ var QuestTracker = QuestTracker || (function () { TreeObjRef: {}, jumpGate: true, events: {}, + calendar: {}, calenderType: 'gregorian', currentDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, defaultDate: CALENDARS[QUEST_TRACKER_calenderType]?.defaultDate, @@ -208,16 +214,16 @@ var QuestTracker = QuestTracker || (function () { }; if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]) { const tableQuests = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTS }); - tableQuests.set('showplayers', false); // Hide table from players + tableQuests.set('showplayers', false); } if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS })[0]) { const tableQuestGroups = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTGROUPS }); - tableQuestGroups.set('showplayers', false); // Hide table from players + tableQuestGroups.set('showplayers', false); } let locationTable = findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS })[0]; if (!locationTable) { locationTable = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_LOCATIONS }); - locationTable.set('showplayers', false); // Hide table from players + locationTable.set('showplayers', false); createObj('tableitem', { _rollabletableid: locationTable.id, name: 'Everywhere', @@ -236,6 +242,9 @@ var QuestTracker = QuestTracker || (function () { if (!findObjs({ type: 'handout', name: QUEST_TRACKER_WeatherHandoutName })[0]) { createObj('handout', { name: QUEST_TRACKER_WeatherHandoutName }); } + if (!findObjs({ type: 'handout', name: QUEST_TRACKER_CalendarHandoutName })[0]) { + createObj('handout', { name: QUEST_TRACKER_CalendarHandoutName }); + } Utils.sendGMMessage("QuestTracker has been initialized."); } }; @@ -358,6 +367,9 @@ var QuestTracker = QuestTracker || (function () { case 'quest': handoutName = QUEST_TRACKER_QuestHandoutName; break; + case 'calendar': + handoutName = QUEST_TRACKER_CalendarHandoutName; + break; default: return; } @@ -384,12 +396,14 @@ var QuestTracker = QuestTracker || (function () { case 'weather': updatedData = QUEST_TRACKER_HISTORICAL_WEATHER; break; - case 'weatherevents': - updatedData = QUEST_TRACKER_Events; + case 'calendar': + updatedData = QUEST_TRACKER_Calendar; break; - default: + case 'quest': updatedData = QUEST_TRACKER_globalQuestData; break; + default: + return; } const updatedContent = QUEST_TRACKER_readableJSON ? JSON.stringify(updatedData, null, 2) @@ -406,9 +420,17 @@ var QuestTracker = QuestTracker || (function () { case 'event': QUEST_TRACKER_Events = JSON.parse(cleanedContent); break; - default: + case 'weather': + QUEST_TRACKER_HISTORICAL_WEATHER = JSON.parse(cleanedContent); + break; + case 'calendar': + QUEST_TRACKER_Calendar = JSON.parse(cleanedContent); + break; + case 'quest': QUEST_TRACKER_globalQuestData = JSON.parse(cleanedContent); break; + default: + return; } } }); @@ -425,7 +447,7 @@ var QuestTracker = QuestTracker || (function () { updateHandoutField('rumour'); updateHandoutField('event'); updateHandoutField('weather'); - updateHandoutField('weatherdescription'); + updateHandoutField('calendar'); }; const toggleWeather = (value) => { QUEST_TRACKER_WEATHER = (value === 'true'); @@ -481,8 +503,7 @@ var QuestTracker = QuestTracker || (function () { importData: (handoutName, dataType) => { let handout = findObjs({ type: 'handout', name: handoutName })[0]; if (!handout) { - errorCheck(7, 'msg', null,`${dataType} handout "${handoutName}" not found. Please create it.`); - return; + createObj('handout', { name: handoutName }); } handout.get('gmnotes', (notes) => { const cleanedContent = Utils.stripJSONContent(notes); @@ -533,9 +554,9 @@ var QuestTracker = QuestTracker || (function () { } else if (dataType === 'Weather') { parsedData = Utils.normalizeKeys(parsedData); QUEST_TRACKER_HISTORICAL_WEATHER = parsedData; - } else if (dataType === 'Weather Description') { + } else if (dataType === 'Calendar') { parsedData = Utils.normalizeKeys(parsedData); - QUEST_TRACKER_WEATHER_DESCRIPTION = parsedData; + QUEST_TRACKER_Calendar = parsedData; } saveQuestTrackerData(); Utils.sendGMMessage(`${dataType} handout "${handoutName}" Imported.`); @@ -599,6 +620,11 @@ var QuestTracker = QuestTracker || (function () { }); saveQuestTrackerData(); Utils.updateHandoutField('quest'); + }, + refreshCalendarData: () => { + Object.keys(CALENDARS).forEach(key => delete CALENDARS[key]); + Object.assign(CALENDARS, state.CalenderData.CALENDARS); + Object.assign(CALENDARS, state.QUEST_TRACKER.calendar); } }; const fullImportProcess = () => { @@ -606,10 +632,12 @@ var QuestTracker = QuestTracker || (function () { H.importData(QUEST_TRACKER_RumourHandoutName, 'Rumour'); H.importData(QUEST_TRACKER_EventHandoutName, 'Events'); H.importData(QUEST_TRACKER_WeatherHandoutName, 'Weather'); + H.importData(QUEST_TRACKER_CalendarHandoutName, 'Calendar'); H.syncQuestRollableTable(); Quest.cleanUpLooseEnds(); H.cleanUpDataFields(); Quest.populateQuestsToAutoAdvance(); + H.refreshCalendarData(); }; return { fullImportProcess diff --git a/QuestTracker/README.md b/QuestTracker/README.md index f3206ec255..443c5ed069 100644 --- a/QuestTracker/README.md +++ b/QuestTracker/README.md @@ -353,8 +353,6 @@ The module includes several pre-configured calendars: * **Greyhawk (Original and 2024 Default setting)** * **Exandria (Critical Role)** -I can add additional Calendars into the module if you were to provide the details and JSON object. - #### Lunar Cycles Each calendar includes options for tracking lunar phases. The lunar cycle will display key phases, such as the below and are setting specific. Each Calander can have multiple moons and their lunar cycle including their custom phases is displayed along with the weather. @@ -393,7 +391,231 @@ Select a calendar type from the configuration menu. The system will automaticall ***Warning: Changing the calendar type resets the current date to the default date for the chosen calendar.*** -### Setting Events +## Adding Custom Calenders + + +Users can add custom calanders by editing the **QuestTracker Calendar** Handout; as with other QuestTracker handouts all data is stored in the GM Notes field; the structure used is below as an example along with explanations for each field. + +*NOTE: it is very easy to mess this object up, so be careful. use a ![JSON Validator](https://jsonlint.com/) to confirm it is a valid object before refreshing the JSON files in the configuration settings.* + +``` +{ + "mythic": { + "name": "Mythic Calendar", + "months": [ + { "id": 1, "name": "Primus", "days": 30 }, + { "id": 2, "name": "Secundus", "days": 30 }, + { "id": 3, "name": "Tertius", "days": 30 }, + { "id": 4, "name": "Quartus", "days": 30 }, + { "id": 5, "name": "Quintus", "days": 30 }, + { "id": 6, "name": "Sextus", "days": 30 }, + { "id": 7, "name": "Septimus", "days": 30 }, + { "id": 8, "name": "Octavus", "days": 30 }, + { "id": 9, "name": "Nonus", "days": 30 }, + { "id": 10, "name": "Decimus", "days": 30 } + ], + "daysOfWeek": [ "Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6", "Day 7" ], + "defaultDate": "0001-01-01", + "startingWeekday": "Day 1", + "dateFormat": "{day}{ordinal} of {month}, {year}", + "significantDays": { + "1-1": "New Era Day", + "10-30": "Harvest Festival" + }, + "lunarCycle": { + "mystara": { + "name": "Mystara", + "baselineNewMoon": "0001-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "First Quarter", "start": 7, "end": 14 }, + { "name": "Full Moon", "start": 14, "end": 21 }, + { "name": "Last Quarter", "start": 21, "end": 28 } + ] + } + }, + "climates": { + "mythic region": { + "seasons": [ "Spring", "Summer", "Autumn", "Winter" ], + "modifiers": { + "temperature": { "Spring": 10, "Summer": 20, "Autumn": 15, "Winter": 0 }, + "precipitation": { "Spring": 15, "Summer": 10, "Autumn": 5, "Winter": 10 }, + "wind": { "Spring": 5, "Summer": 5, "Autumn": 10, "Winter": 15 }, + "humid": { "Spring": 20, "Summer": 15, "Autumn": 10, "Winter": 5 }, + "visibility": { "Spring": 10, "Summer": 20, "Autumn": 15, "Winter": 5 }, + "cloudy": { "Spring": 15, "Summer": 10, "Autumn": 20, "Winter": 25 } + }, + "seasonStart": { "Spring": 3, "Summer": 6, "Autumn": 9, "Winter": 12 } + } + } + } +} +``` + +### Variables and Usage + +#### `name` +**Type**: `String` + +- **Description**: The name of the calendar. +- **Example Value**: `"Mythic Calendar"` +- **Usage**: Used as a display name for the calendar system. + +--- + +#### `months` +**Type**: `Array` + +- **Description**: Defines the months in the calendar year. +- **Structure**: + ```json + { "id": Number, "name": String, "days": Number } + ``` +- **Fields**: + - `id`: Unique identifier for the month. + - `name`: Name of the month. + - `days`: Number of days in the month. +- **Example**: + ```json + [ + { "id": 1, "name": "Primus", "days": 30 }, + { "id": 2, "name": "Secundus", "days": 30 } + ] + ``` +- **Usage**: Determines the structure of the year and is used to calculate dates. + +--- + +#### `daysOfWeek` +**Type**: `Array` + +- **Description**: Defines the days of the week. +- **Example Value**: `["Day 1", "Day 2", "Day 3"]` +- **Usage**: Provides names for weekdays, enabling mapping of days within a week. + +--- + +#### `defaultDate` +**Type**: `String` + +- **Description**: The initial date of the calendar system. +- **Example Value**: `"0001-01-01"` +- **Usage**: Acts as a reference point for date calculations. + +--- + +#### `startingWeekday` +**Type**: `String` + +- **Description**: The name of the first weekday in the calendar. +- **Example Value**: `"Day 1"` +- **Usage**: Determines which day of the week the calendar starts on. + +--- + +#### `dateFormat` +**Type**: `String` + +- **Description**: The format for displaying dates. +- **Example Value**: `"{day}{ordinal} of {month}, {year}"` +- **Usage**: Configures how dates are rendered. + +--- + +#### `significantDays` +**Type**: `Object` + +- **Description**: Highlights specific dates with special events or names. +- **Structure**: + ```json + "-": String + ``` +- **Example**: + ```json + { + "1-1": "New Era Day", + "10-30": "Harvest Festival" + } + ``` +- **Usage**: Used to mark and describe important days. + +--- + +#### `lunarCycle` +**Type**: `Object` + +- **Description**: Defines the phases and duration of lunar cycles. +- **Structure**: + ```json + { + "": { + "name": String, + "baselineNewMoon": String, + "cycleLength": Number, + "phases": Array + } + } + ``` +- **Fields**: + - `name`: Name of the moon. + - `baselineNewMoon`: Reference date for the new moon. + - `cycleLength`: Length of the lunar cycle in days. + - `phases`: Array of phase definitions. +- **Example**: + ```json + { + "mystara": { + "name": "Mystara", + "baselineNewMoon": "0001-01-01", + "cycleLength": 28, + "phases": [ + { "name": "New Moon", "start": 0, "end": 7 }, + { "name": "Full Moon", "start": 14, "end": 21 } + ] + } + } + ``` +- **Usage**: Tracks moon phases for gameplay or storytelling. + +--- + +#### `climates` +**Type**: `Object` + +- **Description**: Defines seasonal and climate data for regions. +- **Structure**: + ```json + { + "": { + "seasons": Array, + "modifiers": Object, + "seasonStart": Object + } + } + ``` +- **Fields**: + - `seasons`: List of seasons. + - `modifiers`: Environmental modifiers (temperature, precipitation, etc.). + - `seasonStart`: Month identifiers marking the start of each season. +- **Example**: + ```json + { + "mythic region": { + "seasons": ["Spring", "Summer"], + "modifiers": { + "temperature": { "Spring": 10, "Summer": 20 } + }, + "seasonStart": { "Spring": 3, "Summer": 6 } + } + } + ``` +- **Usage**: Provides seasonal and environmental context. + +*NOTE: Keeping values between -20 and 20 will allow the weather module to perform correctly.* + + +## Event Module Navigate to the "Events" section in the configuration menu. Add, edit, or remove events as needed. Assign dates for recurring or one-time events. @@ -481,6 +703,8 @@ Yes, that is a workaround to having relationships between quest groups and it *c ## Updates +#### 2025-01-14 +* **v1.0.3** Allowed for users to add their own custom calanders #### 2025-01-13 * **v1.0.2** Added Krynn (Dragonlance) and Galifar (Eberon) Calanders. also expanded to allow for multiple moons and different cycles. Added the smaller and secondary moons to both Exandria and Grekhawk calander. #### 2025-01-10 From e759d74822ba3516c6722de13694f6739c325ded Mon Sep 17 00:00:00 2001 From: Alice Date: Tue, 14 Jan 2025 14:19:15 -0500 Subject: [PATCH 42/42] Update approved.yaml --- approved.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/approved.yaml b/approved.yaml index e00ff0b08c..da2556f751 100644 --- a/approved.yaml +++ b/approved.yaml @@ -1283,3 +1283,11 @@ DrD2StatusMarkers: SheetDefaults: - "SheetDefaults" - "Utility" + +CalenderData: + - "CalenderData" + - "Utility" + +QuestTracker: + - "QuestTracker" + - "Utility"
      
    Weather
    ${QUEST_TRACKER_CURRENT_WEATHER['weatherType']}
    Lunar Phase
    ${Calendar.getLunarPhase(QUEST_TRACKER_currentDate)}
    Location
    ${H.returnCurrentLocation(QUEST_TRACKER_WeatherLocation)}Change
    Temperature${temperatureDisplay}