diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e2227491..ac1696f1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -4,12 +4,28 @@ module.exports = { browser: true, es2021: true, }, - extends: ['eslint:recommended', 'plugin:prettier/recommended'], + plugins: ['jsdoc'], + extends: [ + 'eslint:recommended', + 'plugin:prettier/recommended', + 'plugin:jsdoc/recommended', + // 'plugin:jsdoc/recommended-typescript-flavor', + ], parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, rules: { complexity: ['warn', 14], + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + }, + settings: { + jsdoc: { + tagNamePreference: { + property: 'prop', + }, + }, }, } diff --git a/CHANGELOG.md b/CHANGELOG.md index b895ffc5..c3d4b075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v0.17.1](https://github.com/oskarrough/slaytheweb/compare/v0.17.0...v0.17.1) + +- Refactor dungeon and more jsdocs [`#200`](https://github.com/oskarrough/slaytheweb/pull/200) +- More comments and types [`#199`](https://github.com/oskarrough/slaytheweb/pull/199) +- Fix save game [`#172`](https://github.com/oskarrough/slaytheweb/pull/172) +- Document the dungeon & map code better [`#196`](https://github.com/oskarrough/slaytheweb/pull/196) +- PWA [`#198`](https://github.com/oskarrough/slaytheweb/pull/198) +- Switch to vite for development [`#195`](https://github.com/oskarrough/slaytheweb/pull/195) +- Fix action missing card ref [`#193`](https://github.com/oskarrough/slaytheweb/pull/193) +- Switch to npm for preact+htm [`fa8727e`](https://github.com/oskarrough/slaytheweb/commit/fa8727e8e0761364abf6e087fd949952bce36de5) +- Replace custom service worker with vite-plugin-pwa [`c11914c`](https://github.com/oskarrough/slaytheweb/commit/c11914c5900d00bb123457e9d1ebd738a6abb803) +- Update dependencies [`f1b7d22`](https://github.com/oskarrough/slaytheweb/commit/f1b7d22afcb9f85ee75d56e1ce016cae40704978) + #### [v0.17.0](https://github.com/oskarrough/slaytheweb/compare/v0.16.1...v0.17.0) +> 15 July 2023 + - Start saving game logs to a central database [`#189`](https://github.com/oskarrough/slaytheweb/pull/189) - new card, and added hp/max hp getting functions [`#183`](https://github.com/oskarrough/slaytheweb/pull/183) - fixes a few broken links in DOCUMENTATION.md [`#180`](https://github.com/oskarrough/slaytheweb/pull/180) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 413b018a..9404054d 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -60,8 +60,7 @@ On `state.player` we have you, the player. This object describes the health, pow #### Dungeon -Every game starts in a dungeon. You make your way through rooms to reach the end. - +Every game evolves around and in a dungeon. A dungeon consists of a graph (think a 2d array with rows and columns, or positions and nodes, or floors and rooms). There are different types of rooms. Like Monster and Campfire. One day there'll be more like Merchant and Treasure or a "random" room. #### Monsters diff --git a/jsconfig.json b/jsconfig.json index 3550c28d..da60864a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,8 +1,9 @@ { "compilerOptions": { - "checkJs": true, "allowJs": true, + "checkJs": true, "target": "esnext", - "moduleResolution": "nodenext" - }, + "moduleResolution": "nodenext", + "noEmit": true + } } diff --git a/package-lock.json b/package-lock.json index 0265588f..0a09149b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "slaytheweb", - "version": "0.17.0", + "version": "0.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "slaytheweb", - "version": "0.17.0", + "version": "0.17.1", "license": "AGPL-3.0-or-later", "dependencies": { "gsap": "^3.12.2", @@ -22,6 +22,7 @@ "docco": "^0.9.1", "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", + "eslint-plugin-jsdoc": "^46.4.4", "eslint-plugin-prettier": "^5.0.0", "prettier": "3.0.0", "release-it": "^16.1.3", @@ -1862,6 +1863,20 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.39.4", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz", + "integrity": "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==", + "dev": true, + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.14", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.14.tgz", @@ -2948,6 +2963,15 @@ "node": ">= 8" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "1.0.10", "dev": true, @@ -4080,6 +4104,15 @@ "node": ">= 12" } }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/common-path-prefix": { "version": "3.0.0", "dev": true, @@ -4794,6 +4827,29 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.4.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.4.4.tgz", + "integrity": "sha512-D8TGPOkq3bnzmYmA7Q6jdsW+Slx7CunhJk1tlouVq6wJjlP1p6eigZPvxFn7aufud/D66xBsNVMhkDQEuqumMg==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.39.4", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.1", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", @@ -6182,6 +6238,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -6746,6 +6817,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9281,6 +9361,28 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, diff --git a/package.json b/package.json index 6c5a2951..b5308f9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slaytheweb", - "version": "0.17.0", + "version": "0.17.1", "license": "AGPL-3.0-or-later", "homepage": "https://slaytheweb.cards", "repository": "https://github.com/oskarrough/slaytheweb", @@ -23,6 +23,7 @@ "docco": "^0.9.1", "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", + "eslint-plugin-jsdoc": "^46.4.4", "eslint-plugin-prettier": "^5.0.0", "prettier": "3.0.0", "release-it": "^16.1.3", diff --git a/src/content/dungeon-encounters.js b/src/content/dungeon-encounters.js index 660e8a65..8e85a5a3 100644 --- a/src/content/dungeon-encounters.js +++ b/src/content/dungeon-encounters.js @@ -1,10 +1,14 @@ import Dungeon from '../game/dungeon.js' -import {MonsterRoom, Monster} from '../game/dungeon-rooms.js' -import {random} from '../game/utils.js' +import {MonsterRoom} from '../game/rooms.js' +import {Monster} from '../game/monster.js' +import {random} from '../utils.js' + +// A dungeon encounter is the combination of a Room and Monster(s). // Hello. With the imported functions above you can create a dungeon with different rooms and monsters. // Should be able to support even more monsters (4-5) -// This is the dungeon currently used. + +// This is the efault dungeon currently used. export const dungeonWithMap = () => { return Dungeon({ width: 6, @@ -18,7 +22,7 @@ export const dungeonWithMap = () => { // This is the dungeon used in tests. Don't change it without running tests. export const createTestDungeon = () => { const dungeon = Dungeon({width: 1, height: 3}) - // The tests rely on the first room having a single monster, second two monsters. + // The tests rely on the first room having a single monster, second room two monsters. const intents = [{block: 7}, {damage: 10}, {damage: 8}, {}, {damage: 14}] dungeon.graph[1][0].room = MonsterRoom(Monster({hp: 42, intents})) dungeon.graph[2][0].room = MonsterRoom(Monster({hp: 24, intents}), Monster({hp: 13, intents})) diff --git a/src/game/action-manager.js b/src/game/action-manager.js index b49ac85e..d79f7a75 100644 --- a/src/game/action-manager.js +++ b/src/game/action-manager.js @@ -1,22 +1,34 @@ -import Queue from './queue.js' import actions from './actions.js' +import Queue from '../utils.js' + +/** @typedef {import('./actions.js').State} State */ /** - * @typedef {Object} FutureAction + * @typedef {object} FutureAction * @prop {string} type - the name of a function in actions.js * @prop {any} [any] - arguments are passed to the action */ /** - * @typedef {Object} PastAction + * @typedef {object} PastAction * @prop {string} type - the name of a function in actions.js - * @prop {import('./actions.js').State} state + * @prop {State} state + */ + +/** + * @typedef {object} ActionManager + * @prop {function(FutureAction):void} enqueue + * @prop {function(State):State} dequeue + * @prop {function():PastAction} undo + * @prop {Queue} future + * @prop {Queue} past */ /** * The action manager makes use of queues to keep track of future and past actions in the game state + undo. - * @param {Object} props - * @prop {boolean} props.debug - whether to log actions to the console + * @param {object} props + * @param {boolean} props.debug - whether to log actions to the console + * @returns {ActionManager} action manager */ export default function ActionManager(props) { const future = new Queue() @@ -34,8 +46,8 @@ export default function ActionManager(props) { /** * Deqeueing runs the oldest action (from the `future` queue) on the state. * The action is then moved to the `past` queue. - * @param {import('./actions.js').State} state - * @returns {import('./actions.js').State} new state + * @param {State} state + * @returns {State} new state */ function dequeue(state) { // Get the oldest action diff --git a/src/game/actions.js b/src/game/actions.js index fe0b4739..11c16dea 100644 --- a/src/game/actions.js +++ b/src/game/actions.js @@ -1,23 +1,31 @@ import {produce} from 'immer' -import {createCard, CardTargets} from './cards.js' -import {clamp, shuffle} from './utils.js' -import {getTargets, getCurrRoom} from './utils-state.js' +import {clamp, shuffle} from '../utils.js' +import {isDungeonCompleted, getTargets, getCurrRoom} from './utils-state.js' import powers from './powers.js' -import {dungeonWithMap} from '../content/dungeon-encounters.js' import {conditionsAreValid} from './conditions.js' -import {isDungeonCompleted} from './utils-state.js' +import {createCard, CardTargets} from './cards.js' +import {dungeonWithMap} from '../content/dungeon-encounters.js' -// Without this, immer.js will throw an error if our `state` is modified outside of an action. -// While in theory a good idea, we're not there yet. It is a useful way to spot modifications -// of the game state that should not be there. -// setAutoFreeze(false) +/** @typedef {import('./dungeon.js').Dungeon} Dungeon */ +/** @typedef {import('./cards.js').CARD} CARD */ +/** @typedef {import('./rooms.js').Room} Room */ +/** @typedef {import('./cards.js').CardPowers} CardPowers */ -// In Slay the Web, we have one big object with game state. -// Whenever we want to change something, call an "action" from this file. -// Each action takes two arguments: 1) the current state, 2) an object of arguments. +/** + We don't mutate the state directly, instead we run "action functions" on it. + * @template T + * @callback ActionFn + * @param {State} state the current state + * @param {T} [props] an object of arguments + * @returns {State} a new state object + */ /** + * The big "game state" object * @typedef {object} State + * @prop {Number} createdAt + * @prop {Number} endedAt + * @prop {Boolean} won * @prop {number} turn * @prop {Array} deck * @prop {Array} drawPile @@ -25,10 +33,7 @@ import {isDungeonCompleted} from './utils-state.js' * @prop {Array} discardPile * @prop {Array} exhaustPile * @prop {Player} player - * @prop {Object} dungeon - * @prop {Number} createdAt - * @prop {Number} endedAt - * @prop {Boolean} won + * @prop {Dungeon} [dungeon] */ /** @@ -38,15 +43,7 @@ import {isDungeonCompleted} from './utils-state.js' * @prop {number} currentHealth * @prop {number} maxHealth * @prop {number} block - * @prop {Object} powers - */ - -/** - * @template T - * @callback ActionFn - * @param {State} state - first argument must be the state object - * @param {T} [props] - * @returns {State} returns a new state object + * @prop {object} powers */ /** @@ -69,7 +66,7 @@ function createNewState() { block: 0, powers: {}, }, - dungeon: {}, + dungeon: undefined, createdAt: new Date().getTime(), endedAt: undefined, won: false, @@ -79,7 +76,7 @@ function createNewState() { /** * By default a new game doesn't come with a dungeon. You have to set one explicitly. Look in dungeon-encounters.js for inspiration. * @param {State} state - * @param {import('./dungeon.js').Dungeon} [dungeon] + * @param {Dungeon} [dungeon] * @returns {State} */ function setDungeon(state, dungeon) { @@ -139,7 +136,7 @@ function drawCards(state, options) { /** * Adds a card (from nowhere) directly to your hand. - * @type {ActionFn<{card: import('./cards.js').CARD}>} + * @type {ActionFn<{card: CARD}>} */ function addCardToHand(state, {card}) { return produce(state, (draft) => { @@ -149,7 +146,7 @@ function addCardToHand(state, {card}) { /** * Discard a single card from your hand. - * @type {ActionFn<{card: import('./cards.js').CARD}>} + * @type {ActionFn<{card: CARD}>} */ function discardCard(state, {card}) { return produce(state, (draft) => { @@ -283,7 +280,7 @@ function addHealth(state, {target, amount}) { /** * Adds regen to the player equal to the amount of damage dealt to all enemies. - * @type {ActionFn<{card: import('./cards.js').CARD}>} + * @type {ActionFn<{card: CARD}>} */ function addRegenEqualToAllDamage(state, {card}) { if (!card) throw new Error('missing card!') @@ -354,7 +351,7 @@ const setHealth = (state, {target, amount}) => { /** * Used by playCard. Applies each power on the card to? - * @type {ActionFn<{card: import('./cards.js').CARD, target: CardTargets}>} + * @type {ActionFn<{card: CARD, target: CardTargets}>} */ function applyCardPowers(state, {card, target}) { return produce(state, (draft) => { @@ -385,7 +382,7 @@ function applyCardPowers(state, {card, target}) { /** * Helper to decrease all power stacks by one. - * @param {import('./cards.js').CardPowers} powers + * @param {CardPowers} powers */ function _decreasePowers(powers) { Object.entries(powers).forEach(([name, stacks]) => { @@ -545,7 +542,7 @@ function takeMonsterTurn(state, monsterIndex) { /** * Adds a card to the deck. - * @type {ActionFn<{card: import('./cards.js').CARD}>} + * @type {ActionFn<{card: CARD}>} */ function addCardToDeck(state, {card}) { return produce(state, (draft) => { @@ -555,7 +552,7 @@ function addCardToDeck(state, {card}) { /** * Records a move on the dungeon map. - * @type {ActionFn<{move: {x: number, y: number}}} + * @type {ActionFn<{move: {x: number, y: number}}>} */ function move(state, {move}) { let nextState = endEncounter(state) @@ -577,7 +574,7 @@ function move(state, {move}) { /** * Deals damage to a target equal to the current player's block. - * @type {ActionFn<{target: CardTargets}} + * @type {ActionFn<{target: CardTargets}>} */ function dealDamageEqualToBlock(state, {target}) { if (state.player.block) { @@ -632,7 +629,7 @@ function setPower(state, {target, power, amount}) { /** * Stores a campfire choice on the room (useful for stats and whatnot) - * @type {ActionFn<{room: import('./dungeon-rooms.js').Room, choice: string, reward: import('./cards.js').CARD}>} + * @type {ActionFn<{room: Room, choice: string, reward: CARD}>} */ function makeCampfireChoice(state, {choice, reward}) { return produce(state, (draft) => { diff --git a/src/game/backend.js b/src/game/backend.js index d4cf5f57..02fa7bd2 100644 --- a/src/game/backend.js +++ b/src/game/backend.js @@ -2,14 +2,13 @@ import {isDungeonCompleted} from './utils-state.js' const apiUrl = 'https://api.slaytheweb.cards/api/runs' // const apiUrl = 'http://localhost:3000/api/runs' -// /** * @typedef {object} Run - * @property {string} name - user inputted player name - * @property {boolean} win - whether the player won the game - * @property {object} state - the final state - * @property {Array} past - a list of past states + * @prop {string} name - user inputted player name + * @prop {boolean} win - whether the player won the game + * @prop {object} state - the final state + * @prop {Array} past - a list of past states */ /** diff --git a/src/game/cards.js b/src/game/cards.js index 84f9a213..63f671e9 100644 --- a/src/game/cards.js +++ b/src/game/cards.js @@ -1,4 +1,4 @@ -import {uuid} from './utils.js' +import {uuid} from '../utils.js' import cards from '../content/cards.js' // This file contains the logic to create cards. @@ -29,9 +29,9 @@ export const CardTargets = { */ /** - * @typedef {Object} CardAction - allows the card to run all defined actions + * @typedef {object} CardAction - allows the card to run all defined actions * @prop {string} type - name of the action to call. See game/actions.js - * @prop {Object} [parameter] - props to pass to the action + * @prop {object} [parameter] - props to pass to the action * @prop {Array<{type: string}>} [conditions] - list of conditions */ @@ -81,6 +81,7 @@ export class Card { * Very important, we clone the object. Otherwise, all cards would share the same object. * We do this so we can define the cards without using class syntax. * @param {string} name - exact name of the Card + * @param {boolean} [shouldUpgrade] - whether to upgrade the card * @returns {CARD} */ export function createCard(name, shouldUpgrade) { @@ -95,10 +96,10 @@ export function createCard(name, shouldUpgrade) { } /** - * Returns {amount} of random cards from a {list} - * @param {array} list - collection of POJO cards + * Returns X random cards from a list of cards. + * @param {Array} list - collection of POJO cards * @param {number} amount - how many - * @returns {array} results + * @returns {Array} results */ export function getRandomCards(list, amount) { const cardNames = list.map((card) => card.name) diff --git a/src/game/conditions.js b/src/game/conditions.js index 1f361073..b91e1208 100644 --- a/src/game/conditions.js +++ b/src/game/conditions.js @@ -1,8 +1,11 @@ import {getPlayerHealthPercentage} from './utils-state.js' +/** @typedef {import('./actions.js').State} State */ +/** @typedef {import('./cards.js').CARD} Card */ + /** * Conditions decide whether a card can be played or not. - * @typedef {Object} Condition — all other props will be passed to the condition as well + * @typedef {object} Condition — all other props will be passed to the condition as well * @prop {string} type * @prop {string=} cardType * @prop {number=} percentage @@ -10,9 +13,9 @@ import {getPlayerHealthPercentage} from './utils-state.js' /** * @callback ConditionFn - * @param {import('./actions.js').State} state + * @param {State} State * @param {Condition} condition - * @return {boolean} + * @returns {boolean} */ /** @@ -41,7 +44,7 @@ export function healthPercentageBelow(state, condition) { /** * Returns true if all conditions are valid on a certain game state. - * @param {import('./actions.js').State} state + * @param {State} state * @param {Array.} conditions * @returns {boolean} */ @@ -58,8 +61,8 @@ export function conditionsAreValid(state, conditions) { /** * Returns true if the card can be played. Checks whether the card is in hand, you have enough energy and any conditions are all valid. - * @param {import('./actions.js').State} state - * @param {import('./cards.js').CARD} card + * @param {State} state + * @param {Card} card * @returns {boolean} */ export function canPlay(state, card) { diff --git a/src/game/dungeon-rooms.js b/src/game/dungeon-rooms.js deleted file mode 100644 index eece1f77..00000000 --- a/src/game/dungeon-rooms.js +++ /dev/null @@ -1,111 +0,0 @@ -import {shuffle, range} from './utils.js' - -/** - * @typedef {object} Room - * @prop {string} type - * @prop {string} [choice] - for campfire rooms, the choice made by the player - * @prop {object} [reward] - for campfire rooms, the reward given to the player - * @prop {Array} [monsters] - */ - -/** A map of room types to emojis */ -export const roomTypes = { - start: '👣', - M: '💀', - C: '🏕️', - // $: '💰' - Q: '❓', - E: '👹', - boss: '🌋', -} - -/** - * @returns {Room} - */ -export function StartRoom() { - return { - type: 'start', - } -} - -export const campfireRoomTypes = { - campfire: 'campfire', -} - -/** - * A campfire gives our hero the opportunity to rest, remove or upgrade a card. - * @returns {Room} - */ -export function CampfireRoom() { - return { - type: campfireRoomTypes.campfire, - // choices: ['rest', 'remove', 'upgrade'], - } -} - -/** - * A monster room has one or more monsters. - * @param {...Object} monsters - * @returns {Room} - */ -export function MonsterRoom(...monsters) { - return { - type: 'monster', - monsters, - } -} - -/** - * @typedef MONSTER - * @prop {number} [hp] - * @prop {number} [currentHealth] - * @prop {number} [maxHealth] - * @prop {number} [block] - * @prop {number} [random] - * @prop {Array} [intents] - * @prop {number} [nextIntent] - * @prop {Object} [powers] - */ - -// A monster has health, probably some damage and a list of intents. -// Use a list of intents to describe what the monster should do each turn. -// Supported intents: block, damage, vulnerable and weak. -// Intents are cycled through as the monster plays its turn. - -/** - * - * @param {MONSTER} props - * @returns {MONSTER} - */ -export function Monster( - props = { - currentHealth: 42, - maxHealth: 42, - intents: [], - }, -) { - // By setting props.random to a number, all damage intents will be randomized with this range. - let randomIntents - if (typeof props.random === 'number') { - randomIntents = props.intents.map((intent) => { - if (intent.damage) { - let newDamage = shuffle(range(5, intent.damage - props.random))[0] - intent.damage = newDamage - } - return intent - }) - } - - return { - currentHealth: props.hp || props.currentHealth, - maxHealth: props.hp || props.maxHealth, - block: props.block || 0, - powers: props.powers || {}, - // A list of "actions" the monster will take each turn. - // Example: [{damage: 6}, {block: 2}, {}, {weak: 2}] - // ... meaning turn 1, deal 6 damage, turn 2 gain 2 block, turn 3 do nothing, turn 4 apply 2 weak - intents: randomIntents || props.intents, - // A counter to keep track of which intent to run next. - nextIntent: 0, - } -} diff --git a/src/game/dungeon.js b/src/game/dungeon.js index aaa2017e..e25c4b62 100644 --- a/src/game/dungeon.js +++ b/src/game/dungeon.js @@ -1,48 +1,48 @@ /* eslint-disable complexity */ -import {uuid, shuffle, random as randomBetween, pick} from './utils.js' -import {StartRoom, CampfireRoom} from './dungeon-rooms.js' -import {easyMonsters, monsters, elites, bosses} from '../content/dungeon-encounters.js' +import {uuid, shuffle, random as randomBetween, pick} from '../utils.js' import {emojiFromNodeType} from '../ui/map.js' +import {easyMonsters, monsters, elites, bosses} from '../content/dungeon-encounters.js' +import {StartRoom, CampfireRoom} from './rooms.js' /** * A procedural generated dungeon map for Slay the Web. Again, heavily inspired by Slay the Spire. - * This is kind of complicated, so let me lay down the vocabulary: - - * A "room" could be a "start room", monster encounter, a campfire, a treasure chest, a shop, etc. - * A "node" is a single point on the map. It can contain a room, or just a filler node. - * A "floor" is a row (list) of nodes. - * A "graph" is a list of floors. - * A "move" is a list of two nodes, the "from" and the "to". - * A "path" is a list of moves from one node to another. - * A "dungeon" is a navigatable graph with a list of paths - * + * This is kind of complicated, so let me lay down the vocabulary, starting from the top. + * A "dungeon" wraps it all together. A navigatable graph with a list of paths. + * A "graph" is a list of floors. + * A "path" is a list of moves from one node to another, going from top to bottom. + * A "floor" is a row (list) of nodes. + * A "node" is a point on the map + * A "room" lives on a node, and could be a monster room, a a campfire etc. + * A "move" y/x coodinates */ -/** @typedef {import('./dungeon-rooms.js').Room} Room */ -/** @typedef {{id: string, type?: string, edges: Set, room?: object}} MapNode */ -/** @typedef {Array>} Graph */ -/** @typedef {{x: number, y: number}} Move */ +/** @typedef {import('./cards.js').CARD} Card */ +/** @typedef {import('./rooms.js').Room} Room */ /** - * @typedef {Object} GraphOptions - * @param {number} width how many nodes on each floor - * @param {number} height how many floors - * @param {number} minRoom minimum amount of rooms to generate per floor - * @param {number} maxRoom maximum amount of rooms to generate per floor - * @param {string} roomTypes a string like "MMCE". Repeat a letter to increase the chance of it appearing. M=Monster, C=Campfire, E=Elite. For example "MMMCE" gives 60% chance of a monster, 20% chance of a campfire and 20% chance of an elite. - * @param {string} customPaths a string of indexes (numbers) from where to draw the paths, for example "530" would draw three paths. First at index 5, then 3 and finally 0. + * @typedef {object} MapNode - is a single point on the map. It will either contain a room, or be a filler node. + * @prop {string} id + * @prop {MapNodeTypes} type + * @prop {Room} [room] + * @prop {Set} edges - a list of node ids that this node connects to + * @prop {boolean} didVisit - whether you have visited this node or not */ +/** @typedef {Array>} Graph is a list of floors with nodes*/ +/** @typedef {Array>} Path is a list of moves that describe a path from top to bottom */ +/** @typedef {{x: number, y: number}} Position on the map. Y is the floor. X is the node. */ +/** @typedef {Array} Move also position map, but stored differently */ /** - * @typedef {Object} Dungeon An instance of a dungeon - * @prop {string} id a unique id - * @prop {Graph} graph - * @prop {array} paths - * @prop {number} x current x position - * @prop {number} y current y position - * @prop {Array} pathTaken a list of moves we've taken + * @typedef {object} GraphOptions + * @prop {number} width how many nodes on each floor + * @prop {number} height how many floors + * @prop {number} [minRooms] minimum amount of rooms to generate per floor + * @prop {number} [maxRooms] maximum amount of rooms to generate per floor + * @prop {string} [roomTypes] a string like "MMCE". Repeat a letter to increase the chance of it appearing. M=Monster, C=Campfire, E=Elite. For example "MMMCE" gives 60% chance of a monster, 20% chance of a campfire and 20% chance of an elite. + * @prop {string} [customPaths] a string of indexes (numbers) from where to draw the paths, for example "530" would draw three paths. First at index 5, then 3 and finally 0. */ +/** @type {GraphOptions} */ const defaultOptions = { width: 10, height: 6, @@ -52,6 +52,16 @@ const defaultOptions = { // customPaths: '123' } +/** + * @typedef {object} Dungeon An instance of a dungeon + * @prop {string} id a unique id + * @prop {Graph} graph + * @prop {Array} paths + * @prop {number} x current x position + * @prop {number} y current y position + * @prop {Array} pathTaken a list of moves we've taken + */ + /** * Creates a new dungeon, complete with graph and paths. * @param {GraphOptions} [options] @@ -63,18 +73,6 @@ export default function Dungeon(options) { const graph = generateGraph(options) const paths = generatePaths(graph, options.customPaths) - // Add ".edges" to each node from the paths, so we know which connections it has. - // Would be cool if this was part of "generatePaths". - const nodeFromMove = ([floor, node]) => graph[floor][node] - paths.forEach((path) => { - path.forEach((move) => { - const a = nodeFromMove(move[0]) - const b = nodeFromMove(move[1]) - // a.edges.add(b) - a.edges.add(b.id) - }) - }) - // Add "room" to all valid node in the graph. graph.forEach((floor, floorNumber) => { floor.map((node) => { @@ -94,52 +92,11 @@ export default function Dungeon(options) { } } -/** - * The type of node is decided by the floor number and the room types. - * @param {string} nodeTypes - a string of possible node types - * @param {number} floor - * @returns {string} a single character representing the node type - */ -function decideNodeType(nodeTypes, floor) { - let types = nodeTypes - if (floor < 2) return 'M' - if (floor < 3) return pick('MC') - if (floor > 6) return pick('MMEEC') - return pick(types) -} - -/** - * Create a room from the node's type - * @param {string} type - * @param {number} floor - * @returns {Room} - */ -function decideRoomType(type, floor) { - const pickRandomFromObj = (obj) => obj[shuffle(Object.keys(obj))[0]] - if (floor === 0) return StartRoom() - if (type === 'C') return CampfireRoom() - if (type === 'M' && floor < 2) return pickRandomFromObj(easyMonsters) - if (type === 'M') return pickRandomFromObj(monsters) - if (type === 'E') return pickRandomFromObj(elites) - if (type === 'boss') return pickRandomFromObj(bosses) - throw new Error(`Could not match node type "${type}" with a dungeon room`) -} - -/** - * A node in the dungeon map graph - * @param {string} [nodeType] - a string key to represent the type of room - * @returns {MapNode} - */ -function createMapNode(nodeType) { - return {id: uuid(), type: nodeType, edges: new Set(), room: undefined} -} - /** * Returns a "graph" array representation of the map we want to render. * Each nested array represents a floor with nodes. * All nodes have a type. * Nodes with type of `false` are filler nodes nededed for the layout. - ``` graph = [ [startNode] @@ -152,8 +109,8 @@ function createMapNode(nodeType) { * @param {GraphOptions} [options] * @returns {Graph} */ -export function generateGraph(options = {}) { - options = Object.assign(defaultOptions, options) +export function generateGraph(options) { + options = Object.assign(defaultOptions, options || {}) const {width, height, minRooms, maxRooms, roomTypes} = options const graph = [] @@ -214,38 +171,40 @@ export function generatePaths(graph, customPaths) { } /** - * @typedef {Array>>} Path + * Ensures it's not a filler node + * @param {MapNode} node + * @returns {boolean} */ +function validNode(node) { + return node && Boolean(node.type) +} /** * Finds a path from start to finish in the graph. -* @param {Graph} graph -* @param {number} preferredIndex the column you'd like the path to follow where possible -* @param {boolean} debug if true, logs to console -* @returns {Path} an array of moves. Each move contains the Y/X coords of the graph. - - Starting node connects to all nodes on the first floor - End node connects to all nodes on the last floor - - It is Y/X, not X/Y (think floor/room, not room/floor) - - [ - [[0, 0], [1,4]], <-- first move is from 0,0 to 1,4 - [[1, 4], [2,1]] <-- second move is from 1,4 to 2,1 - ] + * Starting node connects to all nodes on the first floor + * End node connects to all nodes on the last floor + * It is Y/X, not X/Y (think floor/room, not room/floor) + * [ + * [[0, 0], [1,4]], <-- first move is from 0,0 to 1,4 + * [[1, 4], [2,1]] <-- second move is from 1,4 to 2,1 + *] + * @param {Graph} graph + * @param {number} preferredIndex the column you'd like the path to follow where possible + * @param {boolean} debug if true, logs to console + * @returns {Path} an array of moves. Each move contains the Y/X coords of the graph. */ -export function findPath(graph, preferredIndex, debug = false) { - let path = [] - let lastVisited +function findPath(graph, preferredIndex, debug = false) { if (debug) console.groupCollapsed('finding path', preferredIndex) - // Look for a free node in the next floor to the right of the "desired index". - const validNode = (node) => node && Boolean(node.type) + let path = [] + /** @type {MapNode|false} */ + let lastVisited = false // Walk through each floor. for (let [floorIndex, floor] of graph.entries()) { if (debug) console.group(`floor ${floorIndex}`) - // If on last floor, stop drawing. + + // If on last floor, stop moving. const nextFloor = graph[floorIndex + 1] if (!nextFloor) { if (debug) console.log('no next floor, stopping') @@ -253,62 +212,61 @@ export function findPath(graph, preferredIndex, debug = false) { break } - let aIndex = preferredIndex - let bIndex = preferredIndex - - // Find the node we came FROM. - let a = lastVisited - const newAIndex = floor.indexOf(a) - if (a) { - if (debug) console.log('changing a index to', newAIndex) - aIndex = newAIndex - } else { - // Or start from 0,0 - if (debug) console.log('forcing "from" to first node in floor') - a = floor[0] - aIndex = 0 + // Find the "a" node we came FROM. + const aIndex = lastVisited ? floor.indexOf(lastVisited) : 0 + const moveFrom = [floorIndex, aIndex] + if (debug) console.log('setting from', moveFrom) + + // Find the "b" node we are going TO. + const bInfo = + searchValidNode(nextFloor, preferredIndex, 'forward') || + searchValidNode(nextFloor, preferredIndex, 'backward') + if (!bInfo) throw Error('failed to find node to move to') + const moveTo = [floorIndex + 1, bInfo.index] + // Store it for later + lastVisited = bInfo.node + + const move = [moveFrom, moveTo] + path.push(move) + + if (debug) { + console.log(`added move to path ${moveFrom} to ${moveTo}`) + console.groupEnd() } - if (!a) throw Error('missing from') - - // Find the node we are going TO. - // Search to the right of our index. - let b - for (let i = bIndex; i < nextFloor.length; i++) { - if (debug) console.log('searching forwards', i) - let node = nextFloor[i] + } + + storePathOnGraph(graph, path) + + if (debug) console.groupEnd() + + return path +} + +/** + * Searches for the first valid node in a direction + * @param {Array} floor + * @param {number} startX - the index to start search from + * @param {string} direction must be "forward" or "backward" + * @returns {{node: MapNode, index: number}|null} + */ +function searchValidNode(floor, startX, direction) { + const step = direction === 'forward' ? 1 : -1 + if (direction === 'forward') { + for (let i = startX; i >= 0 && i < floor.length; i += step) { + let node = floor[i] if (validNode(node)) { - if (debug) console.log('choosing', i) - b = node - bIndex = i - break + return {node, index: i} } } - // No result? Search to the left instead. - if (!b) { - for (let i = bIndex; i >= 0; i--) { - if (debug) console.log('searching backwards', i) - let node = nextFloor[i] - if (validNode(node)) { - if (debug) console.log('choosing', i) - b = node - bIndex = i - break - } + } else { + for (let i = startX; i >= 0; i += step) { + let node = floor[i] + if (validNode(node)) { + return {node, index: i} } - if (!b) throw Error('missing to') } - lastVisited = b - - if (debug) console.log(`connected floor ${floorIndex}:${aIndex} to ${floorIndex + 1}:${bIndex}`) - const moveFrom = [floorIndex, aIndex] - const moveTo = [floorIndex + 1, bIndex] - path.push([moveFrom, moveTo]) - if (debug) console.groupEnd() } - - if (debug) console.groupEnd() - - return path + return null } /** @@ -325,3 +283,83 @@ export function graphToString(graph) { const str = textGraph.map((floor) => floor.join('')).join('\n') return str } + +/** + * Stores a path directly on a graph + * @param {Graph} graph + * @param {Path} path + * @returns {Graph} + */ +function storePathOnGraph(graph, path) { + path.forEach((move) => { + const a = nodeFromMove(graph, move[0]) + const b = nodeFromMove(graph, move[1]) + a.edges.add(b.id) + }) + return graph +} + +/** + * @param {Graph} graph + * @param {Move} move + * @returns {MapNode} + */ +function nodeFromMove(graph, [floor, node]) { + return graph[floor][node] +} + +/** @enum {string} different type of nodes and their emoji equivalents */ +export const MapNodeTypes = { + start: '👣', + M: '💀', + C: '🏕️', + // $: '💰' + Q: '❓', + E: '👹', + boss: '🌋', +} + +/** + * A node in the dungeon map graph + * @param {MapNodeTypes} [type] - a string key to represent the type of room + * @returns {MapNode} + */ +function createMapNode(type) { + return { + id: uuid(), + type: type, + room: undefined, + edges: new Set(), + didVisit: false, + } +} + +/** + * The type of node is decided by the floor number and the room types. + * @param {MapNodeTypes} nodeTypes - a string of possible node types + * @param {number} [floor] - useful for balance e.g. more monsters on higher floors + * @returns {string} a single character representing the node type + */ +function decideNodeType(nodeTypes, floor) { + if (floor < 2) return 'M' + if (floor < 3) return pick('MC') + if (floor > 6) return pick('MMEEC') + return pick(nodeTypes) +} + +/** + * Create a room from the node's type + * @param {string} type + * @param {number} floor + * @returns {Room} + */ +export function decideRoomType(type, floor) { + const pickRandomFromObj = (obj) => obj[shuffle(Object.keys(obj))[0]] + if (floor === 0) return StartRoom() + if (type === 'C') return CampfireRoom() + if (type === 'M' && floor < 2) return pickRandomFromObj(easyMonsters) + if (type === 'M') return pickRandomFromObj(monsters) + if (type === 'E') return pickRandomFromObj(elites) + if (type === 'boss') return pickRandomFromObj(bosses) + throw new Error(`Could not match node type "${type}" with a dungeon room`) +} diff --git a/src/game/monster.js b/src/game/monster.js new file mode 100644 index 00000000..f1bae99a --- /dev/null +++ b/src/game/monster.js @@ -0,0 +1,54 @@ +import {shuffle, range} from '../utils.js' + +/** + * @typedef MONSTER + * @prop {number} [hp] + * @prop {number} [currentHealth] + * @prop {number} [maxHealth] + * @prop {number} [block] + * @prop {number} [random] + * @prop {Array} [intents] + * @prop {number} [nextIntent] + * @prop {object} [powers] + */ + +/** + * A monster has health, probably some damage and a list of intents. + Use a list of intents to describe what the monster should do each turn. + Supported intents: block, damage, vulnerable and weak. + Intents are cycled through as the monster plays its turn. + * @param {MONSTER} props + * @returns {MONSTER} + */ +export function Monster( + props = { + currentHealth: 42, + maxHealth: 42, + intents: [], + }, +) { + // By setting props.random to a number, all damage intents will be randomized with this range. + let randomIntents + if (typeof props.random === 'number') { + randomIntents = props.intents.map((intent) => { + if (intent.damage) { + let newDamage = shuffle(range(5, intent.damage - props.random))[0] + intent.damage = newDamage + } + return intent + }) + } + + return { + currentHealth: props.hp || props.currentHealth, + maxHealth: props.hp || props.maxHealth, + block: props.block || 0, + powers: props.powers || {}, + // A list of "actions" the monster will take each turn. + // Example: [{damage: 6}, {block: 2}, {}, {weak: 2}] + // ... meaning turn 1, deal 6 damage, turn 2 gain 2 block, turn 3 do nothing, turn 4 apply 2 weak + intents: randomIntents || props.intents, + // A counter to keep track of which intent to run next. + nextIntent: 0, + } +} diff --git a/src/game/new-game.js b/src/game/new-game.js index 684ecf48..0d471d98 100644 --- a/src/game/new-game.js +++ b/src/game/new-game.js @@ -1,15 +1,17 @@ import actions from './actions.js' import ActionManager from './action-manager.js' +/** @typedef {import('./actions.js').State} State */ + /** - * @typedef {Object} Game - * @prop {import('./actions.js').State} state + * @typedef {object} Game + * @prop {State} state * @prop {object} actions * @prop {Function} enqueue - stores an action in the "future" * @prop {Function} dequeue - runs the oldest "future" action, and stores result in the "past" * @prop {Function} undo - undoes the last "past" action * @prop {{list: Array<{type: string}>}} future - * @prop {{list: Array<{type: string, state: import('./actions.js').State}>}} past + * @prop {{list: Array<{type: string, state: State}>}} past */ /** @@ -20,7 +22,9 @@ import ActionManager from './action-manager.js' export default function createNewGame(debug = false) { const actionManager = ActionManager({debug}) - // Adds a dungeon, starter deck and draws cards. + /** + * @returns {State} with a dungeon, start deck and cards drawn + */ function createNewState() { let state = actions.createNewState() state = actions.setDungeon(state) diff --git a/src/game/powers.js b/src/game/powers.js index b43fb1c6..6b98029e 100644 --- a/src/game/powers.js +++ b/src/game/powers.js @@ -1,11 +1,11 @@ /** * The type of a power * @typedef POWER - * @property {string} name - * @property {string} description - * @property {string} type - * @property {string=} target - * @property {Function=} use + * @prop {string} name + * @prop {string} description + * @prop {string} type + * @prop {string=} target + * @prop {Function=} use */ // Class representing a power. diff --git a/src/game/queue.js b/src/game/queue.js deleted file mode 100644 index 793773b2..00000000 --- a/src/game/queue.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * A queue is a list of objects that are inserted and removed first-in-first-out (FIFO). - */ -export default class Queue { - constructor(items = []) { - this.list = items - } - /** Enqueue and add an item at the end of the queue. */ - enqueue(item) { - this.list.push(item) - } - /** Dequeue and remove an item at the front of the queue. */ - dequeue() { - return this.list.shift() - } -} diff --git a/src/game/rooms.js b/src/game/rooms.js new file mode 100644 index 00000000..84e2c688 --- /dev/null +++ b/src/game/rooms.js @@ -0,0 +1,51 @@ +/** @typedef {import('./monster.js').MONSTER} MONSTER */ + +/** + * @typedef {object} Room - a room in the dungeon + * @prop {RoomTypes} type - the type of room + * @prop {Array} [monsters] - for monster rooms + * @prop {object} [reward] - the reward given to the player, if any + * @prop {string} [choice] - for campfire rooms, the choice made by the player + */ + +/** @enum {string} different type of rooms */ +export const RoomTypes = { + start: 'start', + campfire: 'campfire', + monster: 'monster', + elite: 'elite', + boss: 'boss', +} + +/** + * This is usually where you start. The first node on the map. + * @returns {Room} the starting room + */ +export function StartRoom() { + return { + type: 'start', + } +} + +/** + * A campfire gives our hero the opportunity to rest, remove or upgrade a card. + * @typedef {{type: RoomTypes, choice?: string}} CampfireRoom + * @returns {CampfireRoom} + */ +export function CampfireRoom() { + return { + type: 'campfire', + // choices: ['rest', 'remove', 'upgrade'], + } +} + +/** + * @param {...MONSTER} monsters - pass it one or more monsters (as multiple arguments, not an array) + * @returns {Room} + */ +export function MonsterRoom(...monsters) { + return { + type: 'monster', + monsters, + } +} diff --git a/src/game/utils-state.js b/src/game/utils-state.js index 892f832a..f4495425 100644 --- a/src/game/utils-state.js +++ b/src/game/utils-state.js @@ -1,11 +1,18 @@ import {CardTargets} from './cards.js' +/** @typedef {import('./actions.js').State} State */ +/** @typedef {import('./cards.js').CARD} CARD */ +/** @typedef {import('./dungeon.js').Dungeon} Dungeon */ +/** @typedef {import('./dungeon.js').MapNode} MapNode */ +/** @typedef {import('./rooms.js').Room} Room */ +/** @typedef {import('./monster.js').MONSTER} MONSTER */ + /** * A bunch of utilities specific to the game state object. */ /** - * @param {import('./actions.js').State} state + * @param {State} state * @returns {number} The percentage of player health remaining. */ export function getPlayerHealthPercentage(state) { @@ -14,8 +21,8 @@ export function getPlayerHealthPercentage(state) { /** * Returns the node you are currently on. - * @param {import('./dungeon.js').Dungeon} dungeon - * @returns {import('./dungeon.js').MapNode} + * @param {Dungeon} dungeon + * @returns {MapNode} */ export function getCurrentNode(dungeon) { return dungeon.graph[dungeon.y][dungeon.x] @@ -23,8 +30,8 @@ export function getCurrentNode(dungeon) { /** * Returns the current dungeon room from the the y/x props - * @param {import('./actions.js').State} state - * @returns {import('./dungeon-rooms.js').Room} + * @param {State} state + * @returns {Room} */ export function getCurrRoom(state) { const node = getCurrentNode(state.dungeon) @@ -34,9 +41,9 @@ export function getCurrRoom(state) { /** * Returns an array of targets (player or monsters) in the current room. - * @param {import('./actions.js').State} state + * @param {State} state * @param {CardTargets} targetQuery - * @returns {Array} + * @returns {Array} */ export function getTargets(state, targetQuery) { if (!targetQuery || typeof targetQuery !== 'string') { @@ -68,7 +75,7 @@ export function cardHasValidTarget(cardTarget, targetQuery) { } /** - * @param {import('./dungeon.js').Room} room + * @param {Room} room * @returns {boolean} true if the room has been cleared. */ export function isRoomCompleted(room) { @@ -85,7 +92,8 @@ export function isRoomCompleted(room) { /** * Check if the current room in a game has been cleared. - * @param {import('./actions.js').State} state + * @param {State} state + * @returns {boolean} */ export function isCurrRoomCompleted(state) { const room = getCurrRoom(state) @@ -95,7 +103,7 @@ export function isCurrRoomCompleted(state) { /** * Checks if the whole dungeon (all rooms) has been cleared. * As long as there is one cleared node per floor, we are good. - * @param {import('./actions.js').State} state + * @param {State} state * @returns {boolean} */ export function isDungeonCompleted(state) { diff --git a/src/ui/animations.js b/src/ui/animations.js index 09489fed..04fecd24 100644 --- a/src/ui/animations.js +++ b/src/ui/animations.js @@ -1,5 +1,6 @@ import gsap from 'gsap' import {Draggable} from 'gsap/Draggable.js' +// @ts-ignore import Flip from 'https://slaytheweb-assets.netlify.app/gsap/Flip.js' // This file contains some resuable animations/effects. diff --git a/src/ui/game-screen.js b/src/ui/game-screen.js index bda51920..0c006a11 100644 --- a/src/ui/game-screen.js +++ b/src/ui/game-screen.js @@ -390,6 +390,12 @@ stw.dealCards()`) } } +/** + * Renders a form to submit the game to the backend. + * @param {object} props + * @param {import('../game/new-game.js').Game} props.game + * @returns {import('preact').VNode} + */ function PublishRun({game}) { const [loading, setLoading] = useState(false) diff --git a/src/ui/map.js b/src/ui/map.js index 2a3025f6..96de17d9 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1,7 +1,8 @@ +// @ts-nocheck import {Component, html} from './lib.js' -import {random as randomBetween} from '../game/utils.js' +import {random as randomBetween} from '../utils.js' import {isRoomCompleted} from '../game/utils-state.js' -import {roomTypes} from '../game/dungeon-rooms.js' +import {MapNodeTypes} from '../game/dungeon.js' export default function map(props) { const {x, y, pathTaken} = props.dungeon @@ -23,8 +24,8 @@ export default function map(props) { /** * Renders a map of the dungeon. - * @param {Object} props - * @param {Object} props.dungeon + * @param {object} props + * @param {object} props.dungeon * @param {number} props.x * @param {number} props.y * @param {Function} props.onSelect @@ -176,7 +177,7 @@ export class SlayMap extends Component { */ export function emojiFromNodeType(type) { if (!type) return ' ' - return roomTypes[type] + return MapNodeTypes[type] } // Since el.offsetLeft doesn't respect CSS transforms, diff --git a/src/ui/menu.js b/src/ui/menu.js index b24b99ef..2524f7f9 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -2,8 +2,20 @@ import {html} from './lib.js' import History from './history.js' import {saveToUrl} from './save-load.js' -const abandonGame = () => (window.location = window.location.origin) +// @ts-ignore +const abandonGame = () => (window.location.href = window.location.origin) +/** @typedef {import('../game/new-game.js').Game} Game */ +/** @typedef {import('../game/actions.js').State} State */ + +/** + * Do something + * @param {object} props + * @param {Game} props.game + * @param {State} props.gameState + * @param {Function} props.onUndo + * @returns {import('preact').VNode} + */ export default function Menu({game, gameState, onUndo}) { return html`
diff --git a/src/ui/player.js b/src/ui/player.js index 963cd2a5..eea23e81 100644 --- a/src/ui/player.js +++ b/src/ui/player.js @@ -8,7 +8,7 @@ import { } from '../game/powers.js' /** - * @typedef {Object} TargetProps + * @typedef {object} TargetProps * @prop {string} type - a string enum "player" or "enemy"? * @prop {object} model - the player or monster model * @prop {number} model.currentHealth diff --git a/src/ui/sounds.js b/src/ui/sounds.js index d480fc44..ed090405 100644 --- a/src/ui/sounds.js +++ b/src/ui/sounds.js @@ -1,3 +1,4 @@ +// @ts-nocheck import * as Tone from 'tone' // Create synths and connect it to the main output (your speakers). diff --git a/src/ui/start-room.js b/src/ui/start-room.js index d2f1ade3..df852248 100644 --- a/src/ui/start-room.js +++ b/src/ui/start-room.js @@ -8,7 +8,7 @@ export default class StartRoom extends Component {
  • - +

    ` } diff --git a/src/game/utils.js b/src/utils.js similarity index 52% rename from src/game/utils.js rename to src/utils.js index d9a44bbf..b535de2f 100644 --- a/src/game/utils.js +++ b/src/utils.js @@ -1,7 +1,9 @@ // A collection of utility functions. -// None are allowed to modify the game state. -// Returns a random-looking string for ids. +/** + * Creates a random-looking string for ids. + * @returns {string} + */ export function uuid(a) { return a ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) @@ -9,8 +11,12 @@ export function uuid(a) { ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid) } -// Returns a new, shuffled version of an array. -// See https://bost.ocks.org/mike/shuffle/ +/** + * Returns a new, shuffled version of an array. + * See https://bost.ocks.org/mike/shuffle/ + * @param {Array} array + * @returns {Array} + */ export function shuffle(array) { // Make a copy array = array.slice() @@ -32,13 +38,23 @@ export function shuffle(array) { return array } -// Returns a range of numbers. Example: range(3) === [1,2,3] or range(3, 6) === [6,7,8] -// range(3, 2) = [2,3,4] +/** + * Returns a range of numbers. + * Example: range(3) === [1,2,3] or range(3, 6) === [6,7,8] + * range(3, 2) = [2,3,4] + * @param {*} size + * @param {*} startAt + * @returns {Array} + */ export function range(size, startAt = 0) { return [...Array(size).keys()].map((i) => i + startAt) } -// Get a random number within a range +/** + * @param {number} from + * @param {number} to + * @returns {number} a random number within the range + */ export function random(from, to) { const r = range(1 + to - from, from) // random(2,4) = range(3,2) if (from === to) return from // e.g. 5-5 returns 5 instead of 0 @@ -50,10 +66,26 @@ export function clamp(x, lower, upper) { } /** - * Returns a random item from an array. * @param {Array|string} list - * @returns {any} a random item from the array + * @returns {any} random item from the list */ export function pick(list) { return shuffle(Array.from(list))[0] } + +/** + * A queue is a list of objects that are inserted and removed first-in-first-out (FIFO). + */ +export default class Queue { + constructor(items = []) { + this.list = items + } + /** Enqueue and add an item at the end of the queue. */ + enqueue(item) { + this.list.push(item) + } + /** Dequeue and remove an item at the front of the queue. */ + dequeue() { + return this.list.shift() + } +} diff --git a/tests/actions.js b/tests/actions.js index 566d4734..cc49a757 100644 --- a/tests/actions.js +++ b/tests/actions.js @@ -2,7 +2,8 @@ import test from 'ava' import actions from '../src/game/actions.js' import {createCard, CardTargets} from '../src/game/cards.js' -import {MonsterRoom, Monster} from '../src/game/dungeon-rooms.js' +import {MonsterRoom} from '../src/game/rooms.js' +import {Monster} from '../src/game/monster.js' import {createTestDungeon} from '../src/content/dungeon-encounters.js' import {getTargets, getCurrRoom, isCurrRoomCompleted} from '../src/game/utils-state.js' import {canPlay} from '../src/game/conditions.js' diff --git a/tests/ai.js b/tests/ai.js index e2b456bd..34b89288 100644 --- a/tests/ai.js +++ b/tests/ai.js @@ -2,7 +2,8 @@ import test from 'ava' import actions from '../src/game/actions.js' import Dungeon from '../src/game/dungeon.js' -import {MonsterRoom, Monster} from '../src/game/dungeon-rooms.js' +import {MonsterRoom} from '../src/game/rooms.js' +import {Monster} from '../src/game/monster.js' const a = actions diff --git a/tests/dungeon-graph.js b/tests/dungeon-graph.js index 5da249b6..83fe768a 100644 --- a/tests/dungeon-graph.js +++ b/tests/dungeon-graph.js @@ -1,6 +1,6 @@ import test from 'ava' import {generateGraph, generatePaths, graphToString} from '../src/game/dungeon.js' -import {roomTypes} from '../src/game/dungeon-rooms.js' +import {MapNodeTypes} from '../src/game/dungeon.js' test('graph is created with default options', (t) => { let g = generateGraph() @@ -70,9 +70,9 @@ test('string graph works', (t) => { const str = graphToString(g) t.is(typeof str, 'string') - t.true(str.includes(roomTypes.M)) - t.true(str.includes(roomTypes.start)) - t.true(str.includes(roomTypes.boss)) + t.true(str.includes(MapNodeTypes.M)) + t.true(str.includes(MapNodeTypes.start)) + t.true(str.includes(MapNodeTypes.boss)) // console.log(str) }) diff --git a/tests/dungeon.js b/tests/dungeon.js index 640e42d2..17f3efec 100644 --- a/tests/dungeon.js +++ b/tests/dungeon.js @@ -3,7 +3,8 @@ import test from 'ava' import {createTestDungeon} from '../src/content/dungeon-encounters.js' import actions from '../src/game/actions.js' import Dungeon from '../src/game/dungeon.js' -import {MonsterRoom, Monster} from '../src/game/dungeon-rooms.js' +import {MonsterRoom} from '../src/game/rooms.js' +import {Monster} from '../src/game/monster.js' import {getCurrRoom, isCurrRoomCompleted, isDungeonCompleted} from '../src/game/utils-state.js' const a = actions diff --git a/tests/queue.js b/tests/queue.js index 3dcc9129..5ee374fe 100644 --- a/tests/queue.js +++ b/tests/queue.js @@ -1,5 +1,5 @@ import test from 'ava' -import Queue from '../src/game/queue.js' +import Queue from '../src/utils.js' test('can add and run through queue', (t) => { const q = new Queue()