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:
175
src/js/game/hud/base_hud_part.js
Normal file
175
src/js/game/hud/base_hud_part.js
Normal 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",
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
src/js/game/hud/dynamic_dom_attach.js
Normal file
79
src/js/game/hud/dynamic_dom_attach.js
Normal 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
187
src/js/game/hud/hud.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/js/game/hud/parts/beta_overlay.js
Normal file
10
src/js/game/hud/parts/beta_overlay.js
Normal 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() {}
|
||||
}
|
||||
492
src/js/game/hud/parts/building_placer.js
Normal file
492
src/js/game/hud/parts/building_placer.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/js/game/hud/parts/buildings_toolbar.js
Normal file
128
src/js/game/hud/parts/buildings_toolbar.js
Normal 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);
|
||||
}
|
||||
}
|
||||
37
src/js/game/hud/parts/game_menu.js
Normal file
37
src/js/game/hud/parts/game_menu.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
73
src/js/game/hud/parts/keybinding_overlay.js
Normal file
73
src/js/game/hud/parts/keybinding_overlay.js
Normal 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);
|
||||
}
|
||||
}
|
||||
117
src/js/game/hud/parts/processing_overlay.js
Normal file
117
src/js/game/hud/parts/processing_overlay.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
181
src/js/game/hud/parts/shop.js
Normal file
181
src/js/game/hud/parts/shop.js
Normal 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);
|
||||
}
|
||||
}
|
||||
122
src/js/game/hud/parts/unlock_notification.js
Normal file
122
src/js/game/hud/parts/unlock_notification.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user