Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
75fa943
Fix the hidden multiclass skill pick: condition the modifier, not jus…
codeGlaze Sep 20, 2026
5451b29
Merge integration into fix/hidden-multiclass-skill-pick
codeGlaze Oct 1, 2026
65bb097
Pin the hidden-pick research: what the build does today, claim by claim
codeGlaze Oct 1, 2026
babf234
E2E research: which hidden-pick states a player can reach
codeGlaze Oct 1, 2026
e723916
E2E research: shared-slot pick order and one-pick slot clicks
codeGlaze Oct 3, 2026
ab89f88
E2E research: which picks are trapped when their homebrew is not loaded
codeGlaze Oct 4, 2026
756d97d
Merge integration into fix/hidden-multiclass-skill-pick (#47 picks, #…
codeGlaze Oct 5, 2026
b757400
cljs-harness: pass a green full-suite run again
codeGlaze Oct 5, 2026
65299c1
Drop the unused offered-names from multiclass_hidden_pick_test
codeGlaze Oct 5, 2026
ab643c6
Merge integration into fix/hidden-multiclass-skill-pick (#50 spellboo…
codeGlaze Oct 6, 2026
226de9b
picks: find the picks that no longer apply (disqualified, overflow)
codeGlaze Oct 6, 2026
14968c1
picks: the planned hold (to-planned, from-planned, update-planned)
codeGlaze Oct 6, 2026
07037cf
picks: reword a comment the comment check read as history; rename a t…
codeGlaze Oct 6, 2026
674c7ab
Hold picks that stop applying, in the builder
codeGlaze Oct 6, 2026
a272ccc
Settle a draft's picks when it is opened, not at its first edit
codeGlaze Oct 6, 2026
74fcb11
Remove the skills-only fix; the hold covers every pick
codeGlaze Oct 6, 2026
993a831
Deleting the first class keeps the class that becomes first
codeGlaze Oct 6, 2026
8390e95
Settle picks after an edit, never inside it
codeGlaze Oct 6, 2026
79337fe
Character pages settle only when their character or template changes
codeGlaze Oct 6, 2026
427745d
The builder's Save settles picks before sending
codeGlaze Oct 6, 2026
ffa335c
The hold knows which character it holds for
codeGlaze Oct 6, 2026
59c1d0d
A held pick waits while its own requirement fails
codeGlaze Oct 6, 2026
82a5ba3
picks/addresses: every stored pick with the address remove-at takes
codeGlaze Oct 6, 2026
0983bec
Hold the picks an edit removes with their entry
codeGlaze Oct 7, 2026
8fcd174
A designed test pack, and a switch between two real subclasses
codeGlaze Oct 7, 2026
5bfa48b
Merge integration into fix/hidden-multiclass-skill-pick (#59 CI, SRD …
codeGlaze Oct 13, 2026
00641b1
A held pick says what brings it back
codeGlaze Oct 13, 2026
e1180c7
The Planned marker in an open section's row
codeGlaze Oct 13, 2026
1021111
The planned line under each class row
codeGlaze Oct 13, 2026
52228ae
The notice box: an icon and a balanced amber ring
codeGlaze Oct 13, 2026
7c2316d
The restore notice, with Undo and Redo
codeGlaze Oct 13, 2026
7ca45ad
The restore notice fits its one line
codeGlaze Oct 13, 2026
e28cf36
The restore notice is sized by the layout, not its text
codeGlaze Oct 13, 2026
47ee24e
Tell the player when an opened character has choices that don't apply
codeGlaze Oct 14, 2026
189ee34
Label the on-open notice's toggle "Details"
codeGlaze Oct 14, 2026
bfacd90
Show a notice's details as a band of two columns
codeGlaze Oct 14, 2026
d835e84
Give the notice ring the light theme's blue
codeGlaze Oct 14, 2026
0c1f5a6
Fold the branch changelog into Summer Patch
codeGlaze Oct 15, 2026
b452986
Say a planned pick once: the panel leaves out what its line says
codeGlaze Oct 15, 2026
1f99403
Give the planned-pick controls 44px taps
codeGlaze Oct 15, 2026
4fe6956
Open a subclass's spell choices from their class level; review fixes
codeGlaze Oct 15, 2026
2b1705d
Changelog: drop the subclass spell-gate line; say who reaches the gate
codeGlaze Oct 15, 2026
fa4ce91
CI: poll for the killed process in the Windows signal_pid check
codeGlaze Oct 16, 2026
d571b66
Show My Content's repair panel and missing-link marks without opening…
codeGlaze Oct 16, 2026
088d249
quarantine-restore.js: an entry with no source loads under its pack
codeGlaze Oct 16, 2026
7781d43
Order the options column by number and level, not as text
codeGlaze Oct 16, 2026
f57f71c
Make starting class: a "1st" badge on each class row
codeGlaze Oct 16, 2026
fcccc1f
Choices: one list of every choice a character stores
codeGlaze Oct 16, 2026
4351b56
Random starts the hold over; a save summarizes what it sends
codeGlaze Oct 16, 2026
73eaf42
Random resets the hold even when only text changed
codeGlaze Oct 16, 2026
3bd4014
e2e: missing content is never cleared on its own
codeGlaze Oct 16, 2026
a799f0b
missing-content-kept.js: an early return still fails the run
codeGlaze Oct 16, 2026
0e5f106
Opening a character never saves it
codeGlaze Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/windows-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,10 @@ jobs:
echo "msys pid : $MSYS_PID"
pid_alive "$MSYS_PID" || { echo "FAIL: pid_alive cannot see an MSYS pid"; exit 1; }
signal_pid "$MSYS_PID" KILL || true
sleep 2
# A killed process can take a moment to go on a Windows runner: poll, don't guess.
for _ in $(seq 1 20); do pid_alive "$MSYS_PID" || break; sleep 0.5; done
if pid_alive "$MSYS_PID"; then
echo "FAIL: signal_pid did not stop an MSYS pid"; exit 1
echo "FAIL: signal_pid did not stop an MSYS pid within 10s"; exit 1
fi
echo "OK: both pid kinds are handled"

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Rules:** clarification notes on rules, in our own words, linked to where Wizards published
the ruling.
- **Spells:** conditions named in spell and magic item text link to their page, with a preview.
- **Characters:** lowering a level or changing a class, subclass or race keeps the choices that depend on it while you edit; change your mind and they come back by themselves, with Undo.
- **Characters:** the builder shows choices waiting on a higher level beside the level picker, and says when each comes back.
- **Characters:** with more than one class, a "1st" badge marks the starting class; tap another class's badge to make it the starting class, and choices that only apply to the starting class follow.
- **Characters:** a Choices list in the builder shows every choice a character stores, including ones whose homebrew isn't loaded in this browser, and lets those be cleared.

### Changed

Expand All @@ -59,6 +63,7 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Site:** on phones the header, menus and ability buttons fit the screen. (#711)
- **Server:** blank settings count as unset, Docker secrets work for the login key, and the Windows start scripts find their ports. (#701)
- **Server:** without email configured, registration is refused unless explicitly allowed. (#712)
- **Characters:** opening a character whose choices no longer apply sets them aside and lists them in a notice; the saved copy changes only when you save.

### Fixed

Expand Down Expand Up @@ -97,6 +102,9 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Equipment:** tools, musical instruments, gaming sets and equipment packs show their SRD prices,
and tools and instruments their weights.
- **Magic items:** the Alchemy Jug shows that its liquid can be created once a day.
- **Characters:** a choice that no longer applied, such as a skill picked for a level since lowered, stayed hidden on the character and still counted on its sheet.
- **Characters:** deleting or changing a character's first class no longer wipes the other classes' choices.
- **Homebrew:** a homebrew class's choices that share a name show in level order; levels 10 and 17 came before level 3.

### Security

Expand Down
122 changes: 112 additions & 10 deletions src/clj/orcpub/styles/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

;; Color palette — used across UI for consistent theming
(def orange "#f0a100")
;; The light theme's accent: its buttons and highlights are blue where the dark theme's are amber.
(def accent-on-light "#33658A")
(def button-color orange)
(def red "#9a031e")
;; One red cannot serve both themes: #9a031e reads at about 9:1 on white and about
Expand Down Expand Up @@ -1255,6 +1257,7 @@
:text-align :right
:opacity 0.75}
[:&:hover {:opacity 1}]]
[:button.message-close {:background :none :border 0 :font :inherit :cursor :pointer}]

;; Warning/alert styles
[:.bg-warning
Expand All @@ -1265,29 +1268,128 @@
{:background-color "rgba(0, 0, 0, 0.2)"
:border-radius "4px"}]

;; Accent rails for a callout: the .health-rail device, coloured by identity
;; rather than severity, so a notice says whose it is without the warning
;; palette. Named options only -- a caller picks one rather than passing a colour.
;; Callout accents, coloured by identity rather than severity, so a notice says whose it
;; is without the warning palette. Named options only -- a caller picks one rather than
;; passing a colour.
;; A one-line notice: text on the left, its action and close mark together on the right.
[:.callout-compact
{:padding "4px 6px 4px 12px !important"
:gap "12px !important"}
[:.link-button {:padding "8px 4px" :white-space :nowrap}]
[:.message-close {:min-width :auto
:min-height :auto
:margin "0 0 0 2px"
:padding "8px 6px"
:align-self :center}]
;; The action and the close mark keep the line slim but take 44px taps: an invisible
;; layer around each (the icon's own glyph is its :before, so :after).
[:.link-button :.message-close {:position :relative}]
[:.link-button:after :.message-close:after
{:content "''" :position :absolute :top "-7px" :bottom "-7px" :left 0 :right 0}]]

;; A callout's :details: a square band across the box with the box showing below it, names
;; in one column and when each applies in the next. docs/kb/ux-guide.md (2026-10-14).
[:.callout-details
{:flex-basis "100%"
:display :grid
:grid-template-columns "max-content 1fr"
:margin "0 -10px 8px"
:padding "2px 10px 2px 36px"
:background "rgba(0, 0, 0, 0.14)"
:font-size "14px"}
[:.callout-detail {:display :contents}]
[:span {:padding "7px 0"}]
["span:first-child" {:padding-right "20px"}]
[".callout-detail + .callout-detail > span" {:border-top "1px solid rgba(255, 255, 255, 0.08)"}]
[:.callout-aside {:color muted-on-dark}]]
;; Even spacing above and below the line and the band, so the box opens like a shell.
[:.callout-compact.flex-wrap {:row-gap "4px !important"}]
;; A Planned line is a button that reads as the italic link it replaces, with a 44px tap area
;; that does not move the row; its panel's rows share the notice's columns. ux-guide.md.
[:.planned-toggle
{:background :none
:border 0
:font-family :inherit
:font-style :italic
:font-size "16px"
:line-height :inherit
:text-align :left
:text-decoration :underline
:cursor :pointer
:padding "13px 0"
:margin "-13px 0"}
[:i {:display :inline-block :margin-left "6px" :text-decoration :none}]]
;; "1st" before each class row: lit on the starting class, faded on the others (a tap makes
;; one first). A small mark with a 44px tap layer. docs/kb/ux-guide.md.
[:.class-first-badge
{:position :relative
:flex-shrink 0
:margin-right "8px"
:padding "2px 6px"
:border "1px solid rgba(255, 255, 255, 0.3)"
:border-radius "4px"
:background :none
:color "rgba(255, 255, 255, 0.45)"
:font-family :inherit
:font-size "13px"
:font-weight :bold
:line-height 1.2
:cursor :pointer}
[:&:after {:content "''" :position :absolute :inset "-12px -4px"}]
[:&.on {:color orange :border-color orange :cursor :default}]
[:&.blocked {:opacity 0.5}]]
[".app.light-theme .class-first-badge"
{:border-color "rgba(0, 0, 0, 0.3)" :color "rgba(0, 0, 0, 0.45)"}
[:&.on {:color accent-on-light :border-color accent-on-light}]]
;; The Choices link reads as a plain link, not a Planned line; its panel, one line per section.
[:.choices-toggle {:font-style :normal}]
[:.choice-line {:padding "4px 0"}]
[".choices-panel .planned-aside" {:color muted-on-dark}]
[".app.light-theme .choices-panel .planned-aside" {:color muted-on-light}]
[:.planned-rows
{:display :grid
:grid-template-columns "max-content 1fr auto"
:font-size "14px"}
[:&.no-label :&.no-aside {:grid-template-columns "1fr auto"}]
[:.planned-row {:display :contents}]
;; Each row is a 44px tap target, so a Clear never lands on the row beside it.
[:span :.link-button {:display :flex :align-items :center :min-height "44px"}]
[:span {:padding "0 20px 0 0"}]
[:.link-button {:padding "0 0 0 16px" :border-radius 0 :font-size "14px"}]
[".planned-row + .planned-row > *" {:border-top "1px solid rgba(255, 255, 255, 0.08)"}]
[:.planned-aside {:color muted-on-dark}]]
[".app.light-theme .planned-rows"
[".planned-row + .planned-row > *" {:border-top "1px solid rgba(0, 0, 0, 0.08)"}]
[:.planned-aside {:color muted-on-light}]]
[".callout-compact .callout-details" {:margin "0 -6px 7px -12px" :padding-left "38px"}]
[".app.light-theme .callout-details"
{:background "rgba(0, 0, 0, 0.05)"}
[".callout-detail + .callout-detail > span" {:border-top "1px solid rgba(0, 0, 0, 0.08)"}]
[:.callout-aside {:color muted-on-light}]]

[:.callout-accent-brand
;; A gradient ring around the whole box, drawn by a pseudo-element masked to its
;; own edge: border-image squares off rounded corners, and a plain border cannot
;; hold a gradient. The mask punches the middle out, so the translucent fill stays.
;; The outer shadow is a low glow, not a selected-field highlight.
;; A balanced gradient ring (amber at both sides, faint through top and bottom), drawn by
;; a pseudo-element masked to its own edge: border-image squares off rounded corners.
;; No fill colour, so a notice never reads as a warning. docs/kb/ux-guide.md (2026-10-13).
{:position :relative
:border-color :transparent
:box-shadow "0 0 14px rgba(240, 161, 0, 0.13)"}
:border-color :transparent}
[:&:before
{:content "''"
:position :absolute
:inset 0
:border-radius "4px"
:padding "1px"
:pointer-events :none
:background (str "linear-gradient(135deg, " orange ", rgba(240, 161, 0, 0.18) 55%, rgba(240, 161, 0, 0.7))")
:background (str "linear-gradient(90deg, " orange ", rgba(255, 255, 255, 0.12) 30%, "
"rgba(255, 255, 255, 0.12) 70%, " orange ")")
:-webkit-mask "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)"
:-webkit-mask-composite "xor"
:mask "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)"
:mask-composite "exclude"}]]
;; On light, the ring takes the light theme's blue, and its faint stretch darkens to show.
[".app.light-theme .callout-accent-brand:before"
{:background (str "linear-gradient(90deg, " accent-on-light ", rgba(0, 0, 0, 0.12) 30%, "
"rgba(0, 0, 0, 0.12) 70%, " accent-on-light ")")}]

;; A small neutral pill for naming things inline -- the items a feature covers,
;; the styles a sheet comes in. Same shape as .lib-badge without its reason
Expand Down
17 changes: 11 additions & 6 deletions src/cljc/orcpub/dnd/e5/character.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -840,17 +840,22 @@
custom-equipment))))
(meta equipment))))))

(defn use-class-starting-equipment
"`character` with its class starting equipment replaced by `class-option`'s: what the first class
brings. Its other stored picks are untouched."
[character class-option]
(-> character
(remove-starting-equipment ::equip/class-starting-equipment?)
(add-associated-options (::t/associated-options class-option))))

(defn set-class [character class-key class-index new-class-option]
(let [associated-options (::t/associated-options new-class-option)
with-new-class (assoc-in
(let [with-new-class (assoc-in
character
[::entity/options :class class-index]
{::entity/key class-key
::entity/options {:levels [{::entity/key :level-1}]}})
without-starting-equipment (remove-starting-equipment with-new-class ::equip/class-starting-equipment?)
with-new-starting-equipment (add-associated-options without-starting-equipment associated-options)]
::entity/options {:levels [{::entity/key :level-1}]}})]
(if (zero? class-index)
with-new-starting-equipment
(use-class-starting-equipment with-new-class new-class-option)
with-new-class)))

(defn skill-prof-bonuses [prof-bonus skill-profs skill-expertise default-skill-bonus-fns]
Expand Down
40 changes: 27 additions & 13 deletions src/cljc/orcpub/dnd/e5/options.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -2404,8 +2404,16 @@
:prereq-fn prereq-fn
:tags #{:profs :tool-profs}}))))

(defn gated
"`prereq-fn` carrying `gate`, data saying what opens it, as metadata (`::t/gate`), so a held pick
can say when it returns (picks/describe-held). `gate`: {:class-level n :class k},
{:total-level n}, {:first-class k} or {:not-first-class k}."
[gate prereq-fn]
(with-meta prereq-fn {::t/gate gate}))

(defn first-class? [class-kw & [classes]]
(fn [c] (= class-kw (first (or classes (character/classes c))))))
(gated {:first-class class-kw}
(fn [c] (= class-kw (first (or classes (character/classes c)))))))

(defn new-starting-equipment-selection [class-kw {:keys [name options] :as cfg}]
(t/selection-cfg
Expand Down Expand Up @@ -2662,10 +2670,11 @@
(tool-prof-modifiers (keys tool)))}))))

(defn total-levels-prereq [level & [class-key]]
(fn [c] (>= (if class-key
((character/class-level-fn c) class-key)
(character/total-levels c))
level)))
(gated (if class-key {:class-level level :class class-key} {:total-level level})
(fn [c] (>= (if class-key
((character/class-level-fn c) class-key)
(character/total-levels c))
level))))

(defn total-levels-prereq-2 [level & [class-key]]
(fn [c]
Expand Down Expand Up @@ -2738,8 +2747,7 @@
(fn [selection]
(assoc selection
::t/prereq-fn
(fn [c] (let [total-levels (character/total-levels c)]
(>= lvl total-levels)))))
(total-levels-prereq lvl (:key cls))))
selections))
(:selections spellcasting-template))
level-selections (mapcat
Expand Down Expand Up @@ -2974,8 +2982,9 @@
[(assoc
(hit-points-selection hit-die name i)
::t/prereq-fn
(fn [c] (or (not (= kw (first (character/classes c))))
(> i 1))))])))
(gated {:not-first-class kw}
(fn [c] (or (not (= kw (first (character/classes c))))
(> i 1)))))])))
:modifiers (concat
(some-> levels (get i) :modifiers)
(traits-modifiers
Expand Down Expand Up @@ -3058,8 +3067,9 @@
spells-map
(assoc spellcasting :class-key kw)
merged-class)
first-class? (fn [c] (let [first-class (first (character/classes c))]
(= kw first-class)))]
first-class? (gated {:first-class kw}
(fn [c] (let [first-class (first (character/classes c))]
(= kw first-class))))]
(t/option-cfg
{:name name
:key kw
Expand All @@ -3076,7 +3086,9 @@
(when (seq tool-options)
[(tool-prof-selection tool-options :tool-selection first-class?)])
(when (seq multiclass-tool-options)
[(tool-prof-selection multiclass-tool-options :multiclass-tool-selection (fn [c] (not= kw (first (:classes c)))))])
[(tool-prof-selection multiclass-tool-options :multiclass-tool-selection
(gated {:not-first-class kw}
(fn [c] (not= kw (first (:classes c))))))])
(when weapon-choices (class-weapon-options weapon-choices kw weapon-map))
(when armor-choices (class-armor-options armor-choices kw))
(when equipment-choices (class-equipment-options equipment-choices kw))
Expand All @@ -3086,7 +3098,9 @@
(when (seq skill-expertise-kws)
[(skill-expertise-selection skill-expertise-kws (:choose skill-expertise-options))])
(when multiclass-skill-options
[(class-skill-selection multiclass-skill-options :multiclass-skill-proficiency (complement first-class?))])
[(class-skill-selection multiclass-skill-options :multiclass-skill-proficiency
(gated {:not-first-class kw}
(complement first-class?)))])
[(t/selection-cfg
{:name (str name " Levels")
:key :levels
Expand Down
Loading
Loading