- 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
--- /dev/null
+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 {};
+
--- /dev/null
+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';
+ }
+};
+
--- /dev/null
+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;
+ }
+};
+
+
* @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]);
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';
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
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;
}
};
--- /dev/null
+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
+};
+
--- /dev/null
+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
+};
+
--- /dev/null
+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
+};
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
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());
entityB: harness
});
+ // Walls
+
SumoFactory.createInvisibleBlock(this._engine, {
width: this.horizontalResolution * 2,
height: this.verticalResolution * 0.1,
}
});
+ // 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);
--- /dev/null
+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];
+ }
+ }
+ }
+};
--- /dev/null
+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;
+ }
+
+ }
+ }
+};
+
--- /dev/null
+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);
+ }
+ }
+};
--- /dev/null
+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;
+ }
+ }
+ }
+};
+