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

Allow adding custom themesw

This commit is contained in:
tobspr 2022-01-15 13:59:38 +01:00
parent a4df63549b
commit 2a83853b1c
13 changed files with 367 additions and 209 deletions

View File

@ -1,6 +1,5 @@
/** /**
* This example shows how to replace builtin sprites, in this case * This example shows how to add custom css
* the color sprites
*/ */
registerMod(() => { registerMod(() => {
return class ModImpl extends shapez.Mod { return class ModImpl extends shapez.Mod {

View File

@ -0,0 +1,101 @@
/**
* This example shows how to add a new theme to the game
*/
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Game Theme",
version: "1",
id: "custom-theme",
description: "Shows how to add a custom game theme",
},
modLoader
);
}
init() {
console.log("CUSTOM GAME THEME NOW");
this.modInterface.registerGameTheme({
id: "my-theme",
name: "My fancy theme",
theme: RESOURCES["my-theme.json"],
});
}
};
});
const RESOURCES = {
"my-theme.json": {
map: {
background: "#abc",
grid: "#ccc",
gridLineWidth: 1,
selectionOverlay: "rgba(74, 163, 223, 0.7)",
selectionOutline: "rgba(74, 163, 223, 0.5)",
selectionBackground: "rgba(74, 163, 223, 0.2)",
chunkBorders: "rgba(0, 30, 50, 0.03)",
directionLock: {
regular: {
color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)",
},
wires: {
color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)",
},
},
colorBlindPickerTile: "rgba(50, 50, 50, 0.4)",
resources: {
shape: "#eaebec",
red: "#ffbfc1",
green: "#cbffc4",
blue: "#bfdaff",
},
chunkOverview: {
empty: "#a6afbb",
filled: "#c5ccd6",
beltColor: "#777",
},
wires: {
overlayColor: "rgba(97, 161, 152, 0.75)",
previewColor: "rgb(97, 161, 152, 0.4)",
highlightColor: "rgba(72, 137, 255, 1)",
},
connectedMiners: {
overlay: "rgba(40, 50, 60, 0.5)",
textColor: "#fff",
textColorCapped: "#ef5072",
background: "rgba(40, 50, 60, 0.8)",
},
zone: {
borderSolid: "rgba(23, 192, 255, 1)",
outerColor: "rgba(240, 240, 255, 0.5)",
},
},
items: {
outline: "#55575a",
outlineWidth: 0.75,
circleBackground: "rgba(40, 50, 65, 0.1)",
},
shapeTooltip: {
background: "#dee1ea",
outline: "#54565e",
},
},
};

View File

@ -0,0 +1,30 @@
/**
* 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
*/
registerMod(() => {
return class ModImpl extends shapez.Mod {
constructor(app, modLoader) {
super(
app,
{
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify Builtin Themes",
version: "1",
id: "modify-theme",
description: "Shows how to modify builtin themes",
},
modLoader
);
}
init() {
shapez.THEMES.light.map.background = "#eee";
shapez.THEMES.light.items.outline = "#000";
shapez.THEMES.dark.map.background = "#245";
shapez.THEMES.dark.items.outline = "#fff";
}
};
});

View File

@ -65,12 +65,20 @@ if (typeof document.hidden !== "undefined") {
} }
export class Application { export class Application {
constructor() { /**
* Boots the application
*/
async boot() {
console.log("Booting ...");
assert(!GLOBAL_APP, "Tried to construct application twice"); assert(!GLOBAL_APP, "Tried to construct application twice");
logger.log("Creating application, platform =", getPlatformName()); logger.log("Creating application, platform =", getPlatformName());
setGlobalApp(this); setGlobalApp(this);
MODS.app = this; MODS.app = this;
// MODS
await MODS.initMods();
this.unloaded = false; this.unloaded = false;
// Global stuff // Global stuff
@ -132,6 +140,31 @@ export class Application {
// Store the mouse position, or null if not available // Store the mouse position, or null if not available
/** @type {Vector|null} */ /** @type {Vector|null} */
this.mousePosition = 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();
} }
/** /**
@ -152,8 +185,6 @@ export class Application {
this.analytics = new GoogleAnalyticsImpl(this); this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new ShapezGameAnalytics(this); this.gameAnalytics = new ShapezGameAnalytics(this);
this.achievementProvider = new NoAchievementProvider(this); this.achievementProvider = new NoAchievementProvider(this);
MOD_SIGNALS.platformInstancesInitialized.dispatch();
} }
/** /**
@ -329,38 +360,6 @@ export class Application {
} }
} }
/**
* Boots the application
*/
async boot() {
console.log("Booting ...");
await MODS.initMods();
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 * Deinitializes the application
*/ */

View File

@ -1,5 +1,3 @@
import { MOD_SIGNALS } from "../mods/mod_signals";
export const THEMES = { export const THEMES = {
dark: require("./themes/dark.json"), dark: require("./themes/dark.json"),
light: require("./themes/light.json"), light: require("./themes/light.json"),
@ -9,5 +7,4 @@ export let THEME = THEMES.light;
export function applyGameTheme(id) { export function applyGameTheme(id) {
THEME = THEMES[id]; THEME = THEMES[id];
MOD_SIGNALS.preprocessTheme.dispatch({ id, theme: THEME });
} }

View File

@ -1,5 +1,4 @@
{ {
"uiStyle": "dark",
"map": { "map": {
"background": "#3e3f47", "background": "#3e3f47",
"grid": "rgba(255, 255, 255, 0.02)", "grid": "rgba(255, 255, 255, 0.02)",

View File

@ -1,5 +1,4 @@
{ {
"uiStyle": "light",
"map": { "map": {
"background": "#fff", "background": "#fff",
"grid": "#fafafa", "grid": "#fafafa",

View File

@ -23,6 +23,7 @@ import { MODS_ADDITIONAL_SYSTEMS } from "../game/game_system_manager";
import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view"; import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view";
import { KEYMAPPINGS } from "../game/key_action_mapper"; import { KEYMAPPINGS } from "../game/key_action_mapper";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { THEMES } from "../game/theme";
export class ModInterface { export class ModInterface {
/** /**
@ -322,4 +323,25 @@ export class ModInterface {
} }
`); `);
} }
/**
* @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,
},
},
},
},
});
}
} }

View File

@ -10,10 +10,9 @@ import { Signal } from "../core/signal";
// Single file to avoid circular deps // Single file to avoid circular deps
export const MOD_SIGNALS = { export const MOD_SIGNALS = {
postInit: new Signal(), // Called when the application has booted and instances like the app settings etc are available
platformInstancesInitialized: new Signal(), appBooted: new Signal(),
preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()),
modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()),
modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()), modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()),

View File

@ -95,7 +95,7 @@ export class ModLoader {
mods.forEach(modCode => { mods.forEach(modCode => {
try { try {
const func = new Function(modCode); const func = new Function(modCode);
const response = func(); func();
} catch (ex) { } catch (ex) {
console.error(ex); console.error(ex);
alert("Failed to parse mod (launch with --dev for more info): " + ex); alert("Failed to parse mod (launch with --dev for more info): " + ex);
@ -118,7 +118,6 @@ export class ModLoader {
} }
}); });
this.modLoadQueue = []; this.modLoadQueue = [];
this.signals.postInit.dispatch();
delete window.registerMod; delete window.registerMod;
} }

View File

@ -122,7 +122,7 @@ export const autosaveIntervals = [
}, },
]; ];
const refreshRateOptions = ["30", "60", "120", "180", "240"]; export const refreshRateOptions = ["30", "60", "120", "180", "240"];
if (G_IS_DEV) { if (G_IS_DEV) {
refreshRateOptions.unshift("10"); refreshRateOptions.unshift("10");
@ -133,8 +133,9 @@ if (G_IS_DEV) {
refreshRateOptions.push("10000"); refreshRateOptions.push("10000");
} }
/** @type {Array<BaseSetting>} */ /** @returns {Array<BaseSetting>} */
export const allApplicationSettings = [ function initializeSettings() {
return [
new EnumSetting("language", { new EnumSetting("language", {
options: Object.keys(LANGUAGES), options: Object.keys(LANGUAGES),
valueGetter: key => key, valueGetter: key => key,
@ -287,9 +288,6 @@ export const allApplicationSettings = [
new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}),
new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}), new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}),
]; ];
export function getApplicationSettingById(id) {
return allApplicationSettings.find(setting => setting.id === id);
} }
class SettingsStorage { class SettingsStorage {
@ -339,6 +337,8 @@ class SettingsStorage {
export class ApplicationSettings extends ReadWriteProxy { export class ApplicationSettings extends ReadWriteProxy {
constructor(app) { constructor(app) {
super(app, "app_settings.bin"); super(app, "app_settings.bin");
this.settingHandles = initializeSettings();
} }
initialize() { initialize() {
@ -347,8 +347,8 @@ export class ApplicationSettings extends ReadWriteProxy {
.then(() => { .then(() => {
// Apply default setting callbacks // Apply default setting callbacks
const settings = this.getAllSettings(); const settings = this.getAllSettings();
for (let i = 0; i < allApplicationSettings.length; ++i) { for (let i = 0; i < this.settingHandles.length; ++i) {
const handle = allApplicationSettings[i]; const handle = this.settingHandles[i];
handle.apply(this.app, settings[handle.id]); handle.apply(this.app, settings[handle.id]);
} }
}) })
@ -360,6 +360,10 @@ export class ApplicationSettings extends ReadWriteProxy {
return this.writeAsync(); return this.writeAsync();
} }
getSettingHandleById(id) {
return this.settingHandles.find(setting => setting.id === id);
}
// Getters // Getters
/** /**
@ -457,9 +461,10 @@ export class ApplicationSettings extends ReadWriteProxy {
* @param {string|boolean|number} value * @param {string|boolean|number} value
*/ */
updateSetting(key, value) { updateSetting(key, value) {
for (let i = 0; i < allApplicationSettings.length; ++i) { const setting = this.getSettingHandleById(key);
const setting = allApplicationSettings[i]; if (!setting) {
if (setting.id === key) { assertAlways(false, "Unknown setting: " + key);
}
if (!setting.validate(value)) { if (!setting.validate(value)) {
assertAlways(false, "Bad setting value: " + key); assertAlways(false, "Bad setting value: " + key);
} }
@ -469,9 +474,6 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
return this.writeAsync(); return this.writeAsync();
} }
}
assertAlways(false, "Unknown setting: " + key);
}
/** /**
* Sets a new keybinding override * Sets a new keybinding override
@ -510,8 +512,15 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
const settings = data.settings; const settings = data.settings;
for (let i = 0; i < allApplicationSettings.length; ++i) {
const setting = allApplicationSettings[i]; // MODS
if (!THEMES[settings.theme]) {
console.warn("Resetting theme because its no longer available: " + settings.theme);
settings.theme = "light";
}
for (let i = 0; i < this.settingHandles.length; ++i) {
const setting = this.settingHandles[i];
const storedValue = settings[setting.id]; const storedValue = settings[setting.id];
if (!setting.validate(storedValue)) { if (!setting.validate(storedValue)) {
return ExplainedResult.bad( return ExplainedResult.bad(
@ -690,6 +699,12 @@ export class ApplicationSettings extends ReadWriteProxy {
data.version = 31; data.version = 31;
} }
// MODS
if (!THEMES[data.settings.theme]) {
console.warn("Resetting theme because its no longer available: " + data.settings.theme);
data.settings.theme = "light";
}
return ExplainedResult.good(); return ExplainedResult.good();
} }
} }

View File

@ -20,7 +20,6 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { MODS } from "../mods/modloader"; import { MODS } from "../mods/modloader";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
import { getApplicationSettingById } from "../profile/application_settings";
import { T } from "../translations"; import { T } from "../translations";
const trim = require("trim"); const trim = require("trim");
@ -468,7 +467,7 @@ export class MainMenuState extends GameState {
onLanguageChooseClicked() { onLanguageChooseClicked() {
this.app.analytics.trackUiClick("choose_language"); this.app.analytics.trackUiClick("choose_language");
const setting = /** @type {EnumSetting} */ (getApplicationSettingById("language")); const setting = /** @type {EnumSetting} */ (this.app.settings.getSettingHandleById("language"));
const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels.language.title, { const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels.language.title, {
active: this.app.settings.getLanguage(), active: this.app.settings.getLanguage(),

View File

@ -1,7 +1,7 @@
import { THIRDPARTY_URLS } from "../core/config"; import { THIRDPARTY_URLS } from "../core/config";
import { TextualGameState } from "../core/textual_game_state"; import { TextualGameState } from "../core/textual_game_state";
import { formatSecondsToTimeAgo } from "../core/utils"; import { formatSecondsToTimeAgo } from "../core/utils";
import { allApplicationSettings, enumCategories } from "../profile/application_settings"; import { enumCategories } from "../profile/application_settings";
import { T } from "../translations"; import { T } from "../translations";
export class SettingsState extends TextualGameState { export class SettingsState extends TextualGameState {
@ -88,8 +88,8 @@ export class SettingsState extends TextualGameState {
categoriesHTML[catName] = `<div class="category" data-category="${catName}">`; categoriesHTML[catName] = `<div class="category" data-category="${catName}">`;
}); });
for (let i = 0; i < allApplicationSettings.length; ++i) { for (let i = 0; i < this.app.settings.settingHandles.length; ++i) {
const setting = allApplicationSettings[i]; const setting = this.app.settings.settingHandles[i];
if ((G_CHINA_VERSION || G_WEGAME_VERSION) && setting.id === "language") { if ((G_CHINA_VERSION || G_WEGAME_VERSION) && setting.id === "language") {
continue; continue;
@ -171,7 +171,7 @@ export class SettingsState extends TextualGameState {
} }
initSettings() { initSettings() {
allApplicationSettings.forEach(setting => { this.app.settings.settingHandles.forEach(setting => {
if ((G_CHINA_VERSION || G_WEGAME_VERSION) && setting.id === "language") { if ((G_CHINA_VERSION || G_WEGAME_VERSION) && setting.id === "language") {
return; return;
} }