1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-14 02:31:51 +00:00

Make mod loading simpler

This commit is contained in:
tobspr 2022-01-16 17:05:13 +01:00
parent 41a8d4334c
commit 44c4807d74
15 changed files with 599 additions and 720 deletions

View File

@ -3,69 +3,62 @@
* If you are interested in adding more logic to the game, you should also check out * If you are interested in adding more logic to the game, you should also check out
* the advanced example * the advanced example
*/ */
registerMod(() => {
class MetaDemoModBuilding extends shapez.ModMetaBuilding {
constructor() {
super("demoModBuilding");
}
static getAllVariantCombinations() { const METADATA = {
return [ website: "https://tobspr.io",
{ author: "tobspr",
variant: shapez.defaultBuildingVariant, name: "Mod Example: Add new basic building",
name: "A test name", version: "1",
description: "A test building", id: "add-building-basic",
description: "Shows how to add a new basic building",
};
regularImageBase64: RESOURCES["demoBuilding.png"], class MetaDemoModBuilding extends shapez.ModMetaBuilding {
blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], constructor() {
tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], super("demoModBuilding");
},
];
}
getSilhouetteColor() {
return "red";
}
setupEntityComponents(entity) {
// Here you can add components, for example an ItemProcessorComponent.
// To get an idea what you can do with the builtin components, have a look
// at the builtin buildings in <src/js/game/buildings/>
}
} }
return class ModImpl extends shapez.Mod { static getAllVariantCombinations() {
constructor(app, modLoader) { return [
super( {
app, variant: shapez.defaultBuildingVariant,
{ name: "A test name",
website: "https://tobspr.io", description: "A test building",
author: "tobspr",
name: "Mod Example: Add new basic building",
version: "1",
id: "add-building-basic",
description: "Shows how to add a new basic building",
},
modLoader
);
}
init() { regularImageBase64: RESOURCES["demoBuilding.png"],
// Register the new building blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"],
this.modInterface.registerNewBuilding({ tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"],
metaClass: MetaDemoModBuilding, },
buildingIconBase64: RESOURCES["demoBuilding.png"], ];
}); }
// Add it to the regular toolbar getSilhouetteColor() {
this.modInterface.addNewBuildingToToolbar({ return "red";
toolbar: "regular", }
location: "primary",
metaClass: MetaDemoModBuilding, setupEntityComponents(entity) {
}); // Here you can add components, for example an ItemProcessorComponent.
} // To get an idea what you can do with the builtin components, have a look
}; // at the builtin buildings in <src/js/game/buildings/>
}); }
}
class Mod extends shapez.Mod {
init() {
// Register the new building
this.modInterface.registerNewBuilding({
metaClass: MetaDemoModBuilding,
buildingIconBase64: RESOURCES["demoBuilding.png"],
});
// Add it to the regular toolbar
this.modInterface.addNewBuildingToToolbar({
toolbar: "regular",
location: "primary",
metaClass: MetaDemoModBuilding,
});
}
}
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////

View File

@ -2,135 +2,127 @@
* This shows how to add a new extended building (A building which flips shapes). * This shows how to add a new extended building (A building which flips shapes).
* Be sure to check out the "add_building_basic" example first * Be sure to check out the "add_building_basic" example first
*/ */
registerMod(() => { const METADATA = {
// Declare a new type of item processor website: "https://tobspr.io",
shapez.enumItemProcessorTypes.flipper = "flipper"; author: "tobspr",
name: "Mod Example: Add a flipper building",
version: "1",
id: "add-building-extended",
description:
"Shows how to add a new building with logic, in this case it flips/mirrors shapez from top to down",
};
// For now, flipper always has the same speed // Declare a new type of item processor
shapez.MOD_ITEM_PROCESSOR_SPEEDS.flipper = () => 10; shapez.enumItemProcessorTypes.flipper = "flipper";
// Declare a handler for the processor so we define the "flip" operation // For now, flipper always has the same speed
shapez.MOD_ITEM_PROCESSOR_HANDLERS.flipper = function (payload) { shapez.MOD_ITEM_PROCESSOR_SPEEDS.flipper = () => 10;
const shapeDefinition = payload.items.get(0).definition;
// Flip bottom with top on a new, cloned item (NEVER modify the incoming item!) // Declare a handler for the processor so we define the "flip" operation
const newLayers = shapeDefinition.getClonedLayers(); shapez.MOD_ITEM_PROCESSOR_HANDLERS.flipper = function (payload) {
newLayers.forEach(layer => { const shapeDefinition = payload.items.get(0).definition;
const tr = layer[shapez.TOP_RIGHT];
const br = layer[shapez.BOTTOM_RIGHT];
const bl = layer[shapez.BOTTOM_LEFT];
const tl = layer[shapez.TOP_LEFT];
layer[shapez.BOTTOM_LEFT] = tl; // Flip bottom with top on a new, cloned item (NEVER modify the incoming item!)
layer[shapez.BOTTOM_RIGHT] = tr; const newLayers = shapeDefinition.getClonedLayers();
newLayers.forEach(layer => {
const tr = layer[shapez.TOP_RIGHT];
const br = layer[shapez.BOTTOM_RIGHT];
const bl = layer[shapez.BOTTOM_LEFT];
const tl = layer[shapez.TOP_LEFT];
layer[shapez.TOP_LEFT] = bl; layer[shapez.BOTTOM_LEFT] = tl;
layer[shapez.TOP_RIGHT] = br; layer[shapez.BOTTOM_RIGHT] = tr;
});
const newDefinition = new shapez.ShapeDefinition({ layers: newLayers }); layer[shapez.TOP_LEFT] = bl;
payload.outItems.push({ layer[shapez.TOP_RIGHT] = br;
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(newDefinition), });
});
};
// Create the building const newDefinition = new shapez.ShapeDefinition({ layers: newLayers });
class MetaModFlipperBuilding extends shapez.ModMetaBuilding { payload.outItems.push({
constructor() { item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(newDefinition),
super("modFlipperBuilding"); });
} };
static getAllVariantCombinations() { // Create the building
return [ class MetaModFlipperBuilding extends shapez.ModMetaBuilding {
{ constructor() {
name: "Flipper", super("modFlipperBuilding");
description: "Flipps/Mirrors shapez from top to bottom",
variant: shapez.defaultBuildingVariant,
regularImageBase64: RESOURCES["flipper.png"],
blueprintImageBase64: RESOURCES["flipper.png"],
tutorialImageBase64: RESOURCES["flipper.png"],
},
];
}
getSilhouetteColor() {
return "red";
}
getAdditionalStatistics(root) {
const speed = root.hubGoals.getProcessorBaseSpeed(shapez.enumItemProcessorTypes.flipper);
return [[shapez.T.ingame.buildingPlacement.infoTexts.speed, shapez.formatItemsPerSecond(speed)]];
}
getIsUnlocked(root) {
return true;
}
setupEntityComponents(entity) {
// Accept shapes from the bottom
entity.addComponent(
new shapez.ItemAcceptorComponent({
slots: [
{
pos: new shapez.Vector(0, 0),
directions: [shapez.enumDirection.bottom],
filter: "shape",
},
],
})
);
// Process those shapes with tye processor type "flipper" (which we added above)
entity.addComponent(
new shapez.ItemProcessorComponent({
inputsPerCharge: 1,
processorType: shapez.enumItemProcessorTypes.flipper,
})
);
// Eject the result to the top
entity.addComponent(
new shapez.ItemEjectorComponent({
slots: [{ pos: new shapez.Vector(0, 0), direction: shapez.enumDirection.top }],
})
);
}
} }
return class ModImpl extends shapez.Mod { static getAllVariantCombinations() {
constructor(app, modLoader) { return [
super( {
app, name: "Flipper",
{ description: "Flipps/Mirrors shapez from top to bottom",
website: "https://tobspr.io", variant: shapez.defaultBuildingVariant,
author: "tobspr",
name: "Mod Example: Add a flipper building",
version: "1",
id: "add-building-extended",
description:
"Shows how to add a new building with logic, in this case it flips/mirrors shapez from top to down",
},
modLoader
);
}
init() { regularImageBase64: RESOURCES["flipper.png"],
// Register the new building blueprintImageBase64: RESOURCES["flipper.png"],
this.modInterface.registerNewBuilding({ tutorialImageBase64: RESOURCES["flipper.png"],
metaClass: MetaModFlipperBuilding, },
buildingIconBase64: RESOURCES["flipper.png"], ];
}); }
// Add it to the regular toolbar getSilhouetteColor() {
this.modInterface.addNewBuildingToToolbar({ return "red";
toolbar: "regular", }
location: "primary",
metaClass: MetaModFlipperBuilding, getAdditionalStatistics(root) {
}); const speed = root.hubGoals.getProcessorBaseSpeed(shapez.enumItemProcessorTypes.flipper);
} return [[shapez.T.ingame.buildingPlacement.infoTexts.speed, shapez.formatItemsPerSecond(speed)]];
}; }
});
getIsUnlocked(root) {
return true;
}
setupEntityComponents(entity) {
// Accept shapes from the bottom
entity.addComponent(
new shapez.ItemAcceptorComponent({
slots: [
{
pos: new shapez.Vector(0, 0),
directions: [shapez.enumDirection.bottom],
filter: "shape",
},
],
})
);
// Process those shapes with tye processor type "flipper" (which we added above)
entity.addComponent(
new shapez.ItemProcessorComponent({
inputsPerCharge: 1,
processorType: shapez.enumItemProcessorTypes.flipper,
})
);
// Eject the result to the top
entity.addComponent(
new shapez.ItemEjectorComponent({
slots: [{ pos: new shapez.Vector(0, 0), direction: shapez.enumDirection.top }],
})
);
}
}
class Mod extends shapez.Mod {
init() {
// Register the new building
this.modInterface.registerNewBuilding({
metaClass: MetaModFlipperBuilding,
buildingIconBase64: RESOURCES["flipper.png"],
});
// Add it to the regular toolbar
this.modInterface.addNewBuildingToToolbar({
toolbar: "regular",
location: "primary",
metaClass: MetaModFlipperBuilding,
});
}
}
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////

View File

@ -1,25 +1,18 @@
/** /**
* This is the minimal structure of a mod * This is the minimal structure of a mod
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Base",
version: "1",
id: "base",
description: "The most basic mod",
},
modLoader
);
}
init() { const METADATA = {
// Start the modding here website: "https://tobspr.io",
} author: "tobspr",
}; name: "Mod Example: Base",
}); version: "1",
id: "base",
description: "The most basic mod",
};
class Mod extends shapez.Mod {
init() {
// Start the modding here
}
}

View File

@ -1,39 +1,32 @@
/** /**
* This shows how to patch existing methods by making belts have a cost * This shows how to patch existing methods by making belts have a cost
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Patch Methods",
version: "1",
id: "patch-methods",
description:
"Shows how to patch existing methods to change the game by making the belts cost shapes",
},
modLoader
);
}
init() { const METADATA = {
// Define our currency website: "https://tobspr.io",
const CURRENCY = "CyCyCyCy:--------:CuCuCuCu"; author: "tobspr",
name: "Mod Example: Patch Methods",
version: "1",
id: "patch-methods",
description: "Shows how to patch existing methods to change the game by making the belts cost shapes",
};
// Make sure the currency is always pinned class Mod extends shapez.Mod {
this.modInterface.runAfterMethod(shapez.HUDPinnedShapes, "rerenderFull", function () { init() {
this.internalPinShape({ // Define our currency
key: CURRENCY, const CURRENCY = "CyCyCyCy:--------:CuCuCuCu";
canUnpin: false,
className: "currency", // Make sure the currency is always pinned
}); this.modInterface.runAfterMethod(shapez.HUDPinnedShapes, "rerenderFull", function () {
this.internalPinShape({
key: CURRENCY,
canUnpin: false,
className: "currency",
}); });
});
// Style it // Style it
this.modInterface.registerCss(` this.modInterface.registerCss(`
#ingame_HUD_PinnedShapes .shape.currency::after { #ingame_HUD_PinnedShapes .shape.currency::after {
content: " "; content: " ";
position: absolute; position: absolute;
@ -57,41 +50,40 @@ registerMod(() => {
} }
`); `);
// Make the player start with some currency // Make the player start with some currency
this.modInterface.runAfterMethod(shapez.GameCore, "initNewGame", function () { this.modInterface.runAfterMethod(shapez.GameCore, "initNewGame", function () {
this.root.hubGoals.storedShapes[CURRENCY] = 100; this.root.hubGoals.storedShapes[CURRENCY] = 100;
}); });
// Make belts have a cost // Make belts have a cost
this.modInterface.replaceMethod(shapez.MetaBeltBuilding, "getAdditionalStatistics", function ( this.modInterface.replaceMethod(shapez.MetaBeltBuilding, "getAdditionalStatistics", function (
$original, $original,
[root, variant] [root, variant]
) { ) {
const oldStats = $original(root, variant); const oldStats = $original(root, variant);
oldStats.push(["Cost", "1 x <span class='currencyIcon small'></span>"]); oldStats.push(["Cost", "1 x <span class='currencyIcon small'></span>"]);
return oldStats; return oldStats;
}); });
// Only allow placing an entity when there is enough currency // Only allow placing an entity when there is enough currency
this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function ( this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function (
$original, $original,
[entity, offset] [entity, offset]
) { ) {
const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0; const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0;
return storedCurrency > 0 && $original(entity, offset); return storedCurrency > 0 && $original(entity, offset);
}); });
// Take shapes when placing a building // Take shapes when placing a building
this.modInterface.replaceMethod(shapez.GameLogic, "tryPlaceBuilding", function ($original, args) { this.modInterface.replaceMethod(shapez.GameLogic, "tryPlaceBuilding", function ($original, args) {
const result = $original(...args); const result = $original(...args);
if (result && result.components.Belt) { if (result && result.components.Belt) {
this.root.hubGoals.storedShapes[CURRENCY]--; this.root.hubGoals.storedShapes[CURRENCY]--;
} }
return result; return result;
}); });
} }
}; }
});
const RESOURCES = { const RESOURCES = {
"currency.png": "currency.png":

View File

@ -1,231 +1,189 @@
registerMod(() => { class DemoModComponent extends shapez.Component {
class DemoModComponent extends shapez.Component { static getId() {
static getId() { return "DemoMod";
return "DemoMod";
}
static getSchema() {
return {
magicNumber: shapez.types.uint,
};
}
constructor(magicNumber) {
super();
this.magicNumber = magicNumber;
}
} }
class MetaDemoModBuilding extends shapez.MetaBuilding { static getSchema() {
constructor() { return {
super("demoModBuilding"); magicNumber: shapez.types.uint,
} };
getSilhouetteColor() {
return "red";
}
setupEntityComponents(entity) {
entity.addComponent(new DemoModComponent(Math.floor(Math.random() * 100.0)));
}
} }
class DemoModSystem extends shapez.GameSystemWithFilter { constructor(magicNumber) {
constructor(root) { super();
super(root, [DemoModComponent]);
}
update() { this.magicNumber = magicNumber;
// nothing to do here }
} }
drawChunk(parameters, chunk) { class MetaDemoModBuilding extends shapez.MetaBuilding {
const contents = chunk.containedEntitiesByLayer.regular; constructor() {
for (let i = 0; i < contents.length; ++i) { super("demoModBuilding");
const entity = contents[i]; }
const demoComp = entity.components.DemoMod;
if (!demoComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity; getSilhouetteColor() {
return "red";
}
const context = parameters.context; setupEntityComponents(entity) {
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); entity.addComponent(new DemoModComponent(Math.floor(Math.random() * 100.0)));
}
}
// Culling for better performance class DemoModSystem extends shapez.GameSystemWithFilter {
if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) { constructor(root) {
// Circle super(root, [DemoModComponent]);
context.fillStyle = "#53cf47"; }
context.strokeStyle = "#000";
context.lineWidth = 2;
const timeFactor = 5.23 * this.root.time.now(); update() {
context.beginCircle( // nothing to do here
center.x + Math.cos(timeFactor) * 10, }
center.y + Math.sin(timeFactor) * 10,
7
);
context.fill();
context.stroke();
// Text drawChunk(parameters, chunk) {
context.fillStyle = "#fff"; const contents = chunk.containedEntitiesByLayer.regular;
context.textAlign = "center"; for (let i = 0; i < contents.length; ++i) {
context.font = "12px GameFont"; const entity = contents[i];
context.fillText(demoComp.magicNumber, center.x, center.y + 4); const demoComp = entity.components.DemoMod;
} if (!demoComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
const context = parameters.context;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
// Culling for better performance
if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) {
// Circle
context.fillStyle = "#53cf47";
context.strokeStyle = "#000";
context.lineWidth = 2;
const timeFactor = 5.23 * this.root.time.now();
context.beginCircle(
center.x + Math.cos(timeFactor) * 10,
center.y + Math.sin(timeFactor) * 10,
7
);
context.fill();
context.stroke();
// Text
context.fillStyle = "#fff";
context.textAlign = "center";
context.font = "12px GameFont";
context.fillText(demoComp.magicNumber, center.x, center.y + 4);
} }
} }
} }
}
return class ModImpl extends shapez.Mod { class Mod extends shapez.Mod {
constructor(app, modLoader) { constructor(app, modLoader) {
super( super(
app, app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Demo Mod",
version: "1",
id: "demo-mod",
description: "A simple mod to demonstrate the capatibilities of the mod loader.",
},
modLoader
);
}
init() {
// Add some custom css
// this.modInterface.registerCss(`
// * {
// font-family: "Comic Sans", "Comic Sans MS", Tahoma !important;
// }
// `);
// Add an atlas
this.modInterface.registerAtlas(RESOURCES["demoAtlas.png"], RESOURCES["demoAtlas.json"]);
this.modInterface.registerTranslations("en", {
ingame: {
interactiveTutorial: {
title: "Hello",
hints: {
"1_1_extractor": "World!",
},
},
},
});
// Register a new component
this.modInterface.registerComponent(DemoModComponent);
// Register a new game system which can update and draw stuff
this.modInterface.registerGameSystem({
id: "demo_mod",
systemClass: DemoModSystem,
before: "belt",
drawHooks: ["staticAfter"],
});
// Register the new building
this.modInterface.registerNewBuilding({
metaClass: MetaDemoModBuilding,
buildingIconBase64: RESOURCES["demoBuilding.png"],
variantsAndRotations: [
{ {
website: "https://tobspr.io", description: "A test building",
author: "tobspr", name: "A test name",
name: "Demo Mod",
version: "1", regularImageBase64: RESOURCES["demoBuilding.png"],
id: "demo-mod", blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"],
description: "A simple mod to demonstrate the capatibilities of the mod loader.", tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"],
}, },
modLoader ],
); });
}
init() { // Add it to the regular toolbar
// Add some custom css this.modInterface.addNewBuildingToToolbar({
// this.modInterface.registerCss(` toolbar: "regular",
// * { location: "primary",
// font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; metaClass: MetaDemoModBuilding,
// } });
// `);
// Replace a builtin sprite // Register keybinding
["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => { this.modInterface.registerIngameKeybinding({
this.modInterface.registerSprite( id: "demo_mod_binding",
"sprites/colors/" + color + ".png", keyCode: shapez.keyToKeyCode("F"),
RESOURCES[color + ".png"] translation: "mymod: Do something (always with SHIFT)",
); modifiers: {
}); shift: true,
},
handler: root => {
this.dialogs.showInfo("Mod Message", "It worked!");
return shapez.STOP_PROPAGATION;
},
});
// Add an atlas // Add fancy sign to main menu
this.modInterface.registerAtlas(RESOURCES["demoAtlas.png"], RESOURCES["demoAtlas.json"]); this.signals.stateEntered.add(state => {
if (state.key === "MainMenuState") {
const element = document.createElement("div");
element.id = "demo_mod_hello_world_element";
document.body.appendChild(element);
// Add a new type of sub shape ("Line", short code "L") const button = document.createElement("button");
this.modInterface.registerSubShapeType({ button.classList.add("styledButton");
id: "line", button.innerText = "Hello!";
shortCode: "L", button.addEventListener("click", () => {
weightComputation: distanceToOriginInChunks => this.dialogs.showInfo("Mod Message", "Button clicked!");
Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), });
element.appendChild(button);
}
});
draw: ({ context, quadrantSize, layerScale }) => { this.modInterface.registerCss(`
const quadrantHalfSize = quadrantSize / 2;
context.beginPath();
context.moveTo(-quadrantHalfSize, quadrantHalfSize);
context.arc(
-quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.25,
0
);
context.closePath();
},
});
// Modify the theme colors
this.signals.preprocessTheme.add(({ theme }) => {
theme.map.background = "#eee";
theme.items.outline = "#000";
});
// Modify the goal of the first level
this.signals.modifyLevelDefinitions.add(definitions => {
definitions[0].shape = "LuCuLuCu";
});
this.modInterface.registerTranslations("en", {
ingame: {
interactiveTutorial: {
title: "Hello",
hints: {
"1_1_extractor": "World!",
},
},
},
});
// Register a new component
this.modInterface.registerComponent(DemoModComponent);
// Register a new game system which can update and draw stuff
this.modInterface.registerGameSystem({
id: "demo_mod",
systemClass: DemoModSystem,
before: "belt",
drawHooks: ["staticAfter"],
});
// Register the new building
this.modInterface.registerNewBuilding({
metaClass: MetaDemoModBuilding,
buildingIconBase64: RESOURCES["demoBuilding.png"],
variantsAndRotations: [
{
description: "A test building",
name: "A test name",
regularImageBase64: RESOURCES["demoBuilding.png"],
blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"],
tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"],
},
],
});
// Add it to the regular toolbar
this.modInterface.addNewBuildingToToolbar({
toolbar: "regular",
location: "primary",
metaClass: MetaDemoModBuilding,
});
// Register keybinding
this.modInterface.registerIngameKeybinding({
id: "demo_mod_binding",
keyCode: shapez.keyToKeyCode("F"),
translation: "mymod: Do something (always with SHIFT)",
modifiers: {
shift: true,
},
handler: root => {
this.dialogs.showInfo("Mod Message", "It worked!");
return shapez.STOP_PROPAGATION;
},
});
// Add fancy sign to main menu
this.signals.stateEntered.add(state => {
if (state.key === "MainMenuState") {
const element = document.createElement("div");
element.id = "demo_mod_hello_world_element";
document.body.appendChild(element);
const button = document.createElement("button");
button.classList.add("styledButton");
button.innerText = "Hello!";
button.addEventListener("click", () => {
this.dialogs.showInfo("Mod Message", "Button clicked!");
});
element.appendChild(button);
}
});
this.modInterface.registerCss(`
#demo_mod_hello_world_element { #demo_mod_hello_world_element {
position: absolute; position: absolute;
top: calc(10px * var(--ui-scale)); top: calc(10px * var(--ui-scale));
@ -235,9 +193,8 @@ registerMod(() => {
} }
`); `);
} }
}; }
});
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// @notice: Later this part will be autogenerated // @notice: Later this part will be autogenerated

View File

@ -1,28 +1,21 @@
/** /**
* This example shows how to add custom css * This example shows how to add custom css
*/ */
registerMod(() => { const METADATA = {
return class ModImpl extends shapez.Mod { website: "https://tobspr.io",
constructor(app, modLoader) { author: "tobspr",
super( name: "Mod Example: Add custom CSS",
app, version: "1",
{ id: "custom-css",
website: "https://tobspr.io", description: "Shows how to add custom css",
author: "tobspr", };
name: "Mod Example: Add custom CSS",
version: "1",
id: "custom-css",
description: "Shows how to add custom css",
},
modLoader
);
}
init() { class Mod extends shapez.Mod {
// Notice that, since the UI is scaled dynamically, every pixel value init() {
// should be wrapped in '$scaled()' (see below) // Notice that, since the UI is scaled dynamically, every pixel value
// should be wrapped in '$scaled()' (see below)
this.modInterface.registerCss(` this.modInterface.registerCss(`
* { * {
font-family: "Comic Sans", "Comic Sans MS", "ComicSans", Tahoma !important; font-family: "Comic Sans", "Comic Sans MS", "ComicSans", Tahoma !important;
} }
@ -39,9 +32,8 @@ registerMod(() => {
border: $scaled(5px) solid #000 !important; border: $scaled(5px) solid #000 !important;
} }
`); `);
} }
}; }
});
const RESOURCES = { const RESOURCES = {
"cat.png": "cat.png":

View File

@ -1,53 +1,46 @@
/** /**
* This shows how to add custom sub shapes * This shows how to add custom sub shapes
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Sub Shapes",
version: "1",
id: "custom-sub-shapes",
description: "Shows how to add custom sub shapes",
},
modLoader
);
}
init() { const METADATA = {
// Add a new type of sub shape ("Line", short code "L") website: "https://tobspr.io",
this.modInterface.registerSubShapeType({ author: "tobspr",
id: "line", name: "Mod Example: Custom Sub Shapes",
shortCode: "L", version: "1",
id: "custom-sub-shapes",
description: "Shows how to add custom sub shapes",
};
// Make it spawn on the map class Mod extends shapez.Mod {
weightComputation: distanceToOriginInChunks => init() {
Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), // Add a new type of sub shape ("Line", short code "L")
this.modInterface.registerSubShapeType({
id: "line",
shortCode: "L",
// This defines how to draw it // Make it spawn on the map
draw: ({ context, quadrantSize, layerScale }) => { weightComputation: distanceToOriginInChunks =>
const quadrantHalfSize = quadrantSize / 2; Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)),
context.beginPath();
context.moveTo(-quadrantHalfSize, quadrantHalfSize);
context.arc(
-quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.25,
0
);
context.closePath();
},
});
// Modify the goal of the first level to add our goal // This defines how to draw it
this.signals.modifyLevelDefinitions.add(definitions => { draw: ({ context, quadrantSize, layerScale }) => {
definitions[0].shape = "LuLuLuLu"; const quadrantHalfSize = quadrantSize / 2;
}); context.beginPath();
} context.moveTo(-quadrantHalfSize, quadrantHalfSize);
}; context.arc(
}); -quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.25,
0
);
context.closePath();
},
});
// Modify the goal of the first level to add our goal
this.signals.modifyLevelDefinitions.add(definitions => {
definitions[0].shape = "LuLuLuLu";
});
}
}

View File

@ -1,32 +1,24 @@
/** /**
* This example shows how to add a new theme to the game * This example shows how to add a new theme to the game
*/ */
registerMod(() => { const METADATA = {
return class ModImpl extends shapez.Mod { website: "https://tobspr.io",
constructor(app, modLoader) { author: "tobspr",
super( name: "Mod Example: Custom Game Theme",
app, version: "1",
{ id: "custom-theme",
website: "https://tobspr.io", description: "Shows how to add a custom game theme",
author: "tobspr", };
name: "Mod Example: Custom Game Theme",
version: "1",
id: "custom-theme",
description: "Shows how to add a custom game theme",
},
modLoader
);
}
init() { class Mod extends shapez.Mod {
this.modInterface.registerGameTheme({ init() {
id: "my-theme", this.modInterface.registerGameTheme({
name: "My fancy theme", id: "my-theme",
theme: RESOURCES["my-theme.json"], name: "My fancy theme",
}); theme: RESOURCES["my-theme.json"],
} });
}; }
}); }
const RESOURCES = { const RESOURCES = {
"my-theme.json": { "my-theme.json": {

View File

@ -1,36 +1,29 @@
/** /**
* This shows how to modify an existing building * This shows how to modify an existing building
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify existing building",
version: "1",
id: "modify-existing-building",
description: "Shows how to modify an existing building",
},
modLoader
);
}
init() { const METADATA = {
// Make Rotator always unlocked website: "https://tobspr.io",
this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getIsUnlocked", function () { author: "tobspr",
return true; name: "Mod Example: Modify existing building",
}); version: "1",
id: "modify-existing-building",
description: "Shows how to modify an existing building",
};
// Add some custom stats to the info panel when selecting the building class Mod extends shapez.Mod {
this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getAdditionalStatistics", function ( init() {
root, // Make Rotator always unlocked
variant this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getIsUnlocked", function () {
) { return true;
return [["Awesomeness", 5]]; });
});
} // Add some custom stats to the info panel when selecting the building
}; this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getAdditionalStatistics", function (
}); root,
variant
) {
return [["Awesomeness", 5]];
});
}
}

View File

@ -2,29 +2,21 @@
* This example shows how to modify the builtin themes. If you want to create your own theme, * This example shows how to modify the builtin themes. If you want to create your own theme,
* be sure to check out the "custom_theme" example * be sure to check out the "custom_theme" example
*/ */
registerMod(() => { const METADATA = {
return class ModImpl extends shapez.Mod { website: "https://tobspr.io",
constructor(app, modLoader) { author: "tobspr",
super( name: "Mod Example: Modify Builtin Themes",
app, version: "1",
{ id: "modify-theme",
website: "https://tobspr.io", description: "Shows how to modify builtin themes",
author: "tobspr", };
name: "Mod Example: Modify Builtin Themes",
version: "1",
id: "modify-theme",
description: "Shows how to modify builtin themes",
},
modLoader
);
}
init() { class Mod extends shapez.Mod {
shapez.THEMES.light.map.background = "#eee"; init() {
shapez.THEMES.light.items.outline = "#000"; shapez.THEMES.light.map.background = "#eee";
shapez.THEMES.light.items.outline = "#000";
shapez.THEMES.dark.map.background = "#245"; shapez.THEMES.dark.map.background = "#245";
shapez.THEMES.dark.items.outline = "#fff"; shapez.THEMES.dark.items.outline = "#fff";
} }
}; }
});

View File

@ -2,34 +2,24 @@
* This example shows how to replace builtin sprites, in this case * This example shows how to replace builtin sprites, in this case
* the color sprites * the color sprites
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Replace builtin sprites",
version: "1",
id: "replace-builtin-sprites",
description: "Shows how to replace builtin sprites",
},
modLoader
);
}
init() { const METADATA = {
// Replace a builtin sprite website: "https://tobspr.io",
["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => { author: "tobspr",
this.modInterface.registerSprite( name: "Mod Example: Replace builtin sprites",
"sprites/colors/" + color + ".png", version: "1",
RESOURCES[color + ".png"] id: "replace-builtin-sprites",
); description: "Shows how to replace builtin sprites",
}); };
}
}; class Mod extends shapez.Mod {
}); init() {
// Replace a builtin sprite
["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => {
this.modInterface.registerSprite("sprites/colors/" + color + ".png", RESOURCES[color + ".png"]);
});
}
}
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////

View File

@ -1,71 +1,64 @@
/** /**
* Shows to add new translations * Shows to add new translations
*/ */
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Translations",
version: "1",
id: "translations",
description: "Shows how to add and modify translations",
},
modLoader
);
}
init() { const METADATA = {
// Replace an existing translation in the english language website: "https://tobspr.io",
this.modInterface.registerTranslations("en", { author: "tobspr",
ingame: { name: "Mod Example: Translations",
interactiveTutorial: { version: "1",
title: "Hello", id: "translations",
hints: { description: "Shows how to add and modify translations",
"1_1_extractor": "World!", };
},
class Mod extends shapez.Mod {
init() {
// Replace an existing translation in the english language
this.modInterface.registerTranslations("en", {
ingame: {
interactiveTutorial: {
title: "Hello",
hints: {
"1_1_extractor": "World!",
}, },
}, },
}); },
});
// Replace an existing translation in german // Replace an existing translation in german
this.modInterface.registerTranslations("de", { this.modInterface.registerTranslations("de", {
ingame: { ingame: {
interactiveTutorial: { interactiveTutorial: {
title: "Hallo", title: "Hallo",
hints: { hints: {
"1_1_extractor": "Welt!", "1_1_extractor": "Welt!",
},
}, },
}, },
}); },
});
// Add an entirely new translation which is localized in german and english // Add an entirely new translation which is localized in german and english
this.modInterface.registerTranslations("en", { this.modInterface.registerTranslations("en", {
mods: { mods: {
mymod: { mymod: {
test: "Test Translation", test: "Test Translation",
},
}, },
}); },
this.modInterface.registerTranslations("de", { });
mods: { this.modInterface.registerTranslations("de", {
mymod: { mods: {
test: "Test Übersetzung", mymod: {
}, test: "Test Übersetzung",
}, },
}); },
});
// Show a dialog in the main menu // Show a dialog in the main menu
this.signals.stateEntered.add(state => { this.signals.stateEntered.add(state => {
if (state instanceof shapez.MainMenuState) { if (state instanceof shapez.MainMenuState) {
// Will show differently based on the selected language // Will show differently based on the selected language
this.dialogs.showInfo("My translation", shapez.T.mods.mymod.test); this.dialogs.showInfo("My translation", shapez.T.mods.mymod.test);
} }
}); });
} }
}; }
});

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

@ -26,8 +26,6 @@ declare const shapez: any;
declare const ipcRenderer: any; declare const ipcRenderer: any;
declare const registerMod: any;
// Polyfills // Polyfills
declare interface String { declare interface String {
replaceAll(search: string, replacement: string): string; replaceAll(search: string, replacement: string): string;
@ -98,7 +96,7 @@ declare interface Window {
cpmstarAPI: any; cpmstarAPI: any;
// Mods // Mods
registerMod: any; $shapez_registerMod: any;
anyModLoaded: any; anyModLoaded: any;
shapez: any; shapez: any;

View File

@ -7,28 +7,22 @@ import { MOD_SIGNALS } from "./mod_signals";
export class Mod { export class Mod {
/** /**
*
* @param {Application} app * @param {Application} app
* @param {object} metadata
* @param {string} metadata.name
* @param {string} metadata.version
* @param {string} metadata.author
* @param {string} metadata.website
* @param {string} metadata.description
* @param {string} metadata.id
*
* @param {ModLoader} modLoader * @param {ModLoader} modLoader
* @param {import("./modloader").ModMetadata} meta
*/ */
constructor(app, metadata, modLoader) { constructor(app, modLoader, meta) {
this.app = app; this.app = app;
this.metadata = metadata;
this.modLoader = modLoader; this.modLoader = modLoader;
this.metadata = meta;
this.signals = MOD_SIGNALS; this.signals = MOD_SIGNALS;
this.modInterface = modLoader.modInterface; this.modInterface = modLoader.modInterface;
} }
init() {} init() {
// to be overridden
}
get dialogs() { get dialogs() {
return this.modInterface.dialogs; return this.modInterface.dialogs;

View File

@ -9,6 +9,17 @@ import { MOD_SIGNALS } from "./mod_signals";
const LOG = createLogger("mods"); const LOG = createLogger("mods");
/**
* @typedef {{
* name: string;
* version: string;
* author: string;
* website: string;
* description: string;
* id: string;
* }} ModMetadata
*/
export class ModLoader { export class ModLoader {
constructor() { constructor() {
LOG.log("modloader created"); LOG.log("modloader created");
@ -23,7 +34,7 @@ export class ModLoader {
this.modInterface = new ModInterface(this); this.modInterface = new ModInterface(this);
/** @type {((Object) => (new (Application, ModLoader) => Mod))[]} */ /** @type {({ meta: ModMetadata, modClass: typeof Mod})[]} */
this.modLoadQueue = []; this.modLoadQueue = [];
this.initialized = false; this.initialized = false;
@ -73,8 +84,11 @@ export class ModLoader {
this.exposeExports(); this.exposeExports();
window.registerMod = mod => { window.$shapez_registerMod = (modClass, meta) => {
this.modLoadQueue.push(mod); this.modLoadQueue.push({
modClass,
meta,
});
}; };
if (G_IS_STANDALONE || G_IS_DEV) { if (G_IS_STANDALONE || G_IS_DEV) {
@ -103,6 +117,7 @@ export class ModLoader {
mods.forEach(modCode => { mods.forEach(modCode => {
try { try {
modCode += "\n;window.$shapez_registerMod(Mod, METADATA);";
const func = new Function(modCode); const func = new Function(modCode);
func(); func();
} catch (ex) { } catch (ex) {
@ -116,9 +131,9 @@ export class ModLoader {
} }
this.initialized = true; this.initialized = true;
this.modLoadQueue.forEach(modClass => { this.modLoadQueue.forEach(({ modClass, meta }) => {
try { try {
const mod = new (modClass())(this.app, this); const mod = new modClass(this.app, this, meta);
mod.init(); mod.init();
this.mods.push(mod); this.mods.push(mod);
} catch (ex) { } catch (ex) {
@ -128,7 +143,7 @@ export class ModLoader {
}); });
this.modLoadQueue = []; this.modLoadQueue = [];
delete window.registerMod; delete window.$shapez_registerMod;
} }
} }