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

Mod Support - 1.5.0 Update (#1361)

* initial modloader draft

* modloader features

* Refactor mods to use signals

* Add support for modifying and registering new transltions

* Minor adjustments

* Support for string building ids for mods

* Initial support for adding new buildings

* Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading

* Lazy Load mods to make sure all dependencies are loaded

* Expose all exported members automatically to mods

* Fix duplicate exports

* Allow loading mods from standalone

* update changelog

* Fix mods folder incorrect path

* Fix modloading in standalone

* Fix sprites not getting replaced, update demo mod

* Load dev mod via raw loader

* Improve mod developing so mods are directly ready to be deployed, load mods from local file server

* Proper mods ui

* Allow mods to register game systems and draw stuff

* Change mods path

* Fix sprites not loading

* Minor adjustments, closes #1333

* Add support for loading atlases via mods

* Add support for loading mods from external sources in DEV

* Add confirmation when loading mods

* Fix circular dependency

* Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods

* Add some mod signals

* refactor game loading states

* Make shapez exports global

* Start to make mods safer

* Refactor file system electron event handling

* Properly isolate electron renderer process

* Update to latest electron

* Show errors when loading mods

* Update confirm dialgo

* Minor restructure, start to add mod examples

* Allow adding custom themesw

* Add more examples and allow defining custom item processor operations

* Add interface to register new buildings

* Fixed typescript type errors (#1335)

* Refactor building registry, make it easier for mods to add new buildings

* Allow overriding existing methods

* Add more examples and more features

* More mod examples

* Make mod loading simpler

* Add example how to add custom drawings

* Remove unused code

* Minor modloader adjustments

* Support for rotation variants in mods (was broken previously)

* Allow mods to replace builtin sub shapes

* Add helper methods to extend classes

* Fix menu bar on mac os

* Remember window state

* Add support for paste signals

* Add example how to add custom components and systems

* Support for mod settings

* Add example for adding a new item type

* Update class extensions

* Minor adjustments

* Fix typo

* Add notification blocks mod example

* Add small tutorial

* Update readme

* Add better instructions

* Update JSDoc for Replacing Methods (#1336)

* upgraded types for overriding methods

* updated comments

Co-authored-by: Edward Badel <you@example.com>

* Direction lock now indicates when there is a building inbetween

* Fix mod examples

* Fix linter error

* Game state register (#1341)

* Added a gamestate register helper

Added a gamestate register helper

* Update mod_interface.js

* export build options

* Fix runBeforeMethod and runAfterMethod

* Minor game system code cleanup

* Belt path drawing optimization

* Fix belt path optimization

* Belt drawing improvements, again

* Do not render belts in statics disabled view

* Allow external URL to load more than one mod (#1337)

* Allow external URL to load more than one mod

Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though.

* Update modloader.js

* Prettier fixes

* Added link to create-shapezio-mod npm page (#1339)

Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod

* allow command line switch to load more than one mod (#1342)

* Fixed class handle type (#1345)

* Fixed class handle type

* Fixed import game state

* Minor adjustments

* Refactor item acceptor to allow only single direction slots

* Allow specifying minimumGameVersion

* Add sandbox example

* Replaced concatenated strings with template literals (#1347)

* Mod improvements

* Make wired pins component optional on the storage

* Fix mod examples

* Bind `this` for method overriding JSDoc (#1352)

* fix entity debugger reaching HTML elements (#1353)

* Store mods in savegame and show warning when it differs

* Closes #1357

* Fix All Shapez Exports Being Const (#1358)

* Allowed setting of variables inside webpack modules

* remove console log

* Fix stringification of things inside of eval

Co-authored-by: Edward Badel <you@example.com>

* Fix building placer intersection warning

* Add example for storing data in the savegame

* Fix double painter bug (#1349)

* Add example on how to extend builtin buildings

* update readme

* Disable steam achievements when playing with mods

* Update translations

Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com>
Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com>
Co-authored-by: Edward Badel <you@example.com>
Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com>
Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
This commit is contained in:
tobspr
2022-02-01 16:35:49 +01:00
committed by GitHub
parent a7a2aad2b6
commit c41aaa1fc5
170 changed files with 5935 additions and 1551 deletions

View File

@@ -169,6 +169,10 @@
margin: 1px 0;
}
h3 {
@include S(margin-top, 10px);
}
input {
background: #eee;
color: #333438;
@@ -214,6 +218,33 @@
}
}
}
.dialogModsMod {
background: rgba(0, 0, 0, 0.05);
@include S(padding, 5px);
@include S(margin, 10px, 0);
@include S(border-radius, $globalBorderRadius);
display: grid;
grid-template-columns: 1fr D(100px);
@include DarkThemeOverride {
background: rgba(0, 0, 0, 0.2);
}
button {
grid-column: 2 / 3;
grid-row: 1 / 3;
align-self: start;
}
.version {
@include SuperSmallText;
opacity: 0.5;
}
.name {
}
}
}
> .buttons {

View File

@@ -31,6 +31,7 @@
@import "states/mobile_warning";
@import "states/changelog";
@import "states/puzzle_menu";
@import "states/mods";
@import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer";

View File

@@ -61,7 +61,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
background-image: uiResource("res/ui/building_tutorials/virtual_processor-cutter.png") !important;
}
$icons: notification_saved, notification_success, notification_upgrade;
$icons: notification_saved, notification_success, notification_upgrade, notification_info,
notification_warning, notification_error;
@each $icon in $icons {
[data-icon="icons/#{$icon}.png"] {
/* @load-async */

View File

@@ -185,21 +185,20 @@
.updateLabel {
position: absolute;
transform: translateX(50%) rotate(-5deg);
color: #ff590b;
@include Heading;
color: #fff;
@include PlainText;
font-weight: bold;
@include S(right, 40px);
@include S(bottom, 20px);
background: $modsColor;
@include S(border-radius, $globalBorderRadius);
@include S(padding, 0, 5px, 1px, 5px);
@include InlineAnimation(1.3s ease-in-out infinite) {
50% {
transform: translateX(50%) rotate(-7deg) scale(1.1);
}
}
@include DarkThemeOverride {
color: $colorBlueBright;
}
}
}
@@ -290,6 +289,99 @@
}
}
.modsOverview {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #fff;
grid-row: 1 / 2;
grid-column: 2 / 3;
position: relative;
text-align: left;
align-items: flex-start;
@include S(width, 250px);
@include S(padding, 15px);
@include S(padding-bottom, 10px);
@include S(border-radius, $globalBorderRadius);
.header {
display: flex;
width: 100%;
align-items: center;
@include S(margin-bottom, 10px);
.editMods {
margin-left: auto;
@include S(width, 20px);
@include S(height, 20px);
padding: 0;
opacity: 0.5;
background: transparent center center/ 80% no-repeat;
& {
/* @load-async */
background-image: uiResource("icons/edit_key.png") !important;
}
@include DarkThemeInvert;
}
}
h3 {
@include Heading;
color: $modsColor;
margin: 0;
}
.dlcHint {
@include SuperSmallText;
@include S(margin-top, 10px);
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
grid-gap: 20px;
align-items: center;
}
.mod {
background: #eee;
width: 100%;
@include S(border-radius, $globalBorderRadius);
@include S(padding, 5px);
box-sizing: border-box;
@include PlainText;
@include S(margin-bottom, 5px);
display: flex;
flex-direction: column;
.author,
.version {
@include SuperSmallText;
align-self: end;
opacity: 0.4;
}
.name {
overflow: hidden;
}
}
.modsList {
box-sizing: border-box;
@include S(height, 100px);
@include S(padding, 5px);
border: D(1px) solid #eee;
overflow-y: scroll;
width: 100%;
display: flex;
flex-direction: column;
pointer-events: all;
:last-child {
margin-bottom: auto;
}
}
}
.mainContainer {
display: flex;
align-items: center;
@@ -308,6 +400,7 @@
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.modeButtons {
@@ -352,27 +445,33 @@
}
}
.importButton {
.outer {
@include S(margin-top, 15px);
}
.importButton {
@include IncreasedClickArea(0px);
}
.newGameButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
@include S(margin-left, 10px);
}
.playModeButton {
.modsButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
}
@include S(margin-left, 10px);
.editModeButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
// @include S(width, 20px);
// & {
// /* @load-async */
// background-image: uiResource("res/ui/icons/mods_white.png") !important;
// }
background-position: center center;
background-size: D(15px);
background-color: $modsColor !important;
background-repeat: no-repeat;
}
.savegames {
@@ -736,6 +835,23 @@
}
}
.modsOverview {
background: $darkModeControlsBackground;
.modsList {
border-color: darken($darkModeControlsBackground, 5);
.mod {
background: darken($darkModeControlsBackground, 5);
color: white;
}
}
.dlcHint {
color: $accentColorBright;
}
}
.footer {
> a,
.sidelinks > a {

141
src/css/states/mods.scss Normal file
View File

@@ -0,0 +1,141 @@
#state_ModsState {
.mainContent {
display: flex;
flex-direction: column;
}
> .headerBar {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
> h1 {
justify-self: start;
}
.openModsFolder {
background-color: $modsColor;
}
}
.noModSupport {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
.steamLink {
@include S(height, 50px);
@include S(width, 220px);
background: #171a23 center center / contain no-repeat;
overflow: hidden;
display: block;
text-indent: -999em;
cursor: pointer;
@include S(margin-top, 30px);
pointer-events: all;
transition: all 0.12s ease-in;
transition-property: opacity, transform;
@include S(border-radius, $globalBorderRadius);
&:hover {
opacity: 0.9;
}
}
}
.modsStats {
@include PlainText;
color: $accentColorDark;
&.noMods {
@include S(width, 400px);
align-self: center;
justify-self: center;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
@include Text;
@include S(margin-top, 100px);
color: lighten($accentColorDark, 15);
button {
@include S(margin-top, 10px);
@include S(padding, 10px, 20px);
}
&::before {
@include S(margin-bottom, 15px);
content: "";
@include S(width, 50px);
@include S(height, 50px);
background-position: center center;
background-size: contain;
opacity: 0.2;
}
&::before {
/* @load-async */
background-image: uiResource("res/ui/icons/mods.png") !important;
}
}
}
.modsList {
@include S(margin-top, 10px);
overflow-y: scroll;
pointer-events: all;
@include S(padding-right, 5px);
flex-grow: 1;
.mod {
@include S(border-radius, $globalBorderRadius);
background: $accentColorBright;
@include S(margin-bottom, 4px);
@include S(padding, 7px, 10px);
@include S(grid-gap, 15px);
display: grid;
grid-template-columns: 1fr D(100px) D(80px) D(50px);
@include DarkThemeOverride {
background: darken($darkModeControlsBackground, 5);
}
.checkbox {
align-self: center;
justify-self: center;
}
.mainInfo {
display: flex;
flex-direction: column;
.description {
@include SuperSmallText;
@include S(margin-top, 5px);
color: $accentColorDark;
}
.website {
text-transform: uppercase;
align-self: start;
@include SuperSmallText;
@include S(margin-top, 5px);
}
}
.version,
.author {
display: flex;
flex-direction: column;
align-self: center;
strong {
text-transform: uppercase;
color: $accentColorDark;
@include SuperSmallText;
}
}
}
}
}

View File

@@ -15,20 +15,21 @@
}
.sidebar {
display: grid;
display: flex;
@include S(min-width, 210px);
@include S(max-width, 320px);
@include S(grid-gap, 3px);
grid-template-rows: auto auto auto auto auto 1fr;
flex-direction: column;
@include StyleBelowWidth($layoutBreak) {
grid-template-rows: 1fr 1fr;
grid-template-columns: auto auto;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@include S(grid-gap, 5px);
max-width: unset !important;
}
button {
text-align: left;
@include S(margin-bottom, 3px);
&::after {
content: unset;
}
@@ -37,15 +38,26 @@
@include StyleBelowWidth($layoutBreak) {
text-align: center;
height: D(30px) !important;
padding: D(5px) !important;
}
}
.other {
@include S(margin-top, 10px);
align-self: end;
margin-top: auto;
@include StyleBelowWidth($layoutBreak) {
margin-top: 0;
display: grid;
grid-template-columns: 1fr 1fr;
@include S(grid-gap, 5px);
max-width: unset !important;
grid-column: 1 / 3;
button {
margin: 0 !important;
}
}
}
@@ -69,8 +81,28 @@
}
}
button.privacy {
@include S(margin-top, 4px);
button.manageMods {
background-color: lighten($modsColor, 38);
color: $modsColor;
display: flex;
@include S(padding-right, 5px);
.newBadge {
color: #fff;
@include S(border-radius, $globalBorderRadius);
background: $modsColor;
margin-left: auto;
@include S(padding, 0, 3px, 0, 3px);
@include InlineAnimation(1.3s ease-in-out infinite) {
50% {
transform: rotate(0deg) scale(1.1);
}
}
}
&.active {
background-color: $colorGreenBright;
}
}
.versionbar {

View File

@@ -38,6 +38,7 @@ $colorRedBright: #ef5072;
$colorOrangeBright: #ef9d50;
$themeColor: #393747;
$ingameHudBg: rgba(#333438, 0.9);
$modsColor: rgb(214, 60, 228);
$text3dColor: #f4ffff;

View File

@@ -15,8 +15,8 @@
<meta name="theme-color" content="#393747" />
<!-- seo -->
<meta name="copyright" content="2020 Tobias Springer IT Solutions and .io Games" />
<meta name="author" content="Tobias Springer, tobias.springer1@gmail.com" />
<meta name="copyright" content="2022 tobspr Games" />
<meta name="author" content="tobspr Games - tobspr.io" />
<meta
name="description"
content="shapez.io is an open-source factory building game about combining and producing different types of shapes."

View File

@@ -35,6 +35,9 @@ import { PuzzleMenuState } from "./states/puzzle_menu";
import { ClientAPI } from "./platform/api";
import { LoginState } from "./states/login";
import { WegameSplashState } from "./states/wegame_splash";
import { MODS } from "./mods/modloader";
import { MOD_SIGNALS } from "./mods/mod_signals";
import { ModsState } from "./states/mods";
/**
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
@@ -62,10 +65,24 @@ if (typeof document.hidden !== "undefined") {
}
export class Application {
constructor() {
/**
* Boots the application
*/
async boot() {
console.log("Booting ...");
assert(!GLOBAL_APP, "Tried to construct application twice");
logger.log("Creating application, platform =", getPlatformName());
setGlobalApp(this);
MODS.app = this;
// MODS
try {
await MODS.initMods();
} catch (ex) {
alert("Failed to load mods (launch with --dev for more info): \n\n" + ex);
}
this.unloaded = false;
@@ -128,6 +145,31 @@ export class Application {
// Store the mouse position, or null if not available
/** @type {Vector|null} */
this.mousePosition = null;
this.registerStates();
this.registerEventListeners();
Loader.linkAppAfterBoot(this);
if (G_WEGAME_VERSION) {
this.stateMgr.moveToState("WegameSplashState");
}
// Check for mobile
else if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
} else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
this.ticker.start();
window.focus();
MOD_SIGNALS.appBooted.dispatch();
}
/**
@@ -167,6 +209,7 @@ export class Application {
ChangelogState,
PuzzleMenuState,
LoginState,
ModsState,
];
for (let i = 0; i < states.length; ++i) {
@@ -322,35 +365,6 @@ export class Application {
}
}
/**
* Boots the application
*/
boot() {
console.log("Booting ...");
this.registerStates();
this.registerEventListeners();
Loader.linkAppAfterBoot(this);
if (G_WEGAME_VERSION) {
this.stateMgr.moveToState("WegameSplashState");
}
// Check for mobile
else if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
} else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
this.ticker.start();
window.focus();
}
/**
* Deinitializes the application
*/

View File

@@ -1,4 +1,12 @@
export const CHANGELOG = [
{
version: "1.5.0",
date: "unreleased",
entries: [
"This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.",
"When holding shift while placing a belt, the indicator now becomes red when crossing buildings",
],
},
{
version: "1.4.4",
date: "29.08.2021",

View File

@@ -9,6 +9,7 @@ import { SOUNDS, MUSIC } from "../platform/sound";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
import { cachebust } from "./cachebust";
import { MODS } from "../mods/modloader";
const logger = createLogger("background_loader");

View File

@@ -28,6 +28,8 @@ export const THIRDPARTY_URLS = {
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
},
modBrowser: "https://shapez.mod.io/?preview=f55f6304ca4873d9a25f3b575571b948",
};
// export const A_B_TESTING_LINK_TYPE = Math.random() > 0.95 ? "steam_1_pr" : "steam_2_npr";
@@ -42,7 +44,7 @@ export const globalConfig = {
// Which dpi the assets have
assetsDpi: 192 / 32,
assetsSharpness: 1.5,
shapesSharpness: 1.4,
shapesSharpness: 1.3,
// Achievements
achievementSliceDuration: 10, // Seconds
@@ -61,6 +63,8 @@ export const globalConfig = {
mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null, // COMPUTED
maxBeltShapeBundleSize: 20,
// Belt speeds
// NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2,

View File

@@ -116,5 +116,11 @@ export default {
// Disables slow asserts, useful for debugging performance
// disableSlowAsserts: true,
// -----------------------------------------------------------------------------------
// Allows to load a mod from an external source for developing it
// externalModUrl: "http://localhost:3005/combined.js",
// -----------------------------------------------------------------------------------
// Visualizes the shape grouping on belts
// showShapeGrouping: true
// -----------------------------------------------------------------------------------
/* dev:end */
};

View File

@@ -15,3 +15,20 @@ export function setGlobalApp(app) {
assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app;
}
export const BUILD_OPTIONS = {
HAVE_ASSERT: G_HAVE_ASSERT,
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
TRACKING_ENDPOINT: G_TRACKING_ENDPOINT,
CHINA_VERSION: G_CHINA_VERSION,
WEGAME_VERSION: G_WEGAME_VERSION,
IS_DEV: G_IS_DEV,
IS_RELEASE: G_IS_RELEASE,
IS_MOBILE_APP: G_IS_MOBILE_APP,
IS_BROWSER: G_IS_BROWSER,
IS_STANDALONE: G_IS_STANDALONE,
BUILD_TIME: G_BUILD_TIME,
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
BUILD_VERSION: G_BUILD_VERSION,
ALL_UI_IMAGES: G_ALL_UI_IMAGES,
};

View File

@@ -149,6 +149,8 @@ export class InputDistributor {
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
window.addEventListener("blur", this.handleBlur.bind(this));
document.addEventListener("paste", this.handlePaste.bind(this));
}
forwardToReceiver(eventId, payload = null) {
@@ -186,6 +188,13 @@ export class InputDistributor {
this.keysDown.clear();
}
/**
*
*/
handlePaste(ev) {
this.forwardToReceiver("paste", ev);
}
/**
* @param {KeyboardEvent | MouseEvent} event
*/
@@ -211,6 +220,7 @@ export class InputDistributor {
keyCode: keyCode,
shift: event.shiftKey,
alt: event.altKey,
ctrl: event.ctrlKey,
initial: isInitial,
event,
}) === STOP_PROPAGATION

View File

@@ -12,12 +12,15 @@ export class InputReceiver {
// Dispatched on destroy
this.destroyed = new Signal();
this.paste = new Signal();
}
cleanup() {
this.backButton.removeAll();
this.keydown.removeAll();
this.keyup.removeAll();
this.paste.removeAll();
this.destroyed.dispatch();
}

View File

@@ -169,6 +169,9 @@ class LoaderImpl {
sprite = new AtlasSprite(spriteName);
this.sprites.set(spriteName, sprite);
}
if (sprite.frozen) {
continue;
}
const link = new SpriteAtlasLink({
packedX: frame.x,
@@ -181,6 +184,7 @@ class LoaderImpl {
w: sourceSize.w,
h: sourceSize.h,
});
sprite.linksByResolution[scale] = link;
}
}

View File

@@ -90,8 +90,9 @@ export class Dialog {
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
*/
handleKeydown({ keyCode, shift, alt }) {
handleKeydown({ keyCode, shift, alt, ctrl }) {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;

View File

@@ -1,7 +1,6 @@
import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
import { getIPCRenderer } from "./utils";
/*
* ***************************************************
@@ -113,13 +112,11 @@ export class FormElementInput extends FormElement {
if (G_WEGAME_VERSION) {
const value = String(this.element.value);
getIPCRenderer()
.invoke("profanity-check", value)
.then(newValue => {
if (value !== newValue && this.element) {
this.element.value = newValue;
}
});
ipcRenderer.invoke("profanity-check", value).then(newValue => {
if (value !== newValue && this.element) {
this.element.value = newValue;
}
});
}
}

View File

@@ -71,6 +71,8 @@ export class AtlasSprite extends BaseSprite {
/** @type {Object.<string, SpriteAtlasLink>} */
this.linksByResolution = {};
this.spriteName = spriteName;
this.frozen = false;
}
getRawTexture() {

View File

@@ -6,6 +6,7 @@ import { GameState } from "./game_state";
import { createLogger } from "./logging";
import { APPLICATION_ERROR_OCCURED } from "./error_handler";
import { waitNextFrame, removeAllChildren } from "./utils";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("state_manager");
@@ -109,6 +110,8 @@ export class StateManager {
key
);
MOD_SIGNALS.stateEntered.dispatch(this.currentState);
waitNextFrame().then(() => {
document.body.classList.add("arrived");
});

View File

@@ -42,21 +42,6 @@ export function getPlatformName() {
return "unknown";
}
/**
* Returns the IPC renderer, or null if not within the standalone
* @returns {object|null}
*/
let ipcRenderer = null;
export function getIPCRenderer() {
if (!G_IS_STANDALONE) {
return null;
}
if (!ipcRenderer) {
ipcRenderer = eval("require")("electron").ipcRenderer;
}
return ipcRenderer;
}
/**
* Makes a new 2D array with undefined contents
* @param {number} w
@@ -395,7 +380,7 @@ export function clamp(v, minimum = 0, maximum = 1) {
* @param {Array<string>=} classes
* @param {string=} innerHTML
*/
function makeDivElement(id = null, classes = [], innerHTML = "") {
export function makeDivElement(id = null, classes = [], innerHTML = "") {
const div = document.createElement("div");
if (id) {
div.id = id;

View File

@@ -2,9 +2,6 @@ import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization";
/** @type {ItemType[]} **/
export const itemTypes = ["shape", "color", "boolean"];
/**
* Class for items on belts etc. Not an entity for performance reasons
*/

View File

@@ -1,7 +1,9 @@
import { globalConfig } from "../core/config";
import { smoothenDpi } from "../core/dpi_manager";
import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging";
import { Rectangle } from "../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../core/sprites";
import { clamp, epsilonCompare, round4Digits } from "../core/utils";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
@@ -1430,6 +1432,12 @@ export class BeltPath extends BasicSerializableObject {
let trackPos = 0.0;
/**
* @type {Array<[Vector, BaseItem]>}
*/
let drawStack = [];
let drawStackProp = "";
// Iterate whole track and check items
for (let i = 0; i < this.entityPath.length; ++i) {
const entity = this.entityPath[i];
@@ -1449,25 +1457,185 @@ export class BeltPath extends BasicSerializableObject {
const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile();
const distanceAndItem = this.items[currentItemIndex];
const item = distanceAndItem[1 /* item */];
const nextItemDistance = distanceAndItem[0 /* nextDistance */];
distanceAndItem[1 /* item */].drawItemCenteredClipped(
worldPos.x,
worldPos.y,
parameters,
globalConfig.defaultItemDiameter
);
if (
!parameters.visibleRect.containsCircle(
worldPos.x,
worldPos.y,
globalConfig.defaultItemDiameter
)
) {
// this one isn't visible, do not append it
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
} else {
if (drawStack.length > 1) {
// Check if we can append to the stack, since its already a stack of two same items
const referenceItem = drawStack[0];
if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) {
// Will continue stack
} else {
// Start a new stack, since item doesn't follow in row
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else if (drawStack.length === 1) {
const firstItem = drawStack[0];
// Check if we can make it a stack
if (firstItem[1 /* item */].equals(item)) {
// Same item, check if it is either horizontal or vertical
const startPos = firstItem[0 /* pos */];
if (Math.abs(startPos.x - worldPos.x) < 0.001) {
drawStackProp = "x";
} else if (Math.abs(startPos.y - worldPos.y) < 0.001) {
drawStackProp = "y";
} else {
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// Start a new stack, since item doesn't equal
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// First item of stack, do nothing
}
drawStack.push([worldPos, item]);
}
// Check for the next item
currentItemPos += distanceAndItem[0 /* nextDistance */];
currentItemPos += nextItemDistance;
++currentItemIndex;
if (
nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 ||
drawStack.length > globalConfig.maxBeltShapeBundleSize
) {
// If next item is not directly following, abort drawing
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
if (currentItemIndex >= this.items.length) {
// We rendered all items
this.drawDrawStack(drawStack, parameters, drawStackProp);
return;
}
}
trackPos += beltLength;
}
this.drawDrawStack(drawStack, parameters, drawStackProp);
}
/**
*
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} context
* @param {number} w
* @param {number} h
* @param {number} dpi
* @param {object} param0
* @param {string} param0.direction
* @param {Array<[Vector, BaseItem]>} param0.stack
* @param {GameRoot} param0.root
* @param {number} param0.zoomLevel
*/
drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) {
context.scale(dpi, dpi);
if (G_IS_DEV && globalConfig.debug.showShapeGrouping) {
context.fillStyle = "rgba(0, 0, 255, 0.5)";
context.fillRect(0, 0, w, h);
}
const parameters = new DrawParameters({
context,
desiredAtlasScale: ORIGINAL_SPRITE_SCALE,
root,
visibleRect: new Rectangle(-1000, -1000, 2000, 2000),
zoomLevel,
});
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const item = stack[0];
const pos = new Vector(itemSize / 2, itemSize / 2);
for (let i = 0; i < stack.length; i++) {
item[1].drawItemCenteredClipped(pos.x, pos.y, parameters, globalConfig.defaultItemDiameter);
pos[direction] += globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
}
}
/**
* @param {Array<[Vector, BaseItem]>} stack
* @param {DrawParameters} parameters
*/
drawDrawStack(stack, parameters, directionProp) {
if (stack.length === 0) {
return;
}
const firstItem = stack[0];
const firstItemPos = firstItem[0];
if (stack.length === 1) {
firstItem[1].drawItemCenteredClipped(
firstItemPos.x,
firstItemPos.y,
parameters,
globalConfig.defaultItemDiameter
);
return;
}
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const inverseDirection = directionProp === "x" ? "y" : "x";
const dimensions = new Vector(itemSize, itemSize);
dimensions[inverseDirection] *= stack.length;
const directionVector = firstItemPos.copy().sub(stack[1][0]);
const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel);
const sprite = this.root.buffers.getForKey({
key: "beltpaths",
subKey: "stack-" + directionProp + "-" + dpi + "-" + stack.length + firstItem[1].serialize(),
dpi,
w: dimensions.x,
h: dimensions.y,
redrawMethod: this.drawShapesInARow.bind(this),
additionalParams: {
direction: inverseDirection,
stack,
root: this.root,
zoomLevel: parameters.zoomLevel,
},
});
const anchor = directionVector[inverseDirection] < 0 ? firstItem : stack[stack.length - 1];
parameters.context.drawImage(
sprite,
anchor[0].x - itemSize / 2,
anchor[0].y - itemSize / 2,
dimensions.x,
dimensions.y
);
}
}

View File

@@ -82,7 +82,7 @@ export class Blueprint {
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) {
if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
parameters.context.globalAlpha = 0.3;
} else {
parameters.context.globalAlpha = 1;
@@ -131,7 +131,7 @@ export class Blueprint {
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (root.logic.checkCanPlaceEntity(entity, tile)) {
if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
anyPlaceable = true;
}
}
@@ -160,7 +160,7 @@ export class Blueprint {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
continue;
}

View File

@@ -4,6 +4,8 @@ import { AtlasSprite } from "../core/sprites";
import { Vector } from "../core/vector";
/* typehints:end */
import { gMetaBuildingRegistry } from "../core/global_registries";
/**
* @typedef {{
* metaClass: typeof MetaBuilding,
@@ -19,7 +21,7 @@ import { Vector } from "../core/vector";
/**
* Stores a lookup table for all building variants (for better performance)
* @type {Object<number, BuildingVariantIdentifier>}
* @type {Object<number|string, BuildingVariantIdentifier>}
*/
export const gBuildingVariants = {
// Set later
@@ -27,13 +29,13 @@ export const gBuildingVariants = {
/**
* Mapping from 'metaBuildingId/variant/rotationVariant' to building code
* @type {Map<string, number>}
* @type {Map<string, number|string>}
*/
const variantsCache = new Map();
/**
* Registers a new variant
* @param {number} code
* @param {number|string} code
* @param {typeof MetaBuilding} meta
* @param {string} variant
* @param {number} rotationVariant
@@ -47,6 +49,7 @@ export function registerBuildingVariant(
assert(!gBuildingVariants[code], "Duplicate id: " + code);
gBuildingVariants[code] = {
metaClass: meta,
metaInstance: gMetaBuildingRegistry.findByClass(meta),
variant,
rotationVariant,
// @ts-ignore
@@ -54,9 +57,20 @@ export function registerBuildingVariant(
};
}
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotationVariant
* @returns
*/
function generateBuildingHash(buildingId, variant, rotationVariant) {
return buildingId + "/" + variant + "/" + rotationVariant;
}
/**
*
* @param {number} code
* @param {string|number} code
* @returns {BuildingVariantIdentifier}
*/
export function getBuildingDataFromCode(code) {
@@ -70,8 +84,8 @@ export function getBuildingDataFromCode(code) {
export function buildBuildingCodeCache() {
for (const code in gBuildingVariants) {
const data = gBuildingVariants[code];
const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant;
variantsCache.set(hash, +code);
const hash = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant);
variantsCache.set(hash, isNaN(+code) ? code : +code);
}
}
@@ -80,10 +94,10 @@ export function buildBuildingCodeCache() {
* @param {MetaBuilding} metaBuilding
* @param {string} variant
* @param {number} rotationVariant
* @returns {number}
* @returns {number|string}
*/
export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) {
const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant;
const hash = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant);
const result = variantsCache.get(hash);
if (G_IS_DEV) {
if (!result) {

View File

@@ -3,7 +3,7 @@ import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -14,6 +14,15 @@ export class MetaAnalyzerBuilding extends MetaBuilding {
super("analyzer");
}
static getAllVariantCombinations() {
return [
{
internalId: 43,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#3a52bc";
}

View File

@@ -31,6 +31,31 @@ export class MetaBalancerBuilding extends MetaBuilding {
super("balancer");
}
static getAllVariantCombinations() {
return [
{
internalId: 4,
variant: defaultBuildingVariant,
},
{
internalId: 5,
variant: enumBalancerVariants.merger,
},
{
internalId: 6,
variant: enumBalancerVariants.mergerInverse,
},
{
internalId: 47,
variant: enumBalancerVariants.splitter,
},
{
internalId: 48,
variant: enumBalancerVariants.splitterInverse,
},
];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
@@ -154,11 +179,11 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);
@@ -179,15 +204,14 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
directions: [
direction:
variant === enumBalancerVariants.mergerInverse
? enumDirection.left
: enumDirection.right,
],
},
]);
@@ -206,7 +230,7 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);

View File

@@ -5,7 +5,7 @@ import { SOUNDS } from "../../platform/sound";
import { T } from "../../translations";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { THEME } from "../theme";
@@ -22,6 +22,26 @@ export class MetaBeltBuilding extends MetaBuilding {
super("belt");
}
static getAllVariantCombinations() {
return [
{
internalId: 1,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 2,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 3,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
];
}
getSilhouetteColor() {
return THEME.map.chunkOverview.beltColor;
}

View File

@@ -2,13 +2,22 @@
import { Entity } from "../entity";
/* typehints:end */
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaBlockBuilding extends MetaBuilding {
constructor() {
super("block");
}
static getAllVariantCombinations() {
return [
{
internalId: 64,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#333";
}

View File

@@ -2,7 +2,7 @@ import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -11,6 +11,15 @@ export class MetaComparatorBuilding extends MetaBuilding {
super("comparator");
}
static getAllVariantCombinations() {
return [
{
internalId: 46,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#823cab";
}

View File

@@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector";
import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaConstantProducerBuilding extends MetaBuilding {
constructor() {
super("constant_producer");
}
static getAllVariantCombinations() {
return [
{
internalId: 62,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#bfd630";
}

View File

@@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { ConstantSignalComponent } from "../components/constant_signal";
import { generateMatrixRotations } from "../../core/utils";
@@ -14,6 +14,15 @@ export class MetaConstantSignalBuilding extends MetaBuilding {
super("constant_signal");
}
static getAllVariantCombinations() {
return [
{
internalId: 31,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#2b84fd";
}

View File

@@ -17,6 +17,19 @@ export class MetaCutterBuilding extends MetaBuilding {
super("cutter");
}
static getAllVariantCombinations() {
return [
{
internalId: 9,
variant: defaultBuildingVariant,
},
{
internalId: 10,
variant: enumCutterVariants.quad,
},
];
}
getSilhouetteColor() {
return "#7dcda2";
}
@@ -83,7 +96,7 @@ export class MetaCutterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

View File

@@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { DisplayComponent } from "../components/display";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -11,6 +11,15 @@ export class MetaDisplayBuilding extends MetaBuilding {
super("display");
}
static getAllVariantCombinations() {
return [
{
internalId: 40,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#aaaaaa";
}

View File

@@ -6,7 +6,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -15,6 +15,15 @@ export class MetaFilterBuilding extends MetaBuilding {
super("filter");
}
static getAllVariantCombinations() {
return [
{
internalId: 37,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#c45c2e";
}
@@ -69,7 +78,7 @@ export class MetaFilterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

View File

@@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaGoalAcceptorBuilding extends MetaBuilding {
constructor() {
super("goal_acceptor");
}
static getAllVariantCombinations() {
return [
{
internalId: 63,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#ce418a";
}
@@ -36,7 +45,7 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

View File

@@ -3,7 +3,7 @@ import { HubComponent } from "../components/hub";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
export class MetaHubBuilding extends MetaBuilding {
@@ -11,6 +11,15 @@ export class MetaHubBuilding extends MetaBuilding {
super("hub");
}
static getAllVariantCombinations() {
return [
{
internalId: 26,
variant: defaultBuildingVariant,
},
];
}
getDimensions() {
return new Vector(4, 4);
}
@@ -61,80 +70,22 @@ export class MetaHubBuilding extends MetaBuilding {
})
);
/**
* @type {Array<import("../components/item_acceptor").ItemAcceptorSlotConfig>}
*/
const slots = [];
for (let i = 0; i < 4; ++i) {
slots.push(
{ pos: new Vector(i, 0), direction: enumDirection.top, filter: "shape" },
{ pos: new Vector(i, 3), direction: enumDirection.bottom, filter: "shape" },
{ pos: new Vector(0, i), direction: enumDirection.left, filter: "shape" },
{ pos: new Vector(3, i), direction: enumDirection.right, filter: "shape" }
);
}
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.top, enumDirection.left],
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
filter: "shape",
},
{
pos: new Vector(2, 0),
directions: [enumDirection.top],
filter: "shape",
},
{
pos: new Vector(3, 0),
directions: [enumDirection.top, enumDirection.right],
filter: "shape",
},
{
pos: new Vector(0, 3),
directions: [enumDirection.bottom, enumDirection.left],
filter: "shape",
},
{
pos: new Vector(1, 3),
directions: [enumDirection.bottom],
filter: "shape",
},
{
pos: new Vector(2, 3),
directions: [enumDirection.bottom],
filter: "shape",
},
{
pos: new Vector(3, 3),
directions: [enumDirection.bottom, enumDirection.right],
filter: "shape",
},
{
pos: new Vector(0, 1),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(0, 2),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(0, 3),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(3, 1),
directions: [enumDirection.right],
filter: "shape",
},
{
pos: new Vector(3, 2),
directions: [enumDirection.right],
filter: "shape",
},
{
pos: new Vector(3, 3),
directions: [enumDirection.right],
filter: "shape",
},
],
slots,
})
);
}

View File

@@ -3,13 +3,22 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaItemProducerBuilding extends MetaBuilding {
constructor() {
super("item_producer");
}
static getAllVariantCombinations() {
return [
{
internalId: 61,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#b37dcd";
}

View File

@@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { LeverComponent } from "../components/lever";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -11,6 +11,15 @@ export class MetaLeverBuilding extends MetaBuilding {
super("lever");
}
static getAllVariantCombinations() {
return [
{
internalId: 33,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
// @todo: Render differently based on if its activated or not
return "#1a678b";

View File

@@ -15,7 +15,7 @@ export const enumLogicGateVariants = {
};
/** @enum {string} */
export const enumVariantToGate = {
const enumVariantToGate = {
[defaultBuildingVariant]: enumLogicGateType.and,
[enumLogicGateVariants.not]: enumLogicGateType.not,
[enumLogicGateVariants.xor]: enumLogicGateType.xor,
@@ -41,6 +41,27 @@ export class MetaLogicGateBuilding extends MetaBuilding {
super("logic_gate");
}
static getAllVariantCombinations() {
return [
{
internalId: 32,
variant: defaultBuildingVariant,
},
{
internalId: 34,
variant: enumLogicGateVariants.not,
},
{
internalId: 35,
variant: enumLogicGateVariants.xor,
},
{
internalId: 36,
variant: enumLogicGateVariants.or,
},
];
}
getSilhouetteColor(variant) {
return colors[variant];
}

View File

@@ -21,6 +21,19 @@ export class MetaMinerBuilding extends MetaBuilding {
super("miner");
}
static getAllVariantCombinations() {
return [
{
internalId: 7,
variant: defaultBuildingVariant,
},
{
internalId: 8,
variant: enumMinerVariants.chainable,
},
];
}
getSilhouetteColor() {
return "#b37dcd";
}

View File

@@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -14,6 +14,15 @@ export class MetaMixerBuilding extends MetaBuilding {
super("mixer");
}
static getAllVariantCombinations() {
return [
{
internalId: 15,
variant: defaultBuildingVariant,
},
];
}
getDimensions() {
return new Vector(2, 1);
}
@@ -64,12 +73,12 @@ export class MetaMixerBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
],

View File

@@ -22,6 +22,27 @@ export class MetaPainterBuilding extends MetaBuilding {
super("painter");
}
static getAllVariantCombinations() {
return [
{
internalId: 16,
variant: defaultBuildingVariant,
},
{
internalId: 17,
variant: enumPainterVariants.mirrored,
},
{
internalId: 18,
variant: enumPainterVariants.double,
},
{
internalId: 19,
variant: enumPainterVariants.quad,
},
];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
@@ -107,12 +128,12 @@ export class MetaPainterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
direction: enumDirection.top,
filter: "color",
},
],
@@ -139,14 +160,13 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [
direction:
variant === defaultBuildingVariant ? enumDirection.top : enumDirection.bottom,
],
filter: "color",
},
]);
@@ -172,17 +192,17 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 1),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
direction: enumDirection.top,
filter: "color",
},
]);
@@ -230,27 +250,27 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(2, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(3, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
]);

View File

@@ -4,7 +4,7 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { BeltUnderlaysComponent } from "../components/belt_underlays";
import { BeltReaderComponent } from "../components/belt_reader";
@@ -18,6 +18,15 @@ export class MetaReaderBuilding extends MetaBuilding {
super("reader");
}
static getAllVariantCombinations() {
return [
{
internalId: 49,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#25fff2";
}
@@ -75,7 +84,7 @@ export class MetaReaderBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

View File

@@ -23,6 +23,23 @@ export class MetaRotaterBuilding extends MetaBuilding {
super("rotater");
}
static getAllVariantCombinations() {
return [
{
internalId: 11,
variant: defaultBuildingVariant,
},
{
internalId: 12,
variant: enumRotaterVariants.ccw,
},
{
internalId: 13,
variant: enumRotaterVariants.rotate180,
},
];
}
getSilhouetteColor() {
return "#7dc6cd";
}
@@ -111,7 +128,7 @@ export class MetaRotaterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

View File

@@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -14,6 +14,15 @@ export class MetaStackerBuilding extends MetaBuilding {
super("stacker");
}
static getAllVariantCombinations() {
return [
{
internalId: 14,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#9fcd7d";
}
@@ -64,12 +73,12 @@ export class MetaStackerBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

View File

@@ -6,7 +6,7 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -17,6 +17,15 @@ export class MetaStorageBuilding extends MetaBuilding {
super("storage");
}
static getAllVariantCombinations() {
return [
{
internalId: 21,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#bbdf6d";
}
@@ -65,11 +74,11 @@ export class MetaStorageBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 1),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 1),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

View File

@@ -22,6 +22,19 @@ export class MetaTransistorBuilding extends MetaBuilding {
super("transistor");
}
static getAllVariantCombinations() {
return [
{
internalId: 38,
variant: defaultBuildingVariant,
},
{
internalId: 60,
variant: enumTransistorVariants.mirrored,
},
];
}
getSilhouetteColor() {
return "#bc3a61";
}

View File

@@ -4,7 +4,7 @@ import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -15,6 +15,15 @@ export class MetaTrashBuilding extends MetaBuilding {
super("trash");
}
static getAllVariantCombinations() {
return [
{
internalId: 20,
variant: defaultBuildingVariant,
},
];
}
getIsRotateable() {
return false;
}
@@ -67,12 +76,19 @@ export class MetaTrashBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
],
direction: enumDirection.top,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
},
],
})

View File

@@ -40,6 +40,31 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
super("underground_belt");
}
static getAllVariantCombinations() {
return [
{
internalId: 22,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 23,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 24,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 0,
},
{
internalId: 25,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 1,
},
];
}
getSilhouetteColor(variant, rotationVariant) {
return colorsByRotationVariant[rotationVariant];
}
@@ -252,7 +277,7 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);
return;

View File

@@ -19,7 +19,7 @@ export const enumVirtualProcessorVariants = {
};
/** @enum {string} */
export const enumVariantToGate = {
const enumVariantToGate = {
[defaultBuildingVariant]: enumLogicGateType.cutter,
[enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater,
[enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker,
@@ -40,6 +40,31 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
super("virtual_processor");
}
static getAllVariantCombinations() {
return [
{
internalId: 42,
variant: defaultBuildingVariant,
},
{
internalId: 44,
variant: enumVirtualProcessorVariants.rotater,
},
{
internalId: 45,
variant: enumVirtualProcessorVariants.unstacker,
},
{
internalId: 50,
variant: enumVirtualProcessorVariants.stacker,
},
{
internalId: 51,
variant: enumVirtualProcessorVariants.painter,
},
];
}
getSilhouetteColor(variant) {
return colors[variant];
}

View File

@@ -37,6 +37,51 @@ export class MetaWireBuilding extends MetaBuilding {
super("wire");
}
static getAllVariantCombinations() {
return [
{
internalId: 27,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 28,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 29,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
{
internalId: 30,
variant: defaultBuildingVariant,
rotationVariant: 3,
},
{
internalId: 52,
variant: enumWireVariant.second,
rotationVariant: 0,
},
{
internalId: 53,
variant: enumWireVariant.second,
rotationVariant: 1,
},
{
internalId: 54,
variant: enumWireVariant.second,
rotationVariant: 2,
},
{
internalId: 55,
variant: enumWireVariant.second,
rotationVariant: 3,
},
];
}
getHasDirectionLockAvailable() {
return true;
}

View File

@@ -2,7 +2,7 @@ import { generateMatrixRotations } from "../../core/utils";
import { Vector } from "../../core/vector";
import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@@ -13,6 +13,15 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
super("wire_tunnel");
}
static getAllVariantCombinations() {
return [
{
internalId: 39,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#777a86";
}

View File

@@ -22,31 +22,34 @@ import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent);
gComponentRegistry.register(BeltComponent);
gComponentRegistry.register(ItemEjectorComponent);
gComponentRegistry.register(ItemAcceptorComponent);
gComponentRegistry.register(MinerComponent);
gComponentRegistry.register(ItemProcessorComponent);
gComponentRegistry.register(UndergroundBeltComponent);
gComponentRegistry.register(HubComponent);
gComponentRegistry.register(StorageComponent);
gComponentRegistry.register(WiredPinsComponent);
gComponentRegistry.register(BeltUnderlaysComponent);
gComponentRegistry.register(WireComponent);
gComponentRegistry.register(ConstantSignalComponent);
gComponentRegistry.register(LogicGateComponent);
gComponentRegistry.register(LeverComponent);
gComponentRegistry.register(WireTunnelComponent);
gComponentRegistry.register(DisplayComponent);
gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent);
gComponentRegistry.register(ItemProducerComponent);
gComponentRegistry.register(GoalAcceptorComponent);
const components = [
StaticMapEntityComponent,
BeltComponent,
ItemEjectorComponent,
ItemAcceptorComponent,
MinerComponent,
ItemProcessorComponent,
UndergroundBeltComponent,
HubComponent,
StorageComponent,
WiredPinsComponent,
BeltUnderlaysComponent,
WireComponent,
ConstantSignalComponent,
LogicGateComponent,
LeverComponent,
WireTunnelComponent,
DisplayComponent,
BeltReaderComponent,
FilterComponent,
ItemProducerComponent,
GoalAcceptorComponent,
];
components.forEach(component => gComponentRegistry.register(component));
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
// Sanity check - If this is thrown, you (=me, lol) forgot to add a new component here
// Sanity check - If this is thrown, you forgot to add a new component here
assert(
// @ts-ignore

View File

@@ -8,7 +8,7 @@ export const curvedBeltLength = /* Math.PI / 4 */ 0.78;
/** @type {import("./item_acceptor").ItemAcceptorSlot} */
export const FAKE_BELT_ACCEPTOR_SLOT = {
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
};
/** @type {Object<enumDirection, import("./item_ejector").ItemEjectorSlot>} */

View File

@@ -3,9 +3,10 @@ import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
/** @typedef {{
/**
* @typedef {{
* pos: Vector,
* directions: enumDirection[],
* direction: enumDirection,
* filter?: ItemType
* }} ItemAcceptorSlot */
@@ -14,12 +15,12 @@ import { Component } from "../component";
* @typedef {{
* slot: ItemAcceptorSlot,
* index: number,
* acceptedDirection: enumDirection
* }} ItemAcceptorLocatedSlot */
/** @typedef {{
/**
* @typedef {{
* pos: Vector,
* directions: enumDirection[],
* direction: enumDirection,
* filter?: ItemType
* }} ItemAcceptorSlotConfig */
@@ -64,7 +65,7 @@ export class ItemAcceptorComponent extends Component {
const slot = slots[i];
this.slots.push({
pos: slot.pos,
directions: slot.directions,
direction: slot.direction,
// Which type of item to accept (shape | color | all) @see ItemType
filter: slot.filter,
@@ -122,15 +123,11 @@ export class ItemAcceptorComponent extends Component {
}
// Check if the acceptor slot accepts items from our direction
for (let i = 0; i < slot.directions.length; ++i) {
// const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]);
if (desiredDirection === slot.directions[i]) {
return {
slot,
index: slotIndex,
acceptedDirection: desiredDirection,
};
}
if (desiredDirection === slot.direction) {
return {
slot,
index: slotIndex,
};
}
}

View File

@@ -109,6 +109,11 @@ export class ItemProcessorComponent extends Component {
* @type {number}
*/
this.bonusTime = 0;
/**
* @type {Array<EjectorItemToEject>}
*/
this.queuedEjects = [];
}
/**

View File

@@ -19,7 +19,7 @@ export class StaticMapEntityComponent extends Component {
originalRotation: types.float,
// See building_codes.js
code: types.uint,
code: types.uintOrString,
};
}
@@ -99,7 +99,7 @@ export class StaticMapEntityComponent extends Component {
* @param {Vector=} param0.tileSize Size of the entity in tiles
* @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90
* @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90
* @param {number=} param0.code Building code
* @param {number|string=} param0.code Building code
*/
constructor({
origin = new Vector(),

View File

@@ -38,6 +38,7 @@ import { ShapeDefinitionManager } from "./shape_definition_manager";
import { AchievementProxy } from "./achievement_proxy";
import { SoundProxy } from "./sound_proxy";
import { GameTime } from "./time/game_time";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("ingame/core");
@@ -161,6 +162,7 @@ export class GameCore {
}
logger.log("root initialized");
MOD_SIGNALS.gameInitialized.dispatch(root);
}
/**

View File

@@ -197,20 +197,18 @@ export class Entity extends BasicSerializableObject {
for (let i = 0; i < acceptorComp.slots.length; ++i) {
const slot = acceptorComp.slots[i];
const slotTile = staticComp.localTileToWorld(slot.pos);
for (let k = 0; k < slot.directions.length; ++k) {
const direction = staticComp.localDirectionToWorld(slot.directions[k]);
const directionVector = enumDirectionToVector[direction];
const angle = Math.radians(enumDirectionToAngle[direction] + 180);
context.globalAlpha = 0.4;
drawRotatedSprite({
parameters,
sprite: acceptorSprite,
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
angle,
size: globalConfig.tileSize * 0.25,
});
}
const direction = staticComp.localDirectionToWorld(slot.direction);
const directionVector = enumDirectionToVector[direction];
const angle = Math.radians(enumDirectionToAngle[direction] + 180);
context.globalAlpha = 0.4;
drawRotatedSprite({
parameters,
sprite: acceptorSprite,
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
angle,
size: globalConfig.tileSize * 0.25,
});
}
}

View File

@@ -1,4 +1,5 @@
/* typehints:start */
import { GameSystem } from "./game_system";
import { GameRoot } from "./root";
/* typehints:end */
@@ -30,6 +31,11 @@ import { ZoneSystem } from "./systems/zone";
const logger = createLogger("game_system_manager");
/**
* @type {Object<string, Array<{ id: string; systemClass: new (any) => GameSystem}>>}
*/
export const MODS_ADDITIONAL_SYSTEMS = {};
export class GameSystemManager {
/**
*
@@ -123,7 +129,15 @@ export class GameSystemManager {
* Initializes all systems
*/
internalInitSystems() {
const addBefore = id => {
const systems = MODS_ADDITIONAL_SYSTEMS[id];
if (systems) {
systems.forEach(({ id, systemClass }) => add(id, systemClass));
}
};
const add = (id, systemClass) => {
addBefore(id);
this.systems[id] = new systemClass(this.root);
this.systemUpdateOrder.push(id);
};
@@ -173,6 +187,7 @@ export class GameSystemManager {
// IMPORTANT: We have 2 phases: In phase 1 we compute the output values of all gates,
// processors etc. In phase 2 we propagate it through the wires network
add("logicGate", LogicGateSystem);
add("beltReader", BeltReaderSystem);
add("display", DisplaySystem);
@@ -187,6 +202,14 @@ export class GameSystemManager {
add("zone", ZoneSystem);
}
addBefore("end");
for (const key in MODS_ADDITIONAL_SYSTEMS) {
if (!this.systems[key] && key !== "end") {
logger.error("Mod system not attached due to invalid 'before': ", key);
}
}
logger.log("📦 There are", this.systemUpdateOrder.length, "game systems");
}

View File

@@ -9,6 +9,8 @@ import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards } from "./tutorial_goals";
export const MOD_ITEM_PROCESSOR_SPEEDS = {};
export class HubGoals extends BasicSerializableObject {
static getId() {
return "HubGoals";
@@ -433,7 +435,7 @@ export class HubGoals extends BasicSerializableObject {
}
const randomColor = () => rng.choice(colors);
const randomShape = () => rng.choice(Object.values(enumSubShape));
const randomShape = () => rng.choice(availableShapes);
let anyIsMissingTwo = false;
@@ -556,6 +558,9 @@ export class HubGoals extends BasicSerializableObject {
);
}
default:
if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) {
return MOD_ITEM_PROCESSOR_SPEEDS[processorType](this.root);
}
assertAlways(false, "invalid processor type: " + processorType);
}

View File

@@ -1,6 +1,7 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { Signal } from "../../core/signal";
import { MOD_SIGNALS } from "../../mods/mod_signals";
import { KEYMAPPINGS } from "../key_action_mapper";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
@@ -64,7 +65,7 @@ export class GameHUD {
/* typehints:end */
};
if (G_IS_DEV && globalConfig.debug.enableEntityInspector) {
if (G_IS_DEV) {
this.parts.entityDebugger = new HUDEntityDebugger(this.root);
}
@@ -89,8 +90,11 @@ export class GameHUD {
this.parts[partId] = new part(this.root);
}
MOD_SIGNALS.hudInitializer.dispatch(this.root);
const frag = document.createDocumentFragment();
for (const key in this.parts) {
MOD_SIGNALS.hudElementInitialized.dispatch(this.parts[key]);
this.parts[key].createElements(frag);
}
@@ -98,6 +102,7 @@ export class GameHUD {
for (const key in this.parts) {
this.parts[key].initialize();
MOD_SIGNALS.hudElementFinalized.dispatch(this.parts[key]);
}
this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this);

View File

@@ -1,4 +1,5 @@
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { globalWarn } from "../../../core/logging";
import { STOP_PROPAGATION } from "../../../core/signal";
import { makeDiv, safeModulo } from "../../../core/utils";
import { MetaBlockBuilding } from "../../buildings/block";
@@ -101,7 +102,12 @@ export class HUDBaseToolbar extends BaseHUDPart {
rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()];
}
const binding = actionMapper.getBinding(rawBinding);
if (rawBinding) {
const binding = actionMapper.getBinding(rawBinding);
binding.add(() => this.selectBuildingForPlacement(metaBuilding));
} else {
globalWarn("Building has no keybinding:", metaBuilding.getId());
}
const itemContainer = makeDiv(
this.primaryBuildings.includes(allBuildings[i]) ? rowPrimary : rowSecondary,
@@ -110,7 +116,6 @@ export class HUDBaseToolbar extends BaseHUDPart {
);
itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png");
itemContainer.setAttribute("data-id", metaBuilding.getId());
binding.add(() => this.selectBuildingForPlacement(metaBuilding));
const icon = makeDiv(itemContainer, null, ["icon"]);

View File

@@ -61,7 +61,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
this.currentInterpolatedCornerTile = new Vector();
this.lockIndicatorSprites = {};
layers.forEach(layer => {
[...layers, "error"].forEach(layer => {
this.lockIndicatorSprites[layer] = this.makeLockIndicatorSprite(layer);
});
@@ -76,7 +76,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
/**
* Makes the lock indicator sprite for the given layer
* @param {Layer} layer
* @param {string} layer
*/
makeLockIndicatorSprite(layer) {
const dims = 48;
@@ -126,12 +126,15 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()];
}
const binding = this.root.keyMapper.getBinding(rawBinding);
this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace(
"<key>",
"<code class='keybinding'>" + binding.getKeyCodeString() + "</code>"
);
if (rawBinding) {
const binding = this.root.keyMapper.getBinding(rawBinding);
this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace(
"<key>",
"<code class='keybinding'>" + binding.getKeyCodeString() + "</code>"
);
} else {
this.buildingInfoElements.hotkey.innerHTML = "";
}
this.buildingInfoElements.tutorialImage.setAttribute(
"data-icon",
@@ -355,7 +358,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
rotationVariant
);
const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity);
const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity, {});
// Fade in / out
parameters.context.lineWidth = 1;
@@ -394,6 +397,46 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
}
}
/**
* Checks if there are any entities in the way, returns true if there are
* @param {Vector} from
* @param {Vector} to
* @param {Vector[]=} ignorePositions
* @returns
*/
checkForObstales(from, to, ignorePositions = []) {
assert(from.x === to.x || from.y === to.y, "Must be a straight line");
const prop = from.x === to.x ? "y" : "x";
const current = from.copy();
const metaBuilding = this.currentMetaBuilding.get();
this.fakeEntity.layer = metaBuilding.getLayer();
const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.origin = current;
staticComp.rotation = 0;
metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get());
staticComp.code = getCodeFromBuildingData(
this.currentMetaBuilding.get(),
this.currentVariant.get(),
0
);
const start = Math.min(from[prop], to[prop]);
const end = Math.max(from[prop], to[prop]);
for (let i = start; i <= end; i++) {
current[prop] = i;
if (ignorePositions.some(p => p.distanceSquare(current) < 0.1)) {
continue;
}
if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) {
return true;
}
}
return false;
}
/**
* @param {DrawParameters} parameters
*/
@@ -404,55 +447,76 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
return;
}
const applyStyles = look => {
parameters.context.fillStyle = THEME.map.directionLock[look].color;
parameters.context.strokeStyle = THEME.map.directionLock[look].background;
parameters.context.lineWidth = 10;
};
if (!this.lastDragTile) {
// Not dragging yet
applyStyles(this.root.currentLayer);
const mouseWorld = this.root.camera.screenToWorld(mousePosition);
parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4);
parameters.context.fill();
return;
}
const mouseWorld = this.root.camera.screenToWorld(mousePosition);
const mouseTile = mouseWorld.toTileSpace();
parameters.context.fillStyle = THEME.map.directionLock[this.root.currentLayer].color;
parameters.context.strokeStyle = THEME.map.directionLock[this.root.currentLayer].background;
parameters.context.lineWidth = 10;
const startLine = this.lastDragTile.toWorldSpaceCenterOfTile();
const endLine = mouseTile.toWorldSpaceCenterOfTile();
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
const anyObstacle =
this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner, [
this.lastDragTile,
mouseTile,
]) ||
this.checkForObstales(this.currentDirectionLockCorner, mouseTile, [this.lastDragTile, mouseTile]);
if (anyObstacle) {
applyStyles("error");
} else {
applyStyles(this.root.currentLayer);
}
parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4);
parameters.context.fill();
if (this.lastDragTile) {
const startLine = this.lastDragTile.toWorldSpaceCenterOfTile();
const endLine = mouseTile.toWorldSpaceCenterOfTile();
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
parameters.context.beginCircle(startLine.x, startLine.y, 8);
parameters.context.fill();
parameters.context.beginCircle(startLine.x, startLine.y, 8);
parameters.context.fill();
parameters.context.beginPath();
parameters.context.moveTo(startLine.x, startLine.y);
parameters.context.lineTo(midLine.x, midLine.y);
parameters.context.lineTo(endLine.x, endLine.y);
parameters.context.stroke();
parameters.context.beginPath();
parameters.context.moveTo(startLine.x, startLine.y);
parameters.context.lineTo(midLine.x, midLine.y);
parameters.context.lineTo(endLine.x, endLine.y);
parameters.context.stroke();
parameters.context.beginCircle(endLine.x, endLine.y, 5);
parameters.context.fill();
parameters.context.beginCircle(endLine.x, endLine.y, 5);
parameters.context.fill();
// Draw arrow
const arrowSprite = this.lockIndicatorSprites[anyObstacle ? "error" : this.root.currentLayer];
const path = this.computeDirectionLockPath();
for (let i = 0; i < path.length - 1; i += 1) {
const { rotation, tile } = path[i];
const worldPos = tile.toWorldSpaceCenterOfTile();
const angle = Math.radians(rotation);
// Draw arrow
const arrowSprite = this.lockIndicatorSprites[this.root.currentLayer];
const path = this.computeDirectionLockPath();
for (let i = 0; i < path.length - 1; i += 1) {
const { rotation, tile } = path[i];
const worldPos = tile.toWorldSpaceCenterOfTile();
const angle = Math.radians(rotation);
parameters.context.translate(worldPos.x, worldPos.y);
parameters.context.rotate(angle);
parameters.context.drawImage(
arrowSprite,
-6,
-globalConfig.halfTileSize -
clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize +
globalConfig.halfTileSize -
6,
12,
12
);
parameters.context.rotate(-angle);
parameters.context.translate(-worldPos.x, -worldPos.y);
}
parameters.context.translate(worldPos.x, worldPos.y);
parameters.context.rotate(angle);
parameters.context.drawImage(
arrowSprite,
-6,
-globalConfig.halfTileSize -
clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize +
globalConfig.halfTileSize -
6,
12,
12
);
parameters.context.rotate(-angle);
parameters.context.translate(-worldPos.x, -worldPos.y);
}
}
@@ -473,7 +537,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
const offsetShift = 10;
/**
* @type {Array<import("../../components/item_acceptor").ItemAcceptorSlot>}
*/
let acceptorSlots = [];
/**
* @type {Array<import("../../components/item_ejector").ItemEjectorSlot>}
*/
let ejectorSlots = [];
if (ejectorComp) {
@@ -491,71 +561,65 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
acceptorSlots.push(fakeAcceptorSlot);
}
for (let acceptorSlotIndex = 0; acceptorSlotIndex < acceptorSlots.length; ++acceptorSlotIndex) {
const slot = acceptorSlots[acceptorSlotIndex];
// Go over all slots
for (let i = 0; i < acceptorSlots.length; ++i) {
const slot = acceptorSlots[i];
const acceptorSlotWsTile = staticComp.localTileToWorld(slot.pos);
const acceptorSlotWsPos = acceptorSlotWsTile.toWorldSpaceCenterOfTile();
// Go over all slots
for (
let acceptorDirectionIndex = 0;
acceptorDirectionIndex < slot.directions.length;
++acceptorDirectionIndex
) {
const direction = slot.directions[acceptorDirectionIndex];
const worldDirection = staticComp.localDirectionToWorld(direction);
const direction = slot.direction;
const worldDirection = staticComp.localDirectionToWorld(direction);
// Figure out which tile ejects to this slot
const sourceTile = acceptorSlotWsTile.add(enumDirectionToVector[worldDirection]);
// Figure out which tile ejects to this slot
const sourceTile = acceptorSlotWsTile.add(enumDirectionToVector[worldDirection]);
let isBlocked = false;
let isConnected = false;
let isBlocked = false;
let isConnected = false;
// Find all entities which are on that tile
const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y);
// Find all entities which are on that tile
const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y);
// Check for every entity:
for (let i = 0; i < sourceEntities.length; ++i) {
const sourceEntity = sourceEntities[i];
const sourceEjector = sourceEntity.components.ItemEjector;
const sourceBeltComp = sourceEntity.components.Belt;
const sourceStaticComp = sourceEntity.components.StaticMapEntity;
const ejectorAcceptLocalTile = sourceStaticComp.worldToLocalTile(acceptorSlotWsTile);
// Check for every entity:
for (let j = 0; j < sourceEntities.length; ++j) {
const sourceEntity = sourceEntities[j];
const sourceEjector = sourceEntity.components.ItemEjector;
const sourceBeltComp = sourceEntity.components.Belt;
const sourceStaticComp = sourceEntity.components.StaticMapEntity;
const ejectorAcceptLocalTile = sourceStaticComp.worldToLocalTile(acceptorSlotWsTile);
// If this entity is on the same layer as the slot - if so, it can either be
// connected, or it can not be connected and thus block the input
if (sourceEjector && sourceEjector.anySlotEjectsToLocalTile(ejectorAcceptLocalTile)) {
// This one is connected, all good
isConnected = true;
} else if (
sourceBeltComp &&
sourceStaticComp.localDirectionToWorld(sourceBeltComp.direction) ===
enumInvertedDirections[worldDirection]
) {
// Belt connected
isConnected = true;
} else {
// This one is blocked
isBlocked = true;
}
// If this entity is on the same layer as the slot - if so, it can either be
// connected, or it can not be connected and thus block the input
if (sourceEjector && sourceEjector.anySlotEjectsToLocalTile(ejectorAcceptLocalTile)) {
// This one is connected, all good
isConnected = true;
} else if (
sourceBeltComp &&
sourceStaticComp.localDirectionToWorld(sourceBeltComp.direction) ===
enumInvertedDirections[worldDirection]
) {
// Belt connected
isConnected = true;
} else {
// This one is blocked
isBlocked = true;
}
const alpha = isConnected || isBlocked ? 1.0 : 0.3;
const sprite = isBlocked ? badArrowSprite : goodArrowSprite;
parameters.context.globalAlpha = alpha;
drawRotatedSprite({
parameters,
sprite,
x: acceptorSlotWsPos.x,
y: acceptorSlotWsPos.y,
angle: Math.radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]),
size: 13,
offsetY: offsetShift + 13,
});
parameters.context.globalAlpha = 1;
}
const alpha = isConnected || isBlocked ? 1.0 : 0.3;
const sprite = isBlocked ? badArrowSprite : goodArrowSprite;
parameters.context.globalAlpha = alpha;
drawRotatedSprite({
parameters,
sprite,
x: acceptorSlotWsPos.x,
y: acceptorSlotWsPos.y,
angle: Math.radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]),
size: 13,
offsetY: offsetShift + 13,
});
parameters.context.globalAlpha = 1;
}
// Go over all slots

View File

@@ -1,7 +1,19 @@
import { THIRDPARTY_URLS } from "../../../core/config";
import { DialogWithForm } from "../../../core/modal_dialog_elements";
import { FormElementInput, FormElementItemChooser } from "../../../core/modal_dialog_forms";
import { STOP_PROPAGATION } from "../../../core/signal";
import { fillInLinkIntoTranslation } from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { BaseItem } from "../../base_item";
import { enumMouseButton } from "../../camera";
import { Entity } from "../../entity";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../../items/color_item";
import { BaseHUDPart } from "../base_hud_part";
import trim from "trim";
import { enumColors } from "../../colors";
import { ShapeDefinition } from "../../shape_definition";
export class HUDConstantSignalEdit extends BaseHUDPart {
initialize() {
@@ -23,7 +35,7 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
const constantComp = contents.components.ConstantSignal;
if (constantComp) {
if (button === enumMouseButton.left) {
this.root.systemMgr.systems.constantSignal.editConstantSignal(contents, {
this.editConstantSignal(contents, {
deleteOnCancel: false,
});
return STOP_PROPAGATION;
@@ -31,4 +43,171 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
}
}
}
/**
* Asks the entity to enter a valid signal code
* @param {Entity} entity
* @param {object} param0
* @param {boolean=} param0.deleteOnCancel
*/
editConstantSignal(entity, { deleteOnCancel = true }) {
if (!entity.components.ConstantSignal) {
return;
}
// Ok, query, but also save the uid because it could get stale
const uid = entity.uid;
const signal = entity.components.ConstantSignal.signal;
const signalValueInput = new FormElementInput({
id: "signalValue",
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val),
});
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
if (entity.components.WiredPins) {
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
)
);
} else {
// producer which can produce virtually anything
const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"];
items.unshift(
...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key))
);
}
if (this.root.gameMode.hasHub()) {
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition
)
);
}
if (this.root.hud.parts.pinnedShapes) {
items.push(
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
)
);
}
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items,
});
const dialog = new DialogWithForm({
app: this.root.app,
title: T.dialogs.editConstantProducer.title,
desc: T.dialogs.editSignal.descItems,
formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false,
});
this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal
const closeHandler = () => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
if (itemInput.chosenItem) {
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(() => {
closeHandler();
});
dialog.valueChosen.add(() => {
dialog.closeRequested.dispatch();
closeHandler();
});
// When cancelled, destroy the entity again
if (deleteOnCancel) {
dialog.buttonSignals.cancel.add(() => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
this.root.logic.tryDeleteBuilding(entityRef);
});
}
}
/**
* Tries to parse a signal code
* @param {Entity} entity
* @param {string} code
* @returns {BaseItem}
*/
parseSignalCode(entity, code) {
if (!this.root || !this.root.shapeDefinitionMgr) {
// Stale reference
return null;
}
code = trim(code);
const codeLower = code.toLowerCase();
if (enumColors[codeLower]) {
return COLOR_ITEM_SINGLETONS[codeLower];
}
if (entity.components.WiredPins) {
if (code === "1" || codeLower === "true") {
return BOOL_TRUE_SINGLETON;
}
if (code === "0" || codeLower === "false") {
return BOOL_FALSE_SINGLETON;
}
}
if (ShapeDefinition.isValidShortKey(code)) {
return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
}
return null;
}
}

View File

@@ -94,11 +94,12 @@ export class HUDEntityDebugger extends BaseHUDPart {
<div>`;
for (const property in val) {
const isRoot = val[property] == this.root;
const isRecursive = recursion.includes(val[property]);
let hiddenValue = isRoot ? "<root>" : null;
if (isRecursive) {
let hiddenValue = null;
if (val[property] == this.root) {
hiddenValue = "<root>";
} else if (val[property] instanceof Node) {
hiddenValue = `<${val[property].constructor.name}>`;
} else if (recursion.includes(val[property])) {
// Avoid recursion by not "expanding" object more than once
hiddenValue = "<recursion>";
}

View File

@@ -150,7 +150,7 @@ export class HUDModalDialogs extends BaseHUDPart {
internalShowDialog(dialog) {
const elem = dialog.createElement();
dialog.setIndex(this.dialogStack.length);
dialog.setIndex(1000 + this.dialogStack.length);
// Hide last dialog in queue
if (this.dialogStack.length > 0) {

View File

@@ -7,6 +7,9 @@ export const enumNotificationType = {
saved: "saved",
upgrade: "upgrade",
success: "success",
info: "info",
warning: "warning",
error: "error",
};
const notificationDuration = 3;
@@ -17,14 +20,14 @@ export class HUDNotifications extends BaseHUDPart {
}
initialize() {
this.root.hud.signals.notification.add(this.onNotification, this);
this.root.hud.signals.notification.add(this.internalShowNotification, this);
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */
this.notificationElements = [];
// Automatic notifications
this.root.signals.gameSaved.add(() =>
this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
this.internalShowNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
);
}
@@ -32,7 +35,7 @@ export class HUDNotifications extends BaseHUDPart {
* @param {string} message
* @param {enumNotificationType} type
*/
onNotification(message, type) {
internalShowNotification(message, type) {
const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
element.setAttribute("data-icon", "icons/notification_" + type + ".png");

View File

@@ -7,118 +7,152 @@ import { Application } from "../application";
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { IS_MOBILE } from "../core/config";
import { T } from "../translations";
function key(str) {
export function keyToKeyCode(str) {
return str.toUpperCase().charCodeAt(0);
}
const KEYCODE_UP_ARROW = 38;
const KEYCODE_DOWN_ARROW = 40;
const KEYCODE_LEFT_ARROW = 37;
const KEYCODE_RIGHT_ARROW = 39;
export const KEYCODES = {
Tab: 9,
Enter: 13,
Shift: 16,
Ctrl: 17,
Alt: 18,
Escape: 27,
Space: 32,
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40,
Delete: 46,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
Plus: 187,
Minus: 189,
};
export const KEYMAPPINGS = {
// Make sure mods come first so they can override everything
mods: {},
general: {
confirm: { keyCode: 13 }, // enter
back: { keyCode: 27, builtin: true }, // escape
confirm: { keyCode: KEYCODES.Enter },
back: { keyCode: KEYCODES.Escape, builtin: true },
},
ingame: {
menuOpenShop: { keyCode: key("F") },
menuOpenStats: { keyCode: key("G") },
menuClose: { keyCode: key("Q") },
menuOpenShop: { keyCode: keyToKeyCode("F") },
menuOpenStats: { keyCode: keyToKeyCode("G") },
menuClose: { keyCode: keyToKeyCode("Q") },
toggleHud: { keyCode: 113 }, // F2
exportScreenshot: { keyCode: 114 }, // F3PS
toggleFPSInfo: { keyCode: 115 }, // F4
toggleHud: { keyCode: KEYCODES.F2 },
exportScreenshot: { keyCode: KEYCODES.F3 },
toggleFPSInfo: { keyCode: KEYCODES.F4 },
switchLayers: { keyCode: key("E") },
switchLayers: { keyCode: keyToKeyCode("E") },
showShapeTooltip: { keyCode: 18 }, // ALT
showShapeTooltip: { keyCode: KEYCODES.Alt },
},
navigation: {
mapMoveUp: { keyCode: key("W") },
mapMoveRight: { keyCode: key("D") },
mapMoveDown: { keyCode: key("S") },
mapMoveLeft: { keyCode: key("A") },
mapMoveFaster: { keyCode: 16 }, //shift
mapMoveUp: { keyCode: keyToKeyCode("W") },
mapMoveRight: { keyCode: keyToKeyCode("D") },
mapMoveDown: { keyCode: keyToKeyCode("S") },
mapMoveLeft: { keyCode: keyToKeyCode("A") },
mapMoveFaster: { keyCode: KEYCODES.Shift },
centerMap: { keyCode: 32 }, // SPACE
mapZoomIn: { keyCode: 187, repeated: true }, // "+"
mapZoomOut: { keyCode: 189, repeated: true }, // "-"
createMarker: { keyCode: key("M") },
centerMap: { keyCode: KEYCODES.Space },
mapZoomIn: { keyCode: KEYCODES.Plus, repeated: true },
mapZoomOut: { keyCode: KEYCODES.Minus, repeated: true },
createMarker: { keyCode: keyToKeyCode("M") },
},
buildings: {
// Puzzle buildings
constant_producer: { keyCode: key("H") },
goal_acceptor: { keyCode: key("N") },
block: { keyCode: key("4") },
constant_producer: { keyCode: keyToKeyCode("H") },
goal_acceptor: { keyCode: keyToKeyCode("N") },
block: { keyCode: keyToKeyCode("4") },
// Primary Toolbar
belt: { keyCode: key("1") },
balancer: { keyCode: key("2") },
underground_belt: { keyCode: key("3") },
miner: { keyCode: key("4") },
cutter: { keyCode: key("5") },
rotater: { keyCode: key("6") },
stacker: { keyCode: key("7") },
mixer: { keyCode: key("8") },
painter: { keyCode: key("9") },
trash: { keyCode: key("0") },
belt: { keyCode: keyToKeyCode("1") },
balancer: { keyCode: keyToKeyCode("2") },
underground_belt: { keyCode: keyToKeyCode("3") },
miner: { keyCode: keyToKeyCode("4") },
cutter: { keyCode: keyToKeyCode("5") },
rotater: { keyCode: keyToKeyCode("6") },
stacker: { keyCode: keyToKeyCode("7") },
mixer: { keyCode: keyToKeyCode("8") },
painter: { keyCode: keyToKeyCode("9") },
trash: { keyCode: keyToKeyCode("0") },
// Sandbox
item_producer: { keyCode: key("L") },
item_producer: { keyCode: keyToKeyCode("L") },
// Secondary toolbar
storage: { keyCode: key("Y") },
reader: { keyCode: key("U") },
lever: { keyCode: key("I") },
filter: { keyCode: key("O") },
display: { keyCode: key("P") },
storage: { keyCode: keyToKeyCode("Y") },
reader: { keyCode: keyToKeyCode("U") },
lever: { keyCode: keyToKeyCode("I") },
filter: { keyCode: keyToKeyCode("O") },
display: { keyCode: keyToKeyCode("P") },
// Wires toolbar
wire: { keyCode: key("1") },
wire_tunnel: { keyCode: key("2") },
constant_signal: { keyCode: key("3") },
logic_gate: { keyCode: key("4") },
virtual_processor: { keyCode: key("5") },
analyzer: { keyCode: key("6") },
comparator: { keyCode: key("7") },
transistor: { keyCode: key("8") },
wire: { keyCode: keyToKeyCode("1") },
wire_tunnel: { keyCode: keyToKeyCode("2") },
constant_signal: { keyCode: keyToKeyCode("3") },
logic_gate: { keyCode: keyToKeyCode("4") },
virtual_processor: { keyCode: keyToKeyCode("5") },
analyzer: { keyCode: keyToKeyCode("6") },
comparator: { keyCode: keyToKeyCode("7") },
transistor: { keyCode: keyToKeyCode("8") },
},
placement: {
pipette: { keyCode: key("Q") },
rotateWhilePlacing: { keyCode: key("R") },
rotateInverseModifier: { keyCode: 16 }, // SHIFT
rotateToUp: { keyCode: KEYCODE_UP_ARROW },
rotateToDown: { keyCode: KEYCODE_DOWN_ARROW },
rotateToRight: { keyCode: KEYCODE_RIGHT_ARROW },
rotateToLeft: { keyCode: KEYCODE_LEFT_ARROW },
cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB
switchDirectionLockSide: { keyCode: key("R") },
pipette: { keyCode: keyToKeyCode("Q") },
rotateWhilePlacing: { keyCode: keyToKeyCode("R") },
rotateInverseModifier: { keyCode: KEYCODES.Shift },
rotateToUp: { keyCode: KEYCODES.ArrowUp },
rotateToDown: { keyCode: KEYCODES.ArrowDown },
rotateToRight: { keyCode: KEYCODES.ArrowRight },
rotateToLeft: { keyCode: KEYCODES.ArrowLeft },
cycleBuildingVariants: { keyCode: keyToKeyCode("T") },
cycleBuildings: { keyCode: KEYCODES.Tab },
switchDirectionLockSide: { keyCode: keyToKeyCode("R") },
copyWireValue: { keyCode: key("Z") },
copyWireValue: { keyCode: keyToKeyCode("Z") },
},
massSelect: {
massSelectStart: { keyCode: 17 }, // CTRL
massSelectSelectMultiple: { keyCode: 16 }, // SHIFT
massSelectCopy: { keyCode: key("C") },
massSelectCut: { keyCode: key("X") },
massSelectClear: { keyCode: key("B") },
confirmMassDelete: { keyCode: 46 }, // DEL
pasteLastBlueprint: { keyCode: key("V") },
massSelectStart: { keyCode: KEYCODES.Ctrl },
massSelectSelectMultiple: { keyCode: KEYCODES.Shift },
massSelectCopy: { keyCode: keyToKeyCode("C") },
massSelectCut: { keyCode: keyToKeyCode("X") },
massSelectClear: { keyCode: keyToKeyCode("B") },
confirmMassDelete: { keyCode: KEYCODES.Delete },
pasteLastBlueprint: { keyCode: keyToKeyCode("V") },
},
placementModifiers: {
lockBeltDirection: { keyCode: 16 }, // SHIFT
placementDisableAutoOrientation: { keyCode: 17 }, // CTRL
placeMultiple: { keyCode: 16 }, // SHIFT
placeInverse: { keyCode: 18 }, // ALT
lockBeltDirection: { keyCode: KEYCODES.Shift },
placementDisableAutoOrientation: { keyCode: KEYCODES.Ctrl },
placeMultiple: { keyCode: KEYCODES.Shift },
placeInverse: { keyCode: KEYCODES.Alt },
},
};
@@ -153,23 +187,23 @@ export function getStringForKeyCode(code) {
return "MB5";
case 8:
return "⌫";
case 9:
case KEYCODES.Tab:
return T.global.keys.tab;
case 13:
case KEYCODES.Enter:
return "⏎";
case 16:
case KEYCODES.Shift:
return "⇪";
case 17:
case KEYCODES.Ctrl:
return T.global.keys.control;
case 18:
case KEYCODES.Alt:
return T.global.keys.alt;
case 19:
return "PAUSE";
case 20:
return "CAPS";
case 27:
case KEYCODES.Escape:
return T.global.keys.escape;
case 32:
case KEYCODES.Space:
return T.global.keys.space;
case 33:
return "PGUP";
@@ -179,13 +213,13 @@ export function getStringForKeyCode(code) {
return "END";
case 36:
return "HOME";
case KEYCODE_LEFT_ARROW:
case KEYCODES.ArrowLeft:
return "⬅";
case KEYCODE_UP_ARROW:
case KEYCODES.ArrowUp:
return "⬆";
case KEYCODE_RIGHT_ARROW:
case KEYCODES.ArrowRight:
return "➡";
case KEYCODE_DOWN_ARROW:
case KEYCODES.ArrowDown:
return "⬇";
case 44:
return "PRNT";
@@ -225,29 +259,29 @@ export function getStringForKeyCode(code) {
return ".";
case 111:
return "/";
case 112:
case KEYCODES.F1:
return "F1";
case 113:
case KEYCODES.F2:
return "F2";
case 114:
case KEYCODES.F3:
return "F3";
case 115:
case KEYCODES.F4:
return "F4";
case 116:
case KEYCODES.F5:
return "F5";
case 117:
case KEYCODES.F6:
return "F6";
case 118:
case KEYCODES.F7:
return "F7";
case 119:
case KEYCODES.F8:
return "F8";
case 120:
case KEYCODES.F9:
return "F9";
case 121:
case KEYCODES.F10:
return "F10";
case 122:
case KEYCODES.F11:
return "F11";
case 123:
case KEYCODES.F12:
return "F12";
case 144:
@@ -296,8 +330,9 @@ export class Keybinding {
* @param {number} param0.keyCode
* @param {boolean=} param0.builtin
* @param {boolean=} param0.repeated
* @param {{ shift?: boolean; alt?: boolean; ctrl?: boolean; }=} param0.modifiers
*/
constructor(keyMapper, app, { keyCode, builtin = false, repeated = false }) {
constructor(keyMapper, app, { keyCode, builtin = false, repeated = false, modifiers = {} }) {
assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode);
this.keyMapper = keyMapper;
this.app = app;
@@ -305,6 +340,8 @@ export class Keybinding {
this.builtin = builtin;
this.repeated = repeated;
this.modifiers = modifiers;
this.signal = new Signal();
this.toggled = new Signal();
}
@@ -395,7 +432,6 @@ export class KeyActionMapper {
if (overrides[key]) {
payload.keyCode = overrides[key];
}
this.keybindings[key] = new Keybinding(this, this.root.app, payload);
if (G_IS_DEV) {
@@ -459,9 +495,10 @@ export class KeyActionMapper {
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
* @param {boolean=} param0.initial
*/
handleKeydown({ keyCode, shift, alt, initial }) {
handleKeydown({ keyCode, shift, alt, ctrl, initial }) {
let stop = false;
// Find mapping
@@ -469,6 +506,18 @@ export class KeyActionMapper {
/** @type {Keybinding} */
const binding = this.keybindings[key];
if (binding.keyCode === keyCode && (initial || binding.repeated)) {
if (binding.modifiers.shift && !shift) {
continue;
}
if (binding.modifiers.ctrl && !ctrl) {
continue;
}
if (binding.modifiers.alt && !alt) {
continue;
}
/** @type {Signal} */
const signal = this.keybindings[key].signal;
if (signal.dispatch() === STOP_PROPAGATION) {
@@ -505,4 +554,14 @@ export class KeyActionMapper {
assert(this.keybindings[id], "Keybinding " + id + " not known!");
return this.keybindings[id];
}
/**
* Returns a given keybinding
* @param {string} id
* @returns {Keybinding}
*/
getBindingById(id) {
assert(this.keybindings[id], "Keybinding " + id + " not known!");
return this.keybindings[id];
}
}

View File

@@ -53,10 +53,12 @@ export class GameLogic {
/**
* Checks if the given entity can be placed
* @param {Entity} entity
* @param {Vector=} offset Optional, move the entity by the given offset first
* @param {Object} param0
* @param {boolean=} param0.allowReplaceBuildings
* @param {Vector=} param0.offset Optional, move the entity by the given offset first
* @returns {boolean} true if the entity could be placed there
*/
checkCanPlaceEntity(entity, offset = null) {
checkCanPlaceEntity(entity, { allowReplaceBuildings = true, offset = null }) {
// Compute area of the building
const rect = entity.components.StaticMapEntity.getTileSpaceBounds();
if (offset) {
@@ -71,7 +73,7 @@ export class GameLogic {
const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer);
if (otherEntity) {
const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding();
if (!metaClass.getIsReplaceable()) {
if (!allowReplaceBuildings || !metaClass.getIsReplaceable()) {
// This one is a direct blocker
return false;
}
@@ -116,7 +118,7 @@ export class GameLogic {
rotationVariant,
variant,
});
if (this.checkCanPlaceEntity(entity)) {
if (this.checkCanPlaceEntity(entity, {})) {
this.freeEntityAreaBeforeBuild(entity);
this.root.map.placeStaticEntity(entity);
this.root.entityMgr.registerEntity(entity);
@@ -393,7 +395,14 @@ export class GameLogic {
const entity = this.root.map.getLayerContentXY(tile.x + dx, tile.y + dy, "regular");
if (entity) {
/**
* @type {Array<import("./components/item_ejector").ItemEjectorSlot>}
*/
let ejectorSlots = [];
/**
* @type {Array<import("./components/item_acceptor").ItemAcceptorSlot>}
*/
let acceptorSlots = [];
const staticComp = entity.components.StaticMapEntity;
@@ -434,19 +443,16 @@ export class GameLogic {
for (let acceptorSlot = 0; acceptorSlot < acceptorSlots.length; ++acceptorSlot) {
const slot = acceptorSlots[acceptorSlot];
const wsTile = staticComp.localTileToWorld(slot.pos);
for (let k = 0; k < slot.directions.length; ++k) {
const direction = slot.directions[k];
const wsDirection = staticComp.localDirectionToWorld(direction);
const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]);
if (sourceTile.equals(tile)) {
acceptors.push({
entity,
slot,
toTile: wsTile,
fromDirection: wsDirection,
});
}
const direction = slot.direction;
const wsDirection = staticComp.localDirectionToWorld(direction);
const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]);
if (sourceTile.equals(tile)) {
acceptors.push({
entity,
slot,
toTile: wsTile,
fromDirection: wsDirection,
});
}
}
}

View File

@@ -13,6 +13,11 @@ import { Rectangle } from "../core/rectangle";
const logger = createLogger("map_chunk");
/**
* @type {Object<string, (distanceToOriginInChunks: number) => number>}
*/
export const MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS = {};
export class MapChunk {
/**
*
@@ -192,6 +197,10 @@ export class MapChunk {
[enumSubShape.windmill]: Math.round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)),
};
for (const key in MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS) {
weights[key] = MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[key](distanceToOriginInChunks);
}
if (distanceToOriginInChunks < 7) {
// Initial chunks can not spawn the good stuff
weights[enumSubShape.star] = 0;
@@ -274,6 +283,17 @@ export class MapChunk {
const chunkCenter = new Vector(this.x, this.y).addScalar(0.5);
const distanceToOriginInChunks = Math.round(chunkCenter.length());
this.generatePatches({ rng, chunkCenter, distanceToOriginInChunks });
}
/**
*
* @param {object} param0
* @param {RandomNumberGenerator} param0.rng
* @param {Vector} param0.chunkCenter
* @param {number} param0.distanceToOriginInChunks
*/
generatePatches({ rng, chunkCenter, distanceToOriginInChunks }) {
// Determine how likely it is that there is a color patch
const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5;
@@ -424,7 +444,7 @@ export class MapChunk {
* Sets the chunks contents
* @param {number} tileX
* @param {number} tileY
* @param {Entity=} contents
* @param {Entity} contents
* @param {Layer} layer
*/
setLayerContentFromWorldCords(tileX, tileY, contents, layer) {

View File

@@ -2,10 +2,9 @@ import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { drawSpriteClipped } from "../core/draw_utils";
import { safeModulo } from "../core/utils";
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
import { GameRoot } from "./root";
export const CHUNK_OVERLAY_RES = 3;
export class MapChunkAggregate {
/**
*

View File

@@ -5,10 +5,20 @@ import { Entity } from "./entity";
import { MapChunk } from "./map_chunk";
import { GameRoot } from "./root";
import { THEME } from "./theme";
import { drawSpriteClipped } from "../core/draw_utils";
export const CHUNK_OVERLAY_RES = 3;
export const MOD_CHUNK_DRAW_HOOKS = {
backgroundLayerBefore: [],
backgroundLayerAfter: [],
foregroundDynamicBefore: [],
foregroundDynamicAfter: [],
staticBefore: [],
staticAfter: [],
};
export class MapChunkView extends MapChunk {
/**
*
@@ -42,6 +52,11 @@ export class MapChunkView extends MapChunk {
*/
drawBackgroundLayer(parameters) {
const systems = this.root.systemMgr.systems;
MOD_CHUNK_DRAW_HOOKS.backgroundLayerBefore.forEach(systemId =>
systems[systemId].drawChunk(parameters, this)
);
if (systems.zone) {
systems.zone.drawChunk(parameters, this);
}
@@ -52,6 +67,10 @@ export class MapChunkView extends MapChunk {
systems.beltUnderlays.drawChunk(parameters, this);
systems.belt.drawChunk(parameters, this);
MOD_CHUNK_DRAW_HOOKS.backgroundLayerAfter.forEach(systemId =>
systems[systemId].drawChunk(parameters, this)
);
}
/**
@@ -61,9 +80,17 @@ export class MapChunkView extends MapChunk {
drawForegroundDynamicLayer(parameters) {
const systems = this.root.systemMgr.systems;
MOD_CHUNK_DRAW_HOOKS.foregroundDynamicBefore.forEach(systemId =>
systems[systemId].drawChunk(parameters, this)
);
systems.itemEjector.drawChunk(parameters, this);
systems.itemAcceptor.drawChunk(parameters, this);
systems.miner.drawChunk(parameters, this);
MOD_CHUNK_DRAW_HOOKS.foregroundDynamicAfter.forEach(systemId =>
systems[systemId].drawChunk(parameters, this)
);
}
/**
@@ -73,6 +100,8 @@ export class MapChunkView extends MapChunk {
drawForegroundStaticLayer(parameters) {
const systems = this.root.systemMgr.systems;
MOD_CHUNK_DRAW_HOOKS.staticBefore.forEach(systemId => systems[systemId].drawChunk(parameters, this));
systems.staticMapEntities.drawChunk(parameters, this);
systems.lever.drawChunk(parameters, this);
systems.display.drawChunk(parameters, this);
@@ -80,6 +109,8 @@ export class MapChunkView extends MapChunk {
systems.constantProducer.drawChunk(parameters, this);
systems.goalAcceptor.drawChunk(parameters, this);
systems.itemProcessorOverlays.drawChunk(parameters, this);
MOD_CHUNK_DRAW_HOOKS.staticAfter.forEach(systemId => systems[systemId].drawChunk(parameters, this));
}
/**

View File

@@ -18,6 +18,16 @@ export class MetaBuilding {
this.id = id;
}
/**
* Should return all possible variants of this building, no matter
* if they are already available or will be unlocked later on
*
* @returns {Array<{ variant: string, rotationVariant?: number, internalId?: number|string }>}
*/
static getAllVariantCombinations() {
throw new Error("implement getAllVariantCombinations");
}
/**
* Returns the id of this building
*/

View File

@@ -2,202 +2,89 @@ import { gMetaBuildingRegistry } from "../core/global_registries";
import { createLogger } from "../core/logging";
import { T } from "../translations";
import { MetaAnalyzerBuilding } from "./buildings/analyzer";
import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer";
import { MetaBalancerBuilding } from "./buildings/balancer";
import { MetaBeltBuilding } from "./buildings/belt";
import { MetaBlockBuilding } from "./buildings/block";
import { MetaComparatorBuilding } from "./buildings/comparator";
import { MetaConstantProducerBuilding } from "./buildings/constant_producer";
import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter";
import { MetaCutterBuilding } from "./buildings/cutter";
import { MetaDisplayBuilding } from "./buildings/display";
import { MetaFilterBuilding } from "./buildings/filter";
import { MetaGoalAcceptorBuilding } from "./buildings/goal_acceptor";
import { MetaHubBuilding } from "./buildings/hub";
import { MetaItemProducerBuilding } from "./buildings/item_producer";
import { MetaLeverBuilding } from "./buildings/lever";
import { enumLogicGateVariants, MetaLogicGateBuilding } from "./buildings/logic_gate";
import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner";
import { MetaLogicGateBuilding } from "./buildings/logic_gate";
import { MetaMinerBuilding } from "./buildings/miner";
import { MetaMixerBuilding } from "./buildings/mixer";
import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter";
import { MetaPainterBuilding } from "./buildings/painter";
import { MetaReaderBuilding } from "./buildings/reader";
import { enumRotaterVariants, MetaRotaterBuilding } from "./buildings/rotater";
import { MetaRotaterBuilding } from "./buildings/rotater";
import { MetaStackerBuilding } from "./buildings/stacker";
import { MetaStorageBuilding } from "./buildings/storage";
import { enumTransistorVariants, MetaTransistorBuilding } from "./buildings/transistor";
import { MetaTransistorBuilding } from "./buildings/transistor";
import { MetaTrashBuilding } from "./buildings/trash";
import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt";
import { enumVirtualProcessorVariants, MetaVirtualProcessorBuilding } from "./buildings/virtual_processor";
import { MetaUndergroundBeltBuilding } from "./buildings/underground_belt";
import { MetaVirtualProcessorBuilding } from "./buildings/virtual_processor";
import { MetaWireBuilding } from "./buildings/wire";
import { MetaWireTunnelBuilding } from "./buildings/wire_tunnel";
import { buildBuildingCodeCache, gBuildingVariants, registerBuildingVariant } from "./building_codes";
import { enumWireVariant } from "./components/wire";
import { KEYMAPPINGS } from "./key_action_mapper";
import { defaultBuildingVariant } from "./meta_building";
import { defaultBuildingVariant, MetaBuilding } from "./meta_building";
const logger = createLogger("building_registry");
export function initMetaBuildingRegistry() {
gMetaBuildingRegistry.register(MetaBalancerBuilding);
gMetaBuildingRegistry.register(MetaMinerBuilding);
gMetaBuildingRegistry.register(MetaCutterBuilding);
gMetaBuildingRegistry.register(MetaRotaterBuilding);
gMetaBuildingRegistry.register(MetaStackerBuilding);
gMetaBuildingRegistry.register(MetaMixerBuilding);
gMetaBuildingRegistry.register(MetaPainterBuilding);
gMetaBuildingRegistry.register(MetaTrashBuilding);
gMetaBuildingRegistry.register(MetaStorageBuilding);
gMetaBuildingRegistry.register(MetaBeltBuilding);
gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding);
gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding);
gMetaBuildingRegistry.register(MetaHubBuilding);
gMetaBuildingRegistry.register(MetaWireBuilding);
gMetaBuildingRegistry.register(MetaConstantSignalBuilding);
gMetaBuildingRegistry.register(MetaLogicGateBuilding);
gMetaBuildingRegistry.register(MetaLeverBuilding);
gMetaBuildingRegistry.register(MetaFilterBuilding);
gMetaBuildingRegistry.register(MetaWireTunnelBuilding);
gMetaBuildingRegistry.register(MetaDisplayBuilding);
gMetaBuildingRegistry.register(MetaVirtualProcessorBuilding);
gMetaBuildingRegistry.register(MetaReaderBuilding);
gMetaBuildingRegistry.register(MetaTransistorBuilding);
gMetaBuildingRegistry.register(MetaAnalyzerBuilding);
gMetaBuildingRegistry.register(MetaComparatorBuilding);
gMetaBuildingRegistry.register(MetaItemProducerBuilding);
gMetaBuildingRegistry.register(MetaConstantProducerBuilding);
gMetaBuildingRegistry.register(MetaBlockBuilding);
// Belt
registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0);
registerBuildingVariant(2, MetaBeltBuilding, defaultBuildingVariant, 1);
registerBuildingVariant(3, MetaBeltBuilding, defaultBuildingVariant, 2);
// Balancer
registerBuildingVariant(4, MetaBalancerBuilding);
registerBuildingVariant(5, MetaBalancerBuilding, enumBalancerVariants.merger);
registerBuildingVariant(6, MetaBalancerBuilding, enumBalancerVariants.mergerInverse);
registerBuildingVariant(47, MetaBalancerBuilding, enumBalancerVariants.splitter);
registerBuildingVariant(48, MetaBalancerBuilding, enumBalancerVariants.splitterInverse);
// Miner
registerBuildingVariant(7, MetaMinerBuilding);
registerBuildingVariant(8, MetaMinerBuilding, enumMinerVariants.chainable);
// Cutter
registerBuildingVariant(9, MetaCutterBuilding);
registerBuildingVariant(10, MetaCutterBuilding, enumCutterVariants.quad);
// Rotater
registerBuildingVariant(11, MetaRotaterBuilding);
registerBuildingVariant(12, MetaRotaterBuilding, enumRotaterVariants.ccw);
registerBuildingVariant(13, MetaRotaterBuilding, enumRotaterVariants.rotate180);
// Stacker
registerBuildingVariant(14, MetaStackerBuilding);
// Mixer
registerBuildingVariant(15, MetaMixerBuilding);
// Painter
registerBuildingVariant(16, MetaPainterBuilding);
registerBuildingVariant(17, MetaPainterBuilding, enumPainterVariants.mirrored);
registerBuildingVariant(18, MetaPainterBuilding, enumPainterVariants.double);
registerBuildingVariant(19, MetaPainterBuilding, enumPainterVariants.quad);
// Trash
registerBuildingVariant(20, MetaTrashBuilding);
// Storage
registerBuildingVariant(21, MetaStorageBuilding);
// Underground belt
registerBuildingVariant(22, MetaUndergroundBeltBuilding, defaultBuildingVariant, 0);
registerBuildingVariant(23, MetaUndergroundBeltBuilding, defaultBuildingVariant, 1);
registerBuildingVariant(24, MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2, 0);
registerBuildingVariant(25, MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2, 1);
// Hub
registerBuildingVariant(26, MetaHubBuilding);
// Wire
registerBuildingVariant(27, MetaWireBuilding, defaultBuildingVariant, 0);
registerBuildingVariant(28, MetaWireBuilding, defaultBuildingVariant, 1);
registerBuildingVariant(29, MetaWireBuilding, defaultBuildingVariant, 2);
registerBuildingVariant(30, MetaWireBuilding, defaultBuildingVariant, 3);
registerBuildingVariant(52, MetaWireBuilding, enumWireVariant.second, 0);
registerBuildingVariant(53, MetaWireBuilding, enumWireVariant.second, 1);
registerBuildingVariant(54, MetaWireBuilding, enumWireVariant.second, 2);
registerBuildingVariant(55, MetaWireBuilding, enumWireVariant.second, 3);
// Constant signal
registerBuildingVariant(31, MetaConstantSignalBuilding);
// Logic gate
registerBuildingVariant(32, MetaLogicGateBuilding);
registerBuildingVariant(34, MetaLogicGateBuilding, enumLogicGateVariants.not);
registerBuildingVariant(35, MetaLogicGateBuilding, enumLogicGateVariants.xor);
registerBuildingVariant(36, MetaLogicGateBuilding, enumLogicGateVariants.or);
// Transistor
registerBuildingVariant(38, MetaTransistorBuilding, defaultBuildingVariant);
registerBuildingVariant(60, MetaTransistorBuilding, enumTransistorVariants.mirrored);
// Lever
registerBuildingVariant(33, MetaLeverBuilding);
// Filter
registerBuildingVariant(37, MetaFilterBuilding);
// Wire tunnel
registerBuildingVariant(39, MetaWireTunnelBuilding);
// Display
registerBuildingVariant(40, MetaDisplayBuilding);
// Virtual Processor
registerBuildingVariant(42, MetaVirtualProcessorBuilding);
registerBuildingVariant(44, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.rotater);
registerBuildingVariant(45, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.unstacker);
registerBuildingVariant(50, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.stacker);
registerBuildingVariant(51, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.painter);
// Analyzer
registerBuildingVariant(46, MetaComparatorBuilding);
registerBuildingVariant(43, MetaAnalyzerBuilding);
// Reader
registerBuildingVariant(49, MetaReaderBuilding);
// Item producer
registerBuildingVariant(61, MetaItemProducerBuilding);
// Constant producer
registerBuildingVariant(62, MetaConstantProducerBuilding);
// Goal acceptor
registerBuildingVariant(63, MetaGoalAcceptorBuilding);
// Block
registerBuildingVariant(64, MetaBlockBuilding);
// Propagate instances
for (const key in gBuildingVariants) {
gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass(
gBuildingVariants[key].metaClass
/**
*
* @param {typeof MetaBuilding} metaBuilding
*/
export function registerBuildingVariants(metaBuilding) {
gMetaBuildingRegistry.register(metaBuilding);
const combinations = metaBuilding.getAllVariantCombinations();
combinations.forEach(combination => {
registerBuildingVariant(
combination.internalId,
metaBuilding,
combination.variant || defaultBuildingVariant,
combination.rotationVariant || 0
);
}
});
}
for (const key in gBuildingVariants) {
const variant = gBuildingVariants[key];
assert(variant.metaClass, "Variant has no meta: " + key);
export function initMetaBuildingRegistry() {
const buildings = [
MetaBalancerBuilding,
MetaMinerBuilding,
MetaCutterBuilding,
MetaRotaterBuilding,
MetaStackerBuilding,
MetaMixerBuilding,
MetaPainterBuilding,
MetaTrashBuilding,
MetaStorageBuilding,
MetaBeltBuilding,
MetaUndergroundBeltBuilding,
MetaGoalAcceptorBuilding,
MetaHubBuilding,
MetaWireBuilding,
MetaConstantSignalBuilding,
MetaLogicGateBuilding,
MetaLeverBuilding,
MetaFilterBuilding,
MetaWireTunnelBuilding,
MetaDisplayBuilding,
MetaVirtualProcessorBuilding,
MetaReaderBuilding,
MetaTransistorBuilding,
MetaAnalyzerBuilding,
MetaComparatorBuilding,
MetaItemProducerBuilding,
MetaConstantProducerBuilding,
MetaBlockBuilding,
];
if (typeof variant.rotationVariant === "undefined") {
variant.rotationVariant = 0;
}
if (typeof variant.variant === "undefined") {
variant.variant = defaultBuildingVariant;
}
}
buildings.forEach(registerBuildingVariants);
// Check for valid keycodes
if (G_IS_DEV) {
@@ -205,18 +92,15 @@ export function initMetaBuildingRegistry() {
const id = metaBuilding.getId();
if (!["hub"].includes(id)) {
if (!KEYMAPPINGS.buildings[id]) {
assertAlways(
false,
console.error(
"Building " + id + " has no keybinding assigned! Add it to key_action_mapper.js"
);
}
if (!T.buildings[id]) {
assertAlways(false, "Translation for building " + id + " missing!");
}
if (!T.buildings[id].default) {
assertAlways(false, "Translation for building " + id + " missing (default variant)!");
console.error("Translation for building " + id + " missing!");
} else if (!T.buildings[id].default) {
console.error("Translation for building " + id + " missing (default variant)!");
}
}
});

View File

@@ -30,7 +30,7 @@ import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
import { MetaBlockBuilding } from "../buildings/block";
import { MetaBuilding } from "../meta_building";
import { gMetaBuildingRegistry } from "../../core/global_registries";
import { HUDPuzzleNextPuzzle } from "../hud/parts/HUDPuzzleNextPuzzle";
import { HUDPuzzleNextPuzzle } from "../hud/parts/next_puzzle";
const logger = createLogger("puzzle-play");
const copy = require("clipboard-copy");

View File

@@ -38,6 +38,7 @@ import { HUDSandboxController } from "../hud/parts/sandbox_controller";
import { queryParamOptions } from "../../core/query_parameters";
import { MetaBlockBuilding } from "../buildings/block";
import { MetaItemProducerBuilding } from "../buildings/item_producer";
import { MOD_SIGNALS } from "../../mods/mod_signals";
/** @typedef {{
* shape: string,
@@ -68,10 +69,16 @@ const tierGrowth = 2.5;
const chinaShapes = G_WEGAME_VERSION || G_CHINA_VERSION;
const upgradesCache = {};
/**
* Generates all upgrades
* @returns {Object<string, UpgradeTiers>} */
function generateUpgrades(limitedVersion = false) {
if (upgradesCache[limitedVersion]) {
return upgradesCache[limitedVersion];
}
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1;
@@ -264,6 +271,8 @@ function generateUpgrades(limitedVersion = false) {
}
}
MOD_SIGNALS.modifyUpgrades.dispatch(upgrades);
// VALIDATE
if (G_IS_DEV) {
for (const upgradeId in upgrades) {
@@ -279,14 +288,20 @@ function generateUpgrades(limitedVersion = false) {
}
}
upgradesCache[limitedVersion] = upgrades;
return upgrades;
}
const levelDefinitionsCache = {};
/**
* Generates the level definitions
* @param {boolean} limitedVersion
*/
export function generateLevelDefinitions(limitedVersion = false) {
if (levelDefinitionsCache[limitedVersion]) {
return levelDefinitionsCache[limitedVersion];
}
const levelDefinitions = [
// 1
// Circle
@@ -511,6 +526,8 @@ export function generateLevelDefinitions(limitedVersion = false) {
]),
];
MOD_SIGNALS.modifyLevelDefinitions.dispatch(levelDefinitions);
if (G_IS_DEV) {
levelDefinitions.forEach(({ shape }) => {
try {
@@ -521,15 +538,11 @@ export function generateLevelDefinitions(limitedVersion = false) {
});
}
levelDefinitionsCache[limitedVersion] = levelDefinitions;
return levelDefinitions;
}
const fullVersionUpgrades = generateUpgrades(false);
const demoVersionUpgrades = generateUpgrades(true);
const fullVersionLevels = generateLevelDefinitions(false);
const demoVersionLevels = generateLevelDefinitions(true);
export class RegularGameMode extends GameMode {
static getId() {
return enumGameModeIds.regular;
@@ -589,7 +602,7 @@ export class RegularGameMode extends GameMode {
/** @type {(typeof MetaBuilding)[]} */
this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding];
// @ts-expect-error
// @ts-ignore
if (!(G_IS_DEV || window.sandboxMode || queryParamOptions.sandboxMode)) {
this.hiddenBuildings.push(MetaItemProducerBuilding);
}
@@ -600,9 +613,7 @@ export class RegularGameMode extends GameMode {
* @returns {Object<string, UpgradeTiers>}
*/
getUpgrades() {
return this.root.app.restrictionMgr.getHasExtendedUpgrades()
? fullVersionUpgrades
: demoVersionUpgrades;
return generateUpgrades(!this.root.app.restrictionMgr.getHasExtendedUpgrades());
}
/**
@@ -610,9 +621,7 @@ export class RegularGameMode extends GameMode {
* @returns {Array<LevelDefinition>}
*/
getLevelDefinitions() {
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()
? fullVersionLevels
: demoVersionLevels;
return generateLevelDefinitions(!this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay());
}
/**

View File

@@ -7,6 +7,19 @@ import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors";
import { THEME } from "./theme";
/**
* @typedef {{
* context: CanvasRenderingContext2D,
* quadrantSize: number,
* layerScale: number,
* }} SubShapeDrawOptions
*/
/**
* @type {Object<string, (options: SubShapeDrawOptions) => void>}
*/
export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {};
/**
* @typedef {{
* subShape: enumSubShape,
@@ -14,6 +27,11 @@ import { THEME } from "./theme";
* }} ShapeLayerItem
*/
export const TOP_RIGHT = 0;
export const BOTTOM_RIGHT = 1;
export const BOTTOM_LEFT = 2;
export const TOP_LEFT = 3;
/**
* Order is Q1 (tr), Q2(br), Q3(bl), Q4(tl)
* @typedef {[ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?]} ShapeLayer
@@ -51,7 +69,7 @@ for (const key in enumSubShapeToShortcode) {
/**
* Converts the given parameters to a valid shape definition
* @param {*} layers
* @returns {Array<import("./shape_definition").ShapeLayer>}
* @returns {Array<ShapeLayer>}
*/
export function createSimpleShape(layers) {
layers.forEach(layer => {
@@ -229,7 +247,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* Internal method to clone the shape definition
* @returns {Array<ShapeLayer>}
*/
internalCloneLayers() {
getClonedLayers() {
return JSON.parse(JSON.stringify(this.layers));
}
@@ -366,74 +384,79 @@ export class ShapeDefinition extends BasicSerializableObject {
context.strokeStyle = THEME.items.outline;
context.lineWidth = THEME.items.outlineWidth;
const insetPadding = 0.0;
if (MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]) {
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]({
context,
layerScale,
quadrantSize,
});
} else {
switch (subShape) {
case enumSubShape.rect: {
context.beginPath();
const dims = quadrantSize * layerScale;
context.rect(-quadrantHalfSize, quadrantHalfSize - dims, dims, dims);
context.fill();
context.stroke();
break;
}
case enumSubShape.star: {
context.beginPath();
const dims = quadrantSize * layerScale;
switch (subShape) {
case enumSubShape.rect: {
context.beginPath();
const dims = quadrantSize * layerScale;
context.rect(
insetPadding + -quadrantHalfSize,
-insetPadding + quadrantHalfSize - dims,
dims,
dims
);
let originX = -quadrantHalfSize;
let originY = quadrantHalfSize - dims;
break;
}
case enumSubShape.star: {
context.beginPath();
const dims = quadrantSize * layerScale;
const moveInwards = dims * 0.4;
context.moveTo(originX, originY + moveInwards);
context.lineTo(originX + dims, originY);
context.lineTo(originX + dims - moveInwards, originY + dims);
context.lineTo(originX, originY + dims);
context.closePath();
context.fill();
context.stroke();
break;
}
let originX = insetPadding - quadrantHalfSize;
let originY = -insetPadding + quadrantHalfSize - dims;
case enumSubShape.windmill: {
context.beginPath();
const dims = quadrantSize * layerScale;
const moveInwards = dims * 0.4;
context.moveTo(originX, originY + moveInwards);
context.lineTo(originX + dims, originY);
context.lineTo(originX + dims - moveInwards, originY + dims);
context.lineTo(originX, originY + dims);
context.closePath();
break;
}
let originX = -quadrantHalfSize;
let originY = quadrantHalfSize - dims;
const moveInwards = dims * 0.4;
context.moveTo(originX, originY + moveInwards);
context.lineTo(originX + dims, originY);
context.lineTo(originX + dims, originY + dims);
context.lineTo(originX, originY + dims);
context.closePath();
context.fill();
context.stroke();
break;
}
case enumSubShape.windmill: {
context.beginPath();
const dims = quadrantSize * layerScale;
case enumSubShape.circle: {
context.beginPath();
context.moveTo(-quadrantHalfSize, quadrantHalfSize);
context.arc(
-quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.5,
0
);
context.closePath();
context.fill();
context.stroke();
break;
}
let originX = insetPadding - quadrantHalfSize;
let originY = -insetPadding + quadrantHalfSize - dims;
const moveInwards = dims * 0.4;
context.moveTo(originX, originY + moveInwards);
context.lineTo(originX + dims, originY);
context.lineTo(originX + dims, originY + dims);
context.lineTo(originX, originY + dims);
context.closePath();
break;
}
case enumSubShape.circle: {
context.beginPath();
context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize);
context.arc(
insetPadding + -quadrantHalfSize,
-insetPadding + quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.5,
0
);
context.closePath();
break;
}
default: {
assertAlways(false, "Unkown sub shape: " + subShape);
default: {
throw new Error("Unkown sub shape: " + subShape);
}
}
}
context.fill();
context.stroke();
context.rotate(-rotation);
context.translate(-centerQuadrantX, -centerQuadrantY);
}
@@ -446,7 +469,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
cloneFilteredByQuadrants(includeQuadrants) {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];
let anyContents = false;
@@ -472,7 +495,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
cloneRotateCW() {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];
quadrants.unshift(quadrants[3]);
@@ -486,7 +509,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
cloneRotateCCW() {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];
quadrants.push(quadrants[0]);
@@ -500,7 +523,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
cloneRotate180() {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];
quadrants.push(quadrants.shift(), quadrants.shift());
@@ -558,7 +581,7 @@ export class ShapeDefinition extends BasicSerializableObject {
// Can't merge at a layer lower than 0
const layerToMergeAt = Math.max(1 - smallestGapBetweenShapes, 0);
const mergedLayers = this.internalCloneLayers();
const mergedLayers = this.getClonedLayers();
for (let layer = mergedLayers.length; layer < layerToMergeAt + topShapeLayers.length; ++layer) {
mergedLayers.push([null, null, null, null]);
}
@@ -584,7 +607,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @param {enumColors} color
*/
cloneAndPaintWith(color) {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];
@@ -603,7 +626,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* @param {[enumColors, enumColors, enumColors, enumColors]} colors
*/
cloneAndPaintWith4Colors(colors) {
const newLayers = this.internalCloneLayers();
const newLayers = this.getClonedLayers();
for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) {
const quadrants = newLayers[layerIndex];

View File

@@ -11,6 +11,7 @@ import { arrayBeltVariantToRotation, MetaBeltBuilding } from "../buildings/belt"
import { getCodeFromBuildingData } from "../building_codes";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
import { defaultBuildingVariant } from "../meta_building";
@@ -22,9 +23,9 @@ const logger = createLogger("belt");
/**
* Manages all belts
*/
export class BeltSystem extends GameSystemWithFilter {
export class BeltSystem extends GameSystem {
constructor(root) {
super(root, [BeltComponent]);
super(root);
/**
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
@@ -425,8 +426,10 @@ export class BeltSystem extends GameSystemWithFilter {
const result = [];
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const beltEntities = this.root.entityMgr.getAllWithComponent(BeltComponent);
for (let i = 0; i < beltEntities.length; ++i) {
const entity = beltEntities[i];
if (visitedUids.has(entity.uid)) {
continue;
}
@@ -494,6 +497,10 @@ export class BeltSystem extends GameSystemWithFilter {
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
// Limit speed to avoid belts going backwards
const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10);

View File

@@ -16,7 +16,7 @@ import { BeltUnderlaysComponent, enumClippedBeltUnderlayType } from "../componen
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { GameSystem } from "../game_system";
import { MapChunkView } from "../map_chunk_view";
import { BELT_ANIM_COUNT } from "./belt";
@@ -31,9 +31,9 @@ const enumUnderlayTypeToClipRect = {
[enumClippedBeltUnderlayType.bottomOnly]: new Rectangle(0, 0.5, 1, 0.5),
};
export class BeltUnderlaysSystem extends GameSystemWithFilter {
export class BeltUnderlaysSystem extends GameSystem {
constructor(root) {
super(root, [BeltUnderlaysComponent]);
super(root);
this.underlayBeltSprites = [];
@@ -113,12 +113,10 @@ export class BeltUnderlaysSystem extends GameSystemWithFilter {
continue;
}
// Step 2: Check if any of the directions matches
for (let j = 0; j < slot.directions.length; ++j) {
const slotDirection = staticComp.localDirectionToWorld(slot.directions[j]);
if (slotDirection === fromDirection) {
return true;
}
// Step 2: Check if the direction matches
const slotDirection = staticComp.localDirectionToWorld(slot.direction);
if (slotDirection === fromDirection) {
return true;
}
}
}

View File

@@ -5,10 +5,8 @@ import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunk } from "../map_chunk";
import { GameRoot } from "../root";
export class ConstantProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [ConstantSignalComponent, ItemProducerComponent]);
}

View File

@@ -1,25 +1,16 @@
import trim from "trim";
import { THIRDPARTY_URLS } from "../../core/config";
import { DialogWithForm } from "../../core/modal_dialog_elements";
import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms";
import { fillInLinkIntoTranslation } from "../../core/utils";
import { T } from "../../translations";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { ConstantSignalComponent } from "../components/constant_signal";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition";
export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ConstantSignalComponent]);
this.root.signals.entityManuallyPlaced.add(entity =>
this.editConstantSignal(entity, { deleteOnCancel: true })
);
this.root.signals.entityManuallyPlaced.add(entity => {
const editorHud = this.root.hud.parts.constantSignalEdit;
if (editorHud) {
editorHud.editConstantSignal(entity, { deleteOnCancel: true });
}
});
}
update() {
@@ -34,171 +25,4 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
}
}
}
/**
* Asks the entity to enter a valid signal code
* @param {Entity} entity
* @param {object} param0
* @param {boolean=} param0.deleteOnCancel
*/
editConstantSignal(entity, { deleteOnCancel = true }) {
if (!entity.components.ConstantSignal) {
return;
}
// Ok, query, but also save the uid because it could get stale
const uid = entity.uid;
const signal = entity.components.ConstantSignal.signal;
const signalValueInput = new FormElementInput({
id: "signalValue",
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val),
});
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
if (entity.components.WiredPins) {
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
)
);
} else {
// producer which can produce virtually anything
const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"];
items.unshift(
...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key))
);
}
if (this.root.gameMode.hasHub()) {
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition
)
);
}
if (this.root.hud.parts.pinnedShapes) {
items.push(
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
)
);
}
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items,
});
const dialog = new DialogWithForm({
app: this.root.app,
title: T.dialogs.editConstantProducer.title,
desc: T.dialogs.editSignal.descItems,
formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false,
});
this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal
const closeHandler = () => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
if (itemInput.chosenItem) {
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(() => {
closeHandler();
});
dialog.valueChosen.add(() => {
dialog.closeRequested.dispatch();
closeHandler();
});
// When cancelled, destroy the entity again
if (deleteOnCancel) {
dialog.buttonSignals.cancel.add(() => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
this.root.logic.tryDeleteBuilding(entityRef);
});
}
}
/**
* Tries to parse a signal code
* @param {Entity} entity
* @param {string} code
* @returns {BaseItem}
*/
parseSignalCode(entity, code) {
if (!this.root || !this.root.shapeDefinitionMgr) {
// Stale reference
return null;
}
code = trim(code);
const codeLower = code.toLowerCase();
if (enumColors[codeLower]) {
return COLOR_ITEM_SINGLETONS[codeLower];
}
if (entity.components.WiredPins) {
if (code === "1" || codeLower === "true") {
return BOOL_TRUE_SINGLETON;
}
if (code === "0" || codeLower === "false") {
return BOOL_FALSE_SINGLETON;
}
}
if (ShapeDefinition.isValidShortKey(code)) {
return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
}
return null;
}
}

View File

@@ -2,15 +2,14 @@ import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { DisplayComponent } from "../components/display";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { GameSystem } from "../game_system";
import { isTrueItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { MapChunkView } from "../map_chunk_view";
export class DisplaySystem extends GameSystemWithFilter {
export class DisplaySystem extends GameSystem {
constructor(root) {
super(root, [DisplayComponent]);
super(root);
/** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */
this.displaySprites = {};

View File

@@ -5,10 +5,8 @@ import { Vector } from "../../core/vector";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunk } from "../map_chunk";
import { GameRoot } from "../root";
export class GoalAcceptorSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [GoalAcceptorComponent]);

View File

@@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
import { StaleAreaDetector } from "../../core/stale_area_detector";
import { enumDirection, enumDirectionToVector } from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { BaseItem } from "../base_item";
import { BeltComponent } from "../components/belt";
import { ItemAcceptorComponent } from "../components/item_acceptor";
@@ -204,11 +203,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
if (this.tryPassOverItem(item, destEntity, destSlot.index)) {
// Handover successful, clear slot
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
targetAcceptorComp.onItemAccepted(
destSlot.index,
destSlot.acceptedDirection,
item
);
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item);
}
sourceSlot.item = null;
continue;

View File

@@ -8,7 +8,7 @@ import {
} from "../components/item_processor";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item";
import { isTruthyItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
@@ -38,6 +38,11 @@ const MAX_QUEUED_CHARGES = 2;
* }} ProcessorImplementationPayload
*/
/**
* @type {Object<string, (ProcessorImplementationPayload) => void>}
*/
export const MOD_ITEM_PROCESSOR_HANDLERS = {};
export class ItemProcessorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemProcessorComponent]);
@@ -61,6 +66,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
[enumItemProcessorTypes.hub]: this.process_HUB,
[enumItemProcessorTypes.reader]: this.process_READER,
[enumItemProcessorTypes.goal]: this.process_GOAL,
...MOD_ITEM_PROCESSOR_HANDLERS,
};
// Bind all handlers
@@ -88,49 +94,16 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
}
}
// Check if it finished
if (currentCharge.remainingTime <= 0.0) {
// Check if it finished and we don't already have queued ejects
if (currentCharge.remainingTime <= 0.0 && !processorComp.queuedEjects.length) {
const itemsToEject = currentCharge.items;
// Go over all items and try to eject them
// Go over all items and add them to the queue
for (let j = 0; j < itemsToEject.length; ++j) {
const { item, requiredSlot, preferredSlot } = itemsToEject[j];
assert(ejectorComp, "To eject items, the building needs to have an ejector");
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
if (ejectorComp.canEjectOnSlot(requiredSlot)) {
slot = requiredSlot;
}
} else if (preferredSlot !== null && preferredSlot !== undefined) {
// We have a slot preference, try using it but otherwise use a free slot
if (ejectorComp.canEjectOnSlot(preferredSlot)) {
slot = preferredSlot;
} else {
slot = ejectorComp.getFirstFreeSlot();
}
} else {
// We can eject on any slot
slot = ejectorComp.getFirstFreeSlot();
}
if (slot !== null) {
// Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) {
assert(false, "Failed to eject");
} else {
itemsToEject.splice(j, 1);
j -= 1;
}
}
processorComp.queuedEjects.push(itemsToEject[j]);
}
// If the charge was entirely emptied to the outputs, start the next charge
if (itemsToEject.length === 0) {
processorComp.ongoingCharges.shift();
}
processorComp.ongoingCharges.shift();
}
}
@@ -140,6 +113,40 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
this.startNewCharge(entity);
}
}
for (let j = 0; j < processorComp.queuedEjects.length; ++j) {
const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j];
assert(ejectorComp, "To eject items, the building needs to have an ejector");
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
if (ejectorComp.canEjectOnSlot(requiredSlot)) {
slot = requiredSlot;
}
} else if (preferredSlot !== null && preferredSlot !== undefined) {
// We have a slot preference, try using it but otherwise use a free slot
if (ejectorComp.canEjectOnSlot(preferredSlot)) {
slot = preferredSlot;
} else {
slot = ejectorComp.getFirstFreeSlot();
}
} else {
// We can eject on any slot
slot = ejectorComp.getFirstFreeSlot();
}
if (slot !== null) {
// Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) {
assert(false, "Failed to eject");
} else {
processorComp.queuedEjects.splice(j, 1);
j -= 1;
}
}
}
}
}

View File

@@ -1,12 +1,7 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter";
export class ItemProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [ItemProducerComponent]);
this.item = null;

View File

@@ -1,9 +1,8 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { LeverComponent } from "../components/lever";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { LeverComponent } from "../components/lever";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
export class LeverSystem extends GameSystemWithFilter {
constructor(root) {

View File

@@ -1,9 +1,9 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { StorageComponent } from "../components/storage";
import { DrawParameters } from "../../core/draw_parameters";
import { formatBigNumber, lerp } from "../../core/utils";
import { Loader } from "../../core/loader";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { formatBigNumber, lerp } from "../../core/utils";
import { StorageComponent } from "../components/storage";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
export class StorageSystem extends GameSystemWithFilter {
@@ -50,8 +50,13 @@ export class StorageSystem extends GameSystemWithFilter {
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
pinsComp.slots[0].value = storageComp.storedItem;
pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
// a wired pins component is not guaranteed, but if its there, set the value
if (pinsComp) {
pinsComp.slots[0].value = storageComp.storedItem;
pinsComp.slots[1].value = storageComp.getIsFull()
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
}
}

View File

@@ -21,6 +21,7 @@ import { enumWireType, enumWireVariant, WireComponent } from "../components/wire
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity";
import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { isTruthyItem } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
@@ -90,9 +91,9 @@ export class WireNetwork {
}
}
export class WireSystem extends GameSystemWithFilter {
export class WireSystem extends GameSystem {
constructor(root) {
super(root, [WireComponent]);
super(root);
/**
* @type {Object<enumWireVariant, Object<enumWireType, AtlasSprite>>}

View File

@@ -1,5 +1,4 @@
{
"uiStyle": "dark",
"map": {
"background": "#3e3f47",
"grid": "rgba(255, 255, 255, 0.02)",
@@ -19,6 +18,10 @@
"wires": {
"color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)"
},
"error": {
"color": "rgb(255, 137, 137)",
"background": "rgba(255, 137, 137, 0.2)"
}
},

View File

@@ -1,5 +1,4 @@
{
"uiStyle": "light",
"map": {
"background": "#fff",
"grid": "#fafafa",
@@ -19,6 +18,10 @@
"wires": {
"color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)"
},
"error": {
"color": "rgb(255, 137, 137)",
"background": "rgba(255, 137, 137, 0.2)"
}
},

8
src/js/globals.d.ts vendored
View File

@@ -22,6 +22,10 @@ declare const G_IS_RELEASE: boolean;
declare const G_CHINA_VERSION: boolean;
declare const G_WEGAME_VERSION: boolean;
declare const shapez: any;
declare const ipcRenderer: any;
// Polyfills
declare interface String {
replaceAll(search: string, replacement: string): string;
@@ -92,9 +96,11 @@ declare interface Window {
cpmstarAPI: any;
// Mods
registerMod: any;
$shapez_registerMod: any;
anyModLoaded: any;
shapez: any;
webkitRequestAnimationFrame();
assert(condition: boolean, failureMessage: string);

View File

@@ -2,6 +2,8 @@ import "./core/polyfills";
import "./core/assert";
import "./core/error_handler";
import "./mods/modloader";
import { createLogger, logSection } from "./core/logging";
import { Application } from "./application";
import { IS_DEBUG } from "./core/config";
@@ -19,39 +21,8 @@ if (window.coreThreadLoadedCb) {
window.coreThreadLoadedCb();
}
// Logrocket
// if (!G_IS_DEV && !G_IS_STANDALONE) {
// const monthlyUsers = 300; // thousand
// const logrocketLimit = 10; // thousand
// const percentageOfUsers = logrocketLimit / monthlyUsers;
// if (Math.random() <= percentageOfUsers) {
// logger.log("Analyzing this session with logrocket");
// const logrocket = require("logrocket");
// logrocket.init("p1x9zh/shapezio");
// try {
// logrocket.getSessionURL(function (sessionURL) {
// logger.log("Connected lockrocket to GA");
// // @ts-ignore
// try {
// window.ga("send", {
// hitType: "event",
// eventCategory: "LogRocket",
// eventAction: sessionURL,
// });
// } catch (ex) {
// logger.warn("Logrocket connection to analytics failed:", ex);
// }
// });
// } catch (ex) {
// logger.warn("Logrocket connection to analytics failed:", ex);
// }
// }
// }
console.log(
`%cshapez.io %c\n© 2020 Tobias Springer IT Solutions\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date(
`%cshapez.io %c\n© 2022 tobspr Games\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date(
G_BUILD_TIME
).toLocaleString()}\n`,
"font-size: 35px; font-family: Arial;font-weight: bold; padding: 10px 0;",

36
src/js/mods/mod.js Normal file
View File

@@ -0,0 +1,36 @@
/* typehints:start */
import { Application } from "../application";
import { ModLoader } from "./modloader";
/* typehints:end */
import { MOD_SIGNALS } from "./mod_signals";
export class Mod {
/**
* @param {object} param0
* @param {Application} param0.app
* @param {ModLoader} param0.modLoader
* @param {import("./modloader").ModMetadata} param0.meta
* @param {Object} param0.settings
* @param {() => Promise<void>} param0.saveSettings
*/
constructor({ app, modLoader, meta, settings, saveSettings }) {
this.app = app;
this.modLoader = modLoader;
this.metadata = meta;
this.signals = MOD_SIGNALS;
this.modInterface = modLoader.modInterface;
this.settings = settings;
this.saveSettings = saveSettings;
}
init() {
// to be overridden
}
get dialogs() {
return this.modInterface.dialogs;
}
}

View File

@@ -0,0 +1,658 @@
/* typehints:start */
import { ModLoader } from "./modloader";
import { GameSystem } from "../game/game_system";
import { Component } from "../game/component";
import { MetaBuilding } from "../game/meta_building";
/* typehints:end */
import { defaultBuildingVariant } from "../game/meta_building";
import { AtlasSprite, SpriteAtlasLink } from "../core/sprites";
import {
enumShortcodeToSubShape,
enumSubShape,
enumSubShapeToShortcode,
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS,
} from "../game/shape_definition";
import { Loader } from "../core/loader";
import { LANGUAGES } from "../languages";
import { matchDataRecursive, T } from "../translations";
import { gBuildingVariants, registerBuildingVariant } from "../game/building_codes";
import { gComponentRegistry, gMetaBuildingRegistry } from "../core/global_registries";
import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk";
import { MODS_ADDITIONAL_SYSTEMS } from "../game/game_system_manager";
import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view";
import { KEYMAPPINGS } from "../game/key_action_mapper";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { THEMES } from "../game/theme";
import { ModMetaBuilding } from "./mod_meta_building";
import { BaseHUDPart } from "../game/hud/base_hud_part";
import { Vector } from "../core/vector";
import { GameRoot } from "../game/root";
/**
* @typedef {{new(...args: any[]): any, prototype: any}} constructable
*/
/**
* @template {(...args: any) => any} F The function
* @template {object} T The value of this
* @typedef {(this: T, ...args: Parameters<F>) => ReturnType<F>} bindThis
*/
/**
* @template {(...args: any[]) => any} F
* @template P
* @typedef {(...args: [P, Parameters<F>]) => ReturnType<F>} beforePrams IMPORTANT: this puts the original parameters into an array
*/
/**
* @template {(...args: any[]) => any} F
* @template P
* @typedef {(...args: [...Parameters<F>, P]) => ReturnType<F>} afterPrams
*/
/**
* @template {(...args: any[]) => any} F
* @typedef {(...args: [...Parameters<F>, ...any]) => ReturnType<F>} extendsPrams
*/
export class ModInterface {
/**
*
* @param {ModLoader} modLoader
*/
constructor(modLoader) {
this.modLoader = modLoader;
}
registerCss(cssString) {
// Preprocess css
cssString = cssString.replace(/\$scaled\(([^)]*)\)/gim, (substr, expression) => {
return "calc((" + expression + ") * var(--ui-scale))";
});
const element = document.createElement("style");
element.textContent = cssString;
document.head.appendChild(element);
}
registerSprite(spriteId, base64string) {
assert(base64string.startsWith("data:image"));
const img = new Image();
const sprite = new AtlasSprite(spriteId);
sprite.frozen = true;
img.addEventListener("load", () => {
for (const resolution in sprite.linksByResolution) {
const link = sprite.linksByResolution[resolution];
link.w = img.width;
link.h = img.height;
link.packedW = img.width;
link.packedH = img.height;
}
});
img.src = base64string;
const link = new SpriteAtlasLink({
w: 1,
h: 1,
atlas: img,
packOffsetX: 0,
packOffsetY: 0,
packedW: 1,
packedH: 1,
packedX: 0,
packedY: 0,
});
sprite.linksByResolution["0.25"] = link;
sprite.linksByResolution["0.5"] = link;
sprite.linksByResolution["0.75"] = link;
Loader.sprites.set(spriteId, sprite);
}
/**
*
* @param {string} imageBase64
* @param {string} jsonTextData
*/
registerAtlas(imageBase64, jsonTextData) {
const atlasData = JSON.parse(jsonTextData);
const img = new Image();
img.src = imageBase64;
const sourceData = atlasData.frames;
for (const spriteName in sourceData) {
const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName];
const sprite = new AtlasSprite(spriteName);
Loader.sprites.set(spriteName, sprite);
sprite.frozen = true;
const link = new SpriteAtlasLink({
packedX: frame.x,
packedY: frame.y,
packedW: frame.w,
packedH: frame.h,
packOffsetX: spriteSourceSize.x,
packOffsetY: spriteSourceSize.y,
atlas: img,
w: sourceSize.w,
h: sourceSize.h,
});
sprite.linksByResolution["0.25"] = link;
sprite.linksByResolution["0.5"] = link;
sprite.linksByResolution["0.75"] = link;
}
}
/**
*
* @param {object} param0
* @param {string} param0.id
* @param {string} param0.shortCode
* @param {(distanceToOriginInChunks: number) => number} param0.weightComputation
* @param {(options: import("../game/shape_definition").SubShapeDrawOptions) => void} param0.draw
*/
registerSubShapeType({ id, shortCode, weightComputation, draw }) {
if (shortCode.length !== 1) {
throw new Error("Bad short code: " + shortCode);
}
enumSubShape[id] = id;
enumSubShapeToShortcode[id] = shortCode;
enumShortcodeToSubShape[shortCode] = id;
MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[id] = weightComputation;
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = draw;
}
registerTranslations(language, translations) {
const data = LANGUAGES[language];
if (!data) {
throw new Error("Unknown language: " + language);
}
matchDataRecursive(data.data, translations, true);
if (language === "en") {
matchDataRecursive(T, translations, true);
}
}
/**
*
* @param {typeof Component} component
*/
registerComponent(component) {
gComponentRegistry.register(component);
}
/**
*
* @param {Object} param0
* @param {string} param0.id
* @param {new (any) => GameSystem} param0.systemClass
* @param {string=} param0.before
* @param {string[]=} param0.drawHooks
*/
registerGameSystem({ id, systemClass, before, drawHooks }) {
const key = before || "key";
const payload = { id, systemClass };
if (MODS_ADDITIONAL_SYSTEMS[key]) {
MODS_ADDITIONAL_SYSTEMS[key].push(payload);
} else {
MODS_ADDITIONAL_SYSTEMS[key] = [payload];
}
if (drawHooks) {
drawHooks.forEach(hookId => this.registerGameSystemDrawHook(hookId, id));
}
}
/**
*
* @param {string} hookId
* @param {string} systemId
*/
registerGameSystemDrawHook(hookId, systemId) {
if (!MOD_CHUNK_DRAW_HOOKS[hookId]) {
throw new Error("bad game system draw hook: " + hookId);
}
MOD_CHUNK_DRAW_HOOKS[hookId].push(systemId);
}
/**
*
* @param {object} param0
* @param {typeof ModMetaBuilding} param0.metaClass
* @param {string=} param0.buildingIconBase64
*/
registerNewBuilding({ metaClass, buildingIconBase64 }) {
const id = new /** @type {new (...args) => ModMetaBuilding} */ (metaClass)().getId();
if (gMetaBuildingRegistry.hasId(id)) {
throw new Error("Tried to register building twice: " + id);
}
gMetaBuildingRegistry.register(metaClass);
const metaInstance = gMetaBuildingRegistry.findByClass(metaClass);
T.buildings[id] = {};
metaClass.getAllVariantCombinations().forEach(combination => {
const variant = combination.variant || defaultBuildingVariant;
const rotationVariant = combination.rotationVariant || 0;
const buildingIdentifier = id + (variant === defaultBuildingVariant ? "" : "-" + variant);
const uniqueTypeId = buildingIdentifier + (rotationVariant === 0 ? "" : "-" + rotationVariant);
registerBuildingVariant(uniqueTypeId, metaClass, variant, rotationVariant);
gBuildingVariants[id].metaInstance = metaInstance;
this.registerTranslations("en", {
buildings: {
[id]: {
[variant]: {
name: combination.name || "Name",
description: combination.description || "Description",
},
},
},
});
if (combination.regularImageBase64) {
this.registerSprite(
"sprites/buildings/" + buildingIdentifier + ".png",
combination.regularImageBase64
);
}
if (combination.blueprintImageBase64) {
this.registerSprite(
"sprites/blueprints/" + buildingIdentifier + ".png",
combination.blueprintImageBase64
);
}
if (combination.tutorialImageBase64) {
this.setBuildingTutorialImage(id, variant, combination.tutorialImageBase64);
}
});
if (buildingIconBase64) {
this.setBuildingToolbarIcon(id, buildingIconBase64);
}
}
/**
*
* @param {Object} param0
* @param {string} param0.id
* @param {number} param0.keyCode
* @param {string} param0.translation
* @param {boolean=} param0.repeated
* @param {((GameRoot) => void)=} param0.handler
* @param {{shift?: boolean; alt?: boolean; ctrl?: boolean}=} param0.modifiers
* @param {boolean=} param0.builtin
*/
registerIngameKeybinding({
id,
keyCode,
translation,
modifiers = {},
repeated = false,
builtin = false,
handler = null,
}) {
if (!KEYMAPPINGS.mods) {
KEYMAPPINGS.mods = {};
}
const binding = (KEYMAPPINGS.mods[id] = {
keyCode,
id,
repeated,
modifiers,
builtin,
});
this.registerTranslations("en", {
keybindings: {
mappings: {
[id]: translation,
},
},
});
if (handler) {
this.modLoader.signals.gameStarted.add(root => {
root.keyMapper.getBindingById(id).addToTop(handler.bind(null, root));
});
}
return binding;
}
/**
* @returns {HUDModalDialogs}
*/
get dialogs() {
const state = this.modLoader.app.stateMgr.currentState;
// @ts-ignore
if (state.dialogs) {
// @ts-ignore
return state.dialogs;
}
throw new Error("Tried to access dialogs but current state doesn't support it");
}
setBuildingToolbarIcon(buildingId, iconBase64) {
this.registerCss(`
[data-icon="building_icons/${buildingId}.png"] .icon {
background-image: url('${iconBase64}') !important;
}
`);
}
/**
*
* @param {string | (new () => MetaBuilding)} buildingIdOrClass
* @param {*} variant
* @param {*} imageBase64
*/
setBuildingTutorialImage(buildingIdOrClass, variant, imageBase64) {
if (typeof buildingIdOrClass === "function") {
buildingIdOrClass = new buildingIdOrClass().id;
}
const buildingIdentifier =
buildingIdOrClass + (variant === defaultBuildingVariant ? "" : "-" + variant);
this.registerCss(`
[data-icon="building_tutorials/${buildingIdentifier}.png"] {
background-image: url('${imageBase64}') !important;
}
`);
}
/**
* @param {Object} param0
* @param {string} param0.id
* @param {string} param0.name
* @param {Object} param0.theme
*/
registerGameTheme({ id, name, theme }) {
THEMES[id] = theme;
this.registerTranslations("en", {
settings: {
labels: {
theme: {
themes: {
[id]: name,
},
},
},
},
});
}
/**
* Registers a new state class, should be a GameState derived class
* @param {typeof import("../core/game_state").GameState} stateClass
*/
registerGameState(stateClass) {
this.modLoader.app.stateMgr.register(stateClass);
}
/**
* @param {object} param0
* @param {"regular"|"wires"} param0.toolbar
* @param {"primary"|"secondary"} param0.location
* @param {typeof MetaBuilding} param0.metaClass
*/
addNewBuildingToToolbar({ toolbar, location, metaClass }) {
const hudElementName = toolbar === "wires" ? "HUDWiresToolbar" : "HUDBuildingsToolbar";
const property = location === "secondary" ? "secondaryBuildings" : "primaryBuildings";
this.modLoader.signals.hudElementInitialized.add(element => {
if (element.constructor.name === hudElementName) {
element[property].push(metaClass);
}
});
}
/**
* Patches a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will override the old one
* @param {C} classHandle
* @param {M} methodName
* @param {bindThis<beforePrams<O, P[M]>, InstanceType<C>>} override
*/
replaceMethod(classHandle, methodName, override) {
const oldMethod = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
//@ts-ignore This is true I just cant tell it that arguments will be Arguments<O>
return override.call(this, oldMethod.bind(this), arguments);
};
}
/**
* Runs before a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle
* @param {M} methodName
* @param {bindThis<O, InstanceType<C>>} executeBefore
*/
runBeforeMethod(classHandle, methodName, executeBefore) {
const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
//@ts-ignore Same as above
executeBefore.apply(this, arguments);
return oldHandle.apply(this, arguments);
};
}
/**
* Runs after a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle
* @param {M} methodName
* @param {bindThis<O, InstanceType<C>>} executeAfter
*/
runAfterMethod(classHandle, methodName, executeAfter) {
const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
const returnValue = oldHandle.apply(this, arguments);
//@ts-ignore
executeAfter.apply(this, arguments);
return returnValue;
};
}
/**
*
* @param {Object} prototype
* @param {({ $super, $old }) => any} extender
*/
extendObject(prototype, extender) {
const $super = Object.getPrototypeOf(prototype);
const $old = {};
const extensionMethods = extender({ $super, $old });
const properties = Array.from(Object.getOwnPropertyNames(extensionMethods));
properties.forEach(propertyName => {
if (["constructor", "prototype"].includes(propertyName)) {
return;
}
$old[propertyName] = prototype[propertyName];
prototype[propertyName] = extensionMethods[propertyName];
});
}
/**
*
* @param {Class} classHandle
* @param {({ $super, $old }) => any} extender
*/
extendClass(classHandle, extender) {
this.extendObject(classHandle.prototype, extender);
}
/**
*
* @param {string} id
* @param {new (...args) => BaseHUDPart} element
*/
registerHudElement(id, element) {
this.modLoader.signals.hudInitializer.add(root => {
root.hud.parts[id] = new element(root);
});
}
/**
*
* @param {string | (new () => MetaBuilding)} buildingIdOrClass
* @param {string} variant
* @param {object} param0
* @param {string} param0.name
* @param {string} param0.description
* @param {string=} param0.language
*/
registerBuildingTranslation(buildingIdOrClass, variant, { name, description, language = "en" }) {
if (typeof buildingIdOrClass === "function") {
buildingIdOrClass = new buildingIdOrClass().id;
}
this.registerTranslations(language, {
buildings: {
[buildingIdOrClass]: {
[variant]: {
name,
description,
},
},
},
});
}
/**
*
* @param {string | (new () => MetaBuilding)} buildingIdOrClass
* @param {string} variant
* @param {object} param2
* @param {string=} param2.regularBase64
* @param {string=} param2.blueprintBase64
*/
registerBuildingSprites(buildingIdOrClass, variant, { regularBase64, blueprintBase64 }) {
if (typeof buildingIdOrClass === "function") {
buildingIdOrClass = new buildingIdOrClass().id;
}
const spriteId =
buildingIdOrClass + (variant === defaultBuildingVariant ? "" : "-" + variant) + ".png";
if (regularBase64) {
this.registerSprite("sprites/buildings/" + spriteId, regularBase64);
}
if (blueprintBase64) {
this.registerSprite("sprites/blueprints/" + spriteId, blueprintBase64);
}
}
/**
* @param {new () => MetaBuilding} metaClass
* @param {string} variant
* @param {object} payload
* @param {number[]=} payload.rotationVariants
* @param {string=} payload.tutorialImageBase64
* @param {string=} payload.regularSpriteBase64
* @param {string=} payload.blueprintSpriteBase64
* @param {string=} payload.name
* @param {string=} payload.description
* @param {Vector=} payload.dimensions
* @param {(root: GameRoot) => [string, string][]} payload.additionalStatistics
* @param {(root: GameRoot) => boolean[]} payload.isUnlocked
*/
addVariantToExistingBuilding(metaClass, variant, payload) {
if (!payload.rotationVariants) {
payload.rotationVariants = [0];
}
if (payload.tutorialImageBase64) {
this.setBuildingTutorialImage(metaClass, variant, payload.tutorialImageBase64);
}
if (payload.regularSpriteBase64) {
this.registerBuildingSprites(metaClass, variant, { regularBase64: payload.regularSpriteBase64 });
}
if (payload.blueprintSpriteBase64) {
this.registerBuildingSprites(metaClass, variant, {
blueprintBase64: payload.blueprintSpriteBase64,
});
}
if (payload.name && payload.description) {
this.registerBuildingTranslation(metaClass, variant, {
name: payload.name,
description: payload.description,
});
}
const internalId = new metaClass().getId() + "-" + variant;
// Extend static methods
this.extendObject(metaClass, ({ $old }) => ({
getAllVariantCombinations() {
return [
...$old.bind(this).getAllVariantCombinations(),
...payload.rotationVariants.map(rotationVariant => ({
internalId,
variant,
rotationVariant,
})),
];
},
}));
// Dimensions
const $variant = variant;
if (payload.dimensions) {
this.extendClass(metaClass, ({ $old }) => ({
getDimensions(variant) {
if (variant === $variant) {
return payload.dimensions;
}
return $old.getDimensions.bind(this)(...arguments);
},
}));
}
if (payload.additionalStatistics) {
this.extendClass(metaClass, ({ $old }) => ({
getAdditionalStatistics(root, variant) {
if (variant === $variant) {
return payload.additionalStatistics(root);
}
return $old.getAdditionalStatistics.bind(this)(root, variant);
},
}));
}
if (payload.isUnlocked) {
this.extendClass(metaClass, ({ $old }) => ({
getAvailableVariants(root) {
if (payload.isUnlocked(root)) {
return [...$old.getAvailableVariants.bind(this)(root), $variant];
}
return $old.getAvailableVariants.bind(this)(root);
},
}));
}
// Register our variant finally
payload.rotationVariants.forEach(rotationVariant =>
shapez.registerBuildingVariant(internalId, metaClass, variant, rotationVariant)
);
}
}

View File

@@ -0,0 +1,18 @@
import { MetaBuilding } from "../game/meta_building";
export class ModMetaBuilding extends MetaBuilding {
/**
* @returns {({
* variant: string;
* rotationVariant?: number;
* name: string;
* description: string;
* blueprintImageBase64?: string;
* regularImageBase64?: string;
* tutorialImageBase64?: string;
* }[])}
*/
static getAllVariantCombinations() {
throw new Error("Implement getAllVariantCombinations");
}
}

Some files were not shown because too many files have changed in this diff Show More