Merge branch 'streamlining-2' into redux-toolbox

This commit is contained in:
Yanick Champoux 2023-03-24 11:11:29 -04:00
commit add30bb666
35 changed files with 764 additions and 418 deletions

View File

@ -9,5 +9,5 @@ export default defineConfig({
// Options here // Options here
}), }),
], ],
// setupFile: "./src/histoire.setup.js", setupFile: "./src/histoire.setup.js",
}); });

View File

@ -26,31 +26,31 @@
"globby": "^13.1.3", "globby": "^13.1.3",
"jest-image-snapshot": "^4.5.1", "jest-image-snapshot": "^4.5.1",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"prettier": "~2.5.1", "prettier": "~2.8.6",
"prettier-plugin-svelte": "^2.6.0", "prettier-plugin-svelte": "^2.10.0",
"showdown": "^2.0.3", "showdown": "^2.0.3",
"svelte": "^3.55.1", "svelte": "^3.57.0",
"typescript": "^4.9.5", "typescript": "^5.0.2",
"vitest": "^0.29.7", "vitest": "^0.29.7"
"vitest-svelte-kit": "^0.0.6"
}, },
"dependencies": { "dependencies": {
"@picocss/pico": "^1.5.7", "@picocss/pico": "^1.5.7",
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"@sveltejs/adapter-node": "^1.0.0-next.0", "@sveltejs/adapter-node": "^1.0.0-next.0",
"@yanick/updeep-remeda": "^2.1.0", "@yanick/updeep-remeda": "^2.1.1",
"chota": "^0.8.0", "chota": "^0.8.0",
"effector": "^22.5.2", "effector": "^22.5.2",
"histoire": "^0.15.9", "histoire": "^0.15.9",
"jsdom": "^21.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"redux": "^4.1.2", "redux": "^4.1.2",
"remeda": "^1.1.0", "remeda": "^1.9.1",
"reselect": "^4.1.5", "reselect": "^4.1.5",
"svelte-chota": "^1.8.6", "svelte-chota": "^1.8.6",
"svelte-knobby": "^0.3.4", "svelte-knobby": "^0.3.4",
"svelte-moveable": "^0.20.0", "svelte-moveable": "^0.20.0",
"ts-action": "^11.0.0", "ts-action": "^11.0.0",
"vite": "^4.1.4" "vite": "^4.2.1"
}, },
"prettier": { "prettier": {
"svelteSortOrder": "options-markup-scripts-styles", "svelteSortOrder": "options-markup-scripts-styles",

View File

@ -1 +1,3 @@
import "./lib/style/index.js"; //import "./lib/style/index.js";
import "@picocss/pico";
import "../static/global.css";

View File

@ -1,118 +0,0 @@
<script>
import "@picocss/pico";
</script>
<style global>
@font-face {
font-family: "Faktos";
font-style: normal;
src: url(/fonts/Faktos.ttf) format("truetype");
}
@font-face {
font-family: "Dosis";
src: url(/fonts/dosis/Dosis-VariableFont_wght.ttf) format("truetype");
}
:root {
--main-font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--font-scale-9: 0.75rem;
--font-scale-10: 1em;
--font-scale-11: 1.333rem;
--font-scale-12: 1.777rem;
--font-scale-13: 2.369rem;
--font-scale-14: 3.157rem;
--font-scale-15: 4.209rem;
--oxford-blue: hsla(226, 60%, 10%, 1);
--royal-blue-dark: hsla(218, 100%, 16%, 1);
--indigo-dye: hsla(209, 95%, 24%, 1);
--cg-blue: hsla(193, 80%, 35%, 1);
--white: hsla(20, 60%, 99%, 1);
--main-width: 60em;
}
input.short {
width: 5em !important;
}
small {
font-size: var(--font-scale-9);
}
h1 {
margin: 0px;
padding: 0px;
font-size: var(--font-scale-14);
}
h2 {
font-size: var(--font-scale-12);
}
body {
position: relative;
width: 100%;
height: 100%;
background-color: var(--white);
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0, 100, 200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0, 80, 160);
}
label {
display: block;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
/* input, */
/* select { */
/* border: 0px; */
/* border-bottom: 1px solid var(--indigo-dye); */
/* } */
input:focus,
select:focus {
border: 1px solid var(--indigo-dye);
}
</style>

View File

@ -0,0 +1,18 @@
<Hst.Story>
<ShipEdit {...ship} />
</Hst.Story>
<script>
import { setContext } from "svelte";
import ShipEdit from "./ShipEdit.svelte";
import { createApi } from "$lib/store/api";
export let Hst;
const api = createApi();
setContext("api", api);
let ship = api.getState();
api.subscribe(() => (ship = api.getState()));
</script>

View File

@ -0,0 +1,24 @@
<main>
<Identification {...identification} />
<Propulsion {...propulsion} />
<Structure {...structure} />
</main>
<script>
import Identification from "./ShipEdit/Identification.svelte";
import Propulsion from "./ShipEdit/Propulsion.svelte";
import shipDux from "$lib/store/ship";
import Structure from "./ShipEdit/Structure.svelte";
export let identification = {};
export let propulsion = {};
export let structure = {};
</script>
<style>
main {
width: var(--main-width);
margin-left: auto;
margin-right: auto;
}
</style>

View File

@ -1,5 +1,4 @@
<ShipItem {...reqs}> <ShipItem {...reqs}>
<!--
<Field label={`squadron ${id}`}> <Field label={`squadron ${id}`}>
<select bind:value={type}> <select bind:value={type}>
{#each types as type (type)} {#each types as type (type)}
@ -7,7 +6,6 @@
{/each} {/each}
</select> </select>
</Field> </Field>
-->
</ShipItem> </ShipItem>
<script> <script>

View File

@ -0,0 +1,17 @@
<Hst.Story title="ShipEdit/Identification">
<Identification
shipClass="Demeter"
shipType="Destroyer"
isCarrier={false}
reqs={{
cost: 12,
mass: 13,
usedMass: 9,
}}
/>
</Hst.Story>
<script lang="ts">
export let Hst;
import Identification from "./Identification.svelte";
</script>

View File

@ -0,0 +1,53 @@
<div class="identification-row">
<div>
<Field label="ship class" bind:value={shipClass} />
<Field label="ship type">
<select bind:value={shipType}>
{#each shipTypes as name (name)}
<option>{name}</option>
{/each}
</select>
</Field>
</div>
<ShipCost {...reqs} />
</div>
<script>
import { getContext } from "svelte";
import Field from "$lib/components/Field.svelte";
import { candidateShipTypes } from "./Identification/shipTypes.js";
import ShipCost from "./Identification/ShipCost.svelte";
export let shipClass = "";
export let shipType = "";
export let isCarrier = false;
export let reqs = {};
export let api = getContext("api");
$: shipTypes = candidateShipTypes(reqs.mass, isCarrier).map(
({ name }) => name
);
$: if (shipTypes.length > 0 && !shipTypes.includes(shipType))
shipType = shipTypes[0];
$: api?.dispatch?.updateIdentification?.({ shipType, shipClass });
</script>
<style>
div {
display: flex;
align-items: end;
gap: 2em;
}
.identification-row {
display: flex;
align-items: start;
}
.identification-row :global(> *:first-child) {
flex: 1;
}
</style>

View File

@ -1,22 +0,0 @@
<Meta title="ShipEdit/Identification" component={Identification} argTypes={{}} />
<Story name="Primary" args={{}} />
<Template let:args>
<div style="width: 50em">
<Identification {...args} />
</div>
</Template>
<script>
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import { action } from "@storybook/addon-actions";
import { setContext } from "svelte";
import Identification from "./index.svelte";
setContext("ship", {
dispatch: (type, detail) => action(type)(detail),
});
</script>

View File

@ -22,18 +22,20 @@
<script> <script>
import { base } from "$app/paths"; import { base } from "$app/paths";
import { getContext } from "svelte"; import { getContext } from "svelte";
import Field from "$lib/components/Field/index.svelte"; import Field from "$lib/components/Field.svelte";
export let ship = getContext("ship"); export let api = getContext("api");
export let mass = 10; export let mass = 10;
export let cost = 10; export let cost = 10;
export let usedMass = 5; export let usedMass = 10;
$: massUnused = mass - usedMass; $: massUnused = mass - usedMass;
$: withinBudget = massUnused >= 0; $: withinBudget = massUnused >= 0;
$: ship.dispatch(ship.actions.setShipMass(mass)); $: api?.dispatch?.updateIdentification?.({
reqs: { mass },
});
/* const change_tonnage = ({ target: { value } }) => */ /* const change_tonnage = ({ target: { value } }) => */
/* ship.dispatch(ship.actions.set_ship_mass(parseInt(value))); */ /* ship.dispatch(ship.actions.set_ship_mass(parseInt(value))); */

View File

@ -1,40 +0,0 @@
<div>
<Field label="ship class" bind:value={shipClass} />
<Field label="ship type">
<select bind:value={shipType}>
{#each shipTypes as name (name)}
<option>{name}</option>
{/each}
</select>
</Field>
</div>
<script>
import { getContext } from "svelte";
import Field from "$lib/components/Field/index.svelte";
import { candidateShipTypes } from "./shipTypes.js";
export let shipClass = "";
export let shipType = "";
export let mass = 10;
export let isCarrier = false;
const ship = getContext("ship");
$: shipTypes = candidateShipTypes(mass, isCarrier).map(({ name }) => name);
$: if (shipTypes.length > 0 && !shipTypes.includes(shipType))
shipType = shipTypes[0];
$: ship.dispatch(ship.actions.setShipType(shipType));
$: ship.dispatch(ship.actions.setShipClass(shipClass));
</script>
<style>
div {
display: flex;
align-items: end;
gap: 2em;
}
</style>

View File

@ -1,6 +1,6 @@
<Section label="propulsion"> <Section label="propulsion">
<Drive {...propulsion.drive} /> <Drive {...drive} />
<Ftl {...propulsion.ftl} /> <Ftl {...ftl} />
</Section> </Section>
<script lang="ts"> <script lang="ts">
@ -8,8 +8,6 @@
import Drive from "./Propulsion/Engine.svelte"; import Drive from "./Propulsion/Engine.svelte";
import Ftl from "./Propulsion/Ftl.svelte"; import Ftl from "./Propulsion/Ftl.svelte";
export let propulsion = { export let drive = {};
drive: {}, export let ftl = {};
ftl: {},
};
</script> </script>

View File

@ -13,5 +13,4 @@
}; };
import Engine from "./Engine.svelte"; import Engine from "./Engine.svelte";
import "$lib/components/GlobalStyle.svelte";
</script> </script>

View File

@ -25,7 +25,8 @@
export let rating = 0; export let rating = 0;
export let api = getContext("api"); export let api = getContext("api");
$: api?.dispatch?.setEngine({ rating, advanced }); console.log(api?.dispatch?.setDrive);
$: api?.dispatch?.setDrive?.({ rating, advanced });
</script> </script>
<style> <style>

View File

@ -13,5 +13,4 @@
}; };
import Ftl from "./Ftl.svelte"; import Ftl from "./Ftl.svelte";
import "$lib/components/GlobalStyle.svelte";
</script> </script>

View File

@ -1,6 +1,6 @@
<ShipItem {...reqs}> <ShipItem {...reqs}>
<Field label="FTL drive"> <Field label="FTL drive">
{#each types as t (t)} {#each ftlTypes as t (t)}
<label <label
><input type="radio" bind:group={type} value={t} /> ><input type="radio" bind:group={type} value={t} />
{t} {t}
@ -15,13 +15,13 @@
import ShipItem from "$lib/components/ShipItem.svelte"; import ShipItem from "$lib/components/ShipItem.svelte";
import Field from "$lib/components/Field.svelte"; import Field from "$lib/components/Field.svelte";
import { ftlTypes } from "$lib/store/ship/propulsion/ftl";
export let type = "none"; export let type = "none";
export let reqs = { mass: 0, cost: 0 }; export let reqs = { mass: 0, cost: 0 };
export let api = getContext("api"); export let api = getContext("api");
const types = ["none", "standard", "advanced"]; $: api?.dispatch.setFtlType?.(type);
$: api?.dispatch.setFtl(type);
</script> </script>
<style> <style>

View File

@ -0,0 +1,10 @@
<Section label="structure">
<Streamlining {...streamlining} />
</Section>
<script lang="ts">
import Section from "$lib/components/Section.svelte";
import Streamlining from "./Structure/Streamlining.svelte";
export let streamlining = {};
</script>

View File

@ -18,17 +18,17 @@
</ShipItem> </ShipItem>
<script> <script>
import ShipItem from "$lib/components/ShipItem/index.svelte"; import ShipItem from "$lib/components/ShipItem.svelte";
import Field from "$lib/components/Field/index.svelte"; import Field from "$lib/components/Field.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
export let type = "none"; export let type = "none";
export let reqs = {}; export let reqs = {};
export let {dispatch, shipMass} = getContext("ship"); export let api = getContext("api");
$: dispatch.setStreamlining({type, shipMass: $shipMass}); $: api?.dispatch?.setStreamlining?.(type);
</script> </script>
<style> <style>

View File

@ -1,79 +1,79 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { reqs, Reqs } from "./reqs"; import { reqs, type Reqs } from "./reqs";
type Squadron = { type Squadron = {
type: string; type: string;
reqs: Reqs; reqs: Reqs;
}; };
const initialState = { const initialState = {
bays: 0, bays: 0,
squadrons: [] as Squadron[], squadrons: [] as Squadron[],
reqs, reqs,
}; };
export const { actions, reducer } = createSlice({ export const { actions, reducer } = createSlice({
name: "carrier", name: "carrier",
initialState, initialState,
reducers: { reducers: {
setCarrierBays: (state, action: PayloadAction<number>) => { setCarrierBays: (state, action: PayloadAction<number>) => {
state.bays = action.payload; state.bays = action.payload;
state.reqs = calcBaysReqs(action.payload); state.reqs = calcBaysReqs(action.payload);
state.squadrons = adjustSquadrons(action.payload)(state.squadrons); state.squadrons = adjustSquadrons(action.payload)(state.squadrons);
},
setSquadronType: (
state,
action: PayloadAction<{ type: string; id: number }>
) => {
state.squadrons[action.payload.id - 1] = {
type: action.payload.type,
reqs: squadronReqs(action.payload.type),
};
},
}, },
setSquadronType: (
state,
action: PayloadAction<{ type: string; id: number }>
) => {
state.squadrons[action.payload.id - 1] = {
type: action.payload.type,
reqs: squadronReqs(action.payload.type),
};
},
},
}); });
export const squadronTypes = [ export const squadronTypes = [
{ type: "standard", cost: 3 }, { type: "standard", cost: 3 },
{ type: "fast", cost: 4 }, { type: "fast", cost: 4 },
{ type: "heavy", cost: 5 }, { type: "heavy", cost: 5 },
{ type: "interceptor", cost: 3 }, { type: "interceptor", cost: 3 },
{ type: "attack", cost: 4 }, { type: "attack", cost: 4 },
{ type: "long range", cost: 4 }, { type: "long range", cost: 4 },
{ type: "torpedo", cost: 6 }, { type: "torpedo", cost: 6 },
]; ];
function squadronReqs(type: string) { function squadronReqs(type: string) {
return { return {
mass: 6, mass: 6,
cost: 6 * squadronTypes.find((s) => s.type === type)?.cost, cost: 6 * squadronTypes.find((s) => s.type === type)?.cost,
}; };
} }
const adjustSquadrons = (bays) => (squadrons) => { const adjustSquadrons = (bays) => (squadrons) => {
if (squadrons.length > bays) { if (squadrons.length > bays) {
squadrons = squadrons.slice(0, bays); squadrons = squadrons.slice(0, bays);
} }
if (squadrons.length < bays) { if (squadrons.length < bays) {
squadrons = [ squadrons = [
...squadrons, ...squadrons,
..._.times(bays - squadrons.length, () => ({ ..._.times(bays - squadrons.length, () => ({
type: squadronTypes[0].type, type: squadronTypes[0].type,
reqs: { reqs: {
cost: 6 * squadronTypes[0].cost, cost: 6 * squadronTypes[0].cost,
mass: 6, mass: 6,
}, },
})), })),
]; ];
} }
return squadrons; return squadrons;
}; };
function calcBaysReqs(bays) { function calcBaysReqs(bays) {
return { return {
mass: 9 * bays, mass: 9 * bays,
cost: 18 * bays, cost: 18 * bays,
}; };
} }

View File

@ -1,42 +1,41 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { Updux } from "updux";
import u from "updeep"; import u from "updeep";
import { Reqs, reqs } from "./reqs.js"; import { type Reqs, reqs } from "./reqs.js";
const initialState = { const initialState = {
rating: 1, rating: 1,
advanced: false, advanced: false,
reqs, reqs,
}; };
const engine = createSlice({ const engine = createSlice({
name: "engine", name: "engine",
initialState, initialState,
reducers: { reducers: {
setDriveRating(state, action: PayloadAction<number>) { setDriveRating(state, action: PayloadAction<number>) {
state.rating = action.payload; state.rating = action.payload;
},
setDriveAdvanced(state, action: PayloadAction<boolean>) {
state.advanced = action.payload;
},
setDriverReqs(state, action: PayloadAction<Reqs>) {
state.reqs = action.payload;
},
}, },
setDriveAdvanced(state, action: PayloadAction<boolean>) {
state.advanced = action.payload;
},
setDriverReqs(state, action: PayloadAction<Reqs>) {
state.reqs = action.payload;
},
},
}); });
export const { actions, reducer } = engine; export const { actions, reducer } = engine;
export function calcDriveReqs( export function calcDriveReqs(
shipMass: number, shipMass: number,
rating: number, rating: number,
advanced = false advanced = false
) { ) {
const mass = Math.ceil(rating * 0.05 * shipMass); const mass = Math.ceil(rating * 0.05 * shipMass);
const cost = mass * (advanced ? 3 : 2); const cost = mass * (advanced ? 3 : 2);
return { mass, cost }; return { mass, cost };
} }
export default engine; export default engine;

View File

@ -2,28 +2,28 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { reqs, Reqs } from "../reqs"; import { reqs, Reqs } from "../reqs";
type DriveProps = { type DriveProps = {
rating: number; rating: number;
advanced: boolean; advanced: boolean;
}; };
export const initialState: DriveProps & { reqs: Reqs } = { export const initialState: DriveProps & { reqs: Reqs } = {
rating: 0, rating: 0,
advanced: false, advanced: false,
reqs, reqs,
}; };
const driveSlice = createSlice({ const driveSlice = createSlice({
initialState, initialState,
name: "drive", name: "drive",
reducers: { reducers: {
setDrive: (state, action: PayloadAction<DriveProps>) => { setDrive: (state, action: PayloadAction<DriveProps>) => {
state.rating = action.payload.rating; state.rating = action.payload.rating;
state.advanced = action.payload.advanced; state.advanced = action.payload.advanced;
},
setDriveReqs: (state, action: PayloadAction<Reqs>) => {
state.reqs = action.payload;
},
}, },
setDriveReqs: (state, action: PayloadAction<Reqs>) => {
state.reqs = action.payload;
},
},
}); });
export const { actions, reducer } = driveSlice; export const { actions, reducer } = driveSlice;
@ -41,10 +41,4 @@ export const calculateDriveReqs = (store) =>
store.dispatch.setDriveReqs(calcDriveReqs(ship_mass, rating, advanced)) store.dispatch.setDriveReqs(calcDriveReqs(ship_mass, rating, advanced))
); );
export function calcDriveReqs(shipMass, rating, advanced = false) {
const mass = Math.ceil(rating * 0.05 * shipMass);
const cost = mass * (advanced ? 3 : 2);
return { mass, cost };
}
*/ */

View File

@ -5,43 +5,17 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { reqs, Reqs } from "../reqs.js"; import { reqs, Reqs } from "../reqs.js";
export const ftlTypes = ["none", "standard", "advanced"] as const;
type FtlType = typeof ftlTypes[number];
export const initialState = {
reqs,
type: "none" as FtlType,
};
const ftl = createSlice({ const ftl = createSlice({
name: "ftl", name: "ftl",
initialState, initialState,
reducers: { reducers: {
setFtl: (state, { payload }: PayloadAction<FtlType>) => { setFtl: (state, { payload }: PayloadAction<FtlType>) => {
state.type = payload; state.type = payload;
},
setFtlReqs: (state, action: PayloadAction<Reqs>) => {
state.reqs = action.payload;
},
}, },
setFtlReqs: (state, action: PayloadAction<Reqs>) => {
state.reqs = action.payload;
},
},
}); });
export function calcFtlReqs(type: FtlType, shipMass: number): Reqs {
if (type === "none") return { cost: 0, mass: 0 };
const mass = Math.ceil(shipMass / 10);
return {
mass,
cost: mass * (type === "advanced" ? 3 : 2),
};
}
export const { actions, reducer } = ftl; export const { actions, reducer } = ftl;
// needs to be at the top level
export const ftlReqsReaction = (store) =>
createSelector(
[(ship) => ship.propulsion.ftl.type, (ship) => ship.reqs.mass],
(type, shipMass) => store.dispatch.setFtlReqs(calcFtlReqs(type, shipMass))
);

9
src/lib/store/api.ts Normal file
View File

@ -0,0 +1,9 @@
import ship from "./ship";
export type Api = ReturnType<typeof ship.createStore>;
export const createApi = () => {
const api = ship.createStore();
console.log(api);
return api;
};

View File

@ -0,0 +1,32 @@
import { test, expect } from "vitest";
import ship from "./ship";
test("kicking the tires", () => {
const store = ship.createStore();
console.log(store.getState());
console.log(store.getState.getStreamlining());
store.dispatch.setFtlType("standard");
expect(store.getState().propulsion.ftl.reqs.mass).toEqual(1);
expect(store.getState().identification.reqs.usedMass).toEqual(1);
store.dispatch.setDrive({ rating: 3, advanced: true });
expect(store.getState().propulsion.drive.reqs).toEqual({ mass: 2, cost: 6 });
store.dispatch.setNbrCarrierBays(1);
expect(store.getState().carrier.nbrBays).toEqual(1);
store.dispatch.setNbrCarrierBays(2);
expect(store.getState().carrier.nbrBays).toEqual(2);
store.dispatch.setSquadronType(2, "fast");
expect(store.getState().carrier.squadrons[1]).toHaveProperty("type", "fast");
expect(store.getState.isCarrier()).toBe(true);
store.dispatch.setStreamlining("partial");
expect(store.getState.getStreamlining()).toBe("partial");
expect(store.selectors.getStreamlining(store.getState())).toBe("partial");
});

84
src/lib/store/ship.ts Normal file
View File

@ -0,0 +1,84 @@
import { createSelector } from "@reduxjs/toolkit";
import Updux from "updux";
import * as R from "remeda";
import identification from "./ship/identification";
import ftl, { calcFtlReqs } from "./ship/propulsion/ftl";
import drive from "./ship/propulsion/drive";
import { calcDriveReqs } from "$lib/shipDux/engine";
import { carrierDux } from "./ship/carrier";
import { streamliningDux as streamlining } from "./ship/structure/streamlining";
import { calcStreamliningReqs } from "./ship/structure/rules";
const shipDux = new Updux({
subduxes: {
identification,
structure: new Updux({
initialState: {},
subduxes: {
streamlining,
},
}),
propulsion: new Updux({
initialState: {},
subduxes: {
ftl,
drive,
},
}),
carrier: carrierDux,
},
});
shipDux.addReaction((api) =>
createSelector(
api.selectors.getFtlType,
api.selectors.getShipMass,
(type, mass) => api.dispatch.setFtlReqs(calcFtlReqs(type, mass))
)
);
shipDux.addReaction((api) => (state) => {
let cost = 0;
let mass = 0;
let subsystems = R.values(R.omit(state, ["identification"]));
while (subsystems.length > 0) {
const subsystem = subsystems.shift();
if (typeof subsystem !== "object") continue;
if (subsystem.reqs) {
cost += subsystem.reqs.cost;
mass += subsystem.reqs.mass;
}
subsystems.push(...Object.values(subsystem));
}
api.dispatch.setShipReqs({ cost, usedMass: mass });
});
shipDux.addReaction((api) =>
createSelector(
api.selectors.getShipMass,
(state) => state.propulsion.drive.rating,
(state) => state.propulsion.drive.advanced,
(mass, rating, advanced) =>
api.dispatch.setDriveReqs(calcDriveReqs(mass, rating, advanced))
)
);
shipDux.addReaction((api) =>
createSelector(
// (state) => state,
api.selectors.getShipMass,
api.selectors.getStreamlining,
(mass, type) => {
console.log("AH!", mass, type);
api.dispatch.setStreamliningReqs(calcStreamliningReqs(type, mass));
}
)
);
export default shipDux;

View File

@ -0,0 +1,118 @@
import Updux, { createPayloadAction } from "updux";
import u from "@yanick/updeep-remeda";
import { reqs, type Reqs } from "$lib/shipDux/reqs";
type Squadron = {
type: string;
reqs: Reqs;
};
const initialState = {
nbrBays: 0,
squadrons: [] as Squadron[],
reqs,
};
export const squadronTypes = [
{ type: "standard", cost: 3 },
{ type: "fast", cost: 4 },
{ type: "heavy", cost: 5 },
{ type: "interceptor", cost: 3 },
{ type: "attack", cost: 4 },
{ type: "long range", cost: 4 },
{ type: "torpedo", cost: 6 },
];
const setNbrCarrierBays = createPayloadAction<number>("setNbrCarrierBays");
const setSquadronType = createPayloadAction(
"setSquadronType",
(id: number, type: string) => ({ id, type })
);
export const carrierDux = new Updux({
initialState,
actions: { setNbrCarrierBays, setSquadronType },
});
function calcBaysReqs(bays) {
return {
mass: 9 * bays,
cost: 18 * bays,
};
}
const adjustSquadrons = (bays: number) => (squadrons) => {
if (squadrons.length === bays) return squadrons;
if (squadrons.length > bays) {
return squadrons.slice(0, bays);
}
return [
...squadrons,
...Array.from({ length: bays - squadrons.length })
.fill({
type: squadronTypes[0].type,
reqs: {
cost: 6 * squadronTypes[0].cost,
mass: 6,
},
})
.map((s, i) => ({ ...s, id: squadrons.length + i + 1 })),
];
};
carrierDux.addMutation(setNbrCarrierBays, (nbrBays) =>
u({
nbrBays,
reqs: calcBaysReqs(nbrBays),
squadrons: adjustSquadrons(nbrBays),
})
);
carrierDux.addMutation(setSquadronType, ({ id, type }) => {
return u({
squadrons: u.map(
u.if(u.matches({ id }), (state) => {
return u(state, {
type,
reqs: squadronReqs(type),
});
})
),
});
});
function squadronReqs(type: string) {
return {
mass: 6,
cost: 6 * squadronTypes.find((s) => s.type === type)?.cost,
};
}
/*
export const { actions, reducer } = createSlice({
name: "carrier",
initialStateState,
reducers: {
setCarrierBays: (state, action: PayloadAction<number>) => {
state.bays = action.payload;
state.reqs = calcBaysReqs(action.payload);
state.squadrons = adjustSquadrons(action.payload)(state.squadrons);
},
setSquadronType: (
state,
action: PayloadAction<{ type: string; id: number }>
) => {
state.squadrons[action.payload.id - 1] = {
type: action.payload.type,
reqs: squadronReqs(action.payload.type),
};
},
},
});
*/

View File

@ -0,0 +1,44 @@
import Updux, { createAction, withPayload } from "updux";
import u from "@yanick/updeep-remeda";
import * as R from "remeda";
import { carrierDux } from "./carrier";
const initialState = {
shipType: "",
shipClass: "",
isCarrier: false,
reqs: {
mass: 10,
cost: 0,
usedMass: 0,
},
};
const setShipClass = createAction("setShipClass", withPayload<string>());
const updateIdentification = createAction("updateIdentification");
const setShipReqs = createAction("setShipReqs", withPayload());
export const dux = new Updux({
initialState,
actions: {
setShipClass,
updateIdentification,
setShipReqs,
},
selectors: {
getShipMass: (state) => state.reqs.mass,
isCarrier: ({ isCarrier }) => isCarrier,
},
});
dux.addMutation(setShipClass, (shipClass) => u({ shipClass }));
dux.addMutation(updateIdentification, (update) => u(update));
dux.addMutation(setShipReqs, (reqs) => u({ reqs }));
dux.addMutation(carrierDux.actions.setNbrCarrierBays, (nbrBays) =>
u({
isCarrier: nbrBays > 0,
})
);
export default dux;

View File

@ -0,0 +1,38 @@
import { reqs, type Reqs } from "$lib/shipDux/reqs";
import Updux, { createPayloadAction } from "updux";
import u from "@yanick/updeep-remeda";
type DriveProps = {
rating: number;
advanced: boolean;
};
const initialState: DriveProps & { reqs: Reqs } = {
rating: 0,
advanced: false,
reqs,
};
const setDrive = createPayloadAction<DriveProps>("setDrive");
const setDriveReqs = createPayloadAction<Reqs>("setDriveReqs");
const dux = new Updux({
initialState,
actions: { setDrive, setDriveReqs },
});
export default dux;
dux.addMutation(setDrive, (change) => u(change));
dux.addMutation(setDriveReqs, (reqs) => u({ reqs }));
export function calcDriveReqs(
shipMass: number,
rating: number,
advanced = false
) {
const mass = Math.ceil(rating * 0.05 * shipMass);
const cost = mass * (advanced ? 3 : 2);
return { mass, cost };
}

View File

@ -0,0 +1,40 @@
import { reqs, type Reqs } from "$lib/shipDux/reqs";
import Updux, { createAction, withPayload } from "updux";
import u from "@yanick/updeep-remeda";
import * as R from "remeda";
export const ftlTypes = ["none", "standard", "advanced"] as const;
type FtlType = typeof ftlTypes[number];
export const initialState = {
reqs,
type: "none" as FtlType,
};
const setFtlType = createAction("setFtlType", withPayload<FtlType>());
const setFtlReqs = createAction("setFtlReqs", withPayload<Reqs>());
const dux = new Updux({
initialState,
actions: { setFtlType, setFtlReqs },
selectors: {
getFtlType: R.prop<any, any>("type"),
},
});
dux.addMutation(setFtlType, (type) => u({ type }));
dux.addMutation(setFtlReqs, (reqs) => u({ reqs }));
export default dux;
export function calcFtlReqs(type: FtlType, shipMass: number): Reqs {
if (type === "none") return { cost: 0, mass: 0 };
const mass = Math.ceil(shipMass / 10);
return {
mass,
cost: mass * (type === "advanced" ? 3 : 2),
};
}

View File

@ -0,0 +1,9 @@
export type Streamlining = "none" | "partial" | "full";
export function calcStreamliningReqs(type: Streamlining, shipMass: number) {
const mass = Math.ceil(
(shipMass * (type === "none" ? 0 : type === "partial" ? 5 : 10)) / 100
);
return { mass, cost: 2 * mass };
}

View File

@ -0,0 +1,26 @@
import { reqs } from "$lib/shipDux/reqs";
import Updux, { createPayloadAction } from "updux";
import type { Streamlining } from "./rules";
import u from "@yanick/updeep-remeda";
const initialState = {
type: "none" as Streamlining,
reqs,
};
const setStreamlining = createPayloadAction<Streamlining>("setStreamlining");
const setStreamliningReqs = createPayloadAction("setStreamliningReqs");
export const streamliningDux = new Updux({
initialState,
actions: { setStreamlining, setStreamliningReqs },
selectors: {
getStreamlining: (state) => {
return state?.type;
},
},
});
streamliningDux.addMutation(setStreamlining, (type) => u({ type }));
streamliningDux.addMutation(setStreamliningReqs, (reqs) => u({ reqs }));

View File

@ -1,4 +1,26 @@
:root { @font-face {
font-family: "Faktos";
font-style: normal;
src: url(/fonts/Faktos.ttf) format("truetype");
}
@font-face {
font-family: "Dosis";
src: url(/fonts/dosis/Dosis-VariableFont_wght.ttf) format("truetype");
}
:root {
--main-font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--font-scale-9: 0.75rem;
--font-scale-10: 1em;
--font-scale-11: 1.333rem;
--font-scale-12: 1.777rem;
--font-scale-13: 2.369rem;
--font-scale-14: 3.157rem;
--font-scale-15: 4.209rem;
--oxford-blue: hsla(226, 60%, 10%, 1); --oxford-blue: hsla(226, 60%, 10%, 1);
--royal-blue-dark: hsla(218, 100%, 16%, 1); --royal-blue-dark: hsla(218, 100%, 16%, 1);
@ -7,83 +29,95 @@
--white: hsla(20, 60%, 99%, 1); --white: hsla(20, 60%, 99%, 1);
--main-width: 60em; --main-width: 60em;
} }
small {font-size: var(--font-scale-9); } input.short {
width: 5em !important;
}
h1 { small {
font-size: var(--font-scale-9);
}
h1 {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
font-size: var(--font-scale-14); font-size: var(--font-scale-14);
} }
h2 { h2 {
font-size: var(--font-scale-12); font-size: var(--font-scale-12);
} }
body {
html, body { position: relative;
position: relative; width: 100%;
width: 100%; height: 100%;
height: 100%;
}
body {
background-color: var(--white); background-color: var(--white);
color: #333; color: #333;
margin: 0; margin: 0;
padding: 8px; padding: 8px;
box-sizing: border-box; box-sizing: border-box;
font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-family: "Dosis", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
} Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a { a {
color: rgb(0,100,200); color: rgb(0, 100, 200);
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
a:visited { a:visited {
color: rgb(0,80,160); color: rgb(0, 80, 160);
} }
label { label {
display: block; display: block;
} }
input, button, select, textarea { input,
font-family: inherit; button,
font-size: inherit; select,
padding: 0.4em; textarea {
margin: 0 0 0.5em 0; font-family: inherit;
box-sizing: border-box; font-size: inherit;
border: 1px solid #ccc; padding: 0.4em;
border-radius: 2px; margin: 0 0 0.5em 0;
} box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled { input:disabled {
color: #ccc; color: #ccc;
} }
input[type="range"] { input[type="range"] {
height: 0; height: 0;
} }
/* ---- inputs --- */ /* input, */
/* select { */
/* border: 0px; */
/* border-bottom: 1px solid var(--indigo-dye); */
/* } */
input, select { input:focus,
select:focus {
border: 1px solid var(--indigo-dye);
}
input:not([type="checkbox"]):not([type="radio"]) {
border: 0px; border: 0px;
border-bottom: 1px solid var(--indigo-dye); border-bottom: 1px solid var(--indigo-dye);
} border-radius: 0px;
height: calc(
input:focus, select:focus { 1rem * var(--line-height) + var(--form-element-spacing-vertical) * 1
border: 1px solid var(--indigo-dye);; );
} padding: 0 0.5rem;
text-align: center;
input.short { }
width:5em;
}

View File

@ -1,9 +1,13 @@
import { extractFromSvelteConfig } from "vitest-svelte-kit"; import { mergeConfig } from "vite";
import { defineConfig } from "vitest/config";
import viteConfig from "./vite.config.js";
export default extractFromSvelteConfig().then((config) => ({ export default mergeConfig(
...config, viteConfig,
test: { defineConfig({
globals: true, test: {
environment: "jsdom", globals: true,
}, environment: "jsdom",
})); },
})
);