Mod Framework Quick Reference - ahvonenj/synergism-hypersynergy GitHub Wiki
Here's a collection of some "nice-to-knows" regarding what the mod offers for development.
Wiki references:
The HSElementHooker.HookElement()
is an async
and static
method of HSElementHooker
which allows for querying for elements in the DOM in an asynchronous manner. Whereas with e.g. synchronous document.querySelector()
you're left in a kind of a "tough luck" situation if the element is not found at the time of calling this method, you can instead do something like const myElement = await
HSElementHooker.HookElement('#element')` which ensures that the element has been found when using it in code after this call.
There's also a HSElementHooker.hookElements()
method which is equivalent to document.querySelectorAll()
and can be used to query multiple elements and to ensure that all of the elements have been found.
Example:
Here we use HookElement
to await
for the buildings tab button and then set it's color to red.
async applyPatch(): Promise<void> {
const buildingBtn = await HSElementHooker.HookElement('#buildingstab') as HTMLButtonElement;
buildingBtn.style.color = 'red';
}
The HSElementHooker.watchElement()
method is another static
method of HSElementHooker
which allows for observing an element for changes. Under the hood it uses MutationObserver
to achieve this and acts as a sort of an abstraction layer to hide much of the complexity when it comes to working with MutationObservers
.
Example:
Here's a snipped from HSHepteracts
module. In the view where you can expand and craft hepteracts, there is a text like "You possess x Hepteracts! You know where to get these, right?" shown. Since we can't access the variable itself which holds the amount of hepteracts the player currently owns, we instead have to parse it from the DOM.
Now, due to multiple reasons it doesn't do much for us to read this value only once, so we set up a watch for it. This watch will then observe for changes in the element (mainly it's HTML content in this case) and do something when the value changes. In this case we update the HSHepteract module's internal #ownedHepteracts
value. Having this set up we can be pretty sure that whenever we need to use the player's current hepteract count for something, we can use the #ownedHepteracts
value and know that it is automatically updated by the watch when it detects changes.
self.#ownedHepteractsWatch = HSElementHooker.watchElement(self.#ownedHepteractsElement, (value) => {
try {
const hepts = parseFloat(value);
self.#ownedHepteracts = hepts;
} catch (e) {
HSLogger.error(`Failed to parse owned hepteracts`, self.context);
}
self.#watchUpdatePending = false;
},
{
greedy: true,
overrideThrottle: true,
valueParser: (element) => {
const subElement = element.querySelector('span');
const value = subElement?.innerText;
return value;
}
});
The first argument that the HSElementHooker.watchElement()
takes is the element to be watched. It is recommended to use HSElementHooker.HookElement
method for this like:
const elementToBeWatched = await HSElementHooker.HookElement('#element');
HSElementHooker.watchElement(elementToBeWatched, (value) => { ... }, { ... });
The second argument is the callback function which will be called when changes to the element have been detected and AFTER the changes have gone through valueParser
which can be supplied in the third argument which is the options object, so for example:
const elementToBeWatched = await HSElementHooker.HookElement('#element');
HSElementHooker.watchElement(elementToBeWatched, (value) => { ... }, {
valueParser: (element) => {
const value = element?.innerText;
return value;
}
});
So here the valueParser
is triggered first when changes have been detected. The valueParser
gets the changed element as an argument and we can do things like getting the changed element's innerText
and then supply that to the actual callback function of the HookElement
method.
In this example the value
argument in the callback for the HookElement
method would be the element's innerText
as returned by the valueParser
.
Data persistence is needed when we want to have some data to persist through page reloads.
The HSStorage
module is responsible for data persistence aka. saving and loading things from the localStorage
. While the HSStorage
module can be used as is like:
const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;
if(storageModule) {
storageModule.setData(someKey, someData);
} else {
HSLogger.warn(`saveState - Could not find storage module`, this.context);
}
It is recommended to use HSStorage
in conjunction with HSPersistable
interface. To do this you would simply define your module class which needs to persist some data like so:
export class MyClassThatNeedsDataPersistence extends HSModule implements HSPersistable {}
The HSPersistable
interface is defined like so:
export interface HSPersistable {
saveState(): Promise<any>;
loadState(): Promise<void>;
}
So for a class to implement it, we need to add the saveState
and loadState
methods to it:
export class MyClassThatNeedsDataPersistence extends HSModule implements HSPersistable {
async saveState(): Promise<any> {
const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;
if(storageModule) {
storageModule.setData(key, data);
} else {
HSLogger.warn(`saveState - Could not find storage module`, this.context);
}
}
async loadState(): Promise<void> {
const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;
if(storageModule) {
const data = storageModule.getData(key) as string;
if(!data) {
HSLogger.warn(`loadState - No data found`, this.context);
return;
}
try {
const parsedData = JSON.parse(data);
} catch(e) {
HSLogger.warn(`loadState - Error parsing data`, this.context);
return;
}
} else {
HSLogger.warn(`loadState - Could not find storage module`, this.context);
}
}
}
Implementing data persistence like this (by implementing HSPersistable
) makes it clear that your module intends to use localStorage
for something and that it will have saveState
and loadState
methods to do so.
More full example from HSAmbrosia module:
This is how the HSAmbrosia
module remembers the loadout icons (over page refreshes) which the player can save.
export class HSAmbrosia extends HSModule implements HSPersistable {
#loadoutState: HSAmbrosiaLoadoutState = new Map<AMBROSIA_LOADOUT_SLOT, AMBROSIA_ICON>();
constructor(moduleName: string, context: string, moduleColor?: string) {
super(moduleName, context, moduleColor);
}
async init() {
this.loadState();
this.isInitialized = true;
}
async saveState(): Promise<any> {
const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;
if(storageModule) {
const serializedState = JSON.stringify(Array.from(this.#loadoutState.entries()));
storageModule.setData(HSGlobal.HSAmbrosia.storageKey, serializedState);
} else {
HSLogger.warn(`saveState - Could not find storage module`, this.context);
}
}
async loadState(): Promise<void> {
const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;
if(storageModule) {
const data = storageModule.getData(HSGlobal.HSAmbrosia.storageKey) as string;
if(!data) {
HSLogger.warn(`loadState - No data found`, this.context);
return;
}
try {
const parsedData = JSON.parse(data) as [AMBROSIA_LOADOUT_SLOT, AMBROSIA_ICON][];
this.#loadoutState = new Map(parsedData);
} catch(e) {
HSLogger.warn(`loadState - Error parsing data`, this.context);
return;
}
} else {
HSLogger.warn(`loadState - Could not find storage module`, this.context);
}
}
}
Wiki references:
The HSGameState
module is responsible for observing and holding information about the current game state. Currently (2.5.2025) that mainly means keeping track of in which view or page (and subview of that page) the player is currently on.
So for example when the player switches from the "Buildings" view to the "Corruptions" view, the HSGameState
module will be aware of that transition.
The HSGameState
works and can be used by subscribing to whatever changes you are interested in. When HSGameState
module noticed changes, it will then notify all of it's subscribers of these changes.
Example:
The HSHepteracts
module subscribes to view changes in order to start and stop certain high performance element watchers which would otherwise cause needless performance overhead when the player is not interacting with the hepteracts page at all.
const gameStateMod = HSModuleManager.getModule<HSGameState>('HSGameState');
if(gameStateMod) {
gameStateMod.subscribeGameStateChange(GAME_STATE_CHANGE.MAIN_VIEW, (prevView, currentView) => {
if(prevView.getId() === MAIN_VIEW.CUBES &&
currentView.getId() !== MAIN_VIEW.CUBES &&
gameStateMod.getCurrentCubeView().getId() === CUBE_VIEW.HEPTERACT_FORGE
) {
if(self.#ownedHepteractsWatch) {
HSLogger.debug("Hepteract forge view closed, stopping watch", this.context);
// ...code
}
}
});
gameStateMod.subscribeGameStateChange(GAME_STATE_CHANGE.CUBE_VIEW, async (prevView, currentView) => {
if(currentView.getId() === CUBE_VIEW.HEPTERACT_FORGE) {
HSLogger.debug("Hepteract forge view opened, starting watch", this.context);
// ...code
}
});
}
As can be seen, we subscribe to two kinds of game state changes:
-
GAME_STATE_CHANGE.MAIN_VIEW
for when the player switches the main tab (Buildings, Corruptions, Research, ...) -
GAME_STATE_CHANGE.CUBE_VIEW
for when the player switches the tab within the "WOW! Cubes" tab (Cube tributes, Tesseract Gifts, ...)
By subscribing to these two view changes we can then write logic to start the element watcher only when the player is in the "Hepteract Forge" tab and have the watch stopped otherwise as it is not needed anywhere else.