Browse Source

commit first major batch of work

main
Yanick Champoux 3 years ago
parent
commit
2c68f44e88
  1. 5
      .gitignore
  2. 3
      .storybook/main.js
  3. 144
      package.json
  4. 63
      package.yml
  5. 76
      public/global.css
  6. 30
      public/index.html
  7. 5
      public/mass.svg
  8. 102
      rollup.config.js
  9. 36
      scripts/eslint
  10. 59
      scripts/merge_package.pl
  11. 16
      scripts/prettier
  12. 41
      src/App-real.svelte
  13. 93
      src/App.svelte
  14. 19
      src/components/CostMass.svelte
  15. 38
      src/components/Engine/index.svelte
  16. 5
      src/components/Field/BasicStory.svelte
  17. 20
      src/components/Field/index.svelte
  18. 12
      src/components/Field/stories.js
  19. 20
      src/components/Firecons.svelte
  20. 35
      src/components/Ftl/index.svelte
  21. 13
      src/components/Ftl/stories.js
  22. 39
      src/components/Hull.svelte
  23. 49
      src/components/Identification.svelte
  24. 18
      src/components/Propulsion/index.svelte
  25. 31
      src/components/Screens/index.svelte
  26. 8
      src/components/Section/index.svelte
  27. 58
      src/components/ShipCost.svelte
  28. 29
      src/components/ShipItem/index.svelte
  29. 210
      src/components/Weapon/index.svelte
  30. 203
      src/components/Weapons/Add.svelte
  31. 50
      src/components/Weapons/Arc.svelte
  32. 10
      src/components/Weapons/stories.js
  33. 30
      src/dux/calc_ship_cost_mass.test.js
  34. 27
      src/dux/engine/index.js
  35. 23
      src/dux/ftl/index.js
  36. 10
      src/dux/ftl/rules.js
  37. 7
      src/dux/ftl/test.js
  38. 143
      src/dux/index.js
  39. 26
      src/dux/ship_types.js
  40. 17
      src/dux/test.js
  41. 30
      src/dux/utils.js
  42. 19
      src/dux/weaponry/index.js
  43. 25
      src/dux/weaponry/weapons/index.js
  44. 30
      src/dux/weaponry/weapons/weapon/index.js
  45. 59
      src/dux/weapons/rules.js
  46. 29
      src/dux/weapons/rules.test.js
  47. 10
      src/main.js
  48. 58
      src/stores/ship.js
  49. 79
      webpack.config.js

5
.gitignore vendored

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

3
.storybook/main.js

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

144
package.json

@ -1,12 +1,136 @@ @@ -1,12 +1,136 @@
{
"name": "shipyard",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
"author": "Yanick Champoux <yanick@babyl.ca>",
"dependencies": {
"@babel/cli": "^7.10.1",
"@babel/core": "^7.10.2",
"@babel/node": "^7.10.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"
},
"description": "Full Thrust ship builder",
"devDependencies": {
"@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

@ -0,0 +1,63 @@ @@ -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

@ -0,0 +1,76 @@ @@ -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

@ -0,0 +1,30 @@ @@ -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

@ -0,0 +1,5 @@ @@ -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

@ -0,0 +1,102 @@ @@ -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

@ -0,0 +1,36 @@ @@ -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

@ -0,0 +1,59 @@ @@ -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

@ -0,0 +1,16 @@ @@ -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

@ -0,0 +1,41 @@ @@ -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

@ -0,0 +1,93 @@ @@ -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>

19
src/components/CostMass.svelte

@ -0,0 +1,19 @@ @@ -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>

38
src/components/Engine/index.svelte

@ -0,0 +1,38 @@ @@ -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>

5
src/components/Field/BasicStory.svelte

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

20
src/components/Field/index.svelte

@ -0,0 +1,20 @@ @@ -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>

12
src/components/Field/stories.js

@ -0,0 +1,12 @@ @@ -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,
});

20
src/components/Firecons.svelte

@ -0,0 +1,20 @@ @@ -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>

35
src/components/Ftl/index.svelte

@ -0,0 +1,35 @@ @@ -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>

13
src/components/Ftl/stories.js

@ -0,0 +1,13 @@ @@ -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()
}
});

39
src/components/Hull.svelte

@ -0,0 +1,39 @@ @@ -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>

49
src/components/Identification.svelte

@ -0,0 +1,49 @@ @@ -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>

18
src/components/Propulsion/index.svelte

@ -0,0 +1,18 @@ @@ -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>

31
src/components/Screens/index.svelte

@ -0,0 +1,31 @@ @@ -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>

8
src/components/Section/index.svelte

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

58
src/components/ShipCost.svelte

@ -0,0 +1,58 @@ @@ -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>

29
src/components/ShipItem/index.svelte

@ -0,0 +1,29 @@ @@ -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>

210
src/components/Weapon/index.svelte

@ -0,0 +1,210 @@ @@ -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>

203
src/components/Weapons/Add.svelte

@ -0,0 +1,203 @@ @@ -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>

50
src/components/Weapons/Arc.svelte

@ -0,0 +1,50 @@ @@ -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>

10
src/components/Weapons/stories.js

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

30
src/dux/calc_ship_cost_mass.test.js

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
import tap from "tap";
import { calc_ship_req } from "./utils";
const sample = {
general: {