From: Rubén Beltrán del Río Date: Tue, 29 May 2018 07:34:38 +0000 (-0500) Subject: Add Naive Game Rules (#11) X-Git-Url: https://git.r.bdr.sh/rbdr/sumo/commitdiff_plain/3100e0533cb89a185ea021dfb83c4f364750180f?hp=-c Add Naive Game Rules (#11) * Add components for tracking points * Add methods to create new entities * Adds nodes for points * Add system to detect points * Adds system to detect a winner * Add system to render points and winner * Add points required to win to config * Add it all to the engine * Add mention of points to changelog --- 3100e0533cb89a185ea021dfb83c4f364750180f diff --git a/CHANGELOG.md b/CHANGELOG.md index c64a8fe..f3b119f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,5 +20,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Keyboard Support - Dash & grab mechanics - Add support for Gamepads (Tested using PS4 only) +- Add points keeping system [Unreleased]: https://github.com/rbdr/sumo/compare/master...develop diff --git a/lib/components/points.js b/lib/components/points.js new file mode 100644 index 0000000..02b7e44 --- /dev/null +++ b/lib/components/points.js @@ -0,0 +1,11 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores points in arbitrary labels + * + * @extends {external:Serpentity.Component} + * @class PointsComponent + * @param {object} config a configuration object to extend. + */ +export default class PointsComponent extends Component {}; + diff --git a/lib/components/points_collider.js b/lib/components/points_collider.js new file mode 100644 index 0000000..ceec7ee --- /dev/null +++ b/lib/components/points_collider.js @@ -0,0 +1,35 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores a collision target and points accumulation + * target + * + * @extends {external:Serpentity.Component} + * @class PointsColliderComponent + * @param {object} config a configuration object to extend. + */ +export default class PointsColliderComponent extends Component { + constructor(config) { + + super(config); + + /** + * The target entity that will generate points if it collides + * + * @property {external:Serpentity.Entity} collisionTarget + * @instance + * @memberof PointsColliderComponent + */ + this.collisionTarget = this.collisionTarget || null; + + /** + * The target entity that will generate points if it collides + * + * @property {string} pointsTarget + * @instance + * @memberof PointsColliderComponent + */ + this.pointsTarget = this.pointsTarget || 'nobody'; + } +}; + diff --git a/lib/components/winner.js b/lib/components/winner.js new file mode 100644 index 0000000..74de1c8 --- /dev/null +++ b/lib/components/winner.js @@ -0,0 +1,26 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores a winner + * + * @extends {external:Serpentity.Component} + * @class WinnerComponent + * @param {object} config a configuration object to extend. + */ +export default class WinnerComponent extends Component { + constructor(config) { + + super(config); + + /** + * The properthy that holds the winner + * + * @property {string} winner + * @instance + * @memberof WionnerComponent + */ + this.winner = this.winner || null; + } +}; + + diff --git a/lib/config.js b/lib/config.js index 856fc18..c39fedf 100644 --- a/lib/config.js +++ b/lib/config.js @@ -28,14 +28,22 @@ const config = { * @property {number} verticalResolution * @memberof Config */ - verticalResolution: 224 + verticalResolution: 224, + + /** + * The points needed to win + * + * @property {number} maxPoints + * @memberof Config + */ + maxPoints: 200 }; /** - * How many pixels to use per meter + * The horizontal resolution of the screen * - * @property {number} meterSize + * @property {number} horizontalResolution * @memberof Config */ config.horizontalResolution = Math.round(config.verticalResolution * config.aspectRatio[0] / config.aspectRatio[1]); diff --git a/lib/factories/sumo.js b/lib/factories/sumo.js index 18e9d72..009ba8c 100644 --- a/lib/factories/sumo.js +++ b/lib/factories/sumo.js @@ -14,8 +14,11 @@ import GrabAreaComponent from '../components/grab_area'; import GrabbableComponent from '../components/grabbable'; import GrabComponent from '../components/grab'; import MaxVelocityComponent from '../components/max_velocity'; +import PointsColliderComponent from '../components/points_collider'; +import PointsComponent from '../components/points'; import PositionComponent from '@serpentity/components.position'; import PixiContainerComponent from '../components/pixi_container'; +import WinnerComponent from '../components/winner'; import PixiFactory from '../factories/pixi'; import Config from '../config'; @@ -620,14 +623,17 @@ export default { const friction = 0; const frictionStatic = 0; const restitution = 1; + const label = config.label || 'Invisible Block'; + const isSensor = !!(config.isSensor); const body = Bodies.rectangle(position.x / Config.meterSize, position.y / Config.meterSize, config.width / Config.meterSize, config.height / Config.meterSize, { + isSensor, isStatic: true, - label: 'Invisible Block', + label, friction, restitution, frictionStatic @@ -638,6 +644,61 @@ export default { engine.addEntity(entity); } + return entity; + }, + + /** + * Creates an invisible block that accumulates points if certain + * entity collids with it + * + * @function createPointsCollider + * @memberof SumoFactory + * @param {external:Serpentity} [engine] the serpentity engine to attach + * to. If not sent, it will not be attached. + * @param {object} [config] the config to override the entity, accepts + * the key `position` as an object with an x and y property. + * @return {external:Serpentity.Entity} the created entity + */ + createPointsCollider(engine, config = {}) { + + const entity = this.createInvisibleBlock(null, Object.assign({ + isSensor: true, + label: 'Points Detector' + }, config)); + + // Points Collider + + entity.addComponent(new PointsColliderComponent(config)); + + if (engine) { + engine.addEntity(entity); + } + + return entity; + }, + + /** + * Creates an entity representing the game state + * + * @function createGameState + * @memberof SumoFactory + * @param {external:Serpentity} [engine] the serpentity engine to attach + * to. If not sent, it will not be attached. + * @param {object} [config] the config to override the entity, accepts + * the key `position` as an object with an x and y property. + * @return {external:Serpentity.Entity} the created entity + */ + createGameState(engine, config = {}) { + + const entity = new Entity(); + + entity.addComponent(new PointsComponent(config)); + entity.addComponent(new WinnerComponent(config)); + + if (engine) { + engine.addEntity(entity); + } + return entity; } }; diff --git a/lib/nodes/points.js b/lib/nodes/points.js new file mode 100644 index 0000000..40404b4 --- /dev/null +++ b/lib/nodes/points.js @@ -0,0 +1,25 @@ +import { Node } from '@serpentity/serpentity'; + +import PointsComponent from '../components/points'; + +/** + * Node identifying an entity that stores points + * + * @extends {external:Serpentity.Node} + * @class PointsNode + */ +export default class PointsNode extends Node { + +}; + +/** + * Holds the types that are used to identify a points keeping entity + * + * @property {object} types + * @name types + * @memberof PointsNode + */ +PointsNode.types = { + points: PointsComponent +}; + diff --git a/lib/nodes/points_collider.js b/lib/nodes/points_collider.js new file mode 100644 index 0000000..4b8fe92 --- /dev/null +++ b/lib/nodes/points_collider.js @@ -0,0 +1,29 @@ +import { Node } from '@serpentity/serpentity'; + +import BodyComponent from '../components/body'; +import PointsColliderComponent from '../components/points_collider'; + +/** + * Node identifying an entity that can identify a collision and give out + * points + * + * @extends {external:Serpentity.Node} + * @class PointsColliderNode + */ +export default class PointsColliderNode extends Node { + +}; + +/** + * Holds the types that are used to identify an entity that can give out + * points on collision + * + * @property {object} types + * @name types + * @memberof PhysicalWithAttributesNode + */ +PointsColliderNode.types = { + body: BodyComponent, + pointsCollider: PointsColliderComponent +}; + diff --git a/lib/nodes/winner.js b/lib/nodes/winner.js new file mode 100644 index 0000000..8c5a448 --- /dev/null +++ b/lib/nodes/winner.js @@ -0,0 +1,24 @@ +import { Node } from '@serpentity/serpentity'; + +import WinnerComponent from '../components/winner'; + +/** + * Node identifying an entity that stores winner + * + * @extends {external:Serpentity.Node} + * @class WinnerNode + */ +export default class WinnerNode extends Node { + +}; + +/** + * Holds the types that are used to identify a winner keeping entity + * + * @property {object} types + * @name types + * @memberof WinnerNode + */ +WinnerNode.types = { + winner: WinnerComponent +}; diff --git a/lib/sumo.js b/lib/sumo.js index cdf8cc6..835e910 100644 --- a/lib/sumo.js +++ b/lib/sumo.js @@ -8,13 +8,17 @@ import ApplyForceSystem from './systems/apply_force'; import CreateCouplingLineSystem from './systems/create_coupling_line'; import ControlMapperSystem from './systems/control_mapper'; import DashSystem from './systems/dash'; +import DetectPointsCollisionSystem from './systems/detect_points_collision'; +import DetectWinnerSystem from './systems/detect_winner'; import DrawDashSystem from './systems/draw_dash'; import DrawGrabSystem from './systems/draw_grab'; import ElasticSystem from './systems/elastic'; import GrabSystem from './systems/grab'; import PhysicsWorldControlSystem from './systems/physics_world_control'; import PhysicsToAttributesSystem from './systems/physics_to_attributes'; +import RenderPointsSystem from './systems/render_points'; import RenderSystem from './systems/render'; +import RenderWinnerSystem from './systems/render_winner'; import AttributesToRenderableSystem from './systems/attributes_to_renderable'; // Factories @@ -209,6 +213,18 @@ internals.Sumo = class Sumo { engine: this._matterJs })); + this._engine.addSystem(new DetectPointsCollisionSystem()); + + this._engine.addSystem(new DetectWinnerSystem()); + + this._engine.addSystem(new RenderPointsSystem({ + application: this._pixi + })); + + this._engine.addSystem(new RenderWinnerSystem({ + application: this._pixi + })); + this._engine.addSystem(new ElasticSystem()); this._engine.addSystem(new PhysicsToAttributesSystem()); @@ -268,6 +284,8 @@ internals.Sumo = class Sumo { entityB: harness }); + // Walls + SumoFactory.createInvisibleBlock(this._engine, { width: this.horizontalResolution * 2, height: this.verticalResolution * 0.1, @@ -286,6 +304,33 @@ internals.Sumo = class Sumo { } }); + // Points Detector + + SumoFactory.createPointsCollider(this._engine, { + collisionTarget: sumoA, + pointsTarget: 'red', + height: this.verticalResolution, + width: this.horizontalResolution, + position: { + x: this.horizontalResolution + this.horizontalResolution / 2, + y: this.verticalResolution / 2 + } + }); + + SumoFactory.createPointsCollider(this._engine, { + collisionTarget: sumoB, + pointsTarget: 'blue', + height: this.verticalResolution, + width: this.horizontalResolution, + position: { + x: -this.horizontalResolution / 2, + y: this.verticalResolution / 2 + } + }); + + // The game state + SumoFactory.createGameState(this._engine); + // To keep the coupling behind, we'll manually add the sumos later this._engine.addEntity(sumoA); diff --git a/lib/systems/detect_points_collision.js b/lib/systems/detect_points_collision.js new file mode 100644 index 0000000..c84f3fc --- /dev/null +++ b/lib/systems/detect_points_collision.js @@ -0,0 +1,100 @@ +import { System } from '@serpentity/serpentity'; +import { SAT } from 'matter-js'; + +import BodyComponent from '../components/body'; +import PointsColliderNode from '../nodes/points_collider'; +import PointsNode from '../nodes/points'; + +/** + * Handles grabbing between entities + * + * @extends {external:Serpentity.System} + * @class DetectPointsCollisionSystem + * @param {object} config a configuration object to extend. + */ +export default class DetectPointsCollisionSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The collection of points colliders + * + * @property {external:Serpentity.NodeCollection} pointsColliders + * @instance + * @memberof DetectPointsCollisionSystem + */ + this.pointsColliders = null; + + /** + * The collection of points keepers + * + * @property {external:Serpentity.NodeCollection} pointsKeepers + * @instance + * @memberof DetectPointsCollisionSystem + */ + this.pointsKeepers = null; + } + + /** + * Initializes system when added. Requests points colliders and points + * accumulators + * + * @function added + * @memberof DetectPointsCollisionSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.pointsCollider = engine.getNodes(PointsColliderNode); + this.pointsKeepers = engine.getNodes(PointsNode); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof DetectPointsCollisionSystem + */ + removed() { + + this.pointsCollider = null; + this.pointsKeepers = null; + } + + /** + * Runs on every update of the loop. Triggers grab and manages cooldown + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof DetectPointsCollisionSystem + */ + update(currentFrameDuration) { + + const points = {}; + + for (const collider of this.pointsCollider) { + const collisionTargetBody = collider.pointsCollider.collisionTarget.getComponent(BodyComponent); + const pointsTarget = collider.pointsCollider.pointsTarget; + + if (collisionTargetBody) { + const collision = SAT.collides(collider.body.body, collisionTargetBody.body); + if (collision.collided) { + points[pointsTarget] = (points[pointsTarget] || 0) + 1; + } + } + } + + for (const pointsKeeper of this.pointsKeepers) { + for (const pointsLabel of Object.keys(points)) { + pointsKeeper.points[pointsLabel] = (pointsKeeper.points[pointsLabel] || 0) + points[pointsLabel]; + } + } + } +}; diff --git a/lib/systems/detect_winner.js b/lib/systems/detect_winner.js new file mode 100644 index 0000000..e7fbd76 --- /dev/null +++ b/lib/systems/detect_winner.js @@ -0,0 +1,104 @@ +import { System } from '@serpentity/serpentity'; + +import WinnerNode from '../nodes/winner'; +import PointsNode from '../nodes/points'; + +import Config from '../config'; + +/** + * Handles finding the winner + * + * @extends {external:Serpentity.System} + * @class DetectWinnerSystem + * @param {object} config a configuration object to extend. + */ +export default class DetectWinnerSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The collection of points keepers + * + * @property {external:Serpentity.NodeCollection} pointsKeepers + * @instance + * @memberof DetectWinnerSystem + */ + this.pointsKeepers = null; + + /** + * The collection of winner keepers + * + * @property {external:Serpentity.NodeCollection} winnerKeepers + * @instance + * @memberof DetectWinnerSystem + */ + this.winnerKeepers = null; + } + + /** + * Initializes system when added. Requests points and winner keepers + * + * @function added + * @memberof DetectWinnerSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.pointsKeepers = engine.getNodes(PointsNode); + this.winnerKeepers = engine.getNodes(WinnerNode); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof DetectWinnerSystem + */ + removed() { + + this.pointsKeepers = null; + this.winnerKeepers = null; + } + + /** + * Runs on every update of the loop. Checks if someone won + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof DetectWinnerSystem + */ + update(currentFrameDuration) { + + let winner = null; + + for (const pointsKeeper of this.pointsKeepers) { + for (const pointsCandidate of Object.keys(pointsKeeper.points)) { + if (pointsKeeper.points[pointsCandidate] >= Config.maxPoints) { + winner = pointsCandidate; + break; + } + } + } + + if (!winner) { + return; + } + + for (const winnerKeeper of this.winnerKeepers) { + + // Only one winner, another system should reset + if (!winnerKeeper.winner.winner) { + winnerKeeper.winner.winner = winner; + } + + } + } +}; + diff --git a/lib/systems/render_points.js b/lib/systems/render_points.js new file mode 100644 index 0000000..f7fd410 --- /dev/null +++ b/lib/systems/render_points.js @@ -0,0 +1,114 @@ +import { System } from '@serpentity/serpentity'; +import { Graphics } from 'pixi.js'; + +import Config from '../config'; +import PointsNode from '../nodes/points'; + +const internals = { + kNoPixiError: 'No pixi application passed to render system. Make sure you set the `application` key in the config object when initializing.', + redBar: new Graphics(), + blueBar: new Graphics() +}; + +/** + * Renders points assuming there exist a "red" and a "blue" score + * + * @extends {external:Serpentity.System} + * @class RenderPointsSystem + * @param {object} config a configuration object to extend. + */ +export default class RenderPointsSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The node collection of points keepers + * + * @property {external:Serpentity.NodeCollection} pointsKeepers + * @instance + * @memberof RenderPointsSystem + */ + this.pointsKeepers = null; + + /** + * The pixi engine we will use to render + * + * @property {external:PixiJs.Application} application + * @instance + * @memberof RenderPointsSystem + */ + this.application = config.application; + + if (!this.application) { + throw new Error(internals.kNoPixiError); + } + } + + /** + * Initializes system when added. Requests renderable nodes and + * attaches to event listeners to add / remove them to pixi stage + * + * @function added + * @memberof RenderPointsSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.pointsKeepers = engine.getNodes(PointsNode); + this.application.stage.addChild(internals.blueBar); + this.application.stage.addChild(internals.redBar); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof RenderPointsSystem + */ + removed() { + + this.pointsKeepers = null; + this.application.stage.removeChild(internals.blueBar); + this.application.stage.removeChild(internals.redBar); + } + + /** + * Runs on every update of the loop. Updates the bars + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof RenderPointsSystem + */ + update(currentFrameDuration) { + + for (const pointsKeeper of this.pointsKeepers) { + + const redPoints = pointsKeeper.points.red; + const bluePoints = pointsKeeper.points.blue; + + const redBarHeight = redPoints * Config.verticalResolution / Config.maxPoints; + const blueBarHeight = bluePoints * Config.verticalResolution / Config.maxPoints; + + internals.redBar.removeChildren(); + const redBar = new Graphics(); + redBar.lineStyle(20, 0xeaacac, 0.75) + .moveTo(Config.horizontalResolution, 0) + .lineTo(Config.horizontalResolution, redBarHeight); + internals.redBar.addChild(redBar); + + internals.blueBar.removeChildren(); + const blueBar = new Graphics(); + blueBar.lineStyle(20, 0x87c5ea, 0.75) + .moveTo(0, 0) + .lineTo(0, blueBarHeight); + internals.blueBar.addChild(blueBar); + } + } +}; diff --git a/lib/systems/render_winner.js b/lib/systems/render_winner.js new file mode 100644 index 0000000..53f3602 --- /dev/null +++ b/lib/systems/render_winner.js @@ -0,0 +1,114 @@ +import { System } from '@serpentity/serpentity'; +import { Text } from 'pixi.js'; + +import Config from '../config'; +import WinnerNode from '../nodes/winner'; + +const internals = { + kNoPixiError: 'No pixi application passed to render system. Make sure you set the `application` key in the config object when initializing.' +}; + +/** + * Renders a winner if there is one + * + * @extends {external:Serpentity.System} + * @class RenderWinnerSystem + * @param {object} config a configuration object to extend. + */ +export default class RenderWinnerSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The keepers of the winner + * + * @property {external:Serpentity.NodeCollection} pointsKeepers + * @instance + * @memberof RenderWinnerSystem + */ + this.winnerKeepers = null; + + /** + * The pixi engine we will use to render + * + * @property {external:PixiJs.Application} application + * @instance + * @memberof RenderWinnerSystem + */ + this.application = config.application; + + if (!this.application) { + throw new Error(internals.kNoPixiError); + } + } + + /** + * Initializes system when added. Requests winner keepers + * + * @function added + * @memberof RenderWinnerSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.winnerKeepers = engine.getNodes(WinnerNode); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof RenderWinnerSystem + */ + removed() { + + this.pointsKeepers = null; + if (internals.winnerText) { + this.application.stage.removeChild(internals.winnerText); + internals.winnerText = null; + } + } + + /** + * Runs on every update of the loop. Updates the bars + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof RenderWinnerSystem + */ + update(currentFrameDuration) { + + // Right now this is final, once a winner is rendered you would need + // to restart the whole system. + if (internals.winnerText) { + return; + } + + for (const winnerKeeper of this.winnerKeepers) { + const winner = winnerKeeper.winner.winner; + if (winner) { + const message = `${winner} has won`; + internals.winnerText = new Text(message, { + fontFamily: 'Arial', + fontSize: 96, + fontWeight: 'bold', + fill: 0xffffff, + align: 'center' + }); + internals.winnerText.position.x = Config.horizontalResolution / 2; + internals.winnerText.position.y = Config.verticalResolution / 2; + internals.winnerText.anchor.set(0.5); + this.application.stage.addChild(internals.winnerText); + return; + } + } + } +}; +