commit first major batch of work

This commit is contained in:
Yanick Champoux 2020-07-19 16:21:28 -04:00
parent 561aa08d8a
commit 2c68f44e88
49 changed files with 2152 additions and 10 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.nyc_output
pnpm-lock.yaml
public/build
public/bundle.*

3
.storybook/main.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
stories: [ '../src/**/*stories.js' ]
};

View File

@ -1,12 +1,136 @@
{ {
"name": "shipyard", "author": "Yanick Champoux <yanick@babyl.ca>",
"version": "1.0.0", "dependencies": {
"description": "", "@babel/cli": "^7.10.1",
"main": "index.js", "@babel/core": "^7.10.2",
"scripts": { "@babel/node": "^7.10.1",
"test": "echo \"Error: no test specified\" && exit 1" "@material/animation": "^7.0.0",
"@material/base": "^7.0.0",
"@material/density": "^7.0.0",
"@material/feature-targeting": "^7.0.0",
"@material/floating-label": "^7.0.0",
"@material/line-ripple": "^7.0.0",
"@material/notched-outline": "^7.0.0",
"@material/ripple": "^7.0.0",
"@material/rtl": "^7.0.0",
"@material/shape": "^7.0.0",
"@material/textfield": "^6.0.0",
"@material/theme": "^7.0.0",
"@material/typography": "^7.0.0",
"@smui/floating-label": "^1.0.0-beta.21",
"@smui/line-ripple": "^1.0.0-beta.21",
"@smui/notched-outline": "^1.0.0-beta.21",
"@smui/textfield": "^1.0.0-beta.21",
"@storybook/svelte": "^5.3.19",
"babel-loader": "^8.1.0",
"css-loader": "^3.6.0",
"dart-sass": "^1.25.0",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.14.1",
"prettier": "2.0.5",
"prettier-plugin-svelte": "1.1.0",
"redux": "^4.0.5",
"reselect": "^4.0.0",
"rollup-plugin-css-only": "^2.1.0",
"rollup-plugin-postcss": "^3.1.2",
"rollup-plugin-scss": "^2.5.0",
"sass": "^1.26.9",
"sass-loader": "^8.0.2",
"sirv-cli": "^0.4.4",
"style-loader": "^1.2.1",
"svelte-loader": "^2.13.6",
"svelte3-redux": "^0.3.0",
"ts-action": "^11.0.0",
"updeep": "^1.2.0",
"updux": "^2.1.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}, },
"keywords": [], "description": "Full Thrust ship builder",
"author": "", "devDependencies": {
"license": "ISC" "@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^12.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"eslint": "7.4.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-babel": "5.3.1",
"eslint-plugin-lodash": "^7.1.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-svelte3": "2.7.3",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
"npm-run-all": "^4.1.5",
"rollup": "^2.3.4",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^5.1.2",
"svelte": "^3.0.0",
"svelte-material-ui": "^1.0.0-beta.21",
"tap": "^14.10.7"
},
"eslintConfig": {
"env": {
"amd": true,
"browser": true,
"es6": true,
"node": true
},
"extends": [
"prettier",
"eslint:recommended",
"plugin:you-dont-need-lodash-underscore/compatible",
"plugin:lodash/recommended"
],
"ignorePatterns": [
"src/node_modules"
],
"overrides": [
{
"files": [
"*.svelte"
],
"processor": "svelte3/svelte3"
}
],
"parserOptions": {
"ecmaFeatures": {
"modules": true
},
"ecmaVersion": "2020",
"sourceType": "module"
},
"plugins": [
"svelte3"
],
"rules": {
"lodash/prefer-lodash-method": "off"
}
},
"keywords": [
"game"
],
"license": "ISC",
"main": "index.js",
"name": "shipyard",
"prettier": {
"svelteSortOrder": "markup-scripts-styles",
"svelteStrictMode": false
},
"scripts": {
"build": "NODE_ENV=production webpack",
"lint": "npm-run-all --parallel \"lint:prettier -- {@}\" \"lint:eslint -- {@}\" --",
"lint:eslint": "./scripts/eslint",
"lint:eslint:fix": "npm run lint:eslint -- --fix",
"lint:fix": "npm-run-all \"lint:eslint:fix -- {@}\" \"lint:prettier:fix -- {@}\" --",
"lint:prettier": "./scripts/prettier",
"lint:prettier:fix": "npm run lint:prettier -- --fix",
"package": "./scripts/merge_package.pl",
"start": "webpack-dev-server --open --content-base public",
"test": "tap 'src/**test.js' --no-coverage"
},
"tap": {
"coverage": false
},
"version": "0.0.1"
} }

63
package.yml Normal file
View File

@ -0,0 +1,63 @@
---
_merge:
- version
- dependencies
- devDependencies
name: shipyard
description: Full Thrust ship builder
author: 'Yanick Champoux <yanick@babyl.ca>'
scripts:
build: NODE_ENV=production webpack
start: webpack-dev-server --open --content-base public
package: ./scripts/merge_package.pl
test: tap 'src/**test.js' --no-coverage
"lint:prettier": ./scripts/prettier
"lint:eslint": ./scripts/eslint
lint: npm-run-all --parallel "lint:prettier -- {@}" "lint:eslint -- {@}" --
"lint:prettier:fix": npm run lint:prettier -- --fix
"lint:eslint:fix": npm run lint:eslint -- --fix
"lint:fix": npm-run-all "lint:eslint:fix -- {@}" "lint:prettier:fix -- {@}" --
eslintConfig:
env:
amd: true
browser: true
es6: true
node: true
extends:
- prettier
- eslint:recommended
- plugin:you-dont-need-lodash-underscore/compatible
- plugin:lodash/recommended
ignorePatterns:
- src/node_modules
overrides:
- files:
- '*.svelte'
processor: svelte3/svelte3
parserOptions:
ecmaFeatures:
modules: true
ecmaVersion: '2020'
sourceType: module
plugins:
- svelte3
rules:
lodash/prefer-lodash-method: off
keywords: [ game ]
license: ISC
main: index.js
prettier:
svelteSortOrder: markup-scripts-styles
svelteStrictMode: false
tap:
coverage: false

76
public/global.css Normal file
View File

@ -0,0 +1,76 @@
:root {
--font-scale-8: calc(1rem/1.125/1.125);
--font-scale-9: calc(1rem/1.125);
--font-scale-10: 1rem;
--font-scale-11: calc(1rem * 1.125);
--font-scale-12: calc(1rem * 1.125 * 1.125);
}
html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -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;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

30
public/index.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,600,700">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/bundle.css'>
<!--
<link rel='stylesheet' href='/materialize/materialize.css'>
<script src="/materialize/materialize.js"></script>
-->
<script>
process = { env: { NODE_ENV: 'production' } };
</script>
<script defer src='/bundle.js'></script>
</head>
<body>
</body>
</html>

5
public/mass.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='iso-8859-1'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.003 512.003" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512.003 512.003">
<path d="m510.838,471.439l-80-288c-3.852-13.852-16.461-23.438-30.836-23.438h-112v-6.781c28-12.379 48-40.498 48-73.219 0-44.109-35.891-80-80-80s-80,35.891-80,80c0,32.721 20,60.84 48,73.219v6.781h-112c-14.375,0-26.984,9.586-30.836,23.438l-80,288c-2.672,9.633-0.695,19.969 5.359,27.93 6.055,7.961 15.477,12.633 25.477,12.633h448c10,0 19.422-4.672 25.477-12.633 6.054-7.961 8.03-18.297 5.359-27.93zm-254.836-407.437c8.82-1.42109e-14 16,7.18 16,16s-7.18,16-16,16c-8.82,0-16-7.18-16-16s7.179-16 16-16z"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

102
rollup.config.js Normal file
View File

@ -0,0 +1,102 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import alias from '@rollup/plugin-alias';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
import postcss from 'rollup-plugin-postcss';
import path from 'path';
const postcssOptions = () => ({
extensions: ['.scss', '.sass'],
extract: false,
minimize: true,
use: [
['sass', {
includePaths: [
'./sass',
'./node_modules',
// This is only needed because we're using a local module. :-/
// Normally, you would not need this line.
path.resolve(__dirname, '..', 'node_modules')
]
}]
]
});
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
alias({
entries: [
{ find: '~', replacement: './src' },
]
}),
// scss(),
css({ output: 'public/build/import-bundle.css' }),
svelte({
// enable run-time checks when not in production
dev: !production,
// we'll extract any component CSS out into
// a separate file - better for performance
css: css => {
css.write('public/build/bundle.css');
}
}),
postcss(postcssOptions()),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false,
chokidar: {
usePolling: true
}
}
};
function serve() {
let started = false;
return {
writeBundle() {
if (!started) {
started = true;
require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
}
}
};
}

36
scripts/eslint Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env perl
use strict;
# check by default
my @mode;
my @files = grep {
$_ ne '--fix'or not (@mode = ('--fix'));
} @ARGV;
@files = qw/ src / unless @files;
my @ignore = (
qr/\.map$/,
qr!^docs/!,
qr/\.(css|html|md)$/,
qr/package\.(json|yaml)/,
qr#^scripts/#,
qr#^\.#,
qr#\.json$#
);
sub ignored {
my $file = shift;
for my $pattern ( @ignore ) {
return 1 if $file =~ $pattern;
}
return 0;
}
exec 'eslint', @mode, grep {
not ignored($_)
} @files;

59
scripts/merge_package.pl Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env perl
=pod
I'm slightly fed up with `package.json`, because:
* the keys are ordering alphabetically, not logically.
* no comments.
* friggin' no trailing comma. And the quotes, my lord, the bloody
quotes...
Hence this script, which takes a `package.yaml`, and convert it
to `package.json`.
Sections, like `dependencies`, `devDependencies' and 'version' that we want to preserve can be given `_merge`. All data structures under the keys
given in `_merge` will have their `yaml` and `json` values merged,
with the `json` having priority.
By default `package.json` will be updated (like, that's the point isn't?).
For a dry run pass in a `-n` argument.
=cut
use 5.24.0;
use strict;
use warnings;
use File::Serialize;
use Hash::Merge qw/ merge /;
use feature qw/ postderef /;
use YAML::XS;
use Path::Tiny;
use JSON::PP qw//;
use JSON qw/ to_json /;
{
no warnings;
$YAML::XS::Boolean = 'JSON::PP';
}
my $yaml = deserialize_file 'package.yml';
my $json = deserialize_file 'package.json';
my $mergers = delete $yaml->{_merge};
my $result = merge( { $json->%{@$mergers} }, $yaml );
my $encoder = JSON->new->pretty->canonical->space_before(0);
$json = $encoder->encode($result);
$json =~ s/(^|\G) / /g;
if( grep { $_ eq '-n' } @ARGV ) {
print $json;
} else {
path('package.json')->spew($json);
say "regenerated package.json"
}

16
scripts/prettier Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env perl
use strict;
# check by default
my $mode = '-c';
my @files = grep {
$_ ne '--fix'or not ($mode = '--write');
} @ARGV;
@files = qw/ src / unless @files;
@files = grep { !/\.gitignore/ } @files;
exec 'prettier', '--plugin-search-dir=.', $mode, @files;

41
src/App-real.svelte Normal file
View File

@ -0,0 +1,41 @@
<script>
import { setContext } from 'svelte';
import shipStore from './stores/ship';
import Ftl from './components/Ftl/index.svelte';
import Engine from './components/Engine/index.svelte';
import Identification from './components/Identification.svelte';
const ship = shipStore();
setContext('ship',ship);
let ship_name = $ship.ship_type.name;
const change_name = event => ship.dispatch(
ship.actions.set_name( event.target.value) );
const change_mass = ({target: {value}}) =>
ship.dispatch(ship.actions.set_ship_mass(parseInt(value)));
</script>
<main>
Potato?
<Identification />
ship class: <input value={$ship.ship_type.name} on:change={change_name} />
<input type="number" value={$ship.ship_type.mass} on:change={change_mass} />
<Ftl />
<Engine />
</main>
<style>
</style>

93
src/App.svelte Normal file
View File

@ -0,0 +1,93 @@
<script>
import { setContext } from 'svelte';
import shipStore from './stores/ship';
import ShipCost from './components/ShipCost.svelte';
import Hull from './components/Hull.svelte';
import Identification from './components/Identification.svelte';
import Firecons from './components/Firecons.svelte';
import AddWeapon from './components/Weapons/Add.svelte';
import Propulsion from './components/Propulsion/index.svelte';
import Section from '~C/Section';
import Weapon from '~C/Weapon';
const ship = shipStore();
setContext('ship',ship);
let ship_name = $ship.general.name;
const change_name = event => ship.dispatch(
ship.actions.set_name( event.target.value) );
const change_mass = ({target: {value}}) => ship.dispatch(ship.actions.set_ship_mass(parseInt(value)));
const add_weapon = () => ship.dispatch.add_weapon();
const change_ftl = ({detail}) => ship.dispatch.set_ftl(detail);
const change_engine = ({detail}) => ship.dispatch.set_engine(detail);
const change_hull = ({detail}) => ship.dispatch.set_hull(detail);
const change_firecons = ({detail}) => ship.dispatch.set_firecons(detail);
const change_weapon = ({detail}) => ship.dispatch.set_weapon(detail);
const remove_weapon = ({detail}) => ship.dispatch.remove_weapon(detail);
let weapons = [];
$: console.log(weapons);
$: weapons= $ship.weaponry.weapons;
const reset = ship.dispatch.reset;
const add_screen = ({detail}) => ship.dispatch.add_screen(detail);
</script>
<main>
<input type="button" value="reset" on:click={reset} />
<Identification />
<ShipCost />
<Propulsion
ftl={$ship.ftl}
engine={$ship.engine}
on:change_ftl={change_ftl}
on:change_engine={change_engine}
/>
<Hull ship_mass={$ship.general.mass}
{ ... $ship.structure.hull }
on:change_hull={change_hull}
screens={ $ship.structure.screens}
on:add_screen={add_screen}
/>
<Section label="weaponry">
<Firecons { ... $ship.weaponry.firecons }
on:change_firecons={ change_firecons }/>
<input type="button" value="add weapon" on:click={add_weapon}/>
{#each weapons as weapon (weapon.id)}
<Weapon {...weapon} on:change_weapon={change_weapon}
on:remove_weapon={remove_weapon} />
{/each}
</Section>
</main>
<style>
main {
display: grid;
width: 60em;
grid-template-columns: 4fr 1fr 1fr;
}
main :global(> *) {
grid-column: 1;
}
</style>

View File

@ -0,0 +1,19 @@
<div class="mass">{ mass }</div>
<div class="cost">{ cost }</div>
<script>
export let mass;
export let cost;
</script>
<style>
.cost { grid-column: 3; }
.mass { grid-column: 2; }
img {
width: 0.75em;
}
.cost:after { content: '\00A4'; margin-left: 0.5em; }
.mass:after { content: url("/mass.svg"); width: 0.75em; display:
inline-block; margin-left: 0.5em; }
</style>

View File

@ -0,0 +1,38 @@
<ShipItem {cost} {mass}>
<div>
<Field label="thrust rating">
<input type="number" bind:value={ rating }
min="0" max="20" step="1" />
</Field>
<label><input type="checkbox" bind:checked={advanced} /> advanced</label>
</div>
</ShipItem>
<script>
import Field from '~C/Field';
import ShipItem from '~C/ShipItem';
import { createEventDispatcher } from 'svelte';
export let cost;
export let mass;
export let advanced = false;
export let rating = 0;
const dispatch = createEventDispatcher();
console.log(advanced);
$: dispatch( 'change_engine', { rating, advanced } );
</script>
<style>
div {
display: flex;
align-items: end;
}
label { margin-left: 2em; }
</style>

View File

@ -0,0 +1,5 @@
<Field label="the label" />
<script>
import Field from './index.svelte';
</script>

View File

@ -0,0 +1,20 @@
<div>
{#if label}
<label>{label}</label>
{/if}
<slot>
<input type="text" {placeholder} {value} on:change />
</slot>
</div>
<script>
export let label = "";
export let value = "";
export let placeholder;
</script>
<style>
label {
font-size: var(--font-scale-8);
}
</style>

View File

@ -0,0 +1,12 @@
import Component from './index.svelte';
import BasicStory from './BasicStory.svelte';
import '../../../public/global.css';
export default {
title: 'Field'
};
export const basic = () => ({
Component: BasicStory,
});

View File

@ -0,0 +1,20 @@
<ShipItem {cost} {mass}>
<Field label="firecons">
<input type="number" bind:value={nbr} />
</Field>
</ShipItem>
<script>
import { createEventDispatcher } from 'svelte';
import ShipItem from '~C/ShipItem';
import Field from '~C/Field';
export let nbr, cost, mass = (0,0,0);
const dispatch = createEventDispatcher();
$: dispatch( 'change_firecons', nbr);
</script>
<style>
</style>

View File

@ -0,0 +1,35 @@
<script>
import { createEventDispatcher } from 'svelte';
import ShipItem from '../ShipItem/index.svelte';
import Field from '../Field/index.svelte';
export let type = 'none';
export let cost = 0;
export let mass = 0;
const dispatch = createEventDispatcher();
const change = () => dispatch( 'change_ftl', type );
const types = [ 'none', 'standard', 'advanced' ];
</script>
<ShipItem {mass} {cost}>
<Field label="FTL drive">
{#each types as t (t)}
<label><input type="radio" bind:group={type} value={t}
on:change={change} /> {t} </label>
{/each}
</Field>
</ShipItem>
<style>
label {
display: inline;
margin-right: 1em;
}
</style>

View File

@ -0,0 +1,13 @@
export default {
title: 'FTL Drive'
};
import Component from '.';
import shipStore from '../../stores/ship.js';
export const basic = () => ({
Component,
props: {
ship: shipStore()
}
});

View File

@ -0,0 +1,39 @@
<Section label="hull">
<ShipItem {cost} {mass} >
<Field label="integrity">
<input
bind:value={rating}
type="number" {min} {max} />
</Field>
</ShipItem>
<Screens {screens} on:add_screen />
</Section>
<script>
import { createEventDispatcher } from 'svelte';
import Section from '~C/Section';
import Field from '~C/Field';
import ShipItem from '~C/ShipItem';
import Screens from './Screens';
export let cost, mass, ship_mass, rating, screens = (
0, 0, 10, 1, []
);
let min, max;
$: min = Math.ceil(ship_mass / 10);
$: max = ship_mass;
const dispatch = createEventDispatcher();
$: dispatch( 'change_hull', { rating } );
</script>
<style>
</style>

View File

@ -0,0 +1,49 @@
<div class="identification">
<Field label="ship class" value={general.ship_class}
on:change={change_class} />
<Field label="ship type">
<select value={ship_type} on:change={change_ship_type}>
{#each ship_types as type (type)}
<option>{type}</option>
{/each}
</select>
</Field>
</div>
<script>
import { getContext } from 'svelte';
import Field from './Field/index.svelte';
import { candidate_ship_types } from '~/dux/ship_types';
export let ship = getContext('ship');
let general;
$: general = $ship.general;
const change_class = (event) => ship.dispatch(
ship.actions.set_ship_class(event.target.value)
);
let ship_type;
$: ship_type = $ship.general.ship_type;
const change_ship_type = ({ target: {value}}) =>
ship.dispatch.set_ship_type(value);
let ship_types;
$: ship_types = candidate_ship_types($ship.general.mass,false).map(
({name}) => name
);
</script>
<style>
.identification {
grid-column: span 3;
display: flex;
justify-content: space-around;
}
</style>

View File

@ -0,0 +1,18 @@
<Section label="propulsion">
<Ftl {...ftl} on:change_ftl />
<Engine {...engine} on:change_engine />
</Section>
<script>
import Ftl from '../Ftl/index.svelte';
import Engine from '../Engine/index.svelte';
import Section from '../Section/index.svelte';
export let ftl = {};
export let engine = {};
</script>

View File

@ -0,0 +1,31 @@
<input type="button" value="add screen" on:click={() => add_screen()} />
<input type="button" value="add advanced screen"
on:click={ () => add_screen(true) }/>
<ShipItem cost="10" mass="10">
{ nbr_regular } { nbr_advanced }
</ShipItem>
<script>
import { createEventDispatcher } from 'svelte';
import Section from '~C/Section';
import Field from '~C/Field';
import ShipItem from '~C/ShipItem';
export let screens = [];
$: if( !Array.isArray(screens) ) screens = [];
let nbr_regular, nbr_advanced;
$: {
nbr_regular = screens.filter( ({advanced}) => !advanced ).length;
nbr_advanced = screens.length - nbr_regular;
}
const dispatch = createEventDispatcher();
const add_screen = (advanced) => {
console.log(advanced); return dispatch( 'add_screen', advanced ); };
</script>

View File

@ -0,0 +1,8 @@
<fieldset>
<legend>{label}</legend>
<slot />
</fieldset>
<script>
export let label;
</script>

View File

@ -0,0 +1,58 @@
<div class="ship_cost">
<div>
<label>Ship tonnage</label>
<input
value={$ship.general.mass}
on:change={change_tonnage}
type="number" min="10" max="300" />
<div>
<div class="note" class:warning={!within_budget}>
{#if within_budget}
mass unused: { mass_unused }
{:else}
excessive mass: { -mass_unused }
{/if}
</div>
</div>
</div>
<div>
<label>Ship cost</label>
<div>{$ship.general.cost}</div>
</div>
</div>
<script>
import { getContext } from 'svelte';
export let ship = getContext('ship');
const change_tonnage =
({target: {value}}) =>
ship.dispatch(ship.actions.set_ship_mass(parseInt(value)));
let mass_unused;
$: mass_unused = $ship.general.mass - $ship.general.used_mass;
let within_budget = true;
$: within_budget = mass_unused >= 0;
</script>
<style>
.ship_cost {
display: flex;
grid-column: span 3;
justify-content: space-around;
}
.warning {
color: red;
}
.note {
font-size: smaller;
}
</style>

View File

@ -0,0 +1,29 @@
<div class="ship-item">
<slot />
<div class="mass">{ mass }</div>
<div class="cost">{ cost }</div>
</div>
<script>
export let mass;
export let cost;
</script>
<style>
.ship-item {
display: flex;
}
.ship-item :global(> *) {
flex: 1;
}
.cost,.mass { width: 4em; flex: inherit; }
img {
width: 0.75em;
}
.cost:after { content: '\00A4'; margin-left: 0.5em; }
.mass:after { content: url("/mass.svg"); width: 0.75em; display:
inline-block; margin-left: 0.5em; }
</style>

View File

@ -0,0 +1,210 @@
<ShipItem {cost} {mass}>
<div class="remove" on:click={remove}>X</div>
<Field label="weapon type">
<select bind:value={weapon_type}>
<option>beam</option>
</select>
</Field>
<Field label="weapon class">
<select bind:value={weapon_class}>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
</Field>
<Field label="arcs">
<select bind:value={nbr_arcs}>
{#each arc_options[weapon_class]||[] as nbr_arcs (nbr_arcs)}
<option>{nbr_arcs}</option>
{/each}
</select>
</Field>
<svg width="60px" height="60px">
{#each all_arcs as arc (arc)}
<Arc {arc} radius={30}
active={arcs.includes(arc)}
on:click={()=>click_arc(arc)}
/>
{/each}
<circle cx="30" cy="30" r="15" />
</svg>
</ShipItem>
<script>
import Arc from '../Weapons/Arc.svelte';
import { weapon_cost_mass } from '../../dux/weapons/rules.js';
import fp from 'lodash/fp';
import _ from 'lodash';
import { createEventDispatcher } from 'svelte';
import ShipItem from '~C/ShipItem';
import Field from '~C/Field';
const all_arcs = [ 'FS', 'F', 'FP', 'AP', 'A', 'AS' ];
export let weapon_type;
export let weapon_class;
export let id;
export let arcs = [];
export let cost;
export let mass;
let arc_options = {
1: [ 6],
2: [ 3, 6 ],
3: [ 1, 2, 3, 4, 5, 6, 'broadside' ],
4: [ 1, 2, 3, 4, 5, 6, 'broadside' ]
};
let nbr_arcs = 6;
$: console.log(weapon_class);
$: nbr_arcs = arc_options[weapon_class][0];
$: console.log({arcs,nbr_arcs})
$: if ( arcs.length !== nbr_arcs ) {
if( nbr_arcs === 'broadside' ) {
arcs = all_arcs.filter( arc => arc.length === 1 )
}
else{
let first_index = all_arcs.findIndex( arc => arcs[0] );
if( first_index === -1 ) first_index = 0;
const new_arcs = [];
_.range(nbr_arcs).forEach( i => {
new_arcs.push( all_arcs[first_index] )
first_index = ( first_index + 1 ) % all_arcs.length;
});
arcs = new_arcs;
}
}
const click_arc = (first_arc) => {
if( nbr_arcs === 'broadside' ) return;
let first_index = all_arcs.findIndex( arc => arc === first_arc );
const new_arcs = [];
_.range(nbr_arcs).forEach( i => {
new_arcs.push( all_arcs[first_index] );
first_index = ( first_index + 1 ) % all_arcs.length;
});
arcs = new_arcs;
}
let i = 1;
$: if(weapon_class) i = 1;
$: console.log( "id", id);
$: console.log( "weapon_class", weapon_class);
$: console.log( "weapon_type", weapon_type);
$: console.log( "arcs", arcs);
const dispatch = createEventDispatcher();
let cache = '';
$: cache = arcs.join(":");
$: {
//console.log( { id, weapon_class, weapon_type, arcs: cache.split(":") });
dispatch( 'change_weapon', { id, weapon_class, weapon_type, arcs:
cache.split(":") });
}
const remove = () => dispatch( 'remove_weapon', id );
</script>
<style>
.weapon {
display: flex;
align-items: center;
}
.weapon > * {
margin-right: 2em;
}
.arcs {
display: grid;
grid-template-rows: 1fr 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr 1fr;
align-items: center;
justify-items: center;
width: 6em;
}
.arc input {
margin: 0px;
}
.arc.F {
grid-column: 2 / span 2;
grid-row: 1;
}
.arc.FS {
grid-column: 1;
grid-row: 1 / span 2;
}
.arc.FP {
grid-column: 4;
grid-row: 1 / span 2;
}
.arc.AS {
grid-column: 1;
grid-row: 3 / span 2;
}
.arc.AP {
grid-column: 4;
grid-row: 3 / span 2;
}
.arc.A {
grid-column: 2 / span 2;
grid-row: 4;
}
.arc {
display: flex;
flex-direction: column;
margin-right: 1em;
}
.add-weapon {
display: block;
}
circle {
fill: white;
}
.remove {
width: 1em;
flex: 0;
color: white;
background-color: black;
border-radius: 0.5em;
height: 1em;
}
</style>

View File

@ -0,0 +1,203 @@
<input class="add-weapon" type="button" value="add"
on:click={add} />
<div class="weapon">
<select bind:value={weapon_type}>
<option>beam</option>
</select>
<select bind:value={weapon_class}>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
<select bind:value={nbr_arcs}>
{#each arc_options[weapon_class] as opt (opt)}
<option>{opt}</option>
{/each}
</select>
<svg width="60px" height="60px">
{#each arcs as arc (arc)}
<Arc {arc} radius={30}
active={selected_arc[arc]}
on:click={()=>click_arc(arc)}
/>
{/each}
<circle cx="30" cy="30" r="15" />
</svg>
<div>{weapon.cost}</div>
<div>{weapon.mass}</div>
</div>
<script>
let weapon_type = 'beam';
let weapon_class = 1;
let nbr_arcs = 6;
$: nbr_arcs = arc_options[weapon_class][0];
import Arc from './Arc.svelte';
import { weapon_cost_mass } from '../../dux/weapons/rules.js';
const arcs = [
'FS', 'F', 'FP', 'AP', 'A', 'AS'
];
import fp from 'lodash/fp';
import _ from 'lodash';
import { createEventDispatcher } from 'svelte';
let arc_options = {
1: [ 6],
2: [ 3, 6 ],
3: [ 1, 2, 3, 4, 5, 6, 'broadside' ],
4: [ 1, 2, 3, 4, 5, 6, 'broadside' ]
};
let selected_arc = Object.fromEntries(
arcs.map( arc => [ arc, false ] )
);
const nbr_selected_arcs = () => Object.values(selected_arc).filter(
x => x ).length;
$: if ( nbr_selected_arcs() !== nbr_arcs ) {
if( nbr_arcs === 'broadside' ) {
const new_arcs = {};
arcs.forEach( arc => new_arcs[arc] = true );
new_arcs.A = false;
new_arcs.F = false;
selected_arc = new_arcs;
}
else{
let first_index = arcs.findIndex( arc => selected_arc[arc] );
if( first_index === -1 ) first_index = 0;
const new_arcs = {};
arcs.forEach( arc => new_arcs[arc] = false );
_.range(nbr_arcs).forEach( i => {
new_arcs[ arcs[first_index] ] = true;
first_index = ( first_index + 1 ) % arcs.length;
});
selected_arc = new_arcs;
}
}
const click_arc = (first_arc) => {
if( nbr_arcs === 'broadside' ) return;
let first_index = arcs.findIndex( arc => arc === first_arc );
const new_arcs = {};
arcs.forEach( arc => new_arcs[arc] = false );
_.range(nbr_arcs).forEach( i => {
console.log(first_index);
console.log(selected_arc);
new_arcs[ arcs[first_index] ] = true;
first_index = ( first_index + 1 ) % arcs.length;
});
selected_arc = new_arcs;
}
let weapon = {};
$: weapon= {
weapon_type,
weapon_class,
arcs,
...weapon_cost_mass({ weapon_type, weapon_class, arcs: arcs.filter(
arc => selected_arc[arc]
) })
};
const dispatch = createEventDispatcher();
const add = () => {
dispatch('add_weapon', weapon);
}
</script>
<style>
.weapon {
display: flex;
align-items: center;
}
.weapon > * {
margin-right: 2em;
}
.arcs {
display: grid;
grid-template-rows: 1fr 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr 1fr;
align-items: center;
justify-items: center;
width: 6em;
}
.arc input {
margin: 0px;
}
.arc.F {
grid-column: 2 / span 2;
grid-row: 1;
}
.arc.FS {
grid-column: 1;
grid-row: 1 / span 2;
}
.arc.FP {
grid-column: 4;
grid-row: 1 / span 2;
}
.arc.AS {
grid-column: 1;
grid-row: 3 / span 2;
}
.arc.AP {
grid-column: 4;
grid-row: 3 / span 2;
}
.arc.A {
grid-column: 2 / span 2;
grid-row: 4;
}
.arc {
display: flex;
flex-direction: column;
margin-right: 1em;
}
.add-weapon {
display: block;
}
circle {
fill: white;
}
</style>

View File

@ -0,0 +1,50 @@
<script>
export let arc;
export let radius;
export let active = false;
const rotation = {
F: 0,
FS: 300,
AS: 240,
A: 180,
AP: 120,
FP: 60,
};
let y, x_delta;
$: y = Math.round( radius * ( 1 - Math.sin(60/180*Math.PI) ) );
$: x_delta = Math.round( radius*Math.cos(60/180*Math.PI) );
let path;
$: path = `M ${radius},${radius} L ${radius-x_delta},${y} A ${radius},${radius} 0 0 1 ${radius+x_delta},${y} Z`;
let transform;
$: transform = `rotate(${rotation[arc]},${radius},${radius})`
</script>
<g { transform }>
<path d={path} class:active on:click />
</g>
<style>
path {
fill: lightgrey;
stroke: white;
stroke-width: 2px;
}
path:hover {
fill: pink;
}
path.active:hover {
fill: pink;
}
path.active {
fill: #313131;
}
</style>

View File

@ -0,0 +1,10 @@
import AddWeapon from './Add.svelte';
export default {
title: "add weapon",
};
export const beam = () => ({
Component: AddWeapon,
});

View File

@ -0,0 +1,30 @@
import tap from "tap";
import { calc_ship_req } from "./utils";
const sample = {
general: {
mass: 10,
used_mass: 0,
cost: 10,
},
ftl: {
cost: 4,
mass: 2,
},
structure: {
hull: {
cost: 2,
mass: 1,
},
},
};
tap.same(
calc_ship_req(sample),
{
cost: 16,
mass: 3,
},
"sample ship"
);

27
src/dux/engine/index.js Normal file
View File

@ -0,0 +1,27 @@
import Updux from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import { createSelector } from 'reselect';
const set_engine = action('set_engine',payload());
const set_drive_reqs = action('set_drive_reqs',payload());
const dux = new Updux({
initial: {
mass: 1,
cost: 2,
rating: 1,
}
});
dux.addMutation(set_engine, engine => u(engine));
dux.addMutation(set_drive_reqs, rate => u(rate));
export function calc_drive_reqs(ship_mass,rating,advanced=false) {
const mass = Math.ceil(rating * 0.05 * ship_mass);
const cost = mass * ( advanced ? 3 : 2 );
return { mass, cost };
}
export default dux.asDux;

23
src/dux/ftl/index.js Normal file
View File

@ -0,0 +1,23 @@
import Updux from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import { createSelector } from 'reselect';
import { calc_ftl_reqs } from './rules';
// 'none' | 'standard' | 'advanced'
const set_ftl = action('set_ftl',payload());
const set_ftl_reqs = action('set_ftl_reqs', payload() );
const dux = new Updux({
initial: {
mass: 0,
cost: 0,
type: 'none'
}
});
dux.addMutation(set_ftl, type => u({type}));
dux.addMutation(set_ftl_reqs, reqs => u(reqs) );
export default dux.asDux;

10
src/dux/ftl/rules.js Normal file
View File

@ -0,0 +1,10 @@
export function calc_ftl_reqs(type,ship_mass) {
if(type==="none") return { cost: 0, mass: 0 };
const mass = Math.ceil(ship_mass / 10);
return {
mass,
cost: mass * ( type === 'advanced' ? 3 : 2 ),
}
}

7
src/dux/ftl/test.js Normal file
View File

@ -0,0 +1,7 @@
import tap from 'tap';
import { calc_ftl_rate } from './rules';
tap.same( calc_ftl_rate(111,'standard'), { mass: 12, cost: 24 },
'mass rounded up' );

143
src/dux/index.js Normal file
View File

@ -0,0 +1,143 @@
import Updux from "updux";
import { action, payload } from "ts-action";
import u from "updeep";
import { createSelector } from "reselect";
import ftl from "./ftl";
import engine, { calc_drive_reqs } from "./engine";
import weaponry from './weaponry';
import { calc_ftl_reqs } from "./ftl/rules";
import { calc_ship_req } from "./utils";
import { candidate_ship_types } from './ship_types';
const set_ship_mass = action("set_ship_mass", payload());
const set_name = action("set_name", payload());
const set_ship_reqs = action("set_ship_reqs", payload());
const set_hull = action("set_hull", payload());
const set_ship_type = action('set_ship_type',payload());
const reset = action('reset' );
const initial = {
general: {
ship_class: "",
name: "",
ship_type: "",
mass: 10,
used_mass: 0,
cost: 10,
},
structure: {
mass: 0,
cost: 0,
hull: {
rating: 1,
advanced: false,
cost: 2,
mass: 1,
},
screens: [],
},
};
const dux = new Updux({
subduxes: { ftl, engine, weaponry, },
initial
});
const add_screen = action( 'add_screen', payload() );
dux.addMutation(add_screen, advanced => u.updateIn( 'structure.screens', state => {
return [ ...(state||[]), { advanced } ]
}));
dux.addMutation( reset, () => () => initial );
dux.addMutation(set_hull, ({rating}) => (state) => {
return u.updateIn("structure.hull", {
cost: 2 * rating,
rating,
mass: rating,
})(state);
});
dux.addMutation(set_ship_mass, (mass) => u.updateIn("general", { mass }));
dux.addMutation(set_name, (name) => u.updateIn("general", { name }));
dux.addMutation( action('set_ship_class',payload() ),
ship_class => u.updateIn('general',{ship_class})
);
dux.addMutation(set_ship_reqs, ({ cost, mass: used_mass }) =>
u.updateIn("general", { cost, used_mass })
);
// set ship's req
dux.addSubscription((store) =>
createSelector(calc_ship_req, (reqs) => store.dispatch(set_ship_reqs(reqs)))
);
dux.addSubscription((store) =>
createSelector(
store => store.general.mass,
store => store.general.ship_type,
(mass,type) => {
const candidates = candidate_ship_types(mass,false);
console.log({candidates});
if( candidates.length === 0 ) return;
if( candidates.find( ({name}) => name === type ) ) return;
store.dispatch(
store.actions.set_ship_type(
candidates[0].name
)
)
}
)
);
dux.addMutation(set_ship_type, type => u.updateIn('general.ship_type',type) );
dux.addSubscription((store) =>
createSelector(
[(ship) => ship.general.mass, (ship) => ship.ftl.type],
(ship_mass, type) =>
store.dispatch(ftl.actions.set_ftl_reqs(calc_ftl_reqs(type,ship_mass)))
)
);
dux.addSubscription((store) =>
createSelector(
[
(ship) => ship.general.mass,
(ship) => ship.engine.rating,
(ship) => ship.engine.advanced,
],
(ship_mass, rating, advanced) =>
store.dispatch(
dux.actions.set_drive_reqs(calc_drive_reqs(ship_mass, rating, advanced))
)
)
);
const calc_firecons_reqs = (nbr) => ({
cost: 4 * nbr,
mass: nbr,
});
const set_firecons = action("set_firecons", payload());
dux.addMutation(set_firecons, (nbr) =>
u.updateIn("weaponry.firecons", {
nbr,
...calc_firecons_reqs(nbr),
})
);
export default dux.asDux;
export const actions = dux.actions;

26
src/dux/ship_types.js Normal file
View File

@ -0,0 +1,26 @@
const ship_types = [
{ name: "Scout", mass: [4, 10], abbrev: "SC" },
{ name: "Courier", mass: [4, 10], abbrev: "SC" },
{ name: "Corvette", mass: [8, 16], abbrev: "CT" },
{ name: "Frigate", mass: [14, 28], abbrev: "FF" },
{ name: "Destroyer", mass: [24, 36], abbrev: "DD" },
{ name: "Heavy Destroyer", mass: [30, 40], abbrev: "DH" },
{ name: "Light Cruiser", mass: [40, 60], abbrev: "CL" },
{ name: "Patrol", mass: [50, 70], abbrev: "CE" },
{ name: "Escort Cruiser", mass: [50, 70], abbrev: "CE" },
{ name: "Heavy Cruiser", mass: [60, 90], abbrev: "CA" },
{ name: "Battlecruiser", mass: [80, 110], abbrev: "BC" },
{ name: "Battleship", mass: [100, 140], abbrev: "BB" },
{ name: "Heavy Battleship", mass: [120, 160], abbrev: "BDN" },
{ name: "Dreadnought", mass: [140, 180], abbrev: "DN" },
{ name: "Superdreadnought", mass: [160, 300], abbrev: "SDN" },
{ name: "Escort Carrier", mass: [60, 140], abbrev: "CVE", carrier: true },
{ name: "Light Carrier", mass: [120, 180], abbrev: "CVL", carrier: true },
{ name: "Heavy Carrier", mass: [160, 300], abbrev: "CVH", carrier: true },
{ name: "Attack Carrier", mass: [150, 300], abbrev: "CVA", carrier: true },
];
export function candidate_ship_types(mass = 0, carrier = false) {
console.log(mass);
return ship_types.filter((c) => carrier == !!c.carrier).filter((c) => c.mass[0] <= mass).filter((c) => c.mass[1] >= mass);
}

17
src/dux/test.js Normal file
View File

@ -0,0 +1,17 @@
import tap from 'tap';
import ship, { actions } from '.';
ship.dispatch( actions.set_ship_mass(100));
ship.dispatch( actions.set_ftl('standard'));
tap.match(
ship.getState(),
{
ship_type: { mass: 100 },
ftl: { type: 'standard', cost: 20, mass: 10 },
},
'standard ftl drive'
);

30
src/dux/utils.js Normal file
View File

@ -0,0 +1,30 @@
import fp from 'lodash/fp';
const expand_cost_mass = (obj={}) => ([
fp.pick(['cost','mass'],obj),
...Object.values(obj || {}).filter( val => typeof val === 'object' ).map(
val => expand_cost_mass(val)
)
]);
export function calc_ship_req(ship) {
console.log(ship);
let { general, ...rest } = ship;
//if(!general) general = {};
const items = fp.flow(
expand_cost_mass,
fp.flattenDeep,
fp.filter(fp.has('cost'))
)({
...rest,
cost: general.mass, mass: 0
})
return {
mass: fp.sumBy('mass',items),
cost: fp.sumBy('cost',items),
}
}

19
src/dux/weaponry/index.js Normal file
View File

@ -0,0 +1,19 @@
import Updux from "updux";
import { action, payload } from "ts-action";
import u from "updeep";
import { createSelector } from "reselect";
import weapons from './weapons';
const dux = new Updux({
initial: {
firecons: {
nbr: 0, cost: 0, mass: 0
}
},
subduxes: {
weapons,
}
})
export default dux.asDux;

View File

@ -0,0 +1,25 @@
import Updux from "updux";
import { action, payload } from "ts-action";
import u from "updeep";
import { createSelector } from "reselect";
import weapon from './weapon';
const add_weapon = action('add_weapon');
const remove_weapon = action('remove_weapon', payload());
const dux = new Updux({
initial: [],
subduxes: { '*': weapon }
})
dux.addMutation( add_weapon, () => state => {
const id = 1 + Math.max( 0, ... state.map(({id})=>id) );
return [ ...state, { ...weapon.initial, id }];
});
dux.addMutation( remove_weapon, id => state =>
state.filter( w => w.id !== id )
);
export default dux.asDux;

View File

@ -0,0 +1,30 @@
import Updux from "updux";
import { action, payload } from "ts-action";
import u from "updeep";
import { createSelector } from "reselect";
import { weapon_cost_mass } from '../../../weapons/rules';
const uu = transform => state => transform(state)(state)
const with_reqs = uu( weapon => u(weapon_cost_mass(weapon)));
const initial = with_reqs({
weapon_type: 'beam',
weapon_class: 1,
});
console.log(initial);
const dux = new Updux({
initial
});
const set_weapon = action('set_weapon',payload());
dux.addMutation(
set_weapon, payload => u.if( s => s.id === payload.id,
state => with_reqs(u(payload,state))
)
)
export default dux.asDux;

59
src/dux/weapons/rules.js Normal file
View File

@ -0,0 +1,59 @@
export function weapon_cost_mass(weapon){
let cost = 0;
let mass = 0;
if( weapon.weapon_type === 'beam' ) {
return beam_cost_mass(weapon);
}
return { cost, mass };
}
const is_broadside = arcs => {
if( arcs.length !== 4 ) return false;
// that'd be A or F
return !arcs.some( a => a.length === 1 );
}
function beam_cost_mass({weapon_class, arcs}) {
let mass;
if( weapon_class === 1 ) {
mass = 1;
}
if( weapon_class == 2 ) {
mass = 2 + (arcs.length > 3 ? 1 : 0);
}
if( weapon_class == 3 ) {
mass = 4;
if( is_broadside(arcs) ) {
mass += 2;
}
else {
mass += arcs.length - 1;
}
}
if( weapon_class == 4 ) {
mass = 8;
if( is_broadside(arcs) ) {
mass += 4;
}
else {
mass += 2*(arcs.length - 1);
}
}
return {
mass, cost: 3 * mass
}
}

View File

@ -0,0 +1,29 @@
import tap from 'tap';
import { weapon_cost_mass } from './rules';
const cases = [
[{
weapon_type: "beam",
weapon_class: 2,
arcs: [ 'AP', 'A', 'AF' ]
},{
cost: 6,
mass: 2,
}
],
[{
weapon_type: "beam",
weapon_class: 1,
arcs: [ 'AP', 'A', 'AF' ]
},{
mass: 1,
cost: 3,
}
],
[{ weapon_type: "beam", weapon_class: 3, arcs: [ 'AP', 'A', 'AF' ] },{ mass: 6, cost: 18, } ],
[{ weapon_type: "beam", weapon_class: 4, arcs: [ 'AP', 'A', 'AF' ] },{ mass: 12, cost: 36, } ],
]
cases.forEach( ([weapon,expected]) =>
tap.match(
weapon_cost_mass(weapon), expected, JSON.stringify(weapon) ) );

10
src/main.js Normal file
View File

@ -0,0 +1,10 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'world'
}
});
export default app;

58
src/stores/ship.js Normal file
View File

@ -0,0 +1,58 @@
import { readable } from "svelte/store";
import { compose, applyMiddleware } from "redux";
import { calc_ship_req } from "../dux/utils";
const composeEnhancers =
(window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
import shipDux from "../dux";
export default () => {
let saved = window && window.localStorage.getItem('aotds-shipyard');
if( saved ) {
saved = JSON.parse(saved);
}
else {
saved = undefined;
}
console.log(saved);
const duxStore = shipDux.createStore(saved, (mw) =>
composeEnhancers(applyMiddleware(mw))
);
duxStore.dispatch(
duxStore.actions.set_ship_reqs(calc_ship_req(duxStore.getState()))
);
Object.entries(duxStore.actions).forEach( ([type,action]) => {
duxStore.dispatch[ type ] = payload => duxStore.dispatch( action(payload) )
});
let previous = undefined;
duxStore.subscribe( () => {
let current = duxStore.getState();
if ( previous === current ) return;
previous = current;
console.log(current);
window && window.localStorage.setItem(
'aotds-shipyard', JSON.stringify(current)
);
});
const state = readable(duxStore.getState(), (set) =>
duxStore.subscribe(() => {
set(duxStore.getState());
})
);
return {
subscribe: state.subscribe,
dispatch: duxStore.dispatch,
actions: duxStore.actions,
selectors: duxStore.selectors,
};
};

79
webpack.config.js Normal file
View File

@ -0,0 +1,79 @@
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
const mode = process.env.NODE_ENV || "development";
const prod = mode === "production";
module.exports = {
entry: {
bundle: ["./src/main.js"],
},
resolve: {
alias: {
svelte: path.resolve("node_modules", "svelte"),
'~': path.resolve(__dirname, 'src/'),
'~C': path.resolve(__dirname, 'src/components/'),
},
extensions: [".mjs", ".js", ".svelte"],
mainFields: ["svelte", "browser", "module", "main"],
},
output: {
path: __dirname + "/public",
filename: "[name].js",
chunkFilename: "[name].[id].js",
},
module: {
rules: [
{
test: /\.svelte$/,
use: {
loader: "svelte-loader",
options: {
emitCss: true,
hotReload: true,
},
},
},
{
test: /\.css$/,
use: [
/**
* MiniCssExtractPlugin doesn't support HMR.
* For developing, use 'style-loader' instead.
* */
prod ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
],
},
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
{
loader: "sass-loader",
options: {
webpackImporter: false,
//implementation: require("node-sass"),
implementation: require("sass"),
sassOptions: {
indentWidth: 4,
includePaths: ["sass", "node_modules"],
},
},
},
],
},
],
},
mode,
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
],
devtool: prod ? false : "source-map",
};