this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add better positioning logic for new power system

+167 -289
-247
src/components/astral/Power.astro
··· 1 - --- 2 - import { parentPowers, powers } from "../../consts/astral"; 3 - import { 4 - powerData, 5 - type Power, 6 - type powerId, 7 - type powerTree, 8 - } from "../../consts/astral"; 9 - 10 - interface Props { 11 - name: string; 12 - forceSelect?: boolean | undefined; 13 - description: string; 14 - image: { 15 - src: string; 16 - alt: string; 17 - }; 18 - meta: { 19 - name: string; 20 - value: string; 21 - }[]; 22 - id: powerId; 23 - children: (typeof powerTree)[]; 24 - first?: true; 25 - } 26 - 27 - const { name, description, image, meta, id, children, first } = Astro.props; 28 - const findDeepChildren = (children: (typeof powerTree)[]): number => { 29 - return children.reduce( 30 - (acc, curr) => 31 - acc + (curr.children.length > 0 ? findDeepChildren(curr.children) : 1), 32 - 0 33 - ); 34 - }; 35 - const deepChildren = findDeepChildren(children); 36 - 37 - /** find depth; assumes all branches are of equal depth */ 38 - const findDepth = (children: (typeof powerTree)[]): number => { 39 - return children.length === 0 ? 1 : 1 + findDepth(children[0].children); 40 - }; 41 - const depth = findDepth(children); 42 - --- 43 - 44 - <section 45 - class:list={["tree-node", first && "first"]} 46 - id={id !== powers.LOCKED && id !== powers.INVALID 47 - ? `astral-power-section-${id.split("#")[1]}` 48 - : undefined} 49 - data-power-id={id} 50 - style={`--deep-children: ${deepChildren}; --depth: ${depth}`} 51 - > 52 - <button 53 - id={id !== powers.LOCKED && id !== powers.INVALID 54 - ? `astral-power-button-${id.split("#")[1]}` 55 - : undefined} 56 - data-power-id={id} 57 - command={id !== powers.LOCKED && id !== powers.INVALID 58 - ? "show-popover" 59 - : undefined} 60 - commandfor={id !== powers.LOCKED && id !== powers.INVALID 61 - ? `astral-power-info-${id.split("#")[1]}` 62 - : undefined} 63 - disabled={id === powers.LOCKED || id === powers.INVALID} 64 - ><img {...image} /></button 65 - > 66 - { 67 - children.length > 0 && ( 68 - <ul style={`--child-count: ${children.length}`}> 69 - {children.map((x, i) => ( 70 - <li style={`--sibling-index: ${i + 1}`}> 71 - <Astro.self {...x} {...powerData[x.id]} /> 72 - </li> 73 - ))} 74 - </ul> 75 - ) 76 - } 77 - <dialog 78 - id={id !== powers.LOCKED && id !== powers.INVALID 79 - ? `astral-power-info-${id.split("#")[1]}` 80 - : undefined} 81 - data-power-id={id} 82 - popover 83 - > 84 - <h1>{name}</h1> 85 - <p>{description}</p> 86 - { 87 - meta.length > 0 && ( 88 - <dl> 89 - {meta.map((x, i) => ( 90 - <> 91 - <dt>{x.name}</dt> <dd>{x.value}</dd> 92 - </> 93 - ))} 94 - </dl> 95 - ) 96 - } 97 - </dialog> 98 - </section> 99 - 100 - <style> 101 - @layer base { 102 - .tree-node { 103 - --image-width-px: 50; 104 - --small-gap-px: 8; 105 - --large-gap-px: 80; 106 - --outline-size-px: 2; 107 - 108 - --image-width: calc(var(--image-width-px) * 1px); 109 - --small-gap: calc(var(--small-gap-px) * 1px); 110 - --large-gap: calc(var(--large-gap-px) * 1px); 111 - --outline-size: calc(var(--outline-size-px) * 1px); 112 - } 113 - 114 - button { 115 - width: var(--image-width); 116 - height: var(--image-width); 117 - padding: 0; 118 - border: 0; 119 - 120 - &, 121 - &:hover { 122 - background-color: CanvasColor; 123 - } 124 - 125 - & img { 126 - width: 100%; 127 - height: 100%; 128 - display: block; 129 - 130 - &:hover { 131 - filter: brightness(150%) contrast(75%); 132 - } 133 - } 134 - } 135 - 136 - ul { 137 - list-style-type: none; 138 - padding: 0; 139 - margin: 0; 140 - } 141 - 142 - dl { 143 - display: grid; 144 - grid-template-columns: auto 1fr; 145 - gap: 0.2ch 1ch; 146 - 147 - dt::after { 148 - content: ": "; 149 - } 150 - 151 - dd { 152 - margin-inline-start: 0; 153 - } 154 - } 155 - } 156 - 157 - @layer position { 158 - .tree-node { 159 - display: flex; 160 - flex-direction: column; 161 - align-items: center; 162 - justify-content: start; 163 - gap: var(--large-gap); 164 - width: min-content; 165 - 166 - margin-inline: auto; 167 - &.first { 168 - margin-block-start: 2rem; 169 - margin-inline: auto; 170 - padding-inline: 1rem; 171 - } 172 - } 173 - 174 - ul { 175 - container-type: size; 176 - display: flex; 177 - flex-direction: row; 178 - gap: var(--small-gap); 179 - 180 - --section-width: max( 181 - calc( 182 - var(--deep-children) * var(--image-width-px) + 183 - (var(--deep-children) - 1) * var(--small-gap-px) 184 - ), 185 - var(--image-width-px) 186 - ); 187 - width: calc(var(--section-width) * 1px); 188 - } 189 - 190 - ul button { 191 - position: relative; 192 - 193 - &::before { 194 - content: ""; 195 - position: absolute; 196 - width: var(--outline-size); 197 - /* offset due to border fucking w things */ 198 - bottom: calc(var(--image-width) - 2 * var(--outline-size)); 199 - 200 - --diff-width: calc( 201 - var(--section-width) / 2 - 202 - ( 203 - var(--section-width) / var(--child-count) * 204 - (var(--sibling-index) - 0.5) 205 - ) 206 - ); 207 - --length: calc( 208 - sqrt(pow(var(--large-gap-px), 2) + pow(var(--diff-width), 2)) * 1px 209 - ); 210 - --angle: calc(atan((var(--diff-width)) / (var(--large-gap-px)))); 211 - 212 - height: var(--length); 213 - transform-origin: bottom center; 214 - rotate: var(--angle); 215 - } 216 - } 217 - 218 - dialog { 219 - max-width: min(60ch, calc(100vw - 10ch)); 220 - margin: auto; 221 - } 222 - } 223 - 224 - section { 225 - --outline: red; 226 - } 227 - 228 - section[data-power-id="dev.vielle.dnd.power#locked"] { 229 - --outline: gold; 230 - } 231 - 232 - section.selected { 233 - --outline: green; 234 - } 235 - 236 - button { 237 - /* doubled since <img> overlaps inner half */ 238 - border: calc(2 * var(--outline-size)) solid var(--outline); 239 - border-radius: 100%; 240 - & img { 241 - border-radius: 100%; 242 - } 243 - } 244 - ul button::before { 245 - background-color: var(--outline); 246 - } 247 - </style>
+162 -40
src/components/astral/Powers.astro
··· 9 9 interface Props { 10 10 id: powerId; 11 11 children: (typeof powerTree)[]; 12 + i: number; 13 + siblings: number; 12 14 } 13 15 14 16 const { id: power, children } = Astro.props; 15 17 16 - console.log(power, children); 18 + const getDeepChildren = (root: typeof powerTree): number => 19 + root.children.length === 0 20 + ? 1 21 + : root.children.reduce((acc, power) => acc + getDeepChildren(power), 0); 22 + 23 + const getLayer = (children: (typeof powerTree)[], layer: number = 1): number => 24 + children.length === 0 25 + ? layer 26 + : children.reduce( 27 + (prev, curr) => Math.max(prev, getLayer(curr.children, layer + 1)), 28 + 0, 29 + ); 17 30 --- 18 31 19 32 <script> 20 33 import { 21 34 isPowerId, 22 35 parentPowers, 23 - powers, 24 36 type powerId, 37 + powers, 25 38 } from "../../consts/astral"; 26 39 import Powers, { type powerObj } from "../../lib/powers"; 27 40 28 - class PowerHTMLElement extends HTMLElement { 41 + class HTMLPowerElement extends HTMLElement { 29 42 #internals = this.attachInternals(); 30 - power = this.getAttribute("power"); 43 + power: powerId | null = null; 31 44 32 45 constructor() { 33 46 super(); 34 47 35 - Powers.subscribe((...args) => this.handleState(...args)); 48 + this.attributeChangedCallback("power", null, this.getAttribute("power")); 49 + Powers.subscribe(this.handleState.bind(this)); 36 50 } 37 51 38 - handleState( 39 - value: Readonly<powerObj>, 40 - oldValue: Readonly<powerObj> | undefined, 41 - changedKey: keyof powerObj | undefined, 42 - ) { 52 + handleState(value: Readonly<powerObj>) { 43 53 let button: Element | null | undefined = 44 54 this.shadowRoot?.querySelector("dialog button"); 45 55 if (!(button instanceof HTMLButtonElement)) button = undefined; 46 56 47 57 this.#internals.states.clear(); 58 + // missing a [power] or has a locked or invalid power 48 59 if ( 49 60 !this.power || 50 61 this.power === "dev.vielle.dnd.power#locked" || 51 - this.power === "dev.vielle.dnd.power#unknown" || 52 - !isPowerId(this.power) 62 + this.power === "dev.vielle.dnd.power#invalid" 53 63 ) { 54 64 this.#internals.states.add("locked"); 55 65 button && (button.disabled = true); 56 - } else if (value.powers.includes(this.power)) { 66 + } 67 + 68 + // power has been selected by the user 69 + else if (value.powers.includes(this.power)) { 57 70 this.#internals.states.add("selected"); 58 71 button && (button.disabled = false); 72 + 59 73 // if any selected powers rely on it 60 74 if ( 61 - value.powers.reduce( 62 - (acc, curr) => 63 - acc && parentPowers[curr].includes(this.power as powerId), 64 - true, 65 - ) 75 + value.powers.filter((x) => 76 + parentPowers[x].includes(this.power ?? powers.INVALID), 77 + ).length > 1 66 78 ) { 67 79 this.#internals.states.add("dependency"); 68 80 button && (button.disabled = true); 69 81 } 70 - } else if (value.powers.length >= value.points) { 82 + } 83 + 84 + // the user has as many or more powers than they have points 85 + else if (value.powers.length >= value.points) { 71 86 this.#internals.states.add("no-points"); 72 87 button && (button.disabled = true); 73 - } else if (value.powers.includes(parentPowers[this.power].at(-2) ?? "")) { 88 + } 89 + 90 + // the user has not selected this, has selected its parent, and has enough points 91 + else if (value.powers.includes(parentPowers[this.power].at(-2) ?? "")) { 74 92 this.#internals.states.add("avaliable"); 75 93 button && (button.disabled = false); 76 - } else { 94 + } 95 + 96 + // the user has not selected the parent, but otherwise could unlock this 97 + else { 77 98 this.#internals.states.add("no-parent"); 78 99 button && (button.disabled = true); 79 100 } ··· 81 102 82 103 connectedCallback() { 83 104 const button = this.shadowRoot?.querySelector("dialog button"); 84 - console.log(button); 85 105 if (!(button instanceof HTMLButtonElement)) 86 106 throw "`dialog button` could not be selected"; 87 107 button.addEventListener("click", (ev) => { ··· 89 109 // this.power is in some way invalid; drop the event 90 110 !this.power || 91 111 this.power === "dev.vielle.dnd.power#locked" || 92 - this.power === "dev.vielle.dnd.power#unknown" || 93 - !isPowerId(this.power) 112 + this.power === "dev.vielle.dnd.power#invalid" 94 113 ) 95 114 return; 96 115 // the power is selected; deselect it ··· 111 130 }); 112 131 } 113 132 114 - attributeChangedCallback(name: string, oldValue: string, newValue: string) { 115 - console.log(name, "changed"); 116 - if (name === "power") this.power = newValue; 133 + attributeChangedCallback( 134 + name: string, 135 + oldValue: string | null, 136 + newValue: string | null, 137 + ) { 138 + if (name === "power") { 139 + if (isPowerId(newValue)) { 140 + this.power = newValue; 141 + } else { 142 + this.power = null; 143 + } 144 + } 117 145 } 118 146 } 119 147 120 - customElements.define("power-", PowerHTMLElement); 148 + customElements.define("power-", HTMLPowerElement); 121 149 </script> 122 150 123 - <power- {power}> 151 + <power- 152 + {power} 153 + style={{ 154 + "--deep-children": getDeepChildren({ id: power, children }), 155 + "--sibling-index": Astro.props.i + 1, 156 + "--sibling-count": Astro.props.siblings, 157 + "--layer": getLayer(children), 158 + }} 159 + > 124 160 <template shadowrootmode="open"> 125 161 <button commandfor="info" command="show-modal"> 126 162 <slot is:inline name="img" /> 127 163 </button> 164 + 128 165 <dialog id="info" closedby="any"> 129 166 <h2><slot is:inline name="title" /></h2> 130 167 <button id="mode"> ··· 137 174 </button> 138 175 <slot is:inline name="body" /> 139 176 </dialog> 140 - <slot is:inline /> 177 + 178 + <section> 179 + <slot is:inline /> 180 + </section> 141 181 142 182 <style is:inline> 143 183 /* colour mode */ ··· 168 208 /* aestetic styles */ 169 209 button[command] { 170 210 /* display: contents; */ 171 - border: 5px solid var(--outline); 211 + border: var(--outline-size) solid var(--outline); 172 212 margin: 0; 173 213 padding: 0; 174 214 } ··· 227 267 opacity: 1; 228 268 } 229 269 } 270 + 271 + /* tree positioning */ 272 + :host { 273 + --image-width-px: 100; 274 + --small-gap-px: 20; 275 + --large-gap-px: 120; 276 + --outline-size-px: 5; 277 + 278 + --image-width: calc(var(--image-width-px) * 1px); 279 + --small-gap: calc(var(--small-gap-px) * 1px); 280 + --large-gap: calc(var(--large-gap-px) * 1px); 281 + --outline-size: calc(var(--outline-size-px) * 1px); 282 + 283 + display: flex; 284 + flex-direction: column; 285 + align-items: center; 286 + justify-content: start; 287 + gap: var(--large-gap); 288 + width: min-content; 289 + 290 + --section-width: max( 291 + calc( 292 + var(--deep-children) * var(--image-width-px) + 293 + (var(--deep-children) - 1) * var(--small-gap-px) 294 + ), 295 + var(--image-width-px) 296 + ); 297 + 298 + button[command] { 299 + width: var(--image-width); 300 + height: var(--image-width); 301 + border-radius: 100%; 302 + 303 + & ::slotted(img) { 304 + width: 100%; 305 + height: 100%; 306 + border-radius: 100%; 307 + } 308 + 309 + position: relative; 310 + &::before { 311 + content: ""; 312 + position: absolute; 313 + width: var(--outline-size); 314 + /* offset due to border fucking w things */ 315 + bottom: calc(var(--image-width) - var(--outline-size)); 316 + background-color: var(--outline); 317 + 318 + --offset: calc( 319 + var(--section-width) * (var(--sibling-index) - 1) + 320 + var(--small-gap-px) * (var(--sibling-index) - 1) + 321 + var(--section-width) / 2 322 + ); 323 + --parent-offset: calc( 324 + ( 325 + var(--section-width) * var(--sibling-count) + 326 + var(--small-gap-px) * (var(--sibling-count) - 1) 327 + ) / 328 + 2 329 + ); 330 + --offset-diff: calc(var(--parent-offset) - var(--offset)); 331 + --length: calc( 332 + sqrt(pow(var(--offset-diff), 2) + pow(var(--large-gap-px), 2)) * 333 + 1px 334 + ); 335 + --angle: calc(atan(var(--offset-diff) / var(--large-gap-px))); 336 + 337 + height: var(--length); 338 + transform-origin: bottom center; 339 + rotate: var(--angle); 340 + } 341 + } 342 + 343 + section { 344 + container-type: size; 345 + display: flex; 346 + flex-direction: row; 347 + gap: var(--small-gap); 348 + width: calc(var(--section-width) * 1px); 349 + } 350 + } 230 351 </style> 231 352 </template> 232 353 ··· 248 369 } 249 370 </div> 250 371 251 - <ul> 252 - { 253 - children.map((child) => ( 254 - <li> 255 - <Astro.self {...child} /> 256 - </li> 257 - )) 258 - } 259 - </ul> 372 + { 373 + children.map((child, i) => ( 374 + <Astro.self {...child} {i} siblings={children.length} /> 375 + )) 376 + } 260 377 </power-> 261 378 262 379 <style> 380 + ul { 381 + list-style-type: none; 382 + padding-inline-start: 0; 383 + } 384 + 263 385 dl { 264 386 display: grid; 265 387 grid-template-columns: auto 1fr;
+5 -2
src/pages/astral/index.astro
··· 20 20 min-width: 100%; 21 21 width: min-content; 22 22 box-sizing: border-box; 23 - margin: 0; 23 + } 24 + 25 + body { 26 + margin: 2em 0 2em 10em; 24 27 } 25 28 </style> 26 29 27 - <Powers {...powerTree} /> 30 + <Powers {...powerTree} i={0} siblings={1} /> 28 31 </Base>