diff --git a/index.html b/index.html
index 08be214..8b7099b 100644
--- a/index.html
+++ b/index.html
@@ -1,7 +1,7 @@
- RPG Utils
+ Shining Sword
diff --git a/main.sass b/main.sass
index 666128c..02edbaf 100644
--- a/main.sass
+++ b/main.sass
@@ -11,7 +11,12 @@ html
color-scheme: dark light
body
- min-height: 100vh
+ height: 100svh
+ position: absolute // prevent margin collapse, to prevent unneeded scrollbars
+ width: 100%
+
+#feliz-app, #feliz-app > div, #feliz-app > div > div
+ max-height: 100vh
img, picture, svg, video
display: block
@@ -19,6 +24,10 @@ img, picture, svg, video
// ShiningSword-specific logic here
+ul
+ list-style-position: inside
+ margin-left: 1rem
+
.slideFromTop
animation: fromTop 0.5s ease-in-out
.slideFromLeft
@@ -86,6 +95,7 @@ img, picture, svg, video
transform: scale(0)
.hasMargins
+ box-sizing: border-box
margin: 3px
.header
@@ -109,9 +119,30 @@ img, picture, svg, video
display: none
.srcLink
max-width: 6rem
- float: right
+ position: absolute
+ right: 3px
background-color: white
+.horizontalStack
+ display: flex
+ width: calc(100% - 6px)
+ gap: 10px
+ > *
+ flex: 1
+ max-height: 100%
+
+.scrollParent
+ display: flex
+ flex-direction: column
+ align-items: flex-start
+ max-height: 100%
+
+.scrollable
+ overflow: auto
+ flex: 1
+ align-self: stretch
+
.mainPage // page styling for most pages in ShiningSword: display a basic layout with a header, some data items and links
@extend .fadeIn
@extend .hasMargins
+ max-height: calc(100vh - 6px)
diff --git a/src/UI/Components/PriestSpells.fs b/src/UI/Components/PriestSpells.fs
index 1fd5728..2c05395 100644
--- a/src/UI/Components/PriestSpells.fs
+++ b/src/UI/Components/PriestSpells.fs
@@ -24,7 +24,7 @@ let consolidateSpells spheres =
let consolidateSpheres (spells: Spell list) spheres =
let spells = spells |> List.map (fun spell -> spell.name, spell) |> Map.ofList
spheres |> List.map (fun sphere -> { sphere with spells = sphere.spells |> List.map (fun spell -> spells.[spell.name]) })
-let spheres = """
+let spheresData = """
All: Bless 1, Combine 1, Detect Evil 1, Purify Food & Drink 1, Atonement 5
Animal: Animal Friendship 1, Invisibility to Animals 1, Locate Animals or Plants 1, Charm Person or Mammal 2, Messenger 2,
Snake Charm 2, Speak With Animals 2, Hold Animal 3, Summon Insects 3, Animal Summoning I 4, Call Woodland Beings 4,
@@ -54,6 +54,10 @@ Summoning: Abjure 4, Animal Summoning I 4, Call Woodland Beings 4, Animal Summon
Sun: Light 1, Continual Light 3, Starshine 3, Moonbeam 5, Rainbow 5, Sunray 7
Weather: Faerie Fire 1, Obscurement 2, Call Lightning 3, Control Temperature 10' Radius 4, Protection From Lightning 4, Control Winds 5, Rainbow 5, Weather Summoning 6, Control Weather 7
"""
+let deityData = """
+ Isis: All, Protection, Necromantic*
+ Osiris: Necromantic, Summoning
+"""
module private Parser =
// #load @"c:\code\rpg\src\Core\Common.fs"
// #load @"c:\code\rpg\src\Core\CQRS.fs"
@@ -88,6 +92,17 @@ module private Parser =
| Sphere(lhs, OWS (Spheres(rhs, rest))) -> Some(lhs::rhs, rest)
| Sphere(v, rest) -> Some([v], rest)
| _ -> None
+ let (|SphereRef|_|) = function
+ | Word(name, Char('*', rest)) -> Some({ sphere = name; access = Minor }, rest)
+ | Word(name, rest) -> Some({ sphere = name; access = Major }, rest)
+ | _ -> None
+ let rec (|SphereRefs|_|) = pack <| function
+ | SphereRef(lhs, OWSStr "," (SphereRefs(rhs, rest))) -> Some(lhs :: rhs, rest)
+ | SphereRef(v, rest) -> Some([v], rest)
+ | _ -> None
+ let rec (|Deity|_|) = function
+ | NameChunk(name, OWSStr ":" (SphereRefs(spheres, rest))) -> Some({ name = name; spheres = spheres }, rest)
+ | _ -> None
// let partial (|Recognizer|_|) txt = match ParseArgs.Init txt with | Recognizer(v, _) -> v
// let partialR (|Recognizer|_|) txt = match ParseArgs.Init txt with | Recognizer(v, (input, pos)) -> v, input.input.Substring pos
// partial (|Spheres|_|) spheres |> List.collect _.spells |> List.filter (fun spell -> spell.name = "Chariot of Sustarre")
@@ -100,7 +115,7 @@ module Storage =
let key = "Spheres"
let cacheRead, cacheInvalidate = Cache.create()
let read (): Sphere list =
- cacheRead (thunk2 read key (fun () -> Packrat.parser Parser.(|Spheres|_|) (spheres.Trim()) |> fun spheres -> spheres |> consolidateSpheres (consolidateSpells spheres)))
+ cacheRead (thunk2 read key (fun () -> Packrat.parser Parser.(|Spheres|_|) (spheresData.Trim()) |> fun spheres -> spheres |> consolidateSpheres (consolidateSpells spheres)))
let write (v: Sphere list) =
write key v
cacheInvalidate()
@@ -116,7 +131,7 @@ module Storage =
let key = "Deities"
let cacheRead, cacheInvalidate = Cache.create()
let read (): Deity list =
- cacheRead (thunk2 read key (thunk []))
+ cacheRead (thunk2 read key (fun () -> deityData.Trim().Split("\r") |> List.ofArray |> List.map (Packrat.parser Parser.(|Deity|_|))))
let write (v: Deity list) =
write key v
cacheInvalidate()
@@ -142,5 +157,22 @@ let filteredSpells (filter: string) (model: Model) =
| "" -> model.options.spells
| filter ->
let fragments = filter.Split(' ') |> List.ofArray
- model.options.spells |> List.filter (fun spell -> fragments |> List.every (fun fragment -> String.containsIgnoreCase (spell.ToString()) fragment))
+ let grantorsBySphere =
+ [
+ for sphere in model.options.spheres do
+ let grantors = [
+ for d in model.options.deities do
+ match d.spheres |> List.tryFind (fun s -> s.sphere = sphere.name) with
+ | Some v -> d.name, v.access
+ | None -> ()
+ ]
+ sphere.name, grantors
+ ]
+ |> Map.ofList
+ let isMatch (spell: Spell) fragment =
+ if String.containsIgnoreCase(spell.ToString()) fragment then true
+ else spell.spheres |> List.exists (fun sphere -> grantorsBySphere[sphere] |> List.exists (fun (deity, access) -> String.containsIgnoreCase deity fragment && (spell.level <= 3 || access = Major)))
+
+ let matchingDeities spell = model.options.deities |> List.filter (fun deity -> fragments |> List.exists (fun fragment -> String.containsIgnoreCase deity.name fragment))
+ model.options.spells |> List.filter (fun spell -> fragments |> List.every (isMatch spell))
diff --git a/src/UI/Components/PriestSpellsView.fs b/src/UI/Components/PriestSpellsView.fs
index bb6458d..bdf93f4 100644
--- a/src/UI/Components/PriestSpellsView.fs
+++ b/src/UI/Components/PriestSpellsView.fs
@@ -8,22 +8,43 @@ open UI.PriestSpells
let View() =
let model, dispatch = React.useElmishSimple init update
let filter, setFilter = React.useState ""
- Html.div [
- Html.h1 "Priest Spells"
- Html.input [
- prop.value filter
- prop.placeholder "Spell name, sphere or deity"
- prop.onChange (fun txt -> setFilter txt)
+ class' "mainPage horizontalStack" Html.div [
+ class' "scrollParent" Html.div [
+ Html.h1 "Priest Spells"
+ Html.input [
+ prop.value filter
+ prop.placeholder "Spell name, sphere or deity"
+ prop.onChange (fun txt -> setFilter txt)
+ ]
+ class' "scrollable" Html.ul [
+ let spells = filteredSpells filter model |> List.groupBy _.level |> List.sortBy fst
+ for level, spells in spells do
+ let ordinalToText = function
+ | 1 -> "1st"
+ | 2 -> "2nd"
+ | 3 -> "3rd"
+ | n -> sprintf "%dth" n
+ Html.h2 [prop.text (ordinalToText level)]
+ for spell in spells do
+ Html.li [
+ Html.span [
+ prop.text ($"""{spell.name} ({spell.spheres |> String.join "/"})""")
+ ]
+ Html.span [
+ prop.text (match model.picks.TryFind(spell.name) with Some(n) -> $" ({n})" | None -> "")
+ ]
+ ]
+ ]
]
- Html.ul [
- for spell in filteredSpells filter model do
- Html.li [
- Html.span [
- prop.text (spell.ToString())
+ Html.div [
+ Html.h2 "Gods"
+ Html.ul [
+ for deity in model.options.deities do
+ Html.li [prop.text deity.name]
+ Html.ul [
+ for sphere in deity.spheres do
+ Html.li [prop.text (sphere.sphere + if sphere.access = Minor then "*" else "")]
]
- Html.span [
- prop.text (match model.picks.TryFind(spell.name) with Some(n) -> $" ({n})" | None -> "")
- ]
- ]
+ ]
]
]