2022-01-13 20:20:42 +00:00
|
|
|
/* typehints:start */
|
|
|
|
|
import { ModLoader } from "./modloader";
|
2022-01-14 12:09:05 +00:00
|
|
|
import { GameSystem } from "../game/game_system";
|
2022-01-14 10:08:16 +00:00
|
|
|
import { Component } from "../game/component";
|
2022-01-14 06:05:46 +00:00
|
|
|
import { MetaBuilding } from "../game/meta_building";
|
2022-01-13 20:20:42 +00:00
|
|
|
/* typehints:end */
|
|
|
|
|
|
2022-01-14 06:05:46 +00:00
|
|
|
import { defaultBuildingVariant } from "../game/meta_building";
|
2022-01-13 20:20:42 +00:00
|
|
|
import { AtlasSprite, SpriteAtlasLink } from "../core/sprites";
|
2022-01-14 06:18:25 +00:00
|
|
|
import {
|
|
|
|
|
enumShortcodeToSubShape,
|
|
|
|
|
enumSubShape,
|
|
|
|
|
enumSubShapeToShortcode,
|
|
|
|
|
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS,
|
|
|
|
|
} from "../game/shape_definition";
|
2022-01-13 21:14:49 +00:00
|
|
|
import { Loader } from "../core/loader";
|
2022-01-13 21:30:52 +00:00
|
|
|
import { LANGUAGES } from "../languages";
|
|
|
|
|
import { matchDataRecursive, T } from "../translations";
|
2022-01-14 06:37:26 +00:00
|
|
|
import { gBuildingVariants, registerBuildingVariant } from "../game/building_codes";
|
2022-01-14 10:08:16 +00:00
|
|
|
import { gComponentRegistry, gMetaBuildingRegistry } from "../core/global_registries";
|
2022-01-14 06:18:25 +00:00
|
|
|
import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk";
|
2022-01-14 12:09:05 +00:00
|
|
|
import { MODS_ADDITIONAL_SYSTEMS } from "../game/game_system_manager";
|
|
|
|
|
import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view";
|
2022-01-14 18:01:38 +00:00
|
|
|
import { KEYMAPPINGS } from "../game/key_action_mapper";
|
|
|
|
|
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
2022-01-13 20:20:42 +00:00
|
|
|
|
|
|
|
|
export class ModInterface {
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {ModLoader} modLoader
|
|
|
|
|
*/
|
2022-01-13 21:14:49 +00:00
|
|
|
constructor(modLoader) {
|
2022-01-13 20:20:42 +00:00
|
|
|
this.modLoader = modLoader;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
registerCss(cssString) {
|
|
|
|
|
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);
|
2022-01-14 15:38:25 +00:00
|
|
|
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;
|
2022-01-13 20:20:42 +00:00
|
|
|
|
|
|
|
|
const link = new SpriteAtlasLink({
|
2022-01-14 15:38:25 +00:00
|
|
|
w: 1,
|
|
|
|
|
h: 1,
|
2022-01-13 20:20:42 +00:00
|
|
|
atlas: img,
|
|
|
|
|
packOffsetX: 0,
|
|
|
|
|
packOffsetY: 0,
|
2022-01-14 15:38:25 +00:00
|
|
|
packedW: 1,
|
|
|
|
|
packedH: 1,
|
2022-01-13 20:20:42 +00:00
|
|
|
packedX: 0,
|
|
|
|
|
packedY: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sprite.linksByResolution["0.25"] = link;
|
|
|
|
|
sprite.linksByResolution["0.5"] = link;
|
|
|
|
|
sprite.linksByResolution["0.75"] = link;
|
|
|
|
|
|
2022-01-14 09:39:26 +00:00
|
|
|
Loader.sprites.set(spriteId, sprite);
|
2022-01-13 21:14:49 +00:00
|
|
|
}
|
2022-01-14 16:03:03 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-13 20:45:09 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {object} param0
|
|
|
|
|
* @param {string} param0.id
|
|
|
|
|
* @param {string} param0.shortCode
|
|
|
|
|
* @param {(distanceToOriginInChunks: number) => number} param0.weightComputation
|
2022-01-14 06:18:25 +00:00
|
|
|
* @param {(options: import("../game/shape_definition").SubShapeDrawOptions) => void} param0.draw
|
2022-01-13 20:45:09 +00:00
|
|
|
*/
|
2022-01-13 22:16:24 +00:00
|
|
|
registerSubShapeType({ id, shortCode, weightComputation, draw }) {
|
2022-01-13 20:45:09 +00:00
|
|
|
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;
|
2022-01-13 22:16:24 +00:00
|
|
|
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = draw;
|
2022-01-13 20:45:09 +00:00
|
|
|
}
|
2022-01-13 21:30:52 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-14 06:05:46 +00:00
|
|
|
|
2022-01-14 10:08:16 +00:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {typeof Component} component
|
|
|
|
|
*/
|
|
|
|
|
registerComponent(component) {
|
|
|
|
|
gComponentRegistry.register(component);
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-14 12:09:05 +00:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-14 06:05:46 +00:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {object} param0
|
|
|
|
|
* @param {typeof MetaBuilding} param0.metaClass
|
|
|
|
|
* @param {string=} param0.buildingIconBase64
|
|
|
|
|
* @param {({
|
|
|
|
|
* variant?: string;
|
|
|
|
|
* rotationVariant?: number;
|
|
|
|
|
* name: string;
|
|
|
|
|
* description: string;
|
|
|
|
|
* blueprintImageBase64?: string;
|
|
|
|
|
* regularImageBase64?: string;
|
|
|
|
|
* tutorialImageBase64?: string;
|
|
|
|
|
* }[])} param0.variantsAndRotations
|
|
|
|
|
*/
|
|
|
|
|
registerNewBuilding({ metaClass, variantsAndRotations, buildingIconBase64 }) {
|
|
|
|
|
const id = new /** @type {new () => MetaBuilding} */ (metaClass)().getId();
|
|
|
|
|
if (gMetaBuildingRegistry.hasId(id)) {
|
|
|
|
|
throw new Error("Tried to register building twice: " + id);
|
|
|
|
|
}
|
|
|
|
|
gMetaBuildingRegistry.register(metaClass);
|
2022-01-14 06:37:26 +00:00
|
|
|
const metaInstance = gMetaBuildingRegistry.findByClass(metaClass);
|
2022-01-14 06:05:46 +00:00
|
|
|
|
|
|
|
|
T.buildings[id] = {};
|
|
|
|
|
variantsAndRotations.forEach(payload => {
|
|
|
|
|
const actualVariant = payload.variant || defaultBuildingVariant;
|
|
|
|
|
registerBuildingVariant(id, metaClass, actualVariant, payload.rotationVariant || 0);
|
2022-01-14 06:37:26 +00:00
|
|
|
|
|
|
|
|
gBuildingVariants[id].metaInstance = metaInstance;
|
|
|
|
|
|
2022-01-14 06:05:46 +00:00
|
|
|
T.buildings[id][actualVariant] = {
|
|
|
|
|
name: payload.name,
|
|
|
|
|
description: payload.description,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildingIdentifier =
|
|
|
|
|
id + (actualVariant === defaultBuildingVariant ? "" : "-" + actualVariant);
|
|
|
|
|
if (payload.regularImageBase64) {
|
|
|
|
|
this.registerSprite(
|
|
|
|
|
"sprites/buildings/" + buildingIdentifier + ".png",
|
|
|
|
|
payload.regularImageBase64
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (payload.blueprintImageBase64) {
|
|
|
|
|
this.registerSprite(
|
|
|
|
|
"sprites/blueprints/" + buildingIdentifier + ".png",
|
|
|
|
|
payload.blueprintImageBase64
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (payload.tutorialImageBase64) {
|
|
|
|
|
this.setBuildingTutorialImage(id, actualVariant, payload.tutorialImageBase64);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (buildingIconBase64) {
|
|
|
|
|
this.setBuildingToolbarIcon(id, buildingIconBase64);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-14 18:01:38 +00:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @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");
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-14 06:05:46 +00:00
|
|
|
setBuildingToolbarIcon(buildingId, iconBase64) {
|
|
|
|
|
this.registerCss(`
|
|
|
|
|
[data-icon="building_icons/${buildingId}.png"] .icon {
|
|
|
|
|
background-image: url('${iconBase64}') !important;
|
|
|
|
|
}
|
|
|
|
|
`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setBuildingTutorialImage(buildingId, variant, imageBase64) {
|
|
|
|
|
const buildingIdentifier = buildingId + (variant === defaultBuildingVariant ? "" : "-" + variant);
|
|
|
|
|
|
|
|
|
|
this.registerCss(`
|
|
|
|
|
[data-icon="building_tutorials/${buildingIdentifier}.png"] {
|
|
|
|
|
background-image: url('${imageBase64}') !important;
|
|
|
|
|
}
|
|
|
|
|
`);
|
|
|
|
|
}
|
2022-01-13 20:20:42 +00:00
|
|
|
}
|