Skip to content

Writing good scripts

This page collects practical guidelines for writing Melonity scripts that are easier to maintain, easier to debug, and safer to extend later.

These are not abstract TypeScript rules. They are recommendations based on real Melonity scripting workflow.


Keep script logic inside a namespace

Do not scatter script logic across the global script scope. Keep variables, helpers, and callbacks inside a dedicated namespace.

If another script really needs access to part of your logic, export only what should be public.

Good
ts
let MyScript: ScriptDescription = {};

namespace myScript {
	let enabled = false;

	export function IsEnabled(): boolean {
		return enabled;
	}

	MyScript.OnScriptLoad = () => {
		enabled = true;
	};

	RegisterScript(MyScript, 'Example');
}
Avoid
ts
let MyScript: ScriptDescription = {};
let enabled = false;

function isEnabled(): boolean {
	return enabled;
}

MyScript.OnScriptLoad = () => {
	enabled = true;
};

RegisterScript(MyScript, 'Example');

Keep menu state simple

Avoid splitting one piece of state across multiple variables when a single clear value is enough.

Good
ts
let enabled = Menu.AddToggle(PATH, 'Enable', false)
	.OnChange(state => {
		enabled = state.newValue;
	})
	.GetValue();
Avoid
ts
let enabled = Menu.AddToggle(PATH, 'Enable', false)
	.OnChange(state => {
		enabledValue = state.newValue;
	});

let enabledValue = enabled.GetValue();

The first version keeps the script state easy to read. The second version works, but makes the logic harder to follow.

TIP

If you need both the menu option object and the current value, use clear names such as enabledOption and enabled.


Scale rendering values to the current resolution

Hardcoded rendering values usually look correct only on the monitor on which they were created.

Font sizes, positions, padding, and element sizes should be scaled relative to the current screen resolution.

Example
ts
const ratio = screenHeight / 1080;
const fontSize = Math.floor(14 * ratio);
const offsetX = 20 * ratio;
const offsetY = 12 * ratio;

This makes the layout behave more consistently on different resolutions.


Recalculate resolution-dependent values in OnScreenSizeChange

If your script depends on screen size, do not assume the values will stay correct forever.

When the resolution changes, recompute fonts, offsets, and cached layout values inside OnScreenSizeChange.

Example
ts
let font: Font;
let ratio = 1;

let MyScript: ScriptDescription = {};

namespace myScript {
	MyScript.OnScreenSizeChange = (_width, height) => {
		ratio = height / 1080;
		font = Renderer.LoadFont('Tahoma', Math.floor(14 * ratio), Enum.FontWeight.BOLD);
	};

	RegisterScript(MyScript, 'Example');
}

IMPORTANT

If you cache resolution-dependent values and never update them, the result may look correct only until scripts are reloaded with F7.


Cache stable references instead of requesting them every tick

Do not call EntitySystem.GetLocalHero() every OnUpdate if you can cache the result when the game starts.

Better approach
ts
let MyScript: ScriptDescription = {};

namespace myScript {
	let myHero: Hero | null = null;
	let enabled = true;

	MyScript.OnGameStart = MyScript.OnScriptLoad = () => {
		myHero = EntitySystem.GetLocalHero();
	};

	MyScript.OnGameEnd = () => {
		myHero = null;
	};

	MyScript.OnUpdate = () => {
		if (!enabled || !myHero) {
			return;
		}

		// Script logic
	};

	RegisterScript(MyScript, 'Example');
}

This makes the script clearer and avoids unnecessary repeated lookups.


Keep OnDraw focused on rendering

OnDraw is called every frame. At higher FPS, it runs more often.

Because of that:

  • keep rendering code inside OnDraw
  • move heavy logic to OnUpdate
  • cache values whenever possible

This reduces the chance of FPS drops and makes the rendering path easier to debug.


Prefer early guard clauses

Guard clauses make script callbacks easier to read and easier to extend.

Good
ts
MyScript.OnUpdate = () => {
	if (!enabled || !myHero) {
		return;
	}

	// Main logic
};

This is usually clearer than wrapping the entire callback body in several nested if blocks.


Check the console first when UI or rendering breaks

If the menu looks broken, rendering behaves incorrectly, or something visual stops working, open the console first.

Use:

txt
F10

In many cases, the error is easier to identify from the console than by guessing from the visible symptoms.


Summary

Good Melonity scripts usually follow a few simple rules:

  • keep logic organized inside a namespace
  • keep state simple and readable
  • scale UI and rendering to resolution
  • update cached screen-dependent values when the screen size changes
  • cache stable game objects instead of requesting them every tick
  • use OnDraw only for rendering work
  • check the console before debugging blindly