Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Added spell slot management |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
f75d632b0b971ab4edfd8c00977c9039 |
User & Date: | murphy 2020-05-08 00:37:53.000 |
Context
2020-05-08
| ||
18:49 | Updated README and repository metadata check-in: 227672f20a user: murphy tags: trunk | |
00:37 | Added spell slot management check-in: f75d632b0b user: murphy tags: trunk | |
2020-05-07
| ||
20:53 | Allow deltas for skill checks and in some other commands check-in: 89253a68b2 user: murphy tags: trunk | |
Changes
Changes to DragonDice.Bot/Session.fs.
︙ | ︙ | |||
224 225 226 227 228 229 230 | """<b>Character State:</b> /select [<i>NAME</i>] — Select the active character for the user who is asking. /show|stat|state|stats — Display a summary of the active character's statistics. /talk — Display the set of common languages for all the active characters selected in the conversation. /heal <i>HP</i> — Restore <i>HP</i> hit points up to the maximum of the active character. /hurt|damage <i>HP</i> [, <i>DAMAGE</i>] — Deal <i>HP</i> damage to the active character. If a damage type is specified, the character's defenses may apply. /bolster <i>HP</i> — Add temporary hit points to the active character. | > | | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | """<b>Character State:</b> /select [<i>NAME</i>] — Select the active character for the user who is asking. /show|stat|state|stats — Display a summary of the active character's statistics. /talk — Display the set of common languages for all the active characters selected in the conversation. /heal <i>HP</i> — Restore <i>HP</i> hit points up to the maximum of the active character. /hurt|damage <i>HP</i> [, <i>DAMAGE</i>] — Deal <i>HP</i> damage to the active character. If a damage type is specified, the character's defenses may apply. /bolster <i>HP</i> — Add temporary hit points to the active character. /useslot <i>SL</i> — Use a level <i>SL</i> spell slot. /rest[ore] — Restore hit points to maximum, remove temporary hit points, and restore spell slots for the active character. /gain <i>GP</i> — Add an amount of currency to the active character. /spend <i>GP</i> — Remove an amount of currency to the active character. <b>Hit Points:</b> The <i>HP</i> argument to the health management commands may be a dice roll specification or a constant non-negative integer. <b>Currency:</b> |
︙ | ︙ | |||
257 258 259 260 261 262 263 264 265 266 267 268 269 270 | /setrace <i>RACE</i> — Change the race of the active character. This operation does not work on imported characters. /set ac =|+=|-= <i>N</i> — Change the armor class of the active character. /set hp =|+=|-= <i>N</i> — Change the maximum hit points of the active character. /set <i>SPEED</i> =|+=|-= <i>N</i> — Change a movement speed of the active character. /set <i>ABILITY</i> =|+=|-= <i>N</i> — Change an ability score of the active character. /addlevel <i>CLASS</i> [, <i>N</i>] — Add one or more levels in the given class to the active character. This operation does not work on imported characters. /removelevel <i>CLASS</i> [, <i>N</i>] — Remove one or more levels in the given class to the active character. This operation does not work on imported characters. /addproficiency <i>SKILL</i> [, <i>N</i>] — Add a proficiency in the given skill, optionally with a multiplier, to the active character. This operation does not work on imported characters. /removeproficiency <i>SKILL</i> — Remove the proficiency in the given skill from the active character. This operation does not work on imported characters. /set <i>SKILL</i> =|+=|-= <i>N</i> — Change a skill proficiency modifier of the active character. /adddefense <i>DAMAGE</i>, <i>DEFENSE</i> — Add a defense against <i>DAMAGE</i> for the active character. <i>DEFENSE</i> may be a numerical factor or one of the adjectives <i>Normal</i>, <i>Immune</i>, <i>Invulnerable</i>, <i>Resistant</i>, <i>Vulnerable</i>. /removedefense <i>DAMAGE</i> — Remove the defense against <i>DAMAGE</i> for the active character. /set <i>SENSE</i> =|+=|-= <i>N</i> — Change a perception sense range of the active character. /addlanguage <i>NAME</i> — Add a language proficiency to the active character. | > > | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | /setrace <i>RACE</i> — Change the race of the active character. This operation does not work on imported characters. /set ac =|+=|-= <i>N</i> — Change the armor class of the active character. /set hp =|+=|-= <i>N</i> — Change the maximum hit points of the active character. /set <i>SPEED</i> =|+=|-= <i>N</i> — Change a movement speed of the active character. /set <i>ABILITY</i> =|+=|-= <i>N</i> — Change an ability score of the active character. /addlevel <i>CLASS</i> [, <i>N</i>] — Add one or more levels in the given class to the active character. This operation does not work on imported characters. /removelevel <i>CLASS</i> [, <i>N</i>] — Remove one or more levels in the given class to the active character. This operation does not work on imported characters. /addslot <i>SL</i> [, <i>N</i>] — Add one or more spell slots of the given level to the active character. /removeslot <i>SL</i> [, <i>N</i>] — Add one or more spell slots of the given level to the active character. /addproficiency <i>SKILL</i> [, <i>N</i>] — Add a proficiency in the given skill, optionally with a multiplier, to the active character. This operation does not work on imported characters. /removeproficiency <i>SKILL</i> — Remove the proficiency in the given skill from the active character. This operation does not work on imported characters. /set <i>SKILL</i> =|+=|-= <i>N</i> — Change a skill proficiency modifier of the active character. /adddefense <i>DAMAGE</i>, <i>DEFENSE</i> — Add a defense against <i>DAMAGE</i> for the active character. <i>DEFENSE</i> may be a numerical factor or one of the adjectives <i>Normal</i>, <i>Immune</i>, <i>Invulnerable</i>, <i>Resistant</i>, <i>Vulnerable</i>. /removedefense <i>DAMAGE</i> — Remove the defense against <i>DAMAGE</i> for the active character. /set <i>SENSE</i> =|+=|-= <i>N</i> — Change a perception sense range of the active character. /addlanguage <i>NAME</i> — Add a language proficiency to the active character. |
︙ | ︙ | |||
496 497 498 499 500 501 502 | buffer.Append(", ").Append(HttpUtility.HtmlEncode chr.Profession) |> ignore buffer.Append("</i>\n\n") .Append("<b>Armor Class:</b> ").Append(chr.ArmorClass).Append('\n') .Append("<b>Hit Points:</b> ").Append(chr.CurrentHitPoints) |> ignore if chr.TemporaryHitPoints > 0 then buffer.Append(" + ").Append(chr.TemporaryHitPoints) |> ignore | | > | | > | 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | buffer.Append(", ").Append(HttpUtility.HtmlEncode chr.Profession) |> ignore buffer.Append("</i>\n\n") .Append("<b>Armor Class:</b> ").Append(chr.ArmorClass).Append('\n') .Append("<b>Hit Points:</b> ").Append(chr.CurrentHitPoints) |> ignore if chr.TemporaryHitPoints > 0 then buffer.Append(" + ").Append(chr.TemporaryHitPoints) |> ignore buffer.Append(" / ").Append(chr.MaxHitPoints).Append('\n') |> ignore if chr.MaxSlots.Values |> Seq.max > 0 then buffer.Append("<b>Spell Slots:</b> ").Append(Slot.toSummary chr.MaxSlots chr.CurrentSlots).Append('\n') |> ignore buffer.Append("<b>Currency:</b> ").Append(Coin.toString chr.CurrentCoin).Append('\n') |> ignore let pos = buffer.Append("<b>Speed:</b> ").Length for KeyValue (speed, v) in chr.Speeds do if v > 0<ft/rnd> then if buffer.Length > pos then buffer.Append(", ") |> ignore buffer.Append(Speed.toString speed).Append(' ').Append(v).Append(" ft/rnd") |> ignore if buffer.Length = pos then buffer.Append("Immobile") |> ignore let pos = buffer.Append("\n\n").Length |
︙ | ︙ | |||
613 614 615 616 617 618 619 620 621 622 623 624 625 626 | let buffer = Text.StringBuilder("<i>").Append(HttpUtility.HtmlEncode chr.Name).Append("</i> : HP - ").Append(hp) if f <> 1.0 then buffer.Append(" * ").Append(f) |> ignore buffer.Append(" → ").Append("<b>").Append(chr.CurrentHitPoints) |> ignore if chr.TemporaryHitPoints > 0 then buffer.Append(" + ").Append(chr.TemporaryHitPoints) |> ignore buffer.Append("</b> / ").Append(chr.MaxHitPoints) |> ignore return Markup (buffer.ToString(), List.empty) | ["/rest" | "/restore"] -> let chr = selectedCharacter user chr.Rest() return Markup ( sprintf "<i>%s</i> : HP = <b>%d</b>" (HttpUtility.HtmlEncode chr.Name) chr.CurrentHitPoints, List.empty | > > > > > > > > > > > > > > > > > > > > > | 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | let buffer = Text.StringBuilder("<i>").Append(HttpUtility.HtmlEncode chr.Name).Append("</i> : HP - ").Append(hp) if f <> 1.0 then buffer.Append(" * ").Append(f) |> ignore buffer.Append(" → ").Append("<b>").Append(chr.CurrentHitPoints) |> ignore if chr.TemporaryHitPoints > 0 then buffer.Append(" + ").Append(chr.TemporaryHitPoints) |> ignore buffer.Append("</b> / ").Append(chr.MaxHitPoints) |> ignore return Markup (buffer.ToString(), List.empty) | ["/useslot"] -> let chr = selectedCharacter user let buttons = [ for KeyValue (slot, n) in chr.CurrentSlots do if n > 0 then yield sprintf "%c (x%d)" (Slot.positiveSymbol slot) n, Slot.toString slot ] if List.isEmpty buttons then failwith "No spell slots available" return Markup ( "<b>Note:</b> Select spell slot to use", List.singleton buttons ) | ["/useslot"; slot] -> let chr = selectedCharacter user let slot = Slot.ofString slot chr.UseSlot(slot) return Markup ( sprintf "<i>%s</i> : Spell Slots = <b>%s</b>" (HttpUtility.HtmlEncode chr.Name) (Slot.toSummary chr.MaxSlots chr.CurrentSlots), List.empty ) | ["/rest" | "/restore"] -> let chr = selectedCharacter user chr.Rest() return Markup ( sprintf "<i>%s</i> : HP = <b>%d</b>" (HttpUtility.HtmlEncode chr.Name) chr.CurrentHitPoints, List.empty |
︙ | ︙ | |||
845 846 847 848 849 850 851 852 853 854 855 856 857 858 | let prof = Profession.ofString prof let count = int count chr.RemoveLevel(prof, count) return Markup ( sprintf "<i>%s</i> : Profession = %s" (HttpUtility.HtmlEncode chr.Name) chr.Profession, List.empty ) | ["/addproficiency" | "/removeproficiency"] -> return Markup ( "<b>Note:</b> Select a skill proficiency", Skill.All |> Set.remove Skill.StrengthAttack |> Set.remove Skill.DexterityAttack | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 | let prof = Profession.ofString prof let count = int count chr.RemoveLevel(prof, count) return Markup ( sprintf "<i>%s</i> : Profession = %s" (HttpUtility.HtmlEncode chr.Name) chr.Profession, List.empty ) | ["/addslot" | "/removeslot"] -> return Markup ( "<b>Note:</b> Select spell slot level", [ for i = 1 to 3 do yield [ for j = 1 to 3 do let slot = enum<Slot>(i * j) yield Slot.positiveSymbol slot |> string, Slot.toString slot ] ] ) | ["/addslot"; slot] -> let chr = selectedCharacter user let slot = Slot.ofString slot chr.MaxSlots <- let slots = chr.MaxSlots slots.With[slot, slots.[slot] + 1] return Markup ( sprintf "<i>%s</i> : Spell Slots = <b>%s</b>" (HttpUtility.HtmlEncode chr.Name) (Slot.toSummary chr.MaxSlots chr.CurrentSlots), List.empty ) | ["/addslot"; slot; count] -> let chr = selectedCharacter user let slot = Slot.ofString slot let count = int count chr.MaxSlots <- let slots = chr.MaxSlots slots.With[slot, max 0 (slots.[slot] + count)] return Markup ( sprintf "<i>%s</i> : Spell Slots = <b>%s</b>" (HttpUtility.HtmlEncode chr.Name) (Slot.toSummary chr.MaxSlots chr.CurrentSlots), List.empty ) | ["/removeslot"; slot] -> let chr = selectedCharacter user let slot = Slot.ofString slot chr.MaxSlots <- let slots = chr.MaxSlots slots.With[slot, max 0 (slots.[slot] - 1)] return Markup ( sprintf "<i>%s</i> : Spell Slots = <b>%s</b>" (HttpUtility.HtmlEncode chr.Name) (Slot.toSummary chr.MaxSlots chr.CurrentSlots), List.empty ) | ["/removeslot"; slot; count] -> let chr = selectedCharacter user let slot = Slot.ofString slot let count = int count chr.MaxSlots <- let slots = chr.MaxSlots slots.With[slot, max 0 (slots.[slot] - count)] return Markup ( sprintf "<i>%s</i> : Spell Slots = <b>%s</b>" (HttpUtility.HtmlEncode chr.Name) (Slot.toSummary chr.MaxSlots chr.CurrentSlots), List.empty ) | ["/addproficiency" | "/removeproficiency"] -> return Markup ( "<b>Note:</b> Select a skill proficiency", Skill.All |> Set.remove Skill.StrengthAttack |> Set.remove Skill.DexterityAttack |
︙ | ︙ |
Changes to DragonDice/Character.fs.
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | ResizeArray<IStatLayer>(2) let mutable stats : Option<StatBlock> = None let mutable currentHitPoints = 0 let mutable temporaryHitPoints = 0 let mutable currentCoin = 0m<gp> let mutable fullPath = let path = defaultArg path String.Empty if not (String.IsNullOrEmpty path) then IO.Path.GetFullPath(path) else | > > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | ResizeArray<IStatLayer>(2) let mutable stats : Option<StatBlock> = None let mutable currentHitPoints = 0 let mutable temporaryHitPoints = 0 let mutable currentSlots = EnumMap<Slot, int>() let mutable currentCoin = 0m<gp> let mutable fullPath = let path = defaultArg path String.Empty if not (String.IsNullOrEmpty path) then IO.Path.GetFullPath(path) else |
︙ | ︙ | |||
158 159 160 161 162 163 164 165 166 167 168 169 170 171 | /// Get the character's hit point maximum from the stat block or set it in the first custom stat layer. member this.MaxHitPoints with get () = this.StatBlock.MaxHitPoints and set v = let v0 = this.StatBlock.MaxHitPoints let sl = this.GetOrAddStatLayer<CustomStatLayer>() sl.MaxHitPoints <- sl.MaxHitPoints + v - v0 /// Get the character's size from the stat block. member this.Size = this.StatBlock.Size /// Get the character's speeds from the stat block or set them in the first custom stat layer. member this.Speeds with get () = this.StatBlock.Speeds | > > > > > > > > > > | 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | /// Get the character's hit point maximum from the stat block or set it in the first custom stat layer. member this.MaxHitPoints with get () = this.StatBlock.MaxHitPoints and set v = let v0 = this.StatBlock.MaxHitPoints let sl = this.GetOrAddStatLayer<CustomStatLayer>() sl.MaxHitPoints <- sl.MaxHitPoints + v - v0 /// Get the character's spell slots from the stat block or set them in the first custom stat layer. member this.MaxSlots with get () = this.StatBlock.MaxSlots and set v = let v0 = this.StatBlock.MaxSlots let sl = this.GetOrAddStatLayer<CustomStatLayer>() sl.MaxSlots <- EnumMap.map2Values (-) v v0 |> EnumMap.map2Values (+) sl.MaxSlots /// Get the character's size from the stat block. member this.Size = this.StatBlock.Size /// Get the character's speeds from the stat block or set them in the first custom stat layer. member this.Speeds with get () = this.StatBlock.Speeds |
︙ | ︙ | |||
297 298 299 300 301 302 303 | /// Remove a language in the first custom stat layer. member this.RemoveLanguage(lang:Language) = let sl = this.GetOrAddStatLayer<CustomStatLayer>() sl.Languages <- Set.remove lang sl.Languages /// Get the character's level from the stat block. | | | 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | /// Remove a language in the first custom stat layer. member this.RemoveLanguage(lang:Language) = let sl = this.GetOrAddStatLayer<CustomStatLayer>() sl.Languages <- Set.remove lang sl.Languages /// Get the character's level from the stat block. member this.Level = this.StatBlock.TotalLevel /// Get the character's proficiency bonus from the stat block. member this.ProficiencyBonus = this.StatBlock.ProficiencyBonus /// Add one or more levels in the given profession to the character. member this.AddLevel(profession:Profession, ?count:int) = let count = defaultArg count 1 |
︙ | ︙ | |||
371 372 373 374 375 376 377 | /// Temporary hit points of the character. member __.TemporaryHitPoints = temporaryHitPoints /// Give the character temporary hit points. /// Sets the temporary hit points of the character to the maximum of its current value or the given value. member this.AddTemporaryHitPoints(hp:int) = temporaryHitPoints <- max temporaryHitPoints hp | | > > > > > > > > > | > > | > | 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | /// Temporary hit points of the character. member __.TemporaryHitPoints = temporaryHitPoints /// Give the character temporary hit points. /// Sets the temporary hit points of the character to the maximum of its current value or the given value. member this.AddTemporaryHitPoints(hp:int) = temporaryHitPoints <- max temporaryHitPoints hp /// Currently available spell slots of the character. member __.CurrentSlots = currentSlots /// Use a spell slot of the given level. member __.UseSlot(slot:Slot) = let n = currentSlots.[slot] if n <= 0 then failwith "No spell slot of the given level available" currentSlots <- currentSlots.With[slot, n - 1] /// Apply the effects of a full rest. /// Restores current hit points to the character's maximum, removes all temporary hit points, /// restores all spell slots. member this.Rest() = let stats = this.StatBlock currentHitPoints <- stats.MaxHitPoints temporaryHitPoints <- 0 currentSlots <- stats.MaxSlots /// Amount of currency in the character's bank account. member __.CurrentCoin = currentCoin /// Add some gold to the character's bank account. member __.Gain(gp:decimal<gp>) = if gp < 0m<gp> then invalidArg "gp" "Currency amount must be non-negative" |
︙ | ︙ | |||
402 403 404 405 406 407 408 | stats <- None do let layer = Open5eStatLayer() layer.SetJson(object) layers.Add(layer) | < < | | 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | stats <- None do let layer = Open5eStatLayer() layer.SetJson(object) layers.Add(layer) this.Rest() currentCoin <- 0m<gp> /// Deserialize the character from an XML representation. member this.SetXml(element:XElement) = layers.Clear() stats <- None |
︙ | ︙ | |||
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | } temporaryHitPoints <- query { for it in element.Descendants(XNamespace.None + "TemporaryHitPoints") do select (int <| it.Value.Trim()) exactlyOneOrDefault } currentCoin <- query { for it in element.Descendants(XNamespace.None + "CurrentCoin") do select (Coin.ofString <| it.Value.Trim()) exactlyOneOrDefault } /// Serialize the character to an XML representation. member this.GetXml() = let element = XElement(XNamespace.None + "Character") for layer in layers do element.Add(layer.GetXml()) element.Add(XElement(XNamespace.None + "CurrentHitPoints", string currentHitPoints)) element.Add(XElement(XNamespace.None + "TemporaryHitPoints", string temporaryHitPoints)) element.Add(XElement(XNamespace.None + "CurrentCoin", Coin.toString currentCoin)) element /// File path where the character is stored. member __.FullPath = fullPath | > > > > > > > > > > | 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 | } temporaryHitPoints <- query { for it in element.Descendants(XNamespace.None + "TemporaryHitPoints") do select (int <| it.Value.Trim()) exactlyOneOrDefault } currentSlots <- query { for it in element.Descendants(XNamespace.None + "CurrentSlots") do select (Slot.ofString <| it.Attribute(XNamespace.None + "Level").Value, int <| it.Value) } |> EnumMap currentCoin <- query { for it in element.Descendants(XNamespace.None + "CurrentCoin") do select (Coin.ofString <| it.Value.Trim()) exactlyOneOrDefault } /// Serialize the character to an XML representation. member this.GetXml() = let element = XElement(XNamespace.None + "Character") for layer in layers do element.Add(layer.GetXml()) element.Add(XElement(XNamespace.None + "CurrentHitPoints", string currentHitPoints)) element.Add(XElement(XNamespace.None + "TemporaryHitPoints", string temporaryHitPoints)) for KeyValue (slot, v) in currentSlots do element.Add( XElement(XNamespace.None + "CurrentSlots", XAttribute(XNamespace.None + "Level", Slot.toString slot), string v) ) element.Add(XElement(XNamespace.None + "CurrentCoin", Coin.toString currentCoin)) element /// File path where the character is stored. member __.FullPath = fullPath |
︙ | ︙ |
Changes to DragonDice/CustomStatLayer.fs.
︙ | ︙ | |||
24 25 26 27 28 29 30 31 32 33 34 35 36 37 | member val Name : Option<string> = None with get, set /// Difficulty class of attacks against the character. Added to existing value when applied. member val ArmorClass = 0 with get, set /// Natural hit point maximum of the character. Added to existing value when applied. member val MaxHitPoints = 0 with get, set /// Movements speeds of the character. Added to existing values when applied. member val Speeds = EnumMap<Speed, int<ft/rnd>>() with get, set /// Ability scores of the character. Added to existing values when applied. member val Abilities = EnumMap<Ability, int>() with get, set | > > > | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | member val Name : Option<string> = None with get, set /// Difficulty class of attacks against the character. Added to existing value when applied. member val ArmorClass = 0 with get, set /// Natural hit point maximum of the character. Added to existing value when applied. member val MaxHitPoints = 0 with get, set /// Total spell slots available to the character. Added to existing value when applied. member val MaxSlots = EnumMap<Slot, int>() with get, set /// Movements speeds of the character. Added to existing values when applied. member val Speeds = EnumMap<Speed, int<ft/rnd>>() with get, set /// Ability scores of the character. Added to existing values when applied. member val Abilities = EnumMap<Ability, int>() with get, set |
︙ | ︙ | |||
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | } this.MaxHitPoints <- query { for it in element.Descendants(XNamespace.None + "MaxHitPoints") do select (int <| it.Value.Trim()) exactlyOneOrDefault } this.Speeds <- query { for it in element.Descendants(XNamespace.None + "Speed") do select (Speed.ofString <| it.Attribute(XNamespace.None + "Name").Value, int(it.Value.Trim()) * 1<ft/rnd>) } |> EnumMap this.Abilities <- query { | > > > > > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | } this.MaxHitPoints <- query { for it in element.Descendants(XNamespace.None + "MaxHitPoints") do select (int <| it.Value.Trim()) exactlyOneOrDefault } this.MaxSlots <- query { for it in element.Descendants(XNamespace.None + "MaxSlots") do select (Slot.ofString <| it.Attribute(XNamespace.None + "Level").Value, int <| it.Value.Trim()) } |> EnumMap this.Speeds <- query { for it in element.Descendants(XNamespace.None + "Speed") do select (Speed.ofString <| it.Attribute(XNamespace.None + "Name").Value, int(it.Value.Trim()) * 1<ft/rnd>) } |> EnumMap this.Abilities <- query { |
︙ | ︙ | |||
106 107 108 109 110 111 112 113 114 115 116 117 118 119 | | None -> () if this.ArmorClass <> 0 then element.Add(XElement(XNamespace.None + "ArmorClass", sprintf "%+d" this.ArmorClass)) if this.MaxHitPoints <> 0 then element.Add(XElement(XNamespace.None + "MaxHitPoints", sprintf "%+d" this.MaxHitPoints)) this.Speeds |> Seq.iter (fun (KeyValue (speed, v)) -> if v <> 0<ft/rnd> then element.Add( XElement(XNamespace.None + "Speed", XAttribute(XNamespace.None + "Name", Speed.toString speed), sprintf "%+d" v) ) | > > > > > > > > | 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | | None -> () if this.ArmorClass <> 0 then element.Add(XElement(XNamespace.None + "ArmorClass", sprintf "%+d" this.ArmorClass)) if this.MaxHitPoints <> 0 then element.Add(XElement(XNamespace.None + "MaxHitPoints", sprintf "%+d" this.MaxHitPoints)) this.MaxSlots |> Seq.iter (fun (KeyValue (slot, v)) -> if v <> 0 then element.Add( XElement(XNamespace.None + "MaxSlots", XAttribute(XNamespace.None + "Level", Slot.toString slot), sprintf "%+d" v) ) ) this.Speeds |> Seq.iter (fun (KeyValue (speed, v)) -> if v <> 0<ft/rnd> then element.Add( XElement(XNamespace.None + "Speed", XAttribute(XNamespace.None + "Name", Speed.toString speed), sprintf "%+d" v) ) |
︙ | ︙ | |||
166 167 168 169 170 171 172 173 174 175 176 177 178 179 | | Some v -> stats <- { stats with Name = v } | None -> () stats <- { stats with ArmorClass = stats.ArmorClass + this.ArmorClass MaxHitPoints = stats.MaxHitPoints + this.MaxHitPoints Abilities = EnumMap.map2Values (+) stats.Abilities this.Abilities Speeds = EnumMap.map2Values (+) stats.Speeds this.Speeds Defenses = List.append this.Defenses stats.Defenses Skills = EnumMap.map2Values (+) stats.Skills this.Skills Senses = EnumMap.map2Values max stats.Senses this.Senses Languages = Set.union stats.Languages this.Languages } | > | 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | | Some v -> stats <- { stats with Name = v } | None -> () stats <- { stats with ArmorClass = stats.ArmorClass + this.ArmorClass MaxHitPoints = stats.MaxHitPoints + this.MaxHitPoints MaxSlots = EnumMap.map2Values (+) stats.MaxSlots this.MaxSlots Abilities = EnumMap.map2Values (+) stats.Abilities this.Abilities Speeds = EnumMap.map2Values (+) stats.Speeds this.Speeds Defenses = List.append this.Defenses stats.Defenses Skills = EnumMap.map2Values (+) stats.Skills this.Skills Senses = EnumMap.map2Values max stats.Senses this.Senses Languages = Set.union stats.Languages this.Languages } |
︙ | ︙ |
Changes to DragonDice/DerivedStatLayer.fs.
︙ | ︙ | |||
45 46 47 48 49 50 51 | let addArmorClass = Ability.modifier stats.Abilities.[Ability.Dexterity] |> match this.MaxArmorClassModifier with Some v -> min v | None -> id let addHitPoints = Ability.modifier stats.Abilities.[Ability.Constitution] |> (+) (if Race.tryParse stats.Race = Some Race.HillDwarf then 1 else 0) | | > | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | let addArmorClass = Ability.modifier stats.Abilities.[Ability.Dexterity] |> match this.MaxArmorClassModifier with Some v -> min v | None -> id let addHitPoints = Ability.modifier stats.Abilities.[Ability.Constitution] |> (+) (if Race.tryParse stats.Race = Some Race.HillDwarf then 1 else 0) |> (*) stats.TotalLevel { stats with ArmorClass = stats.ArmorClass + addArmorClass MaxHitPoints = stats.MaxHitPoints + addHitPoints MaxSlots = stats.MaxSlots |> EnumMap.map2Values (+) (Slot.ofLevel stats.CasterLevel) Skills = stats.Skills |> EnumMap.mapValues ((*) stats.ProficiencyBonus) } interface IStatLayer with override this.SetXml(element) = this.SetXml(element) override this.GetXml() = this.GetXml() override this.Priority = 0xF0 |
︙ | ︙ |
Changes to DragonDice/DragonDice.fsproj.
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <Compile Include="Speed.fs" /> <Compile Include="Damage.fs" /> <Compile Include="Defense.fs" /> <Compile Include="Sense.fs" /> <Compile Include="EnumMap.fs" /> <Compile Include="Race.fs" /> <Compile Include="Profession.fs" /> <Compile Include="StatBlock.fs" /> <Compile Include="LRUCache.fs" /> <Compile Include="BasicStatLayer.fs" /> <Compile Include="Open5eStatLayer.fs" /> <Compile Include="LevelStatLayer.fs" /> <Compile Include="DerivedStatLayer.fs" /> <Compile Include="CustomStatLayer.fs" /> | > | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <Compile Include="Speed.fs" /> <Compile Include="Damage.fs" /> <Compile Include="Defense.fs" /> <Compile Include="Sense.fs" /> <Compile Include="EnumMap.fs" /> <Compile Include="Race.fs" /> <Compile Include="Profession.fs" /> <Compile Include="Slot.fs" /> <Compile Include="StatBlock.fs" /> <Compile Include="LRUCache.fs" /> <Compile Include="BasicStatLayer.fs" /> <Compile Include="Open5eStatLayer.fs" /> <Compile Include="LevelStatLayer.fs" /> <Compile Include="DerivedStatLayer.fs" /> <Compile Include="CustomStatLayer.fs" /> |
︙ | ︙ |
Changes to DragonDice/LevelStatLayer.fs.
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 | element /// Apply all defined customizations to the given character stats. member this.Apply(stats:StatBlock) = let profession = (if String.IsNullOrEmpty stats.Profession then String.Empty else ", ") + sprintf "%O (%d)" this.Profession this.Count let hitDice = Profession.hitDice this.Profession let addHitPoints = | > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | element /// Apply all defined customizations to the given character stats. member this.Apply(stats:StatBlock) = let profession = (if String.IsNullOrEmpty stats.Profession then String.Empty else ", ") + sprintf "%O (%d)" this.Profession this.Count let hitDice = Profession.hitDice this.Profession let addHitPoints = let count, init = if stats.TotalLevel = 0 then this.Count - 1, hitDice.Maximum else this.Count, 0 init + count * int(ceil <| Dice.estimate hitDice) let addSlots = if this.Profession = Profession.Warlock then let slot = if this.Count >= 9 then Slot.Level5 elif this.Count >= 7 then Slot.Level4 elif this.Count >= 5 then Slot.Level3 elif this.Count >= 3 then Slot.Level2 else Slot.Level1 let n = if this.Count >= 17 then 4 elif this.Count >= 11 then 3 elif this.Count >= 2 then 2 elif this.Count >= 1 then 1 else 0 EnumMap[slot, n] else EnumMap() let addCasterLevel = match this.Profession with | Profession.Alchemist | Profession.Bard | Profession.Cleric | Profession.Druid | Profession.Sorcerer | Profession.Wizard -> this.Count | Profession.Paladin | Profession.Ranger -> this.Count / 2 | _ -> 0 { stats with Profession = stats.Profession + profession MaxHitPoints = stats.MaxHitPoints + addHitPoints MaxSlots = EnumMap.map2Values (+) stats.MaxSlots addSlots Skills = EnumMap.map2Values max stats.Skills <| Profession.proficiencies this.Profession TotalLevel = stats.TotalLevel + this.Count CasterLevel = stats.CasterLevel + addCasterLevel } interface IStatLayer with override this.SetXml(element) = this.SetXml(element) override this.GetXml() = this.GetXml() override this.Priority = 0x10 override this.Apply(stats) = this.Apply(stats) |
Changes to DragonDice/Open5eStatLayer.fs.
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /// Base layer for monster stats imported from Open5e. type Open5eStatLayer() = static let SemicolonPattern = Regex(@"\s*;\s*", RegexOptions.CultureInvariant) static let CommaPattern = Regex(@"\s*,\s*(?:and\s+)?", RegexOptions.CultureInvariant ||| RegexOptions.IgnoreCase) static let RangePattern = Regex(@"^(.+)\s+(\d+)\s+ft\.?$", RegexOptions.CultureInvariant) static let NonMagicalPattern = Regex(@"\s+from\s+non-?magical\b", RegexOptions.CultureInvariant) static let SpellcastingPattern = Regex(@"\bspellcasting\s+ability\s+is\s+(\w+)\s+\(spell\s+save\s+DC\s+(\d+)\)", RegexOptions.CultureInvariant ||| RegexOptions.IgnoreCase) static let parseStats (stats:JObject) : StatBlock = let name = stats.["name"].ToObject<string>() let typ = stats.["type"].ToObject<string>() |> CultureInfo.InvariantCulture.TextInfo.ToTitleCase let subtype = stats.["subtype"].ToObject<string>() |> CultureInfo.InvariantCulture.TextInfo.ToTitleCase let ac = stats.["armor_class"].ToObject<int>() let hp = stats.["hit_points"].ToObject<int>() let size = stats.["size"].ToObject<Size>() | > > | > > > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /// Base layer for monster stats imported from Open5e. type Open5eStatLayer() = static let SemicolonPattern = Regex(@"\s*;\s*", RegexOptions.CultureInvariant) static let CommaPattern = Regex(@"\s*,\s*(?:and\s+)?", RegexOptions.CultureInvariant ||| RegexOptions.IgnoreCase) static let RangePattern = Regex(@"^(.+)\s+(\d+)\s+ft\.?$", RegexOptions.CultureInvariant) static let NonMagicalPattern = Regex(@"\s+from\s+non-?magical\b", RegexOptions.CultureInvariant) static let SpellcastingPattern = Regex(@"\bspellcasting\s+ability\s+is\s+(\w+)\s+\(spell\s+save\s+DC\s+(\d+)\)", RegexOptions.CultureInvariant ||| RegexOptions.IgnoreCase) static let SpellcasterPattern = Regex(@"\bis\s+a\s+(\d+)(?:st|nd|rd)[-\s]level\s+spellcaster\b", RegexOptions.CultureInvariant ||| RegexOptions.IgnoreCase) static let parseStats (stats:JObject) : StatBlock = let name = stats.["name"].ToObject<string>() let typ = stats.["type"].ToObject<string>() |> CultureInfo.InvariantCulture.TextInfo.ToTitleCase let subtype = stats.["subtype"].ToObject<string>() |> CultureInfo.InvariantCulture.TextInfo.ToTitleCase let ac = stats.["armor_class"].ToObject<int>() let hp = stats.["hit_points"].ToObject<int>() let size = stats.["size"].ToObject<Size>() let level = match Int32.TryParse(stats.["challenge_rating"].ToObject<string>(), NumberStyles.Integer, CultureInfo.InvariantCulture) with | true, v -> v | false, _ -> 1 let prof = (level + 3) / 4 + 1 let speeds = stats.["speed"].ToObject<JObject>().Properties() |> Seq.map (fun it -> Speed.ofString it.Path, it.Value.ToObject<int<ft/rnd>>()) |> EnumMap |
︙ | ︙ | |||
128 129 130 131 132 133 134 135 136 137 138 139 140 141 | | _ -> () yield Skill.StrengthAttack, prof yield Skill.DexterityAttack, prof } |> EnumMap let senses = ( match stats.TryGetValue("senses") with | true, it when it.Type <> JTokenType.Null -> it.ToObject<string>() |> CommaPattern.Split | > > > > > > > > > > > > > > > | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | | _ -> () yield Skill.StrengthAttack, prof yield Skill.DexterityAttack, prof } |> EnumMap let casterLevel = match stats.TryGetValue("special_abilities") with | true, skills when skills.Type = JTokenType.Array -> seq { for it in skills.ToObject<JObject[]>() do let m = SpellcasterPattern.Match(it.["desc"].ToObject<string>()) if m.Success then match Int32.TryParse(m.Groups.[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture) with | true, level -> yield level | false, _ -> () } |> Seq.fold max 0 | _ -> 0 let senses = ( match stats.TryGetValue("senses") with | true, it when it.Type <> JTokenType.Null -> it.ToObject<string>() |> CommaPattern.Split |
︙ | ︙ | |||
170 171 172 173 174 175 176 177 178 179 180 181 182 183 | { Name = name Race = if not (String.IsNullOrEmpty subtype) then sprintf "%s (%s)" typ subtype else typ Profession = String.Empty ArmorClass = ac MaxHitPoints = hp Size = size Speeds = speeds Abilities = abilities Defenses = defenses Skills = skills Senses = senses Languages = languages | > | > | 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | { Name = name Race = if not (String.IsNullOrEmpty subtype) then sprintf "%s (%s)" typ subtype else typ Profession = String.Empty ArmorClass = ac MaxHitPoints = hp MaxSlots = Slot.ofLevel casterLevel Size = size Speeds = speeds Abilities = abilities Defenses = defenses Skills = skills Senses = senses Languages = languages TotalLevel = level CasterLevel = casterLevel } static let cache = LRUCache<string, StatBlock>(1024) let mutable slug = "commoner" let mutable stats : Option<StatBlock> = None |
︙ | ︙ |
Changes to DragonDice/Profession.fs.
︙ | ︙ | |||
12 13 14 15 16 17 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *) namespace Murphy.DragonDice open System | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *) namespace Murphy.DragonDice open System /// Character class tags. type Profession = | Alchemist = 0 | Barbarian = 1 | Bard = 2 | Cleric = 3 |
︙ | ︙ |
Added DragonDice/Slot.fs.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | (* * Copyright (c) 2019-2020 by Thomas C. Chust <chust@web.de> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *) namespace Murphy.DragonDice open System /// Spell slot tags. type Slot = | Level1 = 1 | Level2 = 2 | Level3 = 3 | Level4 = 4 | Level5 = 5 | Level6 = 6 | Level7 = 7 | Level8 = 8 | Level9 = 9 /// Companion module for the spell slot type. [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] module Slot = /// Map a spell slot tag to a positive slot symbol let positiveSymbol (slot:Slot) = 0x2460 + int slot - 1 |> char /// Map a spell slot tag to a negative slot symbol let negativeSymbol (slot:Slot) = 0x2776 + int slot - 1 |> char /// Format total and used spell slots into a summary string. let toSummary (total:EnumMap<Slot, int>) (current:EnumMap<Slot, int>) = let buffer = Text.StringBuilder(32) for KeyValue (slot, n) in total do if n > 0 then if buffer.Length > 0 then buffer.Append(' ') |> ignore let u = current.[slot] for i = 0 to n-1 do buffer.Append(if i < u then positiveSymbol slot else negativeSymbol slot) |> ignore buffer.ToString() /// Get standard spell slots for total spellcaster level. let ofLevel level = seq { if level >= 3 then yield Slot.Level1, 4 elif level >= 2 then yield Slot.Level1, 3 elif level >= 1 then yield Slot.Level1, 2 if level >= 4 then yield Slot.Level2, 3 elif level >= 3 then yield Slot.Level2, 2 if level >= 6 then yield Slot.Level3, 3 elif level >= 5 then yield Slot.Level3, 2 if level >= 9 then yield Slot.Level4, 3 elif level >= 8 then yield Slot.Level4, 2 elif level >= 7 then yield Slot.Level4, 1 if level >= 18 then yield Slot.Level5, 3 elif level >= 10 then yield Slot.Level5, 2 elif level >= 9 then yield Slot.Level5, 1 if level >= 19 then yield Slot.Level6, 2 elif level >= 11 then yield Slot.Level6, 1 if level >= 20 then yield Slot.Level7, 2 elif level >= 13 then yield Slot.Level7, 1 if level >= 15 then yield Slot.Level8, 1 if level >= 17 then yield Slot.Level9, 1 } |> EnumMap /// Map a spell slot tag to a name. let inline toString (slot:Slot) = string (int slot) /// Try to map a spell slot name or symbol to a tag. let tryParse (s:string) = match s.ToLowerInvariant() with | "①" | "❶" | "1" -> Some Slot.Level1 | "②" | "❷" | "2" -> Some Slot.Level2 | "③" | "❸" | "3" -> Some Slot.Level3 | "④" | "❹" | "4" -> Some Slot.Level4 | "⑤" | "❺" | "5" -> Some Slot.Level5 | "⑥" | "❻" | "6" -> Some Slot.Level6 | "⑦" | "❼" | "7" -> Some Slot.Level7 | "⑧" | "❽" | "8" -> Some Slot.Level8 | "⑨" | "❾" | "9" -> Some Slot.Level9 | _ -> None /// Map a spell slot name to a tag. let ofString s = match tryParse s with | Some slot -> slot | None -> raise (FormatException "Invalid spell slot tag") |
Changes to DragonDice/StatBlock.fs.
︙ | ︙ | |||
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /// Description of the character's profession. /// May contain character classes and levels. Profession : string /// Difficulty class of attacks against the character. ArmorClass : int /// Natural hit point maximum of the character. MaxHitPoints : int /// Physical size of the character. Size : Size /// Movements speeds of the character. Speeds : EnumMap<Speed, int<ft/rnd>> /// Ability scores of the character. Abilities : EnumMap<Ability, int> /// Damage immunities, resistances, or vulnerabilities of the character. /// Each pair contains damage type flags and a damage multiplier. Defenses : List<Damage * float> /// Modifiers for saving throws and skill checks, including proficiency bonus but excluding ability modifiers. Skills : EnumMap<Skill, int> /// Detection ranges for the character's senses. Senses : EnumMap<Sense, int<ft>> /// Languages the character knows. Languages : Set<Language> /// Total character level. | > > | > > | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | /// Description of the character's profession. /// May contain character classes and levels. Profession : string /// Difficulty class of attacks against the character. ArmorClass : int /// Natural hit point maximum of the character. MaxHitPoints : int /// Total spell slots of the character. MaxSlots : EnumMap<Slot, int> /// Physical size of the character. Size : Size /// Movements speeds of the character. Speeds : EnumMap<Speed, int<ft/rnd>> /// Ability scores of the character. Abilities : EnumMap<Ability, int> /// Damage immunities, resistances, or vulnerabilities of the character. /// Each pair contains damage type flags and a damage multiplier. Defenses : List<Damage * float> /// Modifiers for saving throws and skill checks, including proficiency bonus but excluding ability modifiers. Skills : EnumMap<Skill, int> /// Detection ranges for the character's senses. Senses : EnumMap<Sense, int<ft>> /// Languages the character knows. Languages : Set<Language> /// Total character level. TotalLevel : int /// Spellcaster level of the character. CasterLevel : int } /// The proficiency bonus derived from the total character level. member this.ProficiencyBonus = (this.TotalLevel + 3) / 4 + 1 /// Compute the combined proficiency and ability modifier for a given skill. member this.GetModifier(skill:Skill) = this.Skills.[skill] + Ability.modifier this.Abilities.[Skill.ability skill] /// Interface for character stats builders. type IStatLayer = |
︙ | ︙ | |||
80 81 82 83 84 85 86 87 88 89 90 91 92 93 | let Empty = { Name = String.Empty Race = String.Empty Profession = String.Empty ArmorClass = 0 MaxHitPoints = 0 Size = Size.Medium Speeds = EnumMap() Abilities = EnumMap() Defenses = List.empty Skills = EnumMap() Senses = EnumMap() Languages = Set.empty | > | > | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | let Empty = { Name = String.Empty Race = String.Empty Profession = String.Empty ArmorClass = 0 MaxHitPoints = 0 MaxSlots = EnumMap() Size = Size.Medium Speeds = EnumMap() Abilities = EnumMap() Defenses = List.empty Skills = EnumMap() Senses = EnumMap() Languages = Set.empty TotalLevel = 0 CasterLevel = 0 } /// Build character stats from a list of layers. let build (layers:seq<#IStatLayer>) = layers |> Seq.fold (fun stats layer -> layer.Apply(stats)) Empty |