Merge branch 'main' of github.com:jcalixte/tps

This commit is contained in:
Julien Calixte
2025-08-12 15:44:54 +02:00
13 changed files with 390 additions and 104 deletions

View File

@@ -45,7 +45,7 @@
<li class="takt focusable">Takt</li> <li class="takt focusable">Takt</li>
<li class="one-piece-flow focusable">One piece flow</li> <li class="one-piece-flow focusable">One piece flow</li>
<li class="pull-system focusable"> <li class="pull-system focusable">
<a href="/pull-system.html">Pull system</a> <a href="/pull-system">Pull system</a>
</li> </li>
</ul> </ul>
</section> </section>

View File

@@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^12.2.0", "@vueuse/core": "^12.2.0",
"chart.js": "^4.5.0",
"chart.xkcd": "^1.1.15", "chart.xkcd": "^1.1.15",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"daisyui": "^5.0.50", "daisyui": "^5.0.50",
@@ -26,6 +27,7 @@
"random-js": "^2.1.0", "random-js": "^2.1.0",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-chartjs": "^5.3.2",
"vue-starport": "^0.4.0" "vue-starport": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {

30
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@vueuse/core': '@vueuse/core':
specifier: ^12.2.0 specifier: ^12.2.0
version: 12.2.0(typescript@5.7.2) version: 12.2.0(typescript@5.7.2)
chart.js:
specifier: ^4.5.0
version: 4.5.0
chart.xkcd: chart.xkcd:
specifier: ^1.1.15 specifier: ^1.1.15
version: 1.1.15 version: 1.1.15
@@ -41,6 +44,9 @@ importers:
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.13(typescript@5.7.2) version: 3.5.13(typescript@5.7.2)
vue-chartjs:
specifier: ^5.3.2
version: 5.3.2(chart.js@4.5.0)(vue@3.5.13(typescript@5.7.2))
vue-starport: vue-starport:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0(typescript@5.7.2) version: 0.4.0(typescript@5.7.2)
@@ -408,6 +414,9 @@ packages:
'@jridgewell/trace-mapping@0.3.29': '@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@@ -889,6 +898,10 @@ packages:
resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==}
engines: {node: '>=12'} engines: {node: '>=12'}
chart.js@4.5.0:
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
engines: {pnpm: '>=8'}
chart.xkcd@1.1.15: chart.xkcd@1.1.15:
resolution: {integrity: sha512-FPpNCkaVSPyB6GIb9eXKEAjKSnr6hA7blzQFUeTkZKvSr9wcg1eSK73V6APgfrzkDDK7X+iVtLXJXIGPrCFj8g==} resolution: {integrity: sha512-FPpNCkaVSPyB6GIb9eXKEAjKSnr6hA7blzQFUeTkZKvSr9wcg1eSK73V6APgfrzkDDK7X+iVtLXJXIGPrCFj8g==}
@@ -1387,6 +1400,12 @@ packages:
jsdom: jsdom:
optional: true optional: true
vue-chartjs@5.3.2:
resolution: {integrity: sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==}
peerDependencies:
chart.js: ^4.1.1
vue: ^3.0.0-0 || ^2.7.0
vue-demi@0.14.10: vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1602,6 +1621,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@kurkle/color@0.3.4': {}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -2020,6 +2041,10 @@ snapshots:
loupe: 3.1.3 loupe: 3.1.3
pathval: 2.0.0 pathval: 2.0.0
chart.js@4.5.0:
dependencies:
'@kurkle/color': 0.3.4
chart.xkcd@1.1.15: chart.xkcd@1.1.15:
dependencies: dependencies:
d3-axis: 1.0.12 d3-axis: 1.0.12
@@ -2507,6 +2532,11 @@ snapshots:
- supports-color - supports-color
- terser - terser
vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.13(typescript@5.7.2)):
dependencies:
chart.js: 4.5.0
vue: 3.5.13(typescript@5.7.2)
vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)): vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)):
dependencies: dependencies:
vue: 3.5.13(typescript@5.7.2) vue: 3.5.13(typescript@5.7.2)

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { Chart as ChartJS, registerables } from 'chart.js'
import { useBoardGameStore } from '@/modules/5s/board-game-store'
import { toDuration, toSeconds } from '@/modules/5s/utils'
import { getNatural } from '@/utils'
import { _ } from '@faker-js/faker/dist/airline-D6ksJFwG'
import { computed, ref } from 'vue'
import { Bar } from 'vue-chartjs'
ChartJS.register(...registerables)
const boardGameStore = useBoardGameStore()
const duration = ref<string | null>(null)
const last10Perfs = computed(() =>
[...boardGameStore.meta.perfs].slice(-10).reverse()
)
setInterval(() => {
duration.value = boardGameStore.meta.start
? toDuration(
new Date(boardGameStore.meta.start),
boardGameStore.meta.end ? new Date(boardGameStore.meta.end) : new Date()
)
: null
}, 1000)
</script>
<template>
<div class="board-game-performance">
<p class="numeric">{{ duration }}</p>
<template v-if="boardGameStore.meta.perfs.length > 0">
<h3>Last performances</h3>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Duration</th>
<th>Board Games</th>
<th>Time / board game</th>
</tr>
</thead>
<tbody>
<tr v-for="perf in last10Perfs">
<td class="numeric">
{{ toDuration(new Date(perf.start), new Date(perf.end)) }}
</td>
<td class="numeric">
{{ perf.totalGames }}
</td>
<td class="numeric">
{{
getNatural(
toSeconds(new Date(perf.start), new Date(perf.end)),
perf.totalGames
)
}}s
</td>
</tr>
</tbody>
</table>
</div>
<h3>Progression</h3>
<Bar
id="my-chart-id"
:data="{
labels: boardGameStore.meta.perfs.map((_, i) => `Round #${i + 1}`),
datasets: [
{
label: 'Time per board game (in s)',
data: boardGameStore.meta.perfs.map((perf) =>
toSeconds(new Date(perf.start), new Date(perf.end))
)
}
]
}"
/>
</template>
</div>
</template>
<style scoped lang="scss">
.board-game-performance {
.numeric {
text-align: right;
}
}
</style>

View File

@@ -1,44 +1,92 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBoardGameStore } from '@/modules/5s/board-game-store' import { useBoardGameStore } from '@/modules/5s/board-game-store'
import { shuffleArray } from '@/utils'
import { computed } from 'vue' import { computed } from 'vue'
const boardGameStore = useBoardGameStore() const boardGameStore = useBoardGameStore()
const isSeiriActivated = computed(() => boardGameStore.sUsed.includes('seiri'))
const isSeitonActivated = computed(() => const isSeitonActivated = computed(() =>
boardGameStore.sUsed.includes('seiton') boardGameStore.sUsed.includes('seiton')
) )
// const isSeisoActivated = computed(() => boardGameStore.sUsed.includes('seiso'))
const rawTools = boardGameStore.tools const neededTools = computed(
.map((t) => `${t.name} (${t.alias})`) () =>
.join(', ') new Set(
boardGameStore.boardGames
.flatMap((g) => g.parts)
.flatMap((p) => p.tasks)
.flatMap((t) => t.tools)
.map((t) => t.id)
)
)
const tools = computed(() => {
const toolsToUse = isSeiriActivated.value
? boardGameStore.tools.filter((t) => neededTools.value.has(t.id))
: boardGameStore.tools
return isSeitonActivated.value
? [...toolsToUse].sort(
(a, b) =>
(boardGameStore.countUsedTools[b.id] || 0) -
(boardGameStore.countUsedTools[a.id] || 0)
)
: toolsToUse
})
const toolsToDisplay = computed(() =>
shuffleArray(tools.value.map((t) => `${t.name} (ref: ${t.reference})`)).join(
', '
)
)
</script> </script>
<template> <template>
<aside class="board-game-tools"> <div class="board-game-tools prose">
<h2>Tools</h2>
<div class="overflow-x-auto" v-if="isSeitonActivated"> <div class="overflow-x-auto" v-if="isSeitonActivated">
<table class="table table-zebra"> <table class="table table-md">
<thead> <thead>
<tr> <tr>
<th>Tool</th> <th>Tool</th>
<th>Alias</th> <th>Reference</th>
<th>Used</th>
</tr> </tr>
</thead> </thead>
<tbody> <transition-group name="list" tag="tbody">
<tr v-for="tool in boardGameStore.tools" :key="tool.alias"> <tr v-for="tool in tools" :key="tool.reference">
<td>{{ tool.name }}</td> <td>{{ tool.name }}</td>
<td>{{ tool.alias }}</td> <td class="numeric">{{ tool.reference }}</td>
<td class="numeric count">
{{ boardGameStore.countUsedTools[tool.id] }}
</td>
</tr> </tr>
</tbody> </transition-group>
</table> </table>
</div> </div>
<div v-else> <div v-else>
{{ rawTools }} {{ toolsToDisplay }}
</div> </div>
</aside> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.board-game-tools { .board-game-tools {
flex: 1; .count {
text-align: right;
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
} }
</style> </style>

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBoardGameStore } from '@/modules/5s/board-game-store' import { useBoardGameStore } from '@/modules/5s/board-game-store'
import BoardGamePerformance from '@/modules/5s/BoardGamePerformance.vue'
import BoardGameToolbox from '@/modules/5s/BoardGameToolbox.vue'
import BoardGameToolbox from '@/modules/5s/Toolbox.vue' import BoardGameToolbox from '@/modules/5s/Toolbox.vue'
import { _5S, is5S } from '@/modules/5s/types/5s' import { _5S, is5S } from '@/modules/5s/types/5s'
import { toDuration } from '@/modules/5s/utils' import { _5S, is5S } from '@/modules/5s/types/5s'
import { ref, toValue } from 'vue' import { onMounted, ref, toValue } from 'vue'
const userInput = ref('') const userInput = ref('')
const mode = ref<_5S | null>(null) const mode = ref<_5S | null>(null)
@@ -18,6 +20,11 @@ setInterval(() => {
) )
: null : null
}, 1000) }, 1000)
if (import.meta.env.DEV) {
onMounted(() => {
boardGameStore.initGame()
})
}
const submit = () => { const submit = () => {
const lastInput = toValue(userInput) const lastInput = toValue(userInput)
@@ -43,23 +50,34 @@ const submit = () => {
boardGameStore.activateS(command) boardGameStore.activateS(command)
return return
} }
// d for debug
if (command === 'd') {
boardGameStore.increment()
}
} }
</script> </script>
<template> <template>
<div class="board-game-workshop prose"> <header v-if="!boardGameStore.currentBoardGame">
<BoardGameToolbox /> <button
<div class="main"> class="btn btn-primary"
<h2>Workshop</h2> v-if="!boardGameStore.currentBoardGame"
<button @click="boardGameStore.initGame"
class="btn" >
v-if="!boardGameStore.currentBoardGame" start
@click="boardGameStore.initGame" </button>
> </header>
start <div v-else class="board-game-workshop">
</button> <aside class="prose">
<h2>Toolbox</h2>
<div v-if="boardGameStore.currentBoardGame"> <BoardGameToolbox />
</aside>
<div class="main prose">
<h2 class="title">Workshop</h2>
<div>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<input type="text" v-model="userInput" autofocus /> <input type="text" v-model="userInput" autofocus />
</form> </form>
@@ -85,11 +103,6 @@ const submit = () => {
{{ part.name }} {{ part.name }}
</span> </span>
<template v-if="partIndex === boardGameStore.currentPartIndex"> <template v-if="partIndex === boardGameStore.currentPartIndex">
<div class="inline-grid *:[grid-area:1/1]">
<div class="status status-primary animate-ping"></div>
<div class="status status-primary"></div>
</div>
<ol> <ol>
<li <li
v-for="(task, taskIndex) in boardGameStore.currentPart v-for="(task, taskIndex) in boardGameStore.currentPart
@@ -110,9 +123,9 @@ const submit = () => {
boardGameStore.currentTask boardGameStore.currentTask
" "
> >
<div class="inline-grid *:[grid-area:1/1]"> <div class="inline-grid *:[grid-area:1/1] ml-2">
<div class="status status-primary animate-ping"></div> <div class="status status-info animate-ping"></div>
<div class="status status-primary"></div> <div class="status status-info"></div>
</div> </div>
<ul> <ul>
<li <li
@@ -137,24 +150,9 @@ const submit = () => {
</div> </div>
</div> </div>
</div> </div>
<aside class="performance prose">
<aside
class="performance"
v-if="duration !== null || boardGameStore.meta.perfs.length > 0"
>
<h2>Performance</h2> <h2>Performance</h2>
<BoardGamePerformance />
<p>{{ duration }}</p>
<template v-if="boardGameStore.meta.perfs.length > 0">
<h3>Last performances</h3>
<ul>
<li v-for="perf in boardGameStore.meta.perfs">
{{ toDuration(new Date(perf[0]), new Date(perf[1])) }}
</li>
</ul>
</template>
</aside> </aside>
</div> </div>
</template> </template>
@@ -163,14 +161,9 @@ const submit = () => {
@import url('https://fonts.googleapis.com/css2?family=Google+Sans+Code&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Google+Sans+Code&display=swap');
.board-game-workshop { .board-game-workshop {
flex: 1;
font-family: 'Google Sans Code', monospace;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-size: 14px;
display: flex; display: flex;
gap: 4rem; gap: 1rem;
padding: 1rem;
input { input {
font-family: 'Google Sans Code', monospace; font-family: 'Google Sans Code', monospace;
@@ -180,6 +173,10 @@ const submit = () => {
} }
} }
h2 {
text-align: center;
}
form { form {
text-align: center; text-align: center;
} }
@@ -189,14 +186,23 @@ form {
color: green; color: green;
} }
aside { aside,
.aside {
flex: 1; flex: 1;
} }
.main { .main {
flex: 2; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.card {
margin: 1rem;
}
.card-title {
justify-content: center;
}
</style> </style>

View File

@@ -3,7 +3,7 @@ import { boardGames } from '@/modules/5s/types/board-games'
import { tools } from '@/modules/5s/types/tools' import { tools } from '@/modules/5s/types/tools'
import { BoardGame, Part, Task, Tool } from '@/modules/5s/types/workshop' import { BoardGame, Part, Task, Tool } from '@/modules/5s/types/workshop'
import { toDuration } from '@/modules/5s/utils' import { toDuration } from '@/modules/5s/utils'
import { randomAlias } from '@/utils' import { accumulate, randomAlias } from '@/utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
type State = { type State = {
@@ -17,10 +17,23 @@ type State = {
meta: { meta: {
start: string | null start: string | null
end: string | null end: string | null
perfs: Array<[string, string]> perfs: Array<{
start: string
end: string
boardGames: BoardGame[]
countGames: Record<string, number>
totalGames: number
}>
} }
} }
const firstDemands = [
boardGames[0],
boardGames[0]
// boardGames[0],
// boardGames[0]
]
export const useBoardGameStore = defineStore('board-game', { export const useBoardGameStore = defineStore('board-game', {
state: (): State => ({ state: (): State => ({
tools: [], tools: [],
@@ -38,12 +51,11 @@ export const useBoardGameStore = defineStore('board-game', {
}), }),
actions: { actions: {
initGame() { initGame() {
// this.boardGames = [boardGames[0], boardGames[1]]
this.tools = tools.map((t) => ({ this.tools = tools.map((t) => ({
...t, ...t,
alias: randomAlias() reference: randomAlias()
})) }))
this.boardGames = [boardGames[0]] this.boardGames = firstDemands
this.currentBoardGameIndex = 0 this.currentBoardGameIndex = 0
this.currentPartIndex = 0 this.currentPartIndex = 0
this.currentTaskIndex = 0 this.currentTaskIndex = 0
@@ -55,7 +67,7 @@ export const useBoardGameStore = defineStore('board-game', {
return return
} }
const tool = this.tools.find((t) => t.alias === alias) const tool = this.tools.find((t) => t.reference === alias)
if (!tool) { if (!tool) {
return return
@@ -107,7 +119,17 @@ export const useBoardGameStore = defineStore('board-game', {
// All board games complete // All board games complete
this.meta.end = new Date().toISOString() this.meta.end = new Date().toISOString()
this.meta.perfs = [...this.meta.perfs, [this.meta.start, this.meta.end]] const countGames = accumulate(this.boardGames.map((b) => b.name))
this.meta.perfs = [
...this.meta.perfs,
{
start: this.meta.start,
end: this.meta.end,
boardGames: [...this.boardGames],
countGames,
totalGames: this.boardGames.length
}
]
this.currentBoardGameIndex = null this.currentBoardGameIndex = null
this.currentPartIndex = null this.currentPartIndex = null
this.currentTaskIndex = null this.currentTaskIndex = null
@@ -156,6 +178,50 @@ export const useBoardGameStore = defineStore('board-game', {
} }
return toDuration(new Date(this.meta.start), new Date(this.meta.end)) return toDuration(new Date(this.meta.start), new Date(this.meta.end))
},
countUsedTools(): Record<string, number> {
const metaToolIds = this.meta.perfs
.flatMap((p) => p.boardGames)
.flatMap((b) => b.parts)
.flatMap((p) => p.tasks)
.flatMap((t) => t.tools)
.map((t) => t.id)
if (
!this.meta.start ||
!this.currentTask ||
!this.currentPart ||
!this.currentBoardGame ||
this.currentTaskIndex === null ||
this.currentPartIndex === null ||
this.currentBoardGameIndex === null
) {
return accumulate(metaToolIds)
}
const currentBoardGameIndex = this.currentBoardGameIndex
const currentPartIndex = this.currentPartIndex
const currentTaskIndex = this.currentTaskIndex
const toolIds = this.boardGames
.filter((_, i) => i <= currentBoardGameIndex)
.flatMap((b, boardIndex) =>
boardIndex === currentBoardGameIndex
? b.parts.filter((_, i) => i <= currentPartIndex)
: b.parts
)
.flatMap((p, partIndex) =>
partIndex === currentPartIndex
? p.tasks.filter((_, i) => i <= currentTaskIndex)
: p.tasks
)
.flatMap((t, taskIndex) =>
taskIndex === currentTaskIndex
? this.usedTools
: t.tools.map((t) => t.id)
)
return accumulate([...metaToolIds, ...toolIds])
} }
} }
}) })

View File

@@ -10,7 +10,6 @@ import { NonEmptyArray } from '@/modules/5s/types/tools'
export type Tool = { export type Tool = {
name: string name: string
alias: string alias: string
cooldown: number
} }
export type Task = { export type Task = {
@@ -29,14 +28,14 @@ export type BoardGame = {
} }
export const tools: Tool[] = [ export const tools: Tool[] = [
{ name: 'Card Printer', alias: 'card-printer', cooldown: 5 }, { name: 'Card Printer', alias: 'card-printer' },
{ name: 'Miniature Mold', alias: 'mini-mold', cooldown: 15 }, { name: 'Miniature Mold', alias: 'mini-mold' },
{ name: 'Dice Engraver', alias: 'dice-engraver', cooldown: 10 }, { name: 'Dice Engraver', alias: 'dice-engraver' },
{ name: 'Board Cutter', alias: 'board-cutter', cooldown: 8 }, { name: 'Board Cutter', alias: 'board-cutter' },
{ name: 'Rulebook Designer', alias: 'rulebook-dzn', cooldown: 6 }, { name: 'Rulebook Designer', alias: 'rulebook-dzn' },
{ name: 'Box Assembler', alias: 'box-asm', cooldown: 4 }, { name: 'Box Assembler', alias: 'box-asm' },
{ name: 'Component Painter', alias: 'painter', cooldown: 12 }, { name: 'Component Painter', alias: 'painter' },
{ name: 'Lamination Machine', alias: 'laminator', cooldown: 7 } { name: 'Lamination Machine', alias: 'laminator' }
] ]
export type NonEmptyArray<T> = [T, ...T[]] export type NonEmptyArray<T> = [T, ...T[]]

View File

@@ -8,7 +8,7 @@ export const boardGames: BoardGame[] = [
{ {
name: 'Chessboard Engraving', name: 'Chessboard Engraving',
tasks: [ tasks: [
{ name: 'Cut the board base', tools: chooseTools('board-cutter') }, { name: 'Cut the board base', tools: chooseTools('cutter') },
{ {
name: 'Apply lamination', name: 'Apply lamination',
tools: chooseTools('laminator', 'painter') tools: chooseTools('laminator', 'painter')
@@ -19,12 +19,8 @@ export const boardGames: BoardGame[] = [
name: 'Piece Creation', name: 'Piece Creation',
tasks: [ tasks: [
{ {
name: 'Mold pawns', name: 'Mold pieces',
tools: chooseTools('mini-mold', 'painter') tools: chooseTools('mini-mold', 'cutter')
},
{
name: 'Engrave royalty',
tools: chooseTools('dice-engraver', 'mini-mold')
}, },
{ {
name: 'Paint pieces', name: 'Paint pieces',
@@ -68,7 +64,7 @@ export const boardGames: BoardGame[] = [
tasks: [ tasks: [
{ {
name: 'Cut dungeon tiles', name: 'Cut dungeon tiles',
tools: chooseTools('board-cutter', 'laminator') tools: chooseTools('cutter', 'laminator')
} }
] ]
} }
@@ -82,7 +78,7 @@ export const boardGames: BoardGame[] = [
tasks: [ tasks: [
{ {
name: 'Print scenario deck', name: 'Print scenario deck',
tools: chooseTools('card-printer', 'rulebook-dzn') tools: chooseTools('card-prisnter', 'rulebook-dzn')
}, },
{ {
name: 'Apply finish', name: 'Apply finish',
@@ -113,7 +109,7 @@ export const boardGames: BoardGame[] = [
tasks: [ tasks: [
{ {
name: 'Print map base', name: 'Print map base',
tools: chooseTools('board-cutter', 'laminator') tools: chooseTools('cutter', 'laminator')
}, },
{ {
name: 'Add compass', name: 'Add compass',
@@ -171,7 +167,7 @@ export const boardGames: BoardGame[] = [
tasks: [ tasks: [
{ {
name: 'Print jungle layout', name: 'Print jungle layout',
tools: chooseTools('board-cutter', 'painter') tools: chooseTools('cutter', 'painter')
}, },
{ {
name: 'Seal board', name: 'Seal board',
@@ -216,7 +212,7 @@ export const boardGames: BoardGame[] = [
tasks: [ tasks: [
{ {
name: 'Cut castle walls', name: 'Cut castle walls',
tools: chooseTools('board-cutter', 'painter') tools: chooseTools('cutter', 'painter')
}, },
{ {
name: 'Reinforce walls', name: 'Reinforce walls',

View File

@@ -1,18 +1,51 @@
import { Tool } from '@/modules/5s/types/workshop' import { Tool } from '@/modules/5s/types/workshop'
export const tools: Tool[] = [ export const tools: Tool[] = [
{ name: 'Card Printer', id: 'card-printer', alias: '', cooldown: 5 }, { name: 'Card Printer', id: 'card-printer', reference: '' },
{ name: 'Miniature Mold', id: 'mini-mold', alias: '', cooldown: 15 }, { name: 'Miniature Mold', id: 'mini-mold', reference: '' },
{ name: 'Dice Engraver', id: 'dice-engraver', alias: '', cooldown: 10 }, { name: 'Dice Engraver', id: 'dice-engraver', reference: '' },
{ name: 'Board Cutter', id: 'board-cutter', alias: '', cooldown: 8 }, { name: 'Cutter', id: 'cutter', reference: '' },
{ name: 'Rulebook Designer', id: 'rulebook-dzn', alias: '', cooldown: 6 }, { name: 'Rulebook Designer', id: 'rulebook-dzn', reference: '' },
{ name: 'Box Assembler', id: 'box-asm', alias: '', cooldown: 4 }, { name: 'Box Assembler', id: 'box-asm', reference: '' },
{ name: 'Component Painter', id: 'painter', alias: '', cooldown: 12 }, { name: 'Component Painter', id: 'painter', reference: '' },
{ name: 'Lamination Machine', id: 'laminator', alias: '', cooldown: 7 } { name: 'Lamination Machine', id: 'laminator', reference: '' },
// Additional realistic tools
{
name: 'Shrink Wrap Machine',
id: 'shrink-wrap',
reference: ''
},
{
name: 'Punch Board Cutter',
id: 'punch-cutter',
reference: ''
},
{
name: 'Sticker Applicator',
id: 'sticker-applicator',
reference: ''
},
{
name: 'Foil Stamping Press',
id: 'foil-stamp',
reference: ''
},
{
name: 'Scanner',
id: 'scanner',
reference: ''
},
{
name: 'Instruction Sheet Folder',
id: 'sheet-folder',
reference: ''
},
{ name: 'Plastic Bag Sealer', id: 'bag-sealer', reference: '' },
{ name: 'Barcode Printer', id: 'barcode-printer', reference: '' }
] ]
export type NonEmptyArray<T> = [T, ...T[]] export type NonEmptyArray<T> = [T, ...T[]]
export const idToTools = (id: string): Tool => tools.find((t) => t.id === id)! export const idToTools = (id: string): Tool => tools.find((t) => t.id === id)!
export const chooseTools = (...ides: string[]): NonEmptyArray<Tool> => export const chooseTools = (...ids: string[]): NonEmptyArray<Tool> =>
ides.map(idToTools) as NonEmptyArray<Tool> ids.map(idToTools) as NonEmptyArray<Tool>

View File

@@ -3,8 +3,7 @@ import { NonEmptyArray } from '@/modules/5s/types/tools'
export type Tool = { export type Tool = {
name: string name: string
id: string id: string
alias: string reference: string
cooldown: number
} }
export type Task = { export type Task = {

View File

@@ -18,7 +18,11 @@ export const toDuration = (startDate: Date, endDate: Date = new Date()) => {
parts.push(`${minutes}m`) parts.push(`${minutes}m`)
} }
parts.push(`${seconds}s`) parts.push(`${seconds}s`.padStart(3, '0'))
return parts.join(' ') return parts.join(' ')
} }
export const toSeconds = (start: Date, endDate: Date): number => {
return Math.floor((endDate.getTime() - start.getTime()) / 1000)
}

View File

@@ -21,6 +21,9 @@ export const getMean = (data: number[]) =>
export const getRound = (data: number, total: number) => export const getRound = (data: number, total: number) =>
(Math.round(100 * (data / total)) / 100 || 0).toFixed(2) (Math.round(100 * (data / total)) / 100 || 0).toFixed(2)
export const getNatural = (data: number, total: number) =>
(Math.round(100 * (data / total)) / 100 || 0).toFixed(0)
export const shuffleArray = <T>(array: T[]) => { export const shuffleArray = <T>(array: T[]) => {
let currentIndex = array.length, let currentIndex = array.length,
randomIndex randomIndex
@@ -37,6 +40,13 @@ export const shuffleArray = <T>(array: T[]) => {
return array return array
} }
export const accumulate = (array: string[]) => {
return array.reduce<Record<string, number>>((acc, toolId) => {
acc[toolId] = (acc[toolId] || 0) + 1
return acc
}, {})
}
export const popNElement = <T>(array: T[], numberOfElements: number) => { export const popNElement = <T>(array: T[], numberOfElements: number) => {
const poppedElements: T[] = [] const poppedElements: T[] = []