docks66-json-schema
Yanick Champoux 2023-05-14 17:51:26 -04:00
parent bf99b8dd4e
commit 1b9a5ab253
18 changed files with 591 additions and 18 deletions

View File

@ -8,6 +8,8 @@ releases:
changes:
- desc: add SMRs
type: feat
- desc: add SMLs
type: feat
- version: 3.1.0
changes:
- desc: add version and changelog to the about section

View File

@ -0,0 +1,40 @@
{#if magazines.length > 0}
<ShipItem {...reqs}>
<div>missile magazines</div>
<div class="magazines">
{#each magazines as magazine (magazine.id)}
<Magazine {...magazine} unused={magazine.launchers.length === 0} />
{/each}
</div>
</ShipItem>
{/if}
<script>
import { getContext } from "svelte";
import ShipItem from "$lib/components/ShipItem.svelte";
import Magazine from "./MissileMagazines/Magazine.svelte";
export let magazines = [];
let reqs = { cost: 0, mass: 0 };
const api = getContext("api");
$: {
reqs = { cost: 0, mass: 0 };
magazines.forEach(({ reqs: r }) => {
reqs.cost += r.cost;
reqs.mass += r.mass;
});
}
const handleMagazine = () => {};
</script>
<style>
.magazines {
display: flex;
margin-left: 2em;
}
</style>

View File

@ -0,0 +1,42 @@
<div class="magazine">
<div class="field label">
<input
type="number"
min={unused ? 0 : 1}
disabled={unused}
bind:value={maxAmmo}
/>
<label class="active">magazine {id}</label>
</div>
<label class="checkbox">
<input type="checkbox" bind:checked={extended} disabled={unused} />
<span>extended range</span>
</label>
</div>
<script>
import { getContext } from "svelte";
export let id = 1;
export let maxAmmo = 0;
export let extended = false;
export let unused = false;
export let api = getContext("api");
$: api?.dispatch?.setMissileMagazine?.(id, maxAmmo, extended);
</script>
<style>
.magazine {
max-width: 20em;
margin-right: 2em;
}
.magazine input {
max-width: 8em;
text-align: center;
}
.field {
margin-bottom: 1em;
}
</style>

View File

@ -3,15 +3,18 @@
<ADFC {...adfc} />
<MissileMagazines magazines={missileMagazines} />
<AddWeapon />
{#each weapons as weapon (weapon.id)}
<Weapon {...weapon} />
<Weapon {...weapon} nbrMissileMagazines={missileMagazines.length} />
{/each}
</Section>
<script>
import { getContext } from "svelte";
import u from "@yanick/updeep-remeda";
import Section from "$lib/components/Section.svelte";
@ -21,11 +24,28 @@
import Firecons from "./Weaponry/Firecons.svelte";
import ADFC from "./Weaponry/ADFC.svelte";
import AddWeapon from "./Weaponry/AddWeapon.svelte";
import MissileMagazines from "./MissileMagazines.svelte";
import Weapon from "./Weaponry/Weapon/index.svelte";
export let firecons = {};
export let adfc = {};
import Weapon from "./Weaponry/Weapon/index.svelte";
export let missileMagazines = [];
export let weapons = [];
$: missileMagazines = addLaunchersToMagazines(missileMagazines, weapons);
function addLaunchersToMagazines(magazines, weapons) {
return u.map(magazines, (mag) =>
u(mag, {
launchers: weapons.filter(
u.matches({
specs: {
type: "sml",
missileMagazineId: mag.id,
},
})
),
})
);
}
</script>

View File

@ -0,0 +1,18 @@
import SML from "./index.svelte";
export default {
title: "Salvo Missile Launcher",
component: SML,
};
export const Primary = {
render: (args) => ({
Component: SML,
props: args,
}),
args: {
availableMissileMagazineIds: [1, 2, 3],
missileMagazineId: 1,
arcs: ["F", "FS", "FP"],
},
};

View File

@ -0,0 +1,8 @@
<Hst.Story title="ShipEdit/Weaponry/Weapons/SML">
<SML />
</Hst.Story>
<script>
export let Hst;
import SML from "./index.svelte";
</script>

View File

@ -0,0 +1,74 @@
<span>salvo missile launcher</span>
<div class="arcs">
<Arcs
size={48}
selected={arcs}
on:clickArc={({ detail }) => setFirstArc(detail)}
/>
</div>
<div class="field label suffix">
<select bind:value={missileMagazineId}>
{#each availableMissileMagazineIds as id}
<option value={id}>{id}</option>
{/each}
</select>
<label class:active={true}>magazine</label>
</div>
<script lang="ts">
import u from "@yanick/updeep-remeda";
import { createEventDispatcher } from "svelte";
import memoize from "memoize-one";
import Arcs from "../Arcs.svelte";
import {
weaponTypes,
arcs as allArcs,
} from "$lib/store/ship/weaponry/rules.ts";
export let arcs = ["F", "FS", "FP"];
export let missileMagazineId;
export let nbrMissileMagazines = 1;
$: availableMissileMagazineIds = Array.from({ length: nbrMissileMagazines })
.fill(1)
.map((_, i) => i + 1);
const nbrArcs = 3;
let firstArc = allArcs[0];
const setFirstArc = (a) => (firstArc = a);
$: arcs = setArcs(firstArc, nbrArcs);
function setArcs(firstArc, nbrArcs) {
let first_index = allArcs.findIndex((arc) => arc === firstArc);
if (first_index === -1) first_index = 0;
return Array.from({ length: nbrArcs }).map(
(_dummy, i) => allArcs[(first_index + i) % allArcs.length]
);
}
const dispatch = createEventDispatcher();
const memoChange = memoize((missileMagazineId, ...arcs) =>
dispatch("change", {
missileMagazineId,
arcs,
})
);
$: memoChange(missileMagazineId, ...arcs);
</script>
<style>
.arcs {
margin-top: 0.5rem;
}
label span {
padding-left: 1em;
}
</style>

View File

@ -0,0 +1,79 @@
<span>salvo missile launcher</span>
<div class="arcs">
<Arcs
size={48}
selected={arcs}
on:clickArc={({ detail }) => setFirstArc(detail)}
/>
</div>
<div class="field label suffix">
<select bind:value={missileMagazineId}>
{#each availableMissileMagazineIds as id}
<option value={id}>{id}</option>
{/each}
</select>
<label class:active={true}>magazine</label>
</div>
<script lang="ts">
import u from "@yanick/updeep-remeda";
import { createEventDispatcher } from "svelte";
import memoize from "memoize-one";
import Arcs from "../Arcs.svelte";
import {
weaponTypes,
arcs as allArcs,
} from "$lib/store/ship/weaponry/rules.ts";
export let arcs = ["F", "FS", "FP"];
<<<<<<< HEAD
export let missileMagazineId;
=======
export let availableMissileMagazineIds = [1];
export let missileMagazineId = 1;
>>>>>>> a5a09f9 (storybook is back)
export let nbrMissileMagazines = 1;
$: availableMissileMagazineIds = Array.from({ length: nbrMissileMagazines })
.fill(1)
.map((_, i) => i + 1);
const nbrArcs = 3;
let firstArc = allArcs[0];
const setFirstArc = (a) => (firstArc = a);
$: arcs = setArcs(firstArc, nbrArcs);
function setArcs(firstArc, nbrArcs) {
let first_index = allArcs.findIndex((arc) => arc === firstArc);
if (first_index === -1) first_index = 0;
return Array.from({ length: nbrArcs }).map(
(_dummy, i) => allArcs[(first_index + i) % allArcs.length]
);
}
const dispatch = createEventDispatcher();
const memoChange = memoize((missileMagazineId, ...arcs) =>
dispatch("change", {
missileMagazineId,
arcs,
})
);
$: memoChange(missileMagazineId, ...arcs);
</script>
<style>
.arcs {
margin-top: 0.5rem;
}
label span {
padding-left: 1em;
}
</style>

View File

@ -2,7 +2,12 @@
<div class="weapon_row">
<a on:click={remove}><i>Delete</i> </a>
<svelte:component this={component[type]} {...specs} on:change={update} />
<svelte:component
this={component[type]}
{...specs}
{nbrMissileMagazines}
on:change={update}
/>
</div>
</ShipItem>
@ -21,6 +26,7 @@
import Torpedo from "./Torpedo/index.svelte";
import Missile from "./HeavyMissile/index.svelte";
import SalvoMissileRack from "./SalvoMissileRack.svelte";
import SalvoMissileLauncher from "./SML/index.svelte";
const component = {
beam: Beam,
@ -32,11 +38,13 @@
torpedo: Torpedo,
heavyMissile: Missile,
smr: SalvoMissileRack,
sml: SalvoMissileLauncher,
};
export let reqs = {};
export let specs = {};
export let id;
export let nbrMissileMagazines = 0;
const api = getContext("api");

View File

@ -17,6 +17,7 @@ import { armorDux } from "./ship/structure/armor";
import { fireconsDux } from "./ship/weaponry/firecons";
import { adfcDux } from "./ship/weaponry/adfc";
import { weaponsDux } from "./ship/weaponry/weapons";
import { weaponryDux } from "./ship/weaponry";
if (typeof process !== "undefined") {
process.env.UPDEEP_MODE = "dangerously_never_freeze";
@ -42,15 +43,6 @@ const propulsion = new Updux({
},
});
const weaponry = new Updux({
initialState: {},
subduxes: {
adfc: adfcDux,
firecons: fireconsDux,
weapons: weaponsDux,
},
});
const restore = createPayloadAction<typeof shipDux.initialState>("restore");
const importShip =
createPayloadAction<typeof shipDux.initialState>("importShip");
@ -68,7 +60,7 @@ const shipDux = new Updux({
structure,
propulsion,
carrier: carrierDux,
weaponry,
weaponry: weaponryDux,
},
});

View File

@ -0,0 +1,52 @@
import { weaponryDux } from "./index.ts";
test("sml and magazine", () => {
const store = weaponryDux.createStore();
expect(store.getState().missileMagazines).toHaveLength(0);
store.dispatch.addWeapon("sml");
store.dispatch.addWeapon("sml");
expect(store.getState().missileMagazines).toHaveLength(2);
expect(store.getState().missileMagazines[0]).toMatchObject({
extended: false,
maxAmmo: 1,
});
store.dispatch.setMissileMagazine(1, 3, true);
expect(store.getState().missileMagazines[0]).toMatchObject({
extended: true,
maxAmmo: 3,
});
store.dispatch.addWeapon("sml");
expect(store.getState().missileMagazines).toHaveLength(3);
expect(store.getState().missileMagazines[2]).toHaveProperty("id", 3);
// they all get assigned '1' at birth
expect(store.getState().weapons[2].specs).toHaveProperty(
"missileMagazineId",
1
);
store.dispatch.setWeapon(3, {
missileMagazineId: 3,
arcs: ["F", "FS", "FP"],
});
expect(store.getState().weapons[2].specs).toHaveProperty(
"missileMagazineId",
3
);
store.dispatch.removeWeapon(store.getState().weapons[1].id);
expect(store.getState().missileMagazines).toHaveLength(2);
console.log(store.getState());
expect(store.getState().weapons[1].specs.missileMagazineId).toEqual(2);
expect(store.getState().missileMagazines[1].id).toEqual(2);
});

View File

@ -0,0 +1,116 @@
import Updux, { createPayloadAction } from "updux";
import { adfcDux } from "./adfc";
import { fireconsDux } from "./firecons";
import { weaponsDux } from "./weapons";
import type { Reqs } from "$lib/shipDux/reqs";
import u from "@yanick/updeep-remeda";
import * as R from "remeda";
if (typeof process !== "undefined") {
process.env.UPDEEP_MODE = "dangerously_never_freeze";
}
type MissileMagazine = {
id: number;
maxAmmo: number;
extended: boolean;
};
const setMissileMagazine = createPayloadAction(
"setMissileMagazine",
(id, maxAmmo, extended = undefined) => ({ id, maxAmmo, extended })
);
const moveMissileMagazine = createPayloadAction(
"moveMissileMagazine",
(from, to) => ({
from,
to,
})
);
const removeMissileMagazine = createPayloadAction<number>(
"removeMissileMagazine"
);
const magazinesDux = new Updux({
actions: { setMissileMagazine, removeMissileMagazine, moveMissileMagazine },
initialState: [] as MissileMagazine[],
});
magazinesDux.addMutation(weaponsDux.actions.addWeapon, (payload) => (state) => {
if (payload !== "sml") return state;
state.push({
id: state.length + 1,
extended: false,
maxAmmo: 1,
reqs: magazineReqs({ extended: false, maxAmmo: 1 }),
});
});
magazinesDux.addMutation(setMissileMagazine, (payload) =>
u.map(
u.if(u.matches({ id: payload.id }), (state) => {
state = u(state, payload);
return u(state, { reqs: magazineReqs(state) });
})
)
);
magazinesDux.addMutation(moveMissileMagazine, ({ from, to }) =>
u.map(u.if(u.matches({ id: from }), { id: to }))
);
magazinesDux.addMutation(removeMissileMagazine, (id) =>
u.reject(u.matches({ id }))
);
magazinesDux.addReaction((api) => (store) => {
store
.map(R.prop("id"))
.filter(({ id }, i) => id !== i + 1)
.forEach((id, i) => api.dispatch.moveMissileMagazine(id, i + 1));
});
function magazineReqs(magazine: MissileMagazine): Reqs {
let mass = magazine.maxAmmo * (magazine.extended ? 3 : 2);
return { mass, cost: 3 * mass };
}
export const weaponryDux = new Updux({
initialState: {},
subduxes: {
adfc: adfcDux,
firecons: fireconsDux,
weapons: weaponsDux,
missileMagazines: magazinesDux,
},
});
weaponryDux.addMutation(moveMissileMagazine, ({ from, to }) =>
u({
weapons: u.map(
u.if(u.matches({ specs: { missileMagazineId: from } }), {
specs: { missileMagazineId: to },
})
),
})
);
weaponryDux.addReaction((api) => (state) => {
const smls = state.weapons.filter(u.matches({ specs: { type: "sml" } }));
const usedMagazines = smls.map(
({ specs: { missileMagazineId } }) => missileMagazineId
);
const unusedMags = state.missileMagazines
.map(R.prop("id"))
.filter((id) => !usedMagazines.includes(id));
unusedMags.forEach((id) => api.dispatch.setMissileMagazine(id, 0));
if (smls.length >= state.missileMagazines.length) return;
api.dispatch.removeMissileMagazine(unusedMags[0]);
});

View File

@ -40,6 +40,18 @@ type SalvoMissileRack = {
extended: boolean;
};
type MissileMagazine = {
id: number;
maxAmmo: number;
extended: boolean;
};
type SalvoMissileLauncher = {
type: "salvoMissileLauncher";
arcs: Arc[];
missileMagazineId: number;
};
type Graser = {
type: "graser";
weaponClass: 1 | 2 | 3;
@ -60,7 +72,8 @@ export type Weapon =
| Needle
| Graser
| Torpedo
| HeavyMissile;
| HeavyMissile
| SalvoMissileLauncher;
export const weaponTypes = [
{
@ -155,6 +168,16 @@ export const weaponTypes = [
type: "smr",
},
},
{
name: "salvo missile launcher",
type: "sml",
reqs: { cost: 9, mass: 3 },
initial: {
arcs: ["FP", "F", "FS"],
type: "sml",
missileMagazineId: 1,
},
},
];
export function weaponReqs(weapon): Reqs {

View File

@ -0,0 +1,16 @@
import SML from "./index.svelte";
export default {
title: "PrintShip/Weapons/SML",
component: SML,
tags: ["autodocs"],
};
export const Primary = {
args: {
magazine: {
maxAmmo: 3,
},
launchers: [{ arcs: ["F"] }, { arcs: ["FS"] }],
},
};

View File

@ -0,0 +1,59 @@
<div class="sml">
<div class="launchers">
{#each launchers as launcher, i (i)}
<div>
<SMR {...launcher} />
<div class="line">&nbsp;</div>
</div>
{/each}
</div>
<div class="magazine">
{#each Array.from({ length: magazine.maxAmmo }).fill(1) as a, i (i)}
<img src="/icons/missile.svg" width="18" />
{/each}
</div>
<div>
{#if magazine.extended}
<div>extended range</div>
{/if}
</div>
</div>
<script>
import SMR from "../SMR/index.svelte";
export let launchers = [];
/** the missile magazine feeding the launchers */
export let magazine = {};
</script>
<style>
.sml {
display: flex;
flex-direction: column;
align-items: center;
}
.launchers {
display: flex;
}
.launchers > div {
display: flex;
flex-direction: column;
align-items: center;
}
.line {
width: 0.2em;
background-color: black;
top: -0.5em;
}
.magazine {
border: 0.2em black solid;
border-radius: 0.5em;
padding: 0.3em;
text-align: center;
width: 100%;
display: flex;
justify-content: space-evenly;
}
</style>

View File

@ -7,6 +7,7 @@ import Graser from "./Graser/index.svelte";
import Torpedo from "./Torpedo/index.svelte";
import HeavyMissile from "./HeavyMissile/index.svelte";
import SalvoMissileRack from "./SMR/index.svelte";
import SalvoMissileLauncher from "./SML/index.svelte";
export default {
torpedo: Torpedo,
@ -18,4 +19,5 @@ export default {
needle: Needlebeam,
heavyMissile: HeavyMissile,
smr: SalvoMissileRack,
sml: SalvoMissileLauncher,
};

View File

@ -15,6 +15,15 @@
<HeavyMissiles {heavyMissiles} />
</div>
<div class="weapon-group">
{#each Object.keys(smls) as magId (magId)}
<SML
launchers={smls[magId].map(R.prop("specs"))}
magazine={magazines.find(({ id }) => id == magId)}
/>
{/each}
</div>
<Beams {beams} />
<Weapons {weapons} />
@ -61,6 +70,8 @@
import Beams from "./Weapons/Beams.svelte";
import HeavyMissiles from "./Weapons/HeavyMissiles.svelte";
import SalvoMissileRack from "./Weapons/SMR/index.svelte";
import SML from "./Weapons/SML/index.svelte";
import * as R from "remeda";
export let identification = {};
export let propulsion = {};
@ -76,11 +87,14 @@
weapons,
u.matches({
specs: {
type: (t) => ["smr", "pds", "beam", "heavyMissile"].includes(t),
type: (t) => ["sml", "smr", "pds", "beam", "heavyMissile"].includes(t),
},
})
);
$: magazines = weaponry.missileMagazines;
$: console.log({ magazines });
$: pds = (weaponry?.weapons ?? []).filter(
u.matches({ specs: { type: "pds" } })
);
@ -93,6 +107,14 @@
$: smrs = (weaponry?.weapons ?? []).filter(
u.matches({ specs: { type: "smr" } })
);
$: console.log(
(weaponry?.weapons ?? []).filter(u.matches({ specs: { type: "sml" } }))
);
$: smls = R.groupBy(
(weaponry?.weapons ?? []).filter(u.matches({ specs: { type: "sml" } })),
({ specs: { missileMagazineId } }) => missileMagazineId
);
$: console.log({ smls });
</script>
<style>

View File

@ -7,7 +7,7 @@ import git from "git-describe";
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
// publicDir: "./static",
publicDir: "./static",
ssr: {},
optimizeDeps: {},
define: {