Skip to content

Commit 76ebb90

Browse files
committed
feat: migrate to statically generated html
1 parent bc7fb4e commit 76ebb90

File tree

8 files changed

+225
-127
lines changed

8 files changed

+225
-127
lines changed

calculator/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import de.chasenet.foxhole.DownloadJsonDataTask
2+
import de.chasenet.foxhole.GenerateIndexFileTask
13
import groovy.json.JsonSlurper
24

35
plugins {

calculator/src/HtmlUtils.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
1-
export function also<T>(value: T, body: (obj: T) => void): T {
2-
body(value)
3-
return value
4-
}
5-
6-
7-
export function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, block: (elem: HTMLElementTagNameMap[K]) => void): HTMLElementTagNameMap[K] {
8-
return also(document.createElement(tagName), block)
9-
}
10-
11-
export function appendChild<K extends keyof HTMLElementTagNameMap, T extends Node>(element: T, tagName: K, block: (elem: HTMLElementTagNameMap[K]) => void = undefined): T {
12-
if (block) {
13-
element.appendChild(createElement(tagName, block))
14-
} else {
15-
element.appendChild(document.createElement(tagName))
16-
}
17-
return element
1+
export function as<T extends Element>(element: Element): T {
2+
return element as T
183
}

calculator/src/LocalStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function setPlayerFaction(faction: PlayerFaction) {
99
}
1010

1111
export function getPlayerFaction(): PlayerFaction {
12-
return localStorage.getItem(FACTION_KEY) as PlayerFaction | "colonial"
12+
return localStorage.getItem(FACTION_KEY) as PlayerFaction ?? "colonial"
1313
}
1414

1515
export function getSavedSelectedItemName(category: Category): string {

calculator/src/Models.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import {Cost} from "./Cost";
22

3-
export type PlayerFaction = "colonial" | "warden"
3+
export const playerFactions = [
4+
"colonial", "warden"
5+
] as const
6+
7+
export type PlayerFaction = typeof playerFactions[number]
48
export type Faction = "neutral" | PlayerFaction
59

6-
export const categories = [
7-
"heavy_ammunition",
8-
"heavy_arms",
9-
"shipables",
10-
"small_arms",
11-
"supplies",
12-
"uniforms",
10+
export type Category = "heavy_ammunition" |
11+
"heavy_arms" |
12+
"shipables" |
13+
"small_arms" |
14+
"supplies" |
15+
"uniforms" |
1316
"vehicles"
14-
] as const
15-
16-
export type Category = typeof categories[number]
1717

1818
export const CratePerQueue: Record<Category, number> = {
1919
heavy_ammunition: 9,

calculator/src/index.ts

Lines changed: 56 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,131 +2,90 @@ import '@fontsource/jost';
22
import loadData from "./loadData";
33
import {groupBy} from "./ArrayUtils";
44
import {Category, Item} from './Models'
5-
import {appendChild, createElement} from "./HtmlUtils";
5+
import {as} from "./HtmlUtils";
66
import {combineLatest, concat, fromEvent, map, Observable, of, tap} from "rxjs";
7-
import {add, asCrates, calculateItemQueueCost, Cost, ZERO_COST} from "./Cost";
8-
import {getSavedSelectedItemName, setSavedSelectedItemName} from "./LocalStorage";
7+
import {add, asCrates, calculateItemQueueCost, Cost} from "./Cost";
8+
import {getPlayerFaction, getSavedSelectedItemName, setSavedSelectedItemName} from "./LocalStorage";
99

10-
const items = loadData().filter(value => value.faction.indexOf("colonial") > 0)
10+
const items = loadData()
1111

1212
const itemsByCategory = groupBy(items, item => item.itemCategory)
1313

14-
const costObservables: Record<Category, Observable<Cost> | undefined> = {
15-
heavy_ammunition: undefined,
16-
heavy_arms: undefined,
17-
shipables: undefined,
18-
small_arms: undefined,
19-
supplies: undefined,
20-
uniforms: undefined,
21-
vehicles: undefined
22-
}
23-
24-
const mpfSelectionTable = document.getElementById("mpf-selection")
25-
26-
const headRow = mpfSelectionTable.getElementsByTagName("tr").item(0)
27-
28-
for (const costKey in ZERO_COST) {
29-
appendChild(headRow, "th", (th) => {
30-
th.innerText = costKey
31-
})
32-
}
14+
const costObservables = new Map<Category, Observable<Cost>>()
3315

34-
for (const [category, items] of Object.entries(itemsByCategory)) {
35-
const selectedItemName = getSavedSelectedItemName(category as Category)
36-
37-
appendChild(mpfSelectionTable, "tr", tr => {
38-
appendChild(tr, "td", (td) => {
39-
td.innerText = capitalize(category)
40-
})
16+
Array.from(document.getElementsByClassName("queue-select"))
17+
.map(select => select as HTMLSelectElement)
18+
.map(select => {
19+
const category: Category = select.getAttribute("data-category") as Category
4120

42-
appendChild(tr, "td", (td) => {
43-
appendChild(td, "select", (select) => {
44-
let option = makeOption("");
45-
option.selected = selectedItemName === ""
21+
const selectedItemName = getSavedSelectedItemName(category)
4622

47-
select.append(option)
48-
items.sort((a, b) => a.itemName > b.itemName ? 1 : -1).forEach(item => {
49-
let option = makeOption(item.itemName);
50-
option.selected = selectedItemName === item.itemName
23+
Array.from(select.children)
24+
.map(option => option as HTMLOptionElement)
25+
.forEach(option => {
26+
option.selected = option.value === selectedItemName
27+
})
5128

52-
select.append(option)
53-
})
29+
costObservables[category] = concat(
30+
of(calculateItemQueueCost(getItem(category, getSavedSelectedItemName(category as Category)))),
31+
fromEvent(select, "change")
32+
.pipe(tap(() => setSavedSelectedItemName(category as Category, select.value)))
33+
.pipe(map(() => getItem(category, select.value)))
34+
.pipe(map((item) => calculateItemQueueCost(item))))
35+
})
5436

55-
costObservables[category] = concat(
56-
of(calculateItemQueueCost(getItem(category, getSavedSelectedItemName(category as Category)))),
57-
fromEvent(select, "change")
58-
.pipe(tap(() => setSavedSelectedItemName(category as Category, select.value)))
59-
.pipe(map(() => getItem(category, select.value)))
60-
.pipe(map((item) => calculateItemQueueCost(item))))
37+
Array.from(document.getElementsByClassName("cost-cell"))
38+
.map(td => td as HTMLTableCellElement)
39+
.forEach(td => {
40+
const category = td.getAttribute("data-category")
41+
const resource = td.getAttribute("data-resource")
6142

62-
})
43+
costObservables[category].subscribe(cost => {
44+
td.innerText = cost[resource]
6345
})
64-
65-
for (const costKey in ZERO_COST) {
66-
appendChild(tr, "td", (td) => {
67-
td.id = `${category}-cost-${costKey}`
68-
td.innerText = "0"
69-
70-
costObservables[category].subscribe(cost => {
71-
td.innerText = cost[costKey].toString()
72-
})
73-
})
74-
}
7546
})
76-
}
7747

7848
const totalCostObservable = combineLatest(Object.values(costObservables) as Observable<Cost>[])
7949
.pipe(map(value => value.reduce((acc, curr) => add(acc, curr))))
8050

8151
const totalCrateCostObservable = totalCostObservable.pipe(map(asCrates))
8252

83-
appendChild(mpfSelectionTable, "tr", tr => {
84-
appendChild(tr, "td")
85-
appendChild(tr, "td", td => {
86-
td.innerText = "Total"
53+
Array.from(document.getElementsByClassName("total-cost-cell"))
54+
.map(td => td as HTMLTableCellElement)
55+
.forEach(td => {
56+
const resource = td.getAttribute("data-resource")
57+
58+
totalCostObservable.subscribe(cost => {
59+
td.innerText = cost[resource]
60+
})
8761
})
88-
for (const costKey in ZERO_COST) {
89-
appendChild(tr, "td", (td) => {
90-
td.id = `total-cost-${costKey}`
91-
td.innerText = "0"
9262

93-
totalCostObservable.subscribe(totalCost => {
94-
td.innerText = totalCost[costKey]
95-
})
63+
Array.from(document.getElementsByClassName("total-crate-cell"))
64+
.map(td => td as HTMLTableCellElement)
65+
.forEach(td => {
66+
const resource = td.getAttribute("data-resource")
9667

68+
totalCrateCostObservable.subscribe(cost => {
69+
td.innerText = cost[resource]
9770
})
98-
}
99-
})
100-
101-
appendChild(mpfSelectionTable, "tr", tr => {
102-
appendChild(tr, "td")
103-
appendChild(tr, "td", td => {
104-
td.innerText = "Crates"
10571
})
106-
for (const costKey in ZERO_COST) {
107-
appendChild(tr, "td", (td) => {
108-
td.id = `total-cost-crates-${costKey}`
109-
td.innerText = "0"
11072

111-
totalCrateCostObservable.subscribe(totalCost => {
112-
td.innerText = totalCost[costKey]
113-
})
114-
})
115-
}
73+
const playerFaction = getPlayerFaction()
74+
75+
Array.from(document.getElementsByClassName("item-option")).map(as<HTMLOptionElement>).forEach(option => {
76+
const factions = option.dataset["faction"].split(",").map(faction => faction.trim())
77+
78+
option.hidden = !factions.includes(playerFaction)
11679
})
11780

11881
function getItem(category: string, itemName: string): Item | undefined {
82+
if (itemName === "") return undefined
83+
11984
const items = itemsByCategory[category] as Item[]
120-
return items.find(item => item.itemName === itemName)
121-
}
85+
const item = items.find(item => item.itemName === itemName);
12286

123-
function makeOption(label: string) {
124-
return createElement("option", (option) => {
125-
option.label = label
126-
option.value = label
127-
})
87+
if (item == undefined) {
88+
console.warn(`No item named ${itemName} found in category ${category}`)
89+
}
90+
return item
12891
}
129-
130-
function capitalize(value: string): string {
131-
return value.split("_").map((it) => it.charAt(0).toUpperCase() + it.substring(1)).join(" ")
132-
}

html/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
plugins {
22
`java-gradle-plugin`
33
`kotlin-dsl`
4+
5+
kotlin("plugin.serialization") version "2.0.20"
46
}
57

68
repositories {
@@ -9,13 +11,14 @@ repositories {
911

1012
dependencies {
1113
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
14+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
1215
}
1316

1417
gradlePlugin {
1518
plugins {
1619
register("html-builder") {
1720
id = "html-builder"
18-
implementationClass = "HtmlBuilderPlugin"
21+
implementationClass = "de.chasenet.foxhole.HtmlBuilderPlugin"
1922
}
2023
}
2124
}

html/src/main/kotlin/de/chasenet/foxhole/IndexFile.kt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package de.chasenet.foxhole
22

3+
import de.chasenet.foxhole.model.ItemCategory
4+
import de.chasenet.foxhole.model.LogiItem
5+
import de.chasenet.foxhole.model.resources
36
import kotlinx.html.*
7+
import kotlinx.serialization.ExperimentalSerializationApi
8+
import kotlinx.serialization.json.Json
9+
import kotlinx.serialization.json.decodeFromStream
410
import org.gradle.api.file.RegularFileProperty
511
import org.gradle.api.tasks.InputFile
12+
import org.gradle.internal.extensions.stdlib.capitalized
613

714
abstract class GenerateIndexFileTask : GenerateHtmlTask() {
815
@get:InputFile
@@ -14,7 +21,21 @@ abstract class GenerateIndexFileTask : GenerateHtmlTask() {
1421
)
1522
}
1623

24+
@OptIn(ExperimentalSerializationApi::class)
1725
override fun TagConsumer<*>.generate() {
26+
val items = Json {
27+
ignoreUnknownKeys = true
28+
}.decodeFromStream<List<LogiItem>>(foxholeJsonDataFile.asFile.get().inputStream())
29+
30+
items.filter { it.isMpfCraftable }.also {
31+
it.filter { it.itemCategory == null }
32+
.takeIf { it.isNotEmpty() }
33+
?.joinToString { it.itemName }
34+
?.also { throw IllegalArgumentException("Items without item category found: $it") }
35+
}
36+
37+
val itemsByCategory = items.filter { it.isMpfCraftable }.groupBy { it.itemCategory }
38+
1839
html {
1940
lang = "en"
2041
head {
@@ -39,7 +60,71 @@ abstract class GenerateIndexFileTask : GenerateHtmlTask() {
3960
tr {
4061
th { text("Queue") }
4162
th { text("Item") }
63+
th { text("Bmat") }
64+
th { text("Rmat") }
65+
th { text("Emat") }
66+
th { text("Hemat") }
67+
}
68+
69+
ItemCategory.values().forEach { category ->
70+
tr {
71+
td {
72+
text(category.name.split("_").joinToString(" ") { it.capitalized() })
73+
}
74+
td {
75+
select("queue-select") {
76+
attributes["data-category"] = category.name
77+
78+
option {
79+
text("")
80+
}
81+
itemsByCategory[category]!!.sortedBy { it.itemName }.forEach { item ->
82+
option("item-option") {
83+
label = item.itemName
84+
value = item.itemName
85+
attributes["data-faction"] = item.faction.joinToString()
86+
}
87+
}
88+
}
89+
}
90+
91+
resources.forEach { resource ->
92+
td {
93+
attributes["data-resource"] = resource
94+
attributes["data-category"] = category.name
95+
classes = setOf("cost-cell")
96+
text(0)
97+
}
98+
}
99+
}
42100
}
101+
102+
tr {
103+
td()
104+
td {
105+
text("Total")
106+
}
107+
resources.forEach { resource ->
108+
td("total-cost-cell") {
109+
attributes["data-resource"] = resource
110+
text(0)
111+
}
112+
}
113+
}
114+
115+
tr {
116+
td()
117+
td {
118+
text("Crates")
119+
}
120+
resources.forEach { resource ->
121+
td("total-crate-cell") {
122+
attributes["data-resource"] = resource
123+
text(0)
124+
}
125+
}
126+
}
127+
43128
}
44129
}
45130
}

0 commit comments

Comments
 (0)