1
0
Fork 0

Compare commits

...

14 Commits

Author SHA1 Message Date
André Jaenisch 514b190997
feat: jump to game over 1 year ago
André Jaenisch fa222af8ce
feat: add settings and game over scene 1 year ago
André Jaenisch a600aadfc2
chore: simplify package.json a bit 1 year ago
André Jaenisch 9bc351426e
chore: drop unused assets 1 year ago
André Jaenisch 95174e9380
feat: add reload button 1 year ago
André Jaenisch 39dbf7a04f
feat: draw left and right boundary 1 year ago
André Jaenisch 34d833f027
feat: allow for manipulation of gravity in both dimensions 1 year ago
André Jaenisch 1ae5694638
test: add test for stroke of rectangle 1 year ago
André Jaenisch da945ee840
refactor: swap jsdom + canvas dep in favour of a mock object 1 year ago
André Jaenisch 30b9e3949a
refactor: post-process using nunjucks 1 year ago
André Jaenisch 8f85f9d858
feat: use emoji as favicon 1 year ago
André Jaenisch 12a9d12b66
feat: style internal links as buttons 1 year ago
André Jaenisch 0ecbc429cb
feat: add minimal tab views 1 year ago
André Jaenisch fb9b8a61d1
chore: add some colours 1 year ago
  1. 0
      dist/css/.gitkeep
  2. 0
      dist/js/.gitkeep
  3. 1985
      package-lock.json
  4. 13
      package.json
  5. 12
      rollup.config.js
  6. 152
      scripts/post-process.cjs
  7. 93
      src/css/main.css
  8. BIN
      src/favicon.ico
  9. BIN
      src/icon.png
  10. 47
      src/index.html
  11. 92
      src/index.njk
  12. 74
      src/js/app.js
  13. 12
      src/site.webmanifest
  14. 26
      test/js/draw.test.js
  15. 1
      types/app.d.ts

0
dist/css/.gitkeep vendored

0
dist/js/.gitkeep vendored

1985
package-lock.json generated

File diff suppressed because it is too large Load Diff

13
package.json

@ -7,8 +7,7 @@
"build": "rollup -c",
"check": "node ./scripts/zip-and-check.cjs",
"commit-msg": "commitlint -e",
"copy:css": "cleancss ./src/css/* -o ./dist/css/main.css && cp ./src/css/normalize.css ./dist/css/",
"copy:icons": "cp ./src/favicon.ico ./dist/ && cp ./src/icon.png ./dist",
"copy:css": "cleancss ./src/css/* -o ./dist/main.css",
"coverage": "nyc npm run test",
"deploy": "gh-pages -d ./dist -r git@github.com:Ryuno-Ki/js13kgames-2021.git",
"docs": "jsdoc -c ./.jsdoc.conf.json -d ./docs -P ./package.json -R README.md -r ./src",
@ -16,12 +15,14 @@
"lint:js": "eslint ./src",
"lint:md": "markdownlint --ignore ./node_modules **/*.md",
"postbuild": "npm run copy:css && npm run check",
"postpost-process": "rm ./dist/*.njk ./dist/*.css ./dist/*.js",
"posttest": "npm run tsd",
"post-process": "node ./scripts/post-process.cjs",
"post-process": "node -e \"var n=require('nunjucks');console.log(n.render('./dist/index.njk'))\" > ./dist/index.html",
"prebuild": "npm run docs",
"precheck": "npm run post-process",
"predeploy": "npm run build",
"prepare": "husky install",
"prepost-process": "cp ./src/index.njk ./dist",
"prestart": "npm run build",
"pre-commit": "npm run types && npm run lint && npm run test",
"start": "http-server -a 0.0.0.0 -p 8080 ./dist",
@ -48,7 +49,6 @@
"@rollup/plugin-buble": "0.21.3",
"@rollup/plugin-commonjs": "20.0.0",
"@rollup/plugin-node-resolve": "13.0.4",
"canvas": "2.8.0",
"chai": "4.3.4",
"clean-css-cli": "5.3.3",
"eslint": "7.32.0",
@ -63,16 +63,17 @@
"jsdoc": "3.6.7",
"jsdoc-mermaid": "1.0.0",
"jsdoc-tsimport-plugin": "1.0.5",
"jsdom": "17.0.0",
"markdownlint-cli": "0.28.1",
"mocha": "9.0.3",
"npm": "7.20.6",
"nunjucks": "3.2.3",
"nyc": "15.1.0",
"rollup": "2.56.2",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-license": "2.5.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.5.2",
"sinon": "11.1.2",
"sinon-chai": "3.7.0",
"standard": "16.0.3",
"tap": "15.0.9",
"tsd": "0.17.0",

12
rollup.config.js

@ -1,5 +1,4 @@
import buble from '@rollup/plugin-buble'
import copy from 'rollup-plugin-copy'
import license from 'rollup-plugin-license'
import { terser } from 'rollup-plugin-terser'
@ -19,20 +18,11 @@ along with <insert name here>. If not, see <https://www.gnu.org/licenses/>.`
export default {
input: './src/js/app.js',
output: {
file: './dist/js/app.js',
file: './dist/app.js',
format: 'iife',
name: 'game'
},
plugins: [
copy({
flatten: true,
verbose: true,
targets: [{
src: './src/index.html',
dest: './dist/'
}]
}),
buble(),
terser({
output: {

152
scripts/post-process.cjs

@ -1,152 +0,0 @@
const fs = require('fs')
const path = require('path')
const jsdom = require('jsdom')
const { JSDOM } = jsdom
const distFolder = path.join(__dirname, '..', 'dist')
const indexHtmlFilePath = path.join(distFolder, 'index.html')
async function deleteFile (filePath) {
console.log('Clean up ', filePath)
return new Promise((resolve, reject) => {
fs.unlink(filePath, (error) => {
if (error) {
reject(error)
}
resolve()
})
})
}
async function cleanUp (scriptFileName, styleFileName) {
return Promise.all([
deleteFile(scriptFileName),
deleteFile(styleFileName)
])
}
async function extractScriptFileName (htmlString) {
const dom = new JSDOM(htmlString)
const document = dom.window.document
const scriptElement = document.querySelector('script')
const scriptSrc = scriptElement.getAttribute('src')
const scriptFileName = path.join(distFolder, scriptSrc)
return scriptFileName
}
async function inlineScript (htmlString, scriptSrc) {
const dom = new JSDOM(htmlString)
const document = dom.window.document
const scriptElement = document.querySelector('script')
const script = document.createElement('script')
const source = document.createTextNode(scriptSrc)
script.appendChild(source)
scriptElement.parentNode.replaceChild(script, scriptElement)
return Promise.resolve(document.documentElement.outerHTML)
}
async function extractStyleFileName (htmlString) {
const dom = new JSDOM(htmlString)
const document = dom.window.document
const linkElement = document.querySelector('link[rel="stylesheet"]')
const styleSrc = linkElement.getAttribute('href')
const styleFileName = path.join(distFolder, styleSrc)
return styleFileName
}
async function inlineStyle (htmlString, styleSrc) {
const dom = new JSDOM(htmlString)
const document = dom.window.document
const linkElement = document.querySelector('link[rel="stylesheet"]')
const style = document.createElement('style')
const source = document.createTextNode(styleSrc)
style.appendChild(source) // TODO: Should work with replaceChild, too?
linkElement.remove()
document.head.appendChild(style)
return Promise.resolve(document.documentElement.outerHTML)
}
async function updateFile (originalHtmlString, updatedHtmlString) {
const dom = new JSDOM(originalHtmlString)
const doctype = dom.window.document.doctype
const content = [
'<!DOCTYPE ',
doctype.name,
doctype.internalSubset,
doctype.publicId,
doctype.systemId,
'>'
].join('') + '\n' + updatedHtmlString
return new Promise((resolve, reject) => {
fs.writeFile(indexHtmlFilePath, content, (error) => {
if (error) {
return reject(error)
}
resolve(updatedHtmlString)
})
})
}
function parseFile (filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (error, content) => {
if (error) {
return reject(error)
}
resolve(content)
})
})
}
async function run () {
let htmlString = ''
let updatedHtmlString = htmlString
try {
htmlString = await parseFile(indexHtmlFilePath)
} catch (exc) {
console.error(exc)
return process.exit(1)
}
const scriptFileName = await extractScriptFileName(htmlString)
const styleFileName = await extractStyleFileName(htmlString)
try {
const scriptSrc = await parseFile(scriptFileName)
updatedHtmlString = await inlineScript(htmlString, scriptSrc)
} catch (exc) {
console.error(exc)
return process.exit(2)
}
try {
const styleSrc = await parseFile(styleFileName)
updatedHtmlString = await inlineStyle(updatedHtmlString, styleSrc)
} catch (exc) {
console.error(exc)
return process.exit(3)
}
try {
await updateFile(htmlString, updatedHtmlString)
} catch (exc) {
console.error(exc)
process.exit(4)
}
try {
cleanUp(scriptFileName, styleFileName)
} catch (exc) {
console.error(exc)
return process.exit(5)
}
}
run()

93
src/css/main.css

@ -261,6 +261,99 @@ textarea {
}
}
/* DO NOT DELETE BELOW */
* {
box-sizing: border-box;
}
html,
body {
background-color: midnightblue;
color: gold;
}
main {
/* FIXME: Normalize.css interfers? */
display: grid !important;
min-height: 90vh;
min-width: 90vw;
place-items: center;
}
section {
max-width: 45ch;
}
section:first-of-type {
display: block;
text-align: center;
}
#scene-game:not(:target),
section:not(:target) {
display: none;
}
a {
color: orange;
}
button[type="button"],
a[href^="#"] {
/* FIXME: Normalize.css interfers? */
background-color: orange !important;
border: none;
color: midnightblue;
cursor: pointer;
display: inline-block;
margin-block: 1em;
padding: 0.5em 1em;
text-align: center;
text-decoration: none;
}
canvas {
background-color: grey;
cursor: not-allowed;
}
label,
label span {
display: block;
}
input[type="range"] {
min-width: 100%;
}
#scene-game {
display: grid;
grid-template-areas:
'canvas ygravity'
'xgravity . '
'controls controls';
}
#game {
grid-area: canvas;
}
#xgravity-label {
grid-area: xgravity;
}
#ygravity-label {
display: flex;
grid-area: ygravity;
}
#ygravity {
-webkit-appearance: slider-vertical;
transform: rotate(180deg);
}
#controls {
display: flex;
grid-area: controls;
justify-content: space-between;
}

BIN
src/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

BIN
src/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

47
src/index.html

@ -1,47 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>js13kgames-2021 - SPACE</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="" />
<meta property="og:type" content="" />
<meta property="og:url" content="" />
<meta property="og:image" content="" />
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<section data-scene="title">
<h1>Combat Scorched Earth from Outer Space</h1>
</section>
<section data-scene="game">
<canvas id="game" width="300" height="450"></canvas>
<div>
<label>
<span>Manipulate the gravity!</span>
<input
id="gravity"
type="range"
min="-100"
max="100"
value="0"
step="any"
/>
</label>
</div>
</section>
<section data-scene="credits">
<p>
Video name generated using
<a href="https://videogamena.me/">The Video Game Name Generator</a>.
</p>
<p>
Physics engine adapted from
<a href="https://github.com/xem/mini2Dphysics">mini2Dphysics</a>.
</p>
</section>
<script src="js/app.js"></script>
<script>window.game.app()</script>
</body>
</html>

92
src/index.njk

@ -0,0 +1,92 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>js13kgames-2021 - SPACE</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="" />
<meta property="og:type" content="" />
<meta property="og:url" content="" />
<meta property="og:image" content="" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
<style>{%- include './main.css' -%}</style>
</head>
<body>
<main>
<section id="scene-title" tabindex="0">
<h1>Combat Scorched Earth from Outer Space</h1>
<a href="#scene-game">New game</a>
<a href="#scene-settings">Settings</a>
<a href="#scene-credits">Credits</a>
</section>
<section id="scene-game" tabindex="0">
<canvas id="game" width="300" height="450"></canvas>
<label id=="xgravity-label">
<span>Manipulate the gravity!</span>
<input
id="xgravity"
type="range"
min="-100"
max="100"
value="0"
step="any"
/>
</label>
<label id="ygravity-label">
<span class="sr-only">Manipulate the gravity!</span>
<input
id="ygravity"
type="range"
min="-100"
max="100"
value="0"
step="any"
orient="vertical"
/>
</label>
<div id="controls">
<button type="button" onclick="location.reload()">Restart</button>
<a href="#scene-title">Back to start</a>
</div>
</section>
<section id="scene-gameover" tabindex="0">
<p>Game Over</p>
<p>
<a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjaenis.ch%2Fhobbies%2Fcoding%2Fdemos%2Fjs13kgames%2F2021%2Ftext=I%20played%20%C2%BBCombat%20Scorched%20Earth%20from%20Outer%20Space%C2%AB&hashtags=js13k,js13kgames&via=AndreJaenisch">Share your feedback</a>
</p>
<p>
<button type="button" onclick="location.reload()">Try again?</button>
<a href="#scene-title">Back to start</a>
</p>
</section>
<section id="scene-settings" tabindex="0">
<p>Here you will be able to turn off the volume and load emojis.</p>
<p>
<a href="#scene-title">Back to start</a>
</p>
</section>
<section id="scene-credits" tabindex="0">
<h2>Credits</h2>
<p>
Video name generated using
<a href="https://videogamena.me/">The Video Game Name Generator</a>.
</p>
<p>
Physics engine adapted from
<a href="https://github.com/xem/mini2Dphysics">mini2Dphysics</a>.
</p>
<p>
<a href="#scene-title">Back to start</a>
</p>
</section>
</main>
<script>{%- include './app.js' -%}</script>
<script>location.hash='#scene-title';window.game.app()</script>
</body>
</html>

74
src/js/app.js

@ -1,9 +1,10 @@
import { testCollision } from './collisions.js'
import { drawShape } from './draw.js'
import { Vec2 } from './vector.js'
import { add, Vec2 } from './vector.js'
import { makeAstronaut, makeBoundary } from './world.js'
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
// Global on purpose
/** @type {Shape} */
@ -14,8 +15,8 @@ const boundaries = []
let canvas
/** @type {CanvasRenderingContext2D} */
let context
/** @type {number} */
let gravity
/** @type {Vector2D} */
let gravity = Vec2(0, 0)
/**
* Entry point into the game.
@ -29,13 +30,15 @@ export function app () {
return
}
const input = document.getElementById('gravity')
if (!input) {
const xinput = document.getElementById('xgravity')
const yinput = document.getElementById('ygravity')
if (!xinput || !yinput) {
console.error('Could not start game!')
return
}
input.addEventListener('change', updateGravity)
xinput.addEventListener('input', updateGravity)
yinput.addEventListener('input', updateGravity)
canvas = /** @type {HTMLCanvasElement} */(game)
const ctx = canvas.getContext('2d')
@ -46,7 +49,6 @@ export function app () {
}
context = ctx
gravity = parseFloat(/** @type {HTMLInputElement} */(input).value)
createObjectsInWorld()
tick()
}
@ -59,26 +61,31 @@ function tick () {
drawShape(context, boundary)
})
drawShape(context, astronaut, Vec2(0, gravity))
drawShape(context, astronaut, gravity)
boundaries.forEach(function (boundary) {
if (testCollision(boundary, astronaut)) {
throw new Error('Game Over!')
}
})
try {
boundaries.forEach(function (boundary) {
if (testCollision(boundary, astronaut)) {
throw new Error('Game Over!')
}
})
window.requestAnimationFrame(tick)
window.requestAnimationFrame(tick)
} catch (_) {
console.log('Catched')
location.hash = '#scene-gameover'
}
}
function createObjectsInWorld () {
astronaut = makeAstronaut()
const boundaryHeight = 2
const boundarySize = 2
boundaries.push(
makeBoundary({
x: 0,
y: canvas.height - boundaryHeight,
height: boundaryHeight,
y: canvas.height - boundarySize,
height: boundarySize,
width: canvas.width
})
)
@ -86,11 +93,29 @@ function createObjectsInWorld () {
boundaries.push(
makeBoundary({
x: 0,
y: 0 + boundaryHeight,
height: boundaryHeight,
y: 0 + boundarySize,
height: boundarySize,
width: canvas.width
})
)
boundaries.push(
makeBoundary({
x: 0 + boundarySize,
y: 0,
height: canvas.height,
width: boundarySize
})
)
boundaries.push(
makeBoundary({
x: canvas.width - boundarySize,
y: 0,
height: canvas.height,
width: boundarySize
})
)
}
/**
@ -104,5 +129,14 @@ function updateGravity (event) {
}
const input = /** @type {HTMLInputElement} */(event.target)
gravity = parseFloat(input.value)
const value = parseFloat(input.value)
if (input.id === 'xgravity') {
gravity = add(gravity, Vec2(value, 0))
} else if (input.id === 'ygravity') {
gravity = add(gravity, Vec2(0, value))
} else {
console.error(input)
throw new Error('What input is this?!')
}
}

12
src/site.webmanifest

@ -1,12 +0,0 @@
{
"short_name": "",
"name": "",
"icons": [{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "/?utm_source=homescreen",
"background_color": "#fafafa",
"theme_color": "#fafafa"
}

26
test/js/draw.test.js

@ -1,20 +1,18 @@
import { expect } from 'chai'
import jsdom from 'jsdom'
import chai from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { drawShape } from '../../src/js/draw.js'
import { RigidShape } from '../../src/js/shape.js'
import { Vec2 } from '../../src/js/vector.js'
// Requires npm canvas package here to be installed
const { JSDOM } = jsdom
const { window } = new JSDOM('')
const { document } = window
chai.use(sinonChai)
const { expect } = chai
describe('drawShape', function () {
it('should draw a shape onto a canvas', function () {
// Arrange
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const context = makeContext()
const center = Vec2(500, 200)
const friction = 20
const restitution = 0
@ -37,6 +35,16 @@ describe('drawShape', function () {
drawShape(context, shape)
// Assert
// TODO: Assert pixel on canvas
expect(context.strokeRect).to.have.been.calledOnce;
})
})
function makeContext () {
return {
restore: function () {},
rotate: function () {},
save: function () {},
strokeRect: sinon.spy(),
translate: function () {},
}
}

1
types/app.d.ts vendored

@ -5,3 +5,4 @@
*/
export function app(): void;
export type Shape = import('./shape').Shape;
export type Vector2D = import('./vector').Vector2D;

Loading…
Cancel
Save