/**
* @namespace MATTIE.fxAPI
* @description The api for screen effects. IE: filters, screen shake, etc...
*/
MATTIE.fxAPI = {};
MATTIE.fxAPI.trackedLights = [];
//----------------------------------------------
// Game_Screen
//----------------------------------------------
MATTIE_RPG.fxAPI = {};
/** @description the base start tint function */
MATTIE_RPG.fxAPI.startTint = Game_Screen.prototype.startTint;
/**
* @description the start tint function overriden to only fire if an effect is not being forced
* @param {*} tone [r,b,g,gray]
* @param {*} duration frames till end
*/
Game_Screen.prototype.startTint = function (tone, duration) {
if (!this.forcingTint) MATTIE_RPG.fxAPI.startTint.call(this, tone, duration);
};
/**
* @description set the forcing tint attribute to bool
*/
Game_Screen.prototype.forceTint = function (bool) {
this.forcingTint = bool;
};
/** @description the base update tone method */
MATTIE_RPG.fxAPI.updateTone = Game_Screen.prototype.updateTone;
/** @description the update tone method extended to stop forcing if duration is over */
Game_Screen.prototype.updateTone = function () {
MATTIE_RPG.fxAPI.updateTone.call(this);
if (this._toneDuration <= 0) this.forceTint(false);
};
MATTIE.fxAPI.setupTint = function (red, green, blue, gray, framesDur) {
const tint = this.formatTint(red, green, blue, gray);
$gameScreen.startTint(tint, framesDur);
$gameScreen.forceTint(true);
};
MATTIE.fxAPI.startScreenShake = function (intensity, speed, duration) {
$gameScreen.startShake(intensity, speed, duration);
};
/**
* @description format a tint in rpg maker format
* @param {int} red
* @param {int} green
* @param {int} blue
* @param {int} gray
* @returns formatted tint obj
*/
MATTIE.fxAPI.formatTint = function (red, green, blue, gray) {
const tint = [red, green, blue, gray];
return tint;
};
/**
* @description displays www/data/imgs/picture/name
* @param {string} name name of file in www/data/imgs/picture to display
* @param {int} id the id of the picture to create (this is like the layer number of the layer in photoshop)
* @param {int} x the x to show the image at
* @param {int} y
*/
MATTIE.fxAPI.showImage = function (name, id, x, y) {
$gameScreen.showPicture(id, name, 0, x, y, 100, 100, 255, 0);
};
MATTIE.fxAPI.deleteImage = function (id) {
$gameScreen.erasePicture(id);
};
/**
* @description draw a circle on the light mask
* @param {lightMask} the lightmask to target
* @param {number} x the x cord of the center of the circle
* @param {number} y the y cord of the center of the circle
* @param {number} r1 the radius of the inner circle for gradient
* @param {number} r2 the radius of the outer circle for gradient
* @param {string} clr1 the color in the center of the circle gradient, in hex ie: '#FFFFFF'
* @param {string} clr2 the color on the edge of the circle gradient, in hex
* @param {string} blendMode the blend mode
*/
MATTIE.fxAPI.drawCircleMap = function (x, y, lightMask, r1 = 50, r2 = 250, clr1 = '#FFFFFF', clr2 = 'black', blendMode = 'lighter') {
const pw = $gameMap.tileWidth();
const ph = $gameMap.tileHeight();
const dx = $gameMap.displayX();
const dy = $gameMap.displayY();
const px = x;
const py = y;
// Calculate coordinates
var x1 = (pw / 2) + ((px - dx) * pw);
var y1 = (ph / 2) + ((py - dy) * ph);
// Handle screen wraparound / parallax if needed (standard RPG Maker logic)
if (dx > px && ($gameMap.width() > $gameMap.screenTileX())) {
const xjump = $gameMap.width() - Math.floor(dx - px);
if (xjump < $gameMap.screenTileX()) x1 = (pw / 2) + (xjump * pw);
}
if (dy > py && ($gameMap.height() > $gameMap.screenTileY())) {
const yjump = $gameMap.height() - Math.floor(dy - py);
if (yjump < $gameMap.screenTileY()) y1 = (ph / 2) + (yjump * ph);
}
// Ensure bitmap exists
if (!lightMask._maskBitmap) return;
const { canvas } = lightMask._maskBitmap;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// DEBUG: Log coordinates every 60 frames roughly to check tracking
// if (Math.random() < 0.016) {
// console.log(`[vfxAPI] Drawing Light at Screen: ${x1.toFixed(0)}, ${y1.toFixed(0)} | Map: ${px.toFixed(1)}, ${py.toFixed(1)}`);
// }
const temp = ctx.globalCompositeOperation;
// Terrax uses 'lighter' to accumulate lights on the black mask.
// 'source-over' creates black squares because the gradient edge is black.
// 'lighter' allows the black edge (0,0,0) to add nothing, solving the merging issue.
ctx.globalCompositeOperation = 'lighter';
// DEBUG: Draw a small red box at center to verify position visible
// ctx.fillStyle = 'red';
// ctx.fillRect(x1 - 5, y1 - 5, 10, 10);
// Manual draw for maximum control
try {
const grad = ctx.createRadialGradient(x1, y1, r1, x1, y1, r2);
// Center: Full White (Transparent hole in Multiply mask)
grad.addColorStop(0, '#FFFFFF');
// Middle: Colored tint
grad.addColorStop(0.5, clr1);
// Edge: Black (Full Darkness)
grad.addColorStop(1, 'black');
ctx.fillStyle = grad;
ctx.fillRect(x1 - r2, y1 - r2, r2 * 2, r2 * 2);
} catch (e) {
console.error('[vfxAPI] Error drawing light:', e);
}
ctx.globalCompositeOperation = temp;
};
/**
* @description make the player invisible for X frames or till turned back visible
* @param ms if provided turn the player inviable for this many milliseconds, else just leave them invisible till turned visible by something else
*/
MATTIE.fxAPI.hidePlayer = function (ms = -1) {
$gamePlayer.setTransparent(true);
if (ms > 0) {
setTimeout(() => {
this.showPlayer();
}, ms);
}
};
/**
* @description make the player visible again if they were invisible
*/
MATTIE.fxAPI.showPlayer = function () {
$gamePlayer.setTransparent(false);
};
/**
* @description lock player controls for x milliseconds or till turned back on
* @param ms if provided the player will be unable to act for this many milliseconds, else unable to act till reenabled elsewhere
*/
MATTIE.fxAPI.lockPlayer = function (ms = -1) {
this.orgCanMove = this.orgCanMove || $gamePlayer.canMove;
$gamePlayer.canMove = () => false;
$gameSystem.disableSave();
$gameSystem.disableMenu();
if (ms > 0) {
setTimeout(() => {
this.unlockPlayer();
}, ms);
}
};
/**
* @description allow the player to input and stuff again
*/
MATTIE.fxAPI.unlockPlayer = function () {
$gameSystem.enableMenu();
$gameSystem.enableSave();
if (this.orgCanMove) {
$gamePlayer.canMove = this.orgCanMove;
this.orgCanMove = undefined;
}
};
/**
* @description add a game object to the list of tracked lights
* @param {function} object any object with ._realX and realY
* @param {function} active a function to check if the light is on or not
* @param {number} r1 the radius of the inner circle for gradient
* @param {number} r2 the radius of the outer circle for gradient
* @param {string} clr1 the color in the center of the circle gradient, in hex ie: '#FFFFFF'
* @param {string} clr2 the color on the edge of the circle gradient, in hex
* @param {string} blendMode the blend mode
*/
MATTIE.fxAPI.addLightObject = function (object, active = () => true, r1 = 50, r2 = 250, clr1 = '#FFFFFF', clr2 = 'black', blendMode = 'lighter') {
const obj = {};
obj.getContent = object;
obj.active = active;
obj.r1 = r1;
obj.r2 = r2;
obj.clr1 = clr1;
obj.clr2 = clr2;
obj.blendMode = blendMode;
MATTIE.fxAPI.trackedLights.push(obj);
};
MATTIE.fxAPI.onUpdateMask = function (lightMaskContext) {
if (!MATTIE.fxAPI.trackedLights) return;
// Check if we have any active tracked lights
const hasActiveLights = MATTIE.fxAPI.trackedLights.some((l) => {
const active = l && l.active && l.active();
return active;
});
if (hasActiveLights) {
// If Terrax judged there were no native lights, it might not have added the sprite layer.
if (lightMaskContext._sprites && lightMaskContext._sprites.length === 0) {
if (lightMaskContext._addSprite && lightMaskContext._maskBitmap) {
// console.log('[vfxAPI] Force-adding sprite layer');
lightMaskContext._addSprite(0, 0, lightMaskContext._maskBitmap);
lightMaskContext._maskBitmap.fillRect(0, 0, lightMaskContext._maskBitmap.width, lightMaskContext._maskBitmap.height, 'black');
}
}
}
for (let index = 0; index < MATTIE.fxAPI.trackedLights.length; index++) {
const element = MATTIE.fxAPI.trackedLights[index];
if (element && element.active && element.active()) {
const target = element.getContent();
// Safety check for target existence
if (target && typeof target._realX !== 'undefined' && typeof target._realY !== 'undefined') {
MATTIE.fxAPI.drawCircleMap(
target._realX,
target._realY,
lightMaskContext,
element.r1,
element.r2,
element.clr1,
element.clr2,
element.blendMode,
);
}
}
}
};
MATTIE.fxAPI.hookLightmaskProto = function (proto) {
if (proto._vfxHooked) return;
console.log('[vfxAPI] Hooking Lightmask prototype...');
const oldUpdate = proto._updateMask;
proto._updateMask = function () {
if (oldUpdate) oldUpdate.call(this);
MATTIE.fxAPI.onUpdateMask(this);
};
proto._vfxHooked = true;
console.log('[vfxAPI] Hooked Lightmask successfully via prototype');
};
MATTIE.fxAPI.injectHooks = function () {
// Strategy 1: Hook Spriteset_Map to catch future Lightmasks
if (!Spriteset_Map.prototype._vfxHooked) {
const oldCreate = Spriteset_Map.prototype.createLightmask;
Spriteset_Map.prototype.createLightmask = function () {
if (oldCreate) oldCreate.call(this);
if (this._lightmask) {
// Determine prototype from the instance
const proto = Object.getPrototypeOf(this._lightmask);
MATTIE.fxAPI.hookLightmaskProto(proto);
}
};
Spriteset_Map.prototype._vfxHooked = true;
console.log('[vfxAPI] Hooked Spriteset_Map.createLightmask');
}
// Strategy 2: Attempt to hook existing instance if game is already running
try {
if (SceneManager._scene && SceneManager._scene._spriteset && SceneManager._scene._spriteset._lightmask) {
const proto = Object.getPrototypeOf(SceneManager._scene._spriteset._lightmask);
MATTIE.fxAPI.hookLightmaskProto(proto);
}
} catch (e) {
// Scene might not be ready, ignore
}
};
// Initial injection
MATTIE.fxAPI.injectHooks();
// Retry injection a few times just in case we loaded before SceneManager was ready
setTimeout(() => MATTIE.fxAPI.injectHooks(), 1000);
setTimeout(() => MATTIE.fxAPI.injectHooks(), 3000);
setTimeout(() => MATTIE.fxAPI.injectHooks(), 6000);
/**
* @description a function to zoom in or out focused on the charecter in the middle of the screen
* @param x the percentage to zoom, ie: 1 = none, .1 = 10x out 10 = 10x in.
*/
MATTIE.fxAPI.zoom = (x) => {
const height = Graphics.boxHeight;
const width = Graphics.boxWidth;
$gameScreen.setZoom(width / 2, height / 2, x);
};