1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 18:21: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
* the color sprites
* This example shows how to add custom css
*/
registerMod(() => {
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 {
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
await MODS.initMods();
this.unloaded = false;
// Global stuff
@ -132,6 +140,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();
}
/**
@ -152,8 +185,6 @@ export class Application {
this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new ShapezGameAnalytics(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
*/

View File

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

View File

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

View File

@ -1,5 +1,4 @@
{
"uiStyle": "light",
"map": {
"background": "#fff",
"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 { KEYMAPPINGS } from "../game/key_action_mapper";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { THEMES } from "../game/theme";
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
export const MOD_SIGNALS = {
postInit: new Signal(),
platformInstancesInitialized: new Signal(),
// Called when the application has booted and instances like the app settings etc are available
appBooted: new Signal(),
preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()),
modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()),
modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()),

View File

@ -95,7 +95,7 @@ export class ModLoader {
mods.forEach(modCode => {
try {
const func = new Function(modCode);
const response = func();
func();
} catch (ex) {
console.error(ex);
alert("Failed to parse mod (launch with --dev for more info): " + ex);
@ -118,7 +118,6 @@ export class ModLoader {
}
});
this.modLoadQueue = [];
this.signals.postInit.dispatch();
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) {
refreshRateOptions.unshift("10");
@ -133,163 +133,161 @@ if (G_IS_DEV) {
refreshRateOptions.push("10000");
}
/** @type {Array<BaseSetting>} */
export const allApplicationSettings = [
new EnumSetting("language", {
options: Object.keys(LANGUAGES),
valueGetter: key => key,
textGetter: key => LANGUAGES[key].name,
category: enumCategories.general,
restartRequired: true,
changeCb: (app, id) => null,
magicValue: "auto-detect",
}),
/** @returns {Array<BaseSetting>} */
function initializeSettings() {
return [
new EnumSetting("language", {
options: Object.keys(LANGUAGES),
valueGetter: key => key,
textGetter: key => LANGUAGES[key].name,
category: enumCategories.general,
restartRequired: true,
changeCb: (app, id) => null,
magicValue: "auto-detect",
}),
new EnumSetting("uiScale", {
options: uiScales.sort((a, b) => a.size - b.size),
valueGetter: scale => scale.id,
textGetter: scale => T.settings.labels.uiScale.scales[scale.id],
category: enumCategories.userInterface,
restartRequired: false,
changeCb:
new EnumSetting("uiScale", {
options: uiScales.sort((a, b) => a.size - b.size),
valueGetter: scale => scale.id,
textGetter: scale => T.settings.labels.uiScale.scales[scale.id],
category: enumCategories.userInterface,
restartRequired: false,
changeCb:
/**
* @param {Application} app
*/
(app, id) => app.updateAfterUiScaleChanged(),
}),
new RangeSetting(
"soundVolume",
enumCategories.general,
/**
* @param {Application} app
*/
(app, id) => app.updateAfterUiScaleChanged(),
}),
new RangeSetting(
"soundVolume",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => app.sound.setSoundVolume(value)
),
new RangeSetting(
"musicVolume",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => app.sound.setMusicVolume(value)
),
new BoolSetting(
"fullscreen",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => {
if (app.platformWrapper.getSupportsFullscreen()) {
app.platformWrapper.setFullscreen(value);
}
},
/**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings()
),
new BoolSetting(
"enableColorBlindHelper",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => null
),
new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}),
new EnumSetting("theme", {
options: Object.keys(THEMES),
valueGetter: theme => theme,
textGetter: theme => T.settings.labels.theme.themes[theme],
category: enumCategories.userInterface,
restartRequired: false,
changeCb:
(app, value) => app.sound.setSoundVolume(value)
),
new RangeSetting(
"musicVolume",
enumCategories.general,
/**
* @param {Application} app
*/
(app, id) => {
applyGameTheme(id);
document.documentElement.setAttribute("data-theme", id);
(app, value) => app.sound.setMusicVolume(value)
),
new BoolSetting(
"fullscreen",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => {
if (app.platformWrapper.getSupportsFullscreen()) {
app.platformWrapper.setFullscreen(value);
}
},
enabledCb: /**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
/**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings()
),
new EnumSetting("autosaveInterval", {
options: autosaveIntervals,
valueGetter: interval => interval.id,
textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb:
new BoolSetting(
"enableColorBlindHelper",
enumCategories.general,
/**
* @param {Application} app
*/
(app, id) => null,
}),
(app, value) => null
),
new EnumSetting("scrollWheelSensitivity", {
options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale),
valueGetter: scale => scale.id,
textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb:
/**
new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}),
new EnumSetting("theme", {
options: Object.keys(THEMES),
valueGetter: theme => theme,
textGetter: theme => T.settings.labels.theme.themes[theme],
category: enumCategories.userInterface,
restartRequired: false,
changeCb:
/**
* @param {Application} app
*/
(app, id) => {
applyGameTheme(id);
document.documentElement.setAttribute("data-theme", id);
},
enabledCb: /**
* @param {Application} app
*/
(app, id) => app.updateAfterUiScaleChanged(),
}),
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
new EnumSetting("movementSpeed", {
options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier),
valueGetter: multiplier => multiplier.id,
textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb: (app, id) => {},
}),
new EnumSetting("autosaveInterval", {
options: autosaveIntervals,
valueGetter: interval => interval.id,
textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb:
/**
* @param {Application} app
*/
(app, id) => null,
}),
new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}),
new BoolSetting("shapeTooltipAlwaysOn", enumCategories.advanced, (app, value) => {}),
new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("zoomToCursor", enumCategories.advanced, (app, value) => {}),
new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}),
new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}),
new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}),
new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}),
new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}),
new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}),
new BoolSetting("pickMinerOnPatch", enumCategories.advanced, (app, value) => {}),
new RangeSetting("mapResourcesScale", enumCategories.advanced, () => null),
new EnumSetting("scrollWheelSensitivity", {
options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale),
valueGetter: scale => scale.id,
textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb:
/**
* @param {Application} app
*/
(app, id) => app.updateAfterUiScaleChanged(),
}),
new EnumSetting("refreshRate", {
options: refreshRateOptions,
valueGetter: rate => rate,
textGetter: rate => T.settings.tickrateHz.replace("<amount>", rate),
category: enumCategories.performance,
restartRequired: false,
changeCb: (app, id) => {},
enabledCb: /**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
new EnumSetting("movementSpeed", {
options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier),
valueGetter: multiplier => multiplier.id,
textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id],
category: enumCategories.advanced,
restartRequired: false,
changeCb: (app, id) => {},
}),
new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}),
new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}),
new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}),
new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}),
];
new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}),
new BoolSetting("shapeTooltipAlwaysOn", enumCategories.advanced, (app, value) => {}),
new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("zoomToCursor", enumCategories.advanced, (app, value) => {}),
new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}),
new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}),
new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}),
new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}),
new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}),
new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}),
new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}),
new BoolSetting("pickMinerOnPatch", enumCategories.advanced, (app, value) => {}),
new RangeSetting("mapResourcesScale", enumCategories.advanced, () => null),
export function getApplicationSettingById(id) {
return allApplicationSettings.find(setting => setting.id === id);
new EnumSetting("refreshRate", {
options: refreshRateOptions,
valueGetter: rate => rate,
textGetter: rate => T.settings.tickrateHz.replace("<amount>", rate),
category: enumCategories.performance,
restartRequired: false,
changeCb: (app, id) => {},
enabledCb: /**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings(),
}),
new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}),
new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}),
new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}),
new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}),
];
}
class SettingsStorage {
@ -339,6 +337,8 @@ class SettingsStorage {
export class ApplicationSettings extends ReadWriteProxy {
constructor(app) {
super(app, "app_settings.bin");
this.settingHandles = initializeSettings();
}
initialize() {
@ -347,8 +347,8 @@ export class ApplicationSettings extends ReadWriteProxy {
.then(() => {
// Apply default setting callbacks
const settings = this.getAllSettings();
for (let i = 0; i < allApplicationSettings.length; ++i) {
const handle = allApplicationSettings[i];
for (let i = 0; i < this.settingHandles.length; ++i) {
const handle = this.settingHandles[i];
handle.apply(this.app, settings[handle.id]);
}
})
@ -360,6 +360,10 @@ export class ApplicationSettings extends ReadWriteProxy {
return this.writeAsync();
}
getSettingHandleById(id) {
return this.settingHandles.find(setting => setting.id === id);
}
// Getters
/**
@ -457,20 +461,18 @@ export class ApplicationSettings extends ReadWriteProxy {
* @param {string|boolean|number} value
*/
updateSetting(key, value) {
for (let i = 0; i < allApplicationSettings.length; ++i) {
const setting = allApplicationSettings[i];
if (setting.id === key) {
if (!setting.validate(value)) {
assertAlways(false, "Bad setting value: " + key);
}
this.getAllSettings()[key] = value;
if (setting.changeCb) {
setting.changeCb(this.app, value);
}
return this.writeAsync();
}
const setting = this.getSettingHandleById(key);
if (!setting) {
assertAlways(false, "Unknown setting: " + key);
}
assertAlways(false, "Unknown setting: " + key);
if (!setting.validate(value)) {
assertAlways(false, "Bad setting value: " + key);
}
this.getAllSettings()[key] = value;
if (setting.changeCb) {
setting.changeCb(this.app, value);
}
return this.writeAsync();
}
/**
@ -510,8 +512,15 @@ export class ApplicationSettings extends ReadWriteProxy {
}
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];
if (!setting.validate(storedValue)) {
return ExplainedResult.bad(
@ -690,6 +699,12 @@ export class ApplicationSettings extends ReadWriteProxy {
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();
}
}

View File

@ -20,7 +20,6 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { MODS } from "../mods/modloader";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
import { getApplicationSettingById } from "../profile/application_settings";
import { T } from "../translations";
const trim = require("trim");
@ -468,7 +467,7 @@ export class MainMenuState extends GameState {
onLanguageChooseClicked() {
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, {
active: this.app.settings.getLanguage(),

View File

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