1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Initial commit

This commit is contained in:
Tobias Springer
2020-05-09 16:45:23 +02:00
commit 93c6ea683d
304 changed files with 56031 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
/* typehints:start */
import { GameRoot } from "../root";
import { DrawParameters } from "../../core/draw_parameters";
/* typehints:end */
import { ClickDetector } from "../../core/click_detector";
import { KeyActionMapper } from "../key_action_mapper";
export class BaseHUDPart {
/**
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
/** @type {Array<ClickDetector>} */
this.clickDetectors = [];
}
/**
* Should create all require elements
* @param {HTMLElement} parent
*/
createElements(parent) {}
/**
* Should initialize the element, called *after* the elements have been created
*/
initialize() {
abstract;
}
/**
* Should update any required logic
*/
update() {}
/**
* Should draw the hud
* @param {DrawParameters} parameters
*/
draw(parameters) {}
/**
* Should draw any overlays (screen space)
* @param {DrawParameters} parameters
*/
drawOverlays(parameters) {}
/**
* Should return true if the widget has a modal dialog opened and thus
* the game does not need to update / redraw
* @returns {boolean}
*/
shouldPauseRendering() {
return false;
}
/**
* Should return false if the game should be paused
* @returns {boolean}
*/
shouldPauseGame() {
return false;
}
/**
* Should return true if this overlay is open and currently blocking any user interaction
*/
isBlockingOverlay() {
return false;
}
/**
* Cleans up the hud element, if overridden make sure to call super.cleanups
*/
cleanup() {
if (this.clickDetectors) {
for (let i = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup();
}
this.clickDetectors = [];
}
}
/**
* Should close the element, in case its supported
*/
close() {}
// Helpers
/**
* Calls closeMethod if an overlay is opened
* @param {function=} closeMethod
*/
closeOnOverlayOpen(closeMethod = null) {
this.root.hud.signals.overlayOpened.add(overlay => {
if (overlay !== this) {
(closeMethod || this.close).call(this);
}
}, this);
}
/**
* Helper method to construct a new click detector
* @param {Element} element The element to listen on
* @param {function} handler The handler to call on this object
* @param {import("../../core/click_detector").ClickDetectorConstructorArgs=} args Click detector arguments
*
*/
trackClicks(element, handler, args = {}) {
const detector = new ClickDetector(element, args);
detector.click.add(handler, this);
this.registerClickDetector(detector);
}
/**
* Registers a new click detector
* @param {ClickDetector} detector
*/
registerClickDetector(detector) {
this.clickDetectors.push(detector);
if (G_IS_DEV) {
// @ts-ignore
detector._src = "hud-" + this.constructor.name;
}
}
/**
* Closes this element when its background is clicked
* @param {HTMLElement} element
* @param {function} closeMethod
*/
closeOnBackgroundClick(element, closeMethod = null) {
const bgClickDetector = new ClickDetector(element, {
preventDefault: true,
targetOnly: true,
applyCssClass: null,
consumeEvents: true,
clickSound: null,
});
// If the state defines a close method, use that as fallback
// @ts-ignore
bgClickDetector.touchend.add(closeMethod || this.close, this);
this.registerClickDetector(bgClickDetector);
}
/**
* Forwards the game speed keybindings so you can toggle pause / Fastforward
* in the building tooltip and such
* @param {KeyActionMapper} sourceMapper
*/
forwardGameSpeedKeybindings(sourceMapper) {
sourceMapper.forward(this.root.gameState.keyActionMapper, [
"gamespeed_pause",
"gamespeed_fastforward",
]);
}
/**
* Forwards the map movement keybindings so you can move the map with the
* arrow keys
* @param {KeyActionMapper} sourceMapper
*/
forwardMapMovementKeybindings(sourceMapper) {
sourceMapper.forward(this.root.gameState.keyActionMapper, [
"map_move_up",
"map_move_right",
"map_move_down",
"map_move_left",
]);
}
}

View File

@@ -0,0 +1,79 @@
import { GameRoot } from "../root";
// Automatically attaches and detaches elements from the dom
// Also supports detaching elements after a given time, useful if there is a
// hide animation like for the tooltips
// Also attaches a class name if desired
export class DynamicDomAttach {
constructor(root, element, { timeToKeepSeconds = 0, attachClass = null } = {}) {
/** @type {GameRoot} */
this.root = root;
/** @type {HTMLElement} */
this.element = element;
this.parent = this.element.parentElement;
this.attachClass = attachClass;
this.timeToKeepSeconds = timeToKeepSeconds;
this.lastVisibleTime = 0;
// We start attached, so detach the node first
this.attached = true;
this.internalDetach();
this.internalIsClassAttached = false;
this.classAttachTimeout = null;
}
internalAttach() {
if (!this.attached) {
this.parent.appendChild(this.element);
assert(this.element.parentElement === this.parent, "Invalid parent #1");
this.attached = true;
}
}
internalDetach() {
if (this.attached) {
assert(this.element.parentElement === this.parent, "Invalid parent #2");
this.element.parentElement.removeChild(this.element);
this.attached = false;
}
}
isAttached() {
return this.attached;
}
update(isVisible) {
if (isVisible) {
this.lastVisibleTime = this.root ? this.root.time.realtimeNow() : 0;
this.internalAttach();
} else {
if (!this.root || this.root.time.realtimeNow() - this.lastVisibleTime >= this.timeToKeepSeconds) {
this.internalDetach();
}
}
if (this.attachClass && isVisible !== this.internalIsClassAttached) {
// State changed
this.internalIsClassAttached = isVisible;
if (this.classAttachTimeout) {
clearTimeout(this.classAttachTimeout);
this.classAttachTimeout = null;
}
if (isVisible) {
this.classAttachTimeout = setTimeout(() => {
this.element.classList.add(this.attachClass);
}, 15);
} else {
this.element.classList.remove(this.attachClass);
}
}
}
}

187
src/js/game/hud/hud.js Normal file
View File

@@ -0,0 +1,187 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { Signal } from "../../core/signal";
import { DrawParameters } from "../../core/draw_parameters";
import { HUDProcessingOverlay } from "./parts/processing_overlay";
import { HUDBuildingsToolbar } from "./parts/buildings_toolbar";
import { HUDBuildingPlacer } from "./parts/building_placer";
import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
import { HUDUnlockNotification } from "./parts/unlock_notification";
import { HUDGameMenu } from "./parts/game_menu";
import { HUDShop } from "./parts/shop";
import { IS_MOBILE } from "../../core/config";
export class GameHUD {
/**
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
}
/**
* Initializes the hud parts
*/
initialize() {
this.signals = {
overlayOpened: new Signal(/* overlay */),
};
this.parts = {
processingOverlay: new HUDProcessingOverlay(this.root),
buildingsToolbar: new HUDBuildingsToolbar(this.root),
buildingPlacer: new HUDBuildingPlacer(this.root),
unlockNotification: new HUDUnlockNotification(this.root),
gameMenu: new HUDGameMenu(this.root),
shop: new HUDShop(this.root),
// betaOverlay: new HUDBetaOverlay(this.root),
};
this.signals = {
selectedPlacementBuildingChanged: new Signal(/* metaBuilding|null */),
};
if (!IS_MOBILE) {
this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root);
}
const frag = document.createDocumentFragment();
for (const key in this.parts) {
this.parts[key].createElements(frag);
}
document.body.appendChild(frag);
for (const key in this.parts) {
this.parts[key].initialize();
}
this.internalInitSignalConnections();
this.root.gameState.keyActionMapper.getBinding("toggle_hud").add(this.toggleUi, this);
}
/**
* Attempts to close all overlays
*/
closeAllOverlays() {
for (const key in this.parts) {
this.parts[key].close();
}
}
/**
* Returns true if the game logic should be paused
*/
shouldPauseGame() {
for (const key in this.parts) {
if (this.parts[key].shouldPauseGame()) {
return true;
}
}
return false;
}
/**
* Returns true if the rendering can be paused
*/
shouldPauseRendering() {
for (const key in this.parts) {
if (this.parts[key].shouldPauseRendering()) {
return true;
}
}
return false;
}
/**
* Returns true if the rendering can be paused
*/
hasBlockingOverlayOpen() {
if (this.root.camera.getIsMapOverlayActive()) {
return true;
}
for (const key in this.parts) {
if (this.parts[key].isBlockingOverlay()) {
return true;
}
}
return false;
}
/**
* Toggles the ui
*/
toggleUi() {
document.body.classList.toggle("uiHidden");
}
/**
* Initializes connections between parts
*/
internalInitSignalConnections() {
const p = this.parts;
p.buildingsToolbar.sigBuildingSelected.add(p.buildingPlacer.startSelection, p.buildingPlacer);
}
/**
* Updates all parts
*/
update() {
if (!this.root.gameInitialized) {
return;
}
for (const key in this.parts) {
this.parts[key].update();
}
}
/**
* Draws all parts
* @param {DrawParameters} parameters
*/
draw(parameters) {
const partsOrder = ["buildingPlacer"];
for (let i = 0; i < partsOrder.length; ++i) {
if (this.parts[partsOrder[i]]) {
this.parts[partsOrder[i]].draw(parameters);
}
}
}
/**
* Draws all part overlays
* @param {DrawParameters} parameters
*/
drawOverlays(parameters) {
const partsOrder = [];
for (let i = 0; i < partsOrder.length; ++i) {
if (this.parts[partsOrder[i]]) {
this.parts[partsOrder[i]].drawOverlays(parameters);
}
}
}
/**
* Cleans up everything
*/
cleanup() {
for (const key in this.parts) {
this.parts[key].cleanup();
}
for (const key in this.signals) {
this.signals[key].removeAll();
}
}
}

View File

@@ -0,0 +1,10 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
export class HUDBetaOverlay extends BaseHUDPart {
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA");
}
initialize() {}
}

View File

@@ -0,0 +1,492 @@
import { BaseHUDPart } from "../base_hud_part";
import { MetaBuilding } from "../../meta_building";
import { DrawParameters } from "../../../core/draw_parameters";
import { globalConfig } from "../../../core/config";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { STOP_PROPAGATION, Signal } from "../../../core/signal";
import {
Vector,
enumDirectionToAngle,
enumInvertedDirections,
enumDirectionToVector,
} from "../../../core/vector";
import { pulseAnimation, makeDiv } from "../../../core/utils";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { TrackedState } from "../../../core/tracked_state";
import { Math_abs, Math_radians } from "../../../core/builtins";
import { Loader } from "../../../core/loader";
import { drawRotatedSprite } from "../../../core/draw_utils";
import { Entity } from "../../entity";
export class HUDBuildingPlacer extends BaseHUDPart {
initialize() {
/** @type {TypedTrackedState<MetaBuilding?>} */
this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this);
this.currentBaseRotation = 0;
/** @type {Entity} */
this.fakeEntity = null;
const keyActionMapper = this.root.gameState.keyActionMapper;
keyActionMapper.getBinding("building_abort_placement").add(() => this.currentMetaBuilding.set(null));
keyActionMapper.getBinding("back").add(() => this.currentMetaBuilding.set(null));
keyActionMapper.getBinding("rotate_while_placing").add(this.tryRotate, this);
this.domAttach = new DynamicDomAttach(this.root, this.element, {});
this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this);
this.root.camera.upPostHandler.add(this.abortDragging, this);
this.currentlyDragging = false;
/**
* The tile we last dragged onto
* @type {Vector}
* */
this.lastDragTile = null;
/**
* The tile we initially dragged from
* @type {Vector}
*/
this.initialDragTile = null;
}
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_building_placer", [], ``);
this.buildingLabel = makeDiv(this.element, null, ["buildingLabel"], "Extract");
this.buildingDescription = makeDiv(this.element, null, ["description"], "");
}
/**
* mouse down pre handler
* @param {Vector} pos
*/
onMouseDown(pos) {
if (this.root.camera.getIsMapOverlayActive()) {
return;
}
if (this.currentMetaBuilding.get()) {
this.currentlyDragging = true;
this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace();
// Place initial building
this.tryPlaceCurrentBuildingAt(this.lastDragTile);
return STOP_PROPAGATION;
}
}
/**
* mouse move pre handler
* @param {Vector} pos
*/
onMouseMove(pos) {
if (this.root.camera.getIsMapOverlayActive()) {
return;
}
if (this.currentMetaBuilding.get() && this.lastDragTile) {
const oldPos = this.lastDragTile;
const newPos = this.root.camera.screenToWorld(pos).toTileSpace();
if (!oldPos.equals(newPos)) {
const delta = newPos.sub(oldPos);
// - Using bresenhams algorithmus
let x0 = oldPos.x;
let y0 = oldPos.y;
let x1 = newPos.x;
let y1 = newPos.y;
var dx = Math_abs(x1 - x0);
var dy = Math_abs(y1 - y0);
var sx = x0 < x1 ? 1 : -1;
var sy = y0 < y1 ? 1 : -1;
var err = dx - dy;
while (true) {
this.tryPlaceCurrentBuildingAt(new Vector(x0, y0));
if (x0 === x1 && y0 === y1) break;
var e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
this.lastDragTile = newPos;
return STOP_PROPAGATION;
}
}
update() {
// ALways update since the camera might have moved
const mousePos = this.root.app.mousePosition;
if (mousePos) {
this.onMouseMove(mousePos);
}
}
/**
* aborts any dragging op
*/
abortDragging() {
this.currentlyDragging = true;
this.lastDragTile = null;
}
/**
*
* @param {MetaBuilding} metaBuilding
*/
startSelection(metaBuilding) {
this.currentMetaBuilding.set(metaBuilding);
}
/**
*
* @param {MetaBuilding} metaBuilding
*/
onSelectedMetaBuildingChanged(metaBuilding) {
this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding);
if (metaBuilding) {
this.buildingLabel.innerHTML = metaBuilding.getName();
this.buildingDescription.innerHTML = metaBuilding.getDescription();
this.fakeEntity = new Entity(null);
metaBuilding.setupEntityComponents(this.fakeEntity, null);
this.fakeEntity.addComponent(
new StaticMapEntityComponent({
origin: new Vector(0, 0),
rotationDegrees: 0,
tileSize: metaBuilding.getDimensions().copy(),
})
);
} else {
this.currentlyDragging = false;
this.fakeEntity = null;
}
}
/**
* Tries to rotate
*/
tryRotate() {
const selectedBuilding = this.currentMetaBuilding.get();
if (selectedBuilding) {
this.currentBaseRotation = (this.currentBaseRotation + 90) % 360;
const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.rotationDegrees = this.currentBaseRotation;
}
}
/**
* Tries to delete the building under the mouse
*/
deleteBelowCursor() {
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
const contents = this.root.map.getTileContent(tile);
if (contents) {
this.root.logic.tryDeleteBuilding(contents);
}
}
/**
* Canvas click handler
* @param {Vector} mousePos
* @param {boolean} cancelAction
*/
onCanvasClick(mousePos, cancelAction = false) {
if (cancelAction) {
if (this.currentMetaBuilding.get()) {
this.currentMetaBuilding.set(null);
} else {
this.deleteBelowCursor();
}
return STOP_PROPAGATION;
}
if (!this.currentMetaBuilding.get()) {
return;
}
return STOP_PROPAGATION;
}
/**
* Tries to place the current building at the given tile
* @param {Vector} tile
*/
tryPlaceCurrentBuildingAt(tile) {
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
// Dont allow placing in overview mode
return;
}
// Transform to world space
const metaBuilding = this.currentMetaBuilding.get();
const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile(
this.root,
tile,
this.currentBaseRotation
);
if (
this.root.logic.tryPlaceBuilding({
origin: tile,
rotation,
rotationVariant,
building: this.currentMetaBuilding.get(),
})
) {
// Succesfully placed
if (metaBuilding.getFlipOrientationAfterPlacement()) {
this.currentBaseRotation = (180 + this.currentBaseRotation) % 360;
}
if (!metaBuilding.getStayInPlacementMode() && !this.root.app.inputMgr.shiftIsDown) {
// Stop placement
this.currentMetaBuilding.set(null);
}
return true;
} else {
return false;
}
}
/**
*
* @param {DrawParameters} parameters
*/
draw(parameters) {
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
// Dont allow placing in overview mode
this.domAttach.update(false);
return;
}
this.domAttach.update(this.currentMetaBuilding.get());
const metaBuilding = this.currentMetaBuilding.get();
if (!metaBuilding) {
return;
}
const mousePosition = this.root.app.mousePosition;
if (!mousePosition) {
// Not on screen
return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace();
// Compute best rotation variant
const {
rotation,
rotationVariant,
connectedEntities,
} = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile(
this.root,
tile,
this.currentBaseRotation
);
// Check if there are connected entities
if (connectedEntities) {
for (let i = 0; i < connectedEntities.length; ++i) {
const connectedEntity = connectedEntities[i];
const connectedWsPoint = connectedEntity.components.StaticMapEntity.getTileSpaceBounds()
.getCenter()
.toWorldSpace();
const startWsPoint = tile.toWorldSpaceCenterOfTile();
const startOffset = connectedWsPoint
.sub(startWsPoint)
.normalize()
.multiplyScalar(globalConfig.tileSize * 0.3);
const effectiveStartPoint = startWsPoint.add(startOffset);
const effectiveEndPoint = connectedWsPoint.sub(startOffset);
parameters.context.globalAlpha = 0.6;
// parameters.context.lineCap = "round";
parameters.context.strokeStyle = "#7f7";
parameters.context.lineWidth = 10;
parameters.context.beginPath();
parameters.context.moveTo(effectiveStartPoint.x, effectiveStartPoint.y);
parameters.context.lineTo(effectiveEndPoint.x, effectiveEndPoint.y);
parameters.context.stroke();
parameters.context.globalAlpha = 1;
// parameters.context.lineCap = "square";
}
}
// Synchronize rotation and origin
const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.origin = tile;
staticComp.rotationDegrees = rotation;
metaBuilding.updateRotationVariant(this.fakeEntity, rotationVariant);
// Check if we could place the buildnig
const canBuild = this.root.logic.checkCanPlaceBuilding(tile, rotation, metaBuilding);
// Determine the bounds and visualize them
const entityBounds = staticComp.getTileSpaceBounds();
const drawBorder = 2;
parameters.context.globalAlpha = 0.5;
if (canBuild) {
parameters.context.fillStyle = "rgba(0, 255, 0, 0.2)";
} else {
parameters.context.fillStyle = "rgba(255, 0, 0, 0.2)";
}
parameters.context.fillRect(
entityBounds.x * globalConfig.tileSize - drawBorder,
entityBounds.y * globalConfig.tileSize - drawBorder,
entityBounds.w * globalConfig.tileSize + 2 * drawBorder,
entityBounds.h * globalConfig.tileSize + 2 * drawBorder
);
// Draw ejectors
if (canBuild) {
this.drawMatchingAcceptorsAndEjectors(parameters);
}
// HACK to draw the entity sprite
const previewSprite = metaBuilding.getPreviewSprite(rotationVariant);
parameters.context.globalAlpha = 0.8 + pulseAnimation(this.root.time.realtimeNow(), 1) * 0.1;
staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5);
staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite);
staticComp.origin = tile;
parameters.context.globalAlpha = 1;
}
/**
*
* @param {DrawParameters} parameters
*/
drawMatchingAcceptorsAndEjectors(parameters) {
const acceptorComp = this.fakeEntity.components.ItemAcceptor;
const ejectorComp = this.fakeEntity.components.ItemEjector;
const staticComp = this.fakeEntity.components.StaticMapEntity;
const goodArrowSprite = Loader.getSprite("sprites/misc/slot_good_arrow.png");
const badArrowSprite = Loader.getSprite("sprites/misc/slot_bad_arrow.png");
// Just ignore this code ...
if (acceptorComp) {
const slots = acceptorComp.slots;
for (let acceptorSlotIndex = 0; acceptorSlotIndex < slots.length; ++acceptorSlotIndex) {
const slot = slots[acceptorSlotIndex];
const acceptorSlotWsTile = staticComp.localTileToWorld(slot.pos);
const acceptorSlotWsPos = acceptorSlotWsTile.toWorldSpaceCenterOfTile();
for (
let acceptorDirectionIndex = 0;
acceptorDirectionIndex < slot.directions.length;
++acceptorDirectionIndex
) {
const direction = slot.directions[acceptorDirectionIndex];
const worldDirection = staticComp.localDirectionToWorld(direction);
const sourceTile = acceptorSlotWsTile.add(enumDirectionToVector[worldDirection]);
const sourceEntity = this.root.map.getTileContent(sourceTile);
let sprite = goodArrowSprite;
let alpha = 0.5;
if (sourceEntity) {
sprite = badArrowSprite;
const sourceEjector = sourceEntity.components.ItemEjector;
const sourceStaticComp = sourceEntity.components.StaticMapEntity;
const ejectorAcceptLocalTile = sourceStaticComp.worldToLocalTile(acceptorSlotWsTile);
if (sourceEjector && sourceEjector.anySlotEjectsToLocalTile(ejectorAcceptLocalTile)) {
sprite = goodArrowSprite;
}
alpha = 1.0;
}
parameters.context.globalAlpha = alpha;
drawRotatedSprite({
parameters,
sprite,
x: acceptorSlotWsPos.x,
y: acceptorSlotWsPos.y,
angle: Math_radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]),
size: 13,
offsetY: 15,
});
parameters.context.globalAlpha = 1;
}
}
}
if (ejectorComp) {
const slots = ejectorComp.slots;
for (let ejectorSlotIndex = 0; ejectorSlotIndex < slots.length; ++ejectorSlotIndex) {
const slot = ejectorComp.slots[ejectorSlotIndex];
const ejectorSlotWsTile = staticComp.localTileToWorld(
ejectorComp.getSlotTargetLocalTile(ejectorSlotIndex)
);
const ejectorSLotWsPos = ejectorSlotWsTile.toWorldSpaceCenterOfTile();
const ejectorSlotWsDirection = staticComp.localDirectionToWorld(slot.direction);
const destEntity = this.root.map.getTileContent(ejectorSlotWsTile);
let sprite = goodArrowSprite;
let alpha = 0.5;
if (destEntity) {
alpha = 1;
const destAcceptor = destEntity.components.ItemAcceptor;
const destStaticComp = destEntity.components.StaticMapEntity;
if (destAcceptor) {
const destLocalTile = destStaticComp.worldToLocalTile(ejectorSlotWsTile);
const destLocalDir = destStaticComp.worldDirectionToLocal(ejectorSlotWsDirection);
if (destAcceptor.findMatchingSlot(destLocalTile, destLocalDir)) {
sprite = goodArrowSprite;
} else {
sprite = badArrowSprite;
}
}
}
parameters.context.globalAlpha = alpha;
drawRotatedSprite({
parameters,
sprite,
x: ejectorSLotWsPos.x,
y: ejectorSLotWsPos.y,
angle: Math_radians(enumDirectionToAngle[ejectorSlotWsDirection]),
size: 13,
offsetY: 15,
});
parameters.context.globalAlpha = 1;
}
}
}
}

View File

@@ -0,0 +1,128 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { MetaBuilding } from "../../meta_building";
import { Signal } from "../../../core/signal";
import { MetaSplitterBuilding } from "../../buildings/splitter";
import { MetaMinerBuilding } from "../../buildings/miner";
import { MetaCutterBuilding } from "../../buildings/cutter";
import { MetaRotaterBuilding } from "../../buildings/rotater";
import { MetaStackerBuilding } from "../../buildings/stacker";
import { MetaMixerBuilding } from "../../buildings/mixer";
import { MetaPainterBuilding } from "../../buildings/painter";
import { MetaTrashBuilding } from "../../buildings/trash";
import { MetaBeltBaseBuilding } from "../../buildings/belt_base";
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
import { globalConfig } from "../../../core/config";
import { TrackedState } from "../../../core/tracked_state";
const toolbarBuildings = [
MetaBeltBaseBuilding,
MetaMinerBuilding,
MetaUndergroundBeltBuilding,
MetaSplitterBuilding,
MetaCutterBuilding,
MetaRotaterBuilding,
MetaStackerBuilding,
MetaMixerBuilding,
MetaPainterBuilding,
MetaTrashBuilding,
];
export class HUDBuildingsToolbar extends BaseHUDPart {
constructor(root) {
super(root);
/** @type {Object.<string, { metaBuilding: MetaBuilding, status: boolean, element: HTMLElement}>} */
this.buildingUnlockStates = {};
this.sigBuildingSelected = new Signal();
this.trackedIsVisisible = new TrackedState(this.onVisibilityChanged, this);
}
onVisibilityChanged(visible) {
this.element.classList.toggle("visible", visible);
}
/**
* Should create all require elements
* @param {HTMLElement} parent
*/
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_buildings_toolbar", [], "");
}
initialize() {
const actionMapper = this.root.gameState.keyActionMapper;
const items = makeDiv(this.element, null, ["buildings"]);
const iconSize = 32;
for (let i = 0; i < toolbarBuildings.length; ++i) {
const metaBuilding = gMetaBuildingRegistry.findByClass(toolbarBuildings[i]);
const binding = actionMapper.getBinding("building_" + metaBuilding.getId());
const dimensions = metaBuilding.getDimensions();
const itemContainer = makeDiv(items, null, ["building"]);
itemContainer.setAttribute("data-tilewidth", dimensions.x);
itemContainer.setAttribute("data-tileheight", dimensions.y);
const label = makeDiv(itemContainer, null, ["label"]);
label.innerText = metaBuilding.getName();
const tooltip = makeDiv(
itemContainer,
null,
["tooltip"],
`
<span class="title">${metaBuilding.getName()}</span>
<span class="desc">${metaBuilding.getDescription()}</span>
<span class="tutorialImage" data-icon="building_tutorials/${metaBuilding.getId()}.png"></span>
`
);
const sprite = metaBuilding.getPreviewSprite(0);
const spriteWrapper = makeDiv(itemContainer, null, ["iconWrap"]);
spriteWrapper.innerHTML = sprite.getAsHTML(iconSize * dimensions.x, iconSize * dimensions.y);
binding.appendLabelToElement(itemContainer);
binding.add(() => this.selectBuildingForPlacement(metaBuilding));
this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), {});
this.buildingUnlockStates[metaBuilding.id] = {
metaBuilding,
element: itemContainer,
status: false,
};
}
}
update() {
this.trackedIsVisisible.set(!this.root.camera.getIsMapOverlayActive());
for (const buildingId in this.buildingUnlockStates) {
const handle = this.buildingUnlockStates[buildingId];
const newStatus = handle.metaBuilding.getIsUnlocked(this.root);
if (handle.status !== newStatus) {
handle.status = newStatus;
handle.element.classList.toggle("unlocked", newStatus);
}
}
}
/**
*
* @param {MetaBuilding} metaBuilding
*/
selectBuildingForPlacement(metaBuilding) {
if (!metaBuilding.getIsUnlocked(this.root)) {
this.root.soundProxy.playUiError();
return;
}
this.sigBuildingSelected.dispatch(metaBuilding);
}
}

View File

@@ -0,0 +1,37 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
export class HUDGameMenu extends BaseHUDPart {
initialize() {}
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_GameMenu");
const buttons = [
{
id: "shop",
label: "Upgrades",
handler: () => this.root.hud.parts.shop.show(),
keybinding: "menu_open_shop",
},
{
id: "stats",
label: "Stats",
handler: () => null,
keybinding: "menu_open_stats",
},
];
buttons.forEach(({ id, label, handler, keybinding }) => {
const button = document.createElement("button");
button.setAttribute("data-button-id", id);
this.element.appendChild(button);
this.trackClicks(button, handler);
if (keybinding) {
const binding = this.root.gameState.keyActionMapper.getBinding(keybinding);
binding.add(handler);
binding.appendLabelToElement(button);
}
});
}
}

View File

@@ -0,0 +1,73 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
import { getStringForKeyCode } from "../../key_action_mapper";
import { TrackedState } from "../../../core/tracked_state";
export class HUDKeybindingOverlay extends BaseHUDPart {
initialize() {
this.shiftDownTracker = new TrackedState(this.onShiftStateChanged, this);
}
onShiftStateChanged(shiftDown) {
this.element.classList.toggle("shiftDown", shiftDown);
}
createElements(parent) {
const mapper = this.root.gameState.keyActionMapper;
const getKeycode = id => {
return getStringForKeyCode(mapper.getBinding(id).keyCode);
};
this.element = makeDiv(
parent,
"ingame_HUD_KeybindingOverlay",
[],
`
<div class="binding">
<code class="keybinding">${getKeycode("center_map")}</code>
<label>Center</label>
</div>
<div class="binding">
<code class="keybinding leftMouse"></code><i></i>
<code class="keybinding">${getKeycode("map_move_up")}</code>
<code class="keybinding">${getKeycode("map_move_left")}</code>
<code class="keybinding">${getKeycode("map_move_down")}</code>
<code class="keybinding">${getKeycode("map_move_right")}</code>
<label>Move</label>
</div>
<div class="binding noPlacementOnly">
<code class="keybinding rightMouse"></code>
<label>Delete</label>
</div>
<div class="binding placementOnly">
<code class="keybinding rightMouse"></code> <i></i>
<code class="keybinding">${getKeycode("building_abort_placement")}</code>
<label>Stop placement</label>
</div>
<div class="binding placementOnly">
<code class="keybinding">${getKeycode("rotate_while_placing")}</code>
<label>Rotate Building</label>
</div>
<div class="binding placementOnly shift">
<code class="keybinding">SHIFT</code>
<label>Place Multiple</label>
</div>
`
);
}
onSelectedBuildingForPlacementChanged(selectedMetaBuilding) {
this.element.classList.toggle("placementActive", !!selectedMetaBuilding);
}
update() {
this.shiftDownTracker.set(this.root.app.inputMgr.shiftIsDown);
}
}

View File

@@ -0,0 +1,117 @@
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { BaseHUDPart } from "../base_hud_part";
import { performanceNow } from "../../../core/builtins";
import { makeDiv } from "../../../core/utils";
import { Signal } from "../../../core/signal";
import { InputReceiver } from "../../../core/input_receiver";
import { createLogger } from "../../../core/logging";
const logger = createLogger("hud/processing_overlay");
export class HUDProcessingOverlay extends BaseHUDPart {
constructor(root) {
super(root);
this.tasks = [];
this.computeTimeout = null;
this.root.signals.performAsync.add(this.queueTask, this);
this.allTasksFinished = new Signal();
this.inputReceiver = new InputReceiver("processing-overlay");
this.root.signals.aboutToDestruct.add(() =>
this.root.app.inputMgr.destroyReceiver(this.inputReceiver)
);
}
createElements(parent) {
this.element = makeDiv(
parent,
"rg_HUD_ProcessingOverlay",
["hudElement"],
`
<span class="prefab_LoadingTextWithAnim">
Computing
</span>
`
);
}
initialize() {
this.domWatcher = new DynamicDomAttach(this.root, this.element, {
timeToKeepSeconds: 0,
});
}
queueTask(task, name) {
if (!this.root.gameInitialized) {
// Tasks before the game started can be done directlry
task();
return;
}
// if (name) {
// console.warn("QUEUE", name);
// }
task.__name = name;
this.tasks.push(task);
}
hasTasks() {
return this.tasks.length > 0;
}
isRunning() {
return this.computeTimeout !== null;
}
processSync() {
const now = performanceNow();
while (this.tasks.length > 0) {
const workload = this.tasks[0];
workload.call();
this.tasks.shift();
}
const duration = performanceNow() - now;
if (duration > 100) {
logger.log("Tasks done slow (SYNC!) within", (performanceNow() - now).toFixed(2), "ms");
}
}
process() {
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.domWatcher.update(true);
if (this.tasks.length === 0) {
logger.warn("No tasks but still called process");
return;
}
if (this.computeTimeout) {
assert(false, "Double compute queued");
clearTimeout(this.computeTimeout);
}
this.computeTimeout = setTimeout(() => {
const now = performanceNow();
while (this.tasks.length > 0) {
const workload = this.tasks[0];
workload.call();
this.tasks.shift();
}
const duration = performanceNow() - now;
if (duration > 100) {
logger.log("Tasks done slow within", (performanceNow() - now).toFixed(2), "ms");
}
this.domWatcher.update(false);
this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
clearTimeout(this.computeTimeout);
this.computeTimeout = null;
this.allTasksFinished.dispatch();
});
}
}

View File

@@ -0,0 +1,181 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv, removeAllChildren, formatBigNumber } from "../../../core/utils";
import { UPGRADES, TIER_LABELS } from "../../upgrades";
import { ShapeDefinition } from "../../shape_definition";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { InputReceiver } from "../../../core/input_receiver";
import { KeyActionMapper } from "../../key_action_mapper";
import { Math_min } from "../../../core/builtins";
export class HUDShop extends BaseHUDPart {
createElements(parent) {
this.background = makeDiv(parent, "ingame_HUD_Shop", ["ingameDialog"]);
// DIALOG Inner / Wrapper
this.dialogInner = makeDiv(this.background, null, ["dialogInner"]);
this.title = makeDiv(this.dialogInner, null, ["title"], `Upgrades`);
this.closeButton = makeDiv(this.title, null, ["closeButton"]);
this.trackClicks(this.closeButton, this.close);
this.contentDiv = makeDiv(this.dialogInner, null, ["content"]);
this.upgradeToElements = {};
// Upgrades
for (const upgradeId in UPGRADES) {
const { label } = UPGRADES[upgradeId];
const handle = {};
handle.requireIndexToElement = [];
// Wrapper
handle.elem = makeDiv(this.contentDiv, null, ["upgrade"]);
handle.elem.setAttribute("data-upgrade-id", upgradeId);
// Title
const title = makeDiv(handle.elem, null, ["title"], label);
// Title > Tier
handle.elemTierLabel = makeDiv(title, null, ["tier"], "Tier ?");
// Icon
handle.icon = makeDiv(handle.elem, null, ["icon"]);
handle.icon.setAttribute("data-icon", "upgrades/" + upgradeId + ".png");
// Description
handle.elemDescription = makeDiv(handle.elem, null, ["description"], "??");
handle.elemRequirements = makeDiv(handle.elem, null, ["requirements"]);
// Buy button
handle.buyButton = document.createElement("button");
handle.buyButton.classList.add("buy", "styledButton");
handle.buyButton.innerText = "Upgrade";
handle.elem.appendChild(handle.buyButton);
this.trackClicks(handle.buyButton, () => this.tryUnlockNextTier(upgradeId));
// Assign handle
this.upgradeToElements[upgradeId] = handle;
}
}
rerenderFull() {
for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId];
const { description, tiers } = UPGRADES[upgradeId];
// removeAllChildren(handle.elem);
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const tierHandle = tiers[currentTier];
// Set tier
handle.elemTierLabel.innerText = "Tier " + TIER_LABELS[currentTier];
handle.elemTierLabel.setAttribute("data-tier", currentTier);
// Cleanup
handle.requireIndexToElement = [];
removeAllChildren(handle.elemRequirements);
handle.elem.classList.toggle("maxLevel", !tierHandle);
if (!tierHandle) {
// Max level
handle.elemDescription.innerText = "Maximum level";
continue;
}
// Set description
handle.elemDescription.innerText = description(tierHandle.improvement);
tierHandle.required.forEach(({ shape, amount }) => {
const requireDiv = makeDiv(handle.elemRequirements, null, ["requirement"]);
const shapeDef = this.root.shapeDefinitionMgr.registerOrReturnHandle(
ShapeDefinition.fromShortKey(shape)
);
const shapeCanvas = shapeDef.generateAsCanvas(120);
shapeCanvas.classList.add();
requireDiv.appendChild(shapeCanvas);
const progressContainer = makeDiv(requireDiv, null, ["amount"]);
const progressBar = document.createElement("label");
progressBar.classList.add("progressBar");
progressContainer.appendChild(progressBar);
const progressLabel = document.createElement("label");
progressContainer.appendChild(progressLabel);
handle.requireIndexToElement.push({
progressLabel,
progressBar,
definition: shapeDef,
required: amount,
});
});
}
}
renderCountsAndStatus() {
for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId];
for (let i = 0; i < handle.requireIndexToElement.length; ++i) {
const { progressLabel, progressBar, definition, required } = handle.requireIndexToElement[i];
const haveAmount = this.root.hubGoals.getShapesStored(definition);
const progress = Math_min(haveAmount / required, 1.0);
progressLabel.innerText = formatBigNumber(haveAmount) + " / " + formatBigNumber(required);
progressBar.style.width = progress * 100.0 + "%";
progressBar.classList.toggle("complete", progress >= 1.0);
}
handle.buyButton.classList.toggle("buyable", this.root.hubGoals.canUnlockUpgrade(upgradeId));
}
}
initialize() {
this.domAttach = new DynamicDomAttach(this.root, this.background, {
attachClass: "visible",
});
this.inputReciever = new InputReceiver("shop");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
this.keyActionMapper.getBinding("back").add(this.close, this);
this.keyActionMapper.getBinding("menu_open_shop").add(this.close, this);
this.close();
this.rerenderFull();
this.root.signals.upgradePurchased.add(this.rerenderFull, this);
}
cleanup() {
document.body.classList.remove("ingameDialogOpen");
}
show() {
this.visible = true;
document.body.classList.add("ingameDialogOpen");
// this.background.classList.add("visible");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.update();
}
close() {
this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update();
}
update() {
this.domAttach.update(this.visible);
if (this.visible) {
this.renderCountsAndStatus();
}
}
tryUnlockNextTier(upgradeId) {
// Nothing
this.root.hubGoals.tryUnlockUgprade(upgradeId);
}
}

View File

@@ -0,0 +1,122 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { MetaBuilding } from "../../meta_building";
import { MetaSplitterBuilding } from "../../buildings/splitter";
import { MetaCutterBuilding } from "../../buildings/cutter";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { MetaTrashBuilding } from "../../buildings/trash";
import { MetaMinerBuilding } from "../../buildings/miner";
import { MetaPainterBuilding } from "../../buildings/painter";
import { MetaMixerBuilding } from "../../buildings/mixer";
import { MetaRotaterBuilding } from "../../buildings/rotater";
import { MetaStackerBuilding } from "../../buildings/stacker";
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
import { globalConfig } from "../../../core/config";
export class HUDUnlockNotification extends BaseHUDPart {
initialize() {
this.visible = false;
this.domAttach = new DynamicDomAttach(this.root, this.element, {
timeToKeepSeconds: 0,
});
if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) {
this.root.signals.storyGoalCompleted.add(this.showForLevel, this);
}
}
shouldPauseGame() {
return this.visible;
}
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", []);
const dialog = makeDiv(this.element, null, ["dialog"]);
this.elemTitle = makeDiv(dialog, null, ["title"], ``);
this.elemSubTitle = makeDiv(dialog, null, ["subTitle"], `Completed`);
this.elemContents = makeDiv(
dialog,
null,
["contents"],
`
Ready for the next one?
`
);
this.btnClose = document.createElement("button");
this.btnClose.classList.add("close", "styledButton");
this.btnClose.innerText = "Next level";
dialog.appendChild(this.btnClose);
this.trackClicks(this.btnClose, this.close);
}
showForLevel(level, reward) {
this.elemTitle.innerText = "Level " + ("" + level).padStart(2, "0");
let html = `<span class='reward'>Unlocked ${reward}!</span>`;
const addBuildingExplanation = metaBuildingClass => {
const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass);
html += `<div class="buildingExplanation" data-icon="building_tutorials/${metaBuilding.getId()}.png"></div>`;
};
switch (reward) {
case enumHubGoalRewards.reward_cutter_and_trash: {
addBuildingExplanation(MetaCutterBuilding);
addBuildingExplanation(MetaTrashBuilding);
break;
}
case enumHubGoalRewards.reward_mixer: {
addBuildingExplanation(MetaMixerBuilding);
break;
}
case enumHubGoalRewards.reward_painter: {
addBuildingExplanation(MetaPainterBuilding);
break;
}
case enumHubGoalRewards.reward_rotater: {
addBuildingExplanation(MetaRotaterBuilding);
break;
}
case enumHubGoalRewards.reward_splitter: {
addBuildingExplanation(MetaSplitterBuilding);
break;
}
case enumHubGoalRewards.reward_stacker: {
addBuildingExplanation(MetaStackerBuilding);
break;
}
case enumHubGoalRewards.reward_tunnel: {
addBuildingExplanation(MetaUndergroundBeltBuilding);
break;
}
}
// addBuildingExplanation(MetaSplitterBuilding);
// addBuildingExplanation(MetaCutterBuilding);
this.elemContents.innerHTML = html;
this.visible = true;
}
close() {
this.visible = false;
}
update() {
this.domAttach.update(this.visible);
}
}