src/blip.mjs
import { InstanceMarker } from './instanceMarker.mjs';
import { Logger } from './vendor/logger.min.mjs';
import { Collector } from './vendor/collector.min.mjs';
class BlipManagerSingleton {
/**
* The size of the game.
* @private
* @type {number}
*/
static GAME_SIZE = (() => {
if (VYLO) {
const gameSize = VYLO.World.getGameSize();
return gameSize;
}
return { 'width': 960, 'height': 540 };
})();
/**
* The size of the game halfed.
* @private
* @type {number}
*/
static GAME_SIZE_HALF = (() => {
return { 'width': BlipManagerSingleton.GAME_SIZE.width / 2, 'height': BlipManagerSingleton.GAME_SIZE.height / 2 };
})();
/**
* The center of the screen's coordinates (in pixels).
* @private
* @type {number}
*/
static CENTER_SCREEN_POSITION = (() => {
return { x: BlipManagerSingleton.GAME_SIZE.width / 2 , y: BlipManagerSingleton.GAME_SIZE.height / 2};
})();
/**
* The default tile size. This is used a backup value when an icon's width/height is not accessible.
* @private
* @type {number}
*/
static TILE_SIZE = 32;
/**
* The maximum number of blips that can exist on the screen at once.
* @private
* @type {number}
*/
static MAX_BLIPS = 200;
/**
* An array tracking all stored blips.
* @private
* @type {Array}
*/
static storedBlips = [];
/**
* Gets the angle between two points.
*
* @private
* @param {Object} pStartPoint - The starting point.
* @param {Object} pEndPoint - The ending point.
* @param {boolean} pCenter - Whether to get the angle from the center of the points.
* @returns {number} The angle between the starting point and the ending point.
*/
static getAngle(pStartPoint, pEndPoint, pCenter) {
let y;
let x;
if (pCenter) {
const iconWidth = pStartPoint.icon ? pStartPoint.icon.width : 32;
const iconHeight = pStartPoint.icon ? pStartPoint.icon.height : 32;
y = (pStartPoint.y + iconHeight / 2) - (pEndPoint.y + iconHeight / 2);
x = (pStartPoint.x + iconWidth / 2) - (pEndPoint.x + iconWidth / 2);
} else {
y = pStartPoint.y - pEndPoint.y;
x = pStartPoint.x - pEndPoint.x;
}
return -Math.atan2(y, x) - Math.PI;
}
/**
* API to get distance between points.
* @private
* @param {Object} pStartPoint - The starting point.
* @param {Object} pEndPoint - The ending point.
* @param {boolean} pCenter - Whether to get the distance from the center of the points.
* @returns {number} The distance between the two points.
*/
static getDistance(pStartPoint, pEndPoint, pCenter) {
let y;
let x;
if (pCenter) {
const iconWidth = pStartPoint.icon ? pStartPoint.icon.width : 32;
const iconHeight = pStartPoint.icon ? pStartPoint.icon.height : 32;
y = (pStartPoint.y + iconHeight / 2) - (pEndPoint.y + iconHeight / 2);
x = (pStartPoint.x + iconWidth / 2) - (pEndPoint.x + iconWidth / 2);
} else {
y = (pStartPoint.y - pEndPoint.y);
x = (pStartPoint.x - pEndPoint.x);
}
return Math.sqrt(x * x + y * y);
}
/**
* Gets the direction of the angle passed.
* @private
* @param {number} pAngle - The angle in radians to convert into a cardinal direction.
* @returns The direction of the angle.
*/
static getDirection(pAngle) {
const degree = Math.abs(Math.floor(((pAngle * (180 / Math.PI)) / 45) + 0.5));
const compassDirections = ['east', 'southeast', 'south', 'southwest', 'west', 'northwest', 'north', 'northeast'];
return compassDirections[(degree % 8)];
}
/**
* The version of the module.
*/
version = "VERSION_REPLACE_ME";
/**
* Whether the manager is actively managing the state of blips.
* @private
* @type {boolean}
*/
paused = false;
/**
* An array tracking all active blips (hidden or not).
* @private
* @type {Array}
*/
activeBlips = [];
/**
* @private
*/
constructor() {
/**
* The interface used to handle the blips.
* @type {string}
* @private
*/
this.interfaceHandle = 'blip-interface-' + (Math.random() * Math.PI / 2);
// Create a logger
/** The logger module this module uses to log errors / logs
* @private
* @type {Object}
*/
this.logger = new Logger();
this.logger.registerType('BlipComponent-Module', '#ff6600');
// Create the interface
VYLO.Client.createInterface(this.interfaceHandle);
/**
* Update loop for this blip manager to manage blips.
*/
const self = this;
const update = function() {
self.manageBlips();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
/**
* Tracks a blip within the activeBlips array.
* @param {Object} pBlip - The blip to track.
*/
track(pBlip) {
if (!this.activeBlips.includes(pBlip)) {
this.activeBlips.push(pBlip);
}
}
/**
* Untracks a blip from the activeBlips array.
* @param {Object} pBlip - The blip to untrack.
*/
untrack(pBlip) {
if (this.activeBlips.includes(pBlip)) {
this.activeBlips.splice(this.activeBlips.indexOf(pBlip), 1);
}
}
/**
* Pauses the blip manager from managing blips
*/
pause() {
this.paused = true;
}
/**
* Resumes the blip manager to manage blips
*/
resume() {
this.paused = false;
}
/**
* Method for updating the "state" of each blip.
* @private
*/
manageBlips() {
// Do not manage blips when paused.
if (this.paused) {
return;
}
for (const blip of this.activeBlips) {
const blipIconWidth = blip.icon ? blip.icon.width : 32;
const blipIconHeight = blip.icon ? blip.icon.height : 32;
const mapInstanceIconWidth = blip.mapInstance.icon ? blip.mapInstance.icon.width : 32;
const mapInstanceHeight = blip.mapInstance.icon ? blip.mapInstance.icon.height : 32;
blip.mapInstance.getScreenPos(blip.screenPos);
// Get the position the blip should be placed at
const x = VYLO.Math.clamp((blip.screenPos.x + scrM.xMapPos + (mapInstanceIconWidth / 2) - blipIconWidth / VYLO.Client.mapView.scale.x) * VYLO.Client.mapView.scale.x, -blipIconWidth + blip.settings.buffer, BlipManagerSingleton.GAME_SIZE.width - blipIconWidth - blip.settings.buffer);
const y = VYLO.Math.clamp((blip.screenPos.y + scrM.yMapPos + (mapInstanceHeight / 2) - blipIconHeight / 2 / VYLO.Client.mapView.scale.y) * VYLO.Client.mapView.scale.y, (-blipIconHeight / 2) + blip.settings.buffer, BlipManagerSingleton.GAME_SIZE.height - (blipIconHeight / 2) - blip.settings.buffer);
// Check distance between map instance and client mob
const distance = Math.round(BlipManagerSingleton.getDistance(VYLO.Client.mob, blip.mapInstance, true));
// Check angle from client mob to map instance
const angle = BlipManagerSingleton.getAngle(VYLO.Client.mob, blip.mapInstance, true);
// Check the direction of the angle
const direction = BlipManagerSingleton.getDirection(angle);
// Whether the blip will be shown or not
let showBlip = false;
// Setting the angle of the blip
blip.angle = angle;
// Setting the blip to the proper coordinates
blip.setPos(x, y);
if (distance < blip.settings.maxDistance) {
const furtherThanHalfHorizontalScreenSize = distance >= (BlipManagerSingleton.GAME_SIZE_HALF.width - (mapInstanceIconWidth / 2)) / VYLO.Client.mapView.scale.x;
const furtherThanHalfVerticalScreenSize = distance >= (BlipManagerSingleton.GAME_SIZE_HALF.height - (mapInstanceHeight / 2)) / VYLO.Client.mapView.scale.y;
// Check the direction so we know whether to show the blip or not. Some directions make it so the formula changes on whether to show it or not
switch (direction) {
case 'north':
case 'south':
if (furtherThanHalfVerticalScreenSize) {
showBlip = true;
}
break;
case 'east':
case 'west':
if (furtherThanHalfHorizontalScreenSize) {
showBlip = true;
}
break;
/**
* @todo Fix issue where lower distance sometimes makes blip hide
*/
case 'northwest':
case 'northeast':
case 'southwest':
case 'southeast':
if (furtherThanHalfHorizontalScreenSize || furtherThanHalfVerticalScreenSize) {
showBlip = true;
}
break;
}
}
if (showBlip) {
blip.show();
} else {
blip.hide();
}
/**
* @todo Handle the distance text being shown
*/
}
}
/**
* Hides all blips
*/
hideBlips() {
for (const blip of this.activeBlips) {
blip.hide();
}
}
}
// Create an instance of the blip manager
const BlipManager = new BlipManagerSingleton();
/**
* A Blip Component
* @class BlipComponent
* @version {@versionPlaceholder}
* @license Blip does not have a license at this time. For licensing contact the author
* @author https://github.com/doubleactii
* Copyright (c) 2023 Evitca Studio
*/
export class BlipComponent {
/**
* This is the static distance it will be from the side of the game window
* @type {number}
*/
static BUFFER = 25;
/**
* This is the maximum distance at which the blip will be removed. (Distance from the center of the screen)
* @type {number}
*/
static MAX_DIST = 1000;
/**
* This is the max plane/layer at which this blip will exist. This will overlay this blip icon over any other interface element
* @type {number}
*/
static MAX_DISPLAY = 9999;
/**
* The blip instance that is created/recycled. This will be a Diob instance
* @type {Object}
*/
instance = null;
/**
* @param {Object} pMapInstance - The instance this blip will represent
* @param {Object} pIconSettings - An object with settings holding the icon information for the blip
* @param {Object} pBlipSettings - AN object with settings on how the blip will behave
*/
constructor(pMapInstance, pIconSettings = { 'atlasName': '', 'iconName': '' }, pBlipSettings = { 'buffer': BlipComponent.BUFFER, 'showsDistance': false, 'maxDistance': BlipComponent.MAX_DIST, 'alwaysOnTop': false }, pMarkerSettings) {
// Do not allow more than the max amount of blips to be created.
if (BlipManager.activeBlips.length >= BlipManagerSingleton.MAX_BLIPS) {
BlipManager.logger.prefix('BlipComponent-Module').log('Max blip limit reached');
return;
}
// Creating/Recyling the blip
const blip = Collector.isInCollection('Interface', 1, BlipManagerSingleton.storedBlips);
blip.touchOpacity = 0;
blip.mouseOpacity = 0;
blip.preventAutoScale = false;
blip.anchor.x = 1;
blip.anchor.y = 0.5;
// Assigning icon data to the blip
blip.atlasName = pIconSettings.atlasName;
blip.iconName = pIconSettings.iconName;
// Allow map instance to be referenced via the blip
blip.mapInstance = pMapInstance;
blip.screenPos = { x: 0, y: 0 };
blip.settings = {
// The padding between the blip and the screen
'buffer': pBlipSettings.buffer,
// Whether this blip will show its distance
'showsDistance': pBlipSettings.showsDistance,
// The max distance at which the map instance this blip represents can be from the center of the screen
'maxDistance': pBlipSettings.maxDistance
};
// Render above all others
if (pBlipSettings.alwaysOnTop) {
blip.plane = BlipComponent.MAX_DISPLAY;
blip.layer = BlipComponent.MAX_DISPLAY;
}
// Shows a marker when the blip is hidden due to the map instance being ON SCREEN.
if (pMarkerSettings) {
/**
* @todo Create Instance Marker with settings.
* Remember the atlasName and iconName is inside of the marker settings
*/
}
// Adding blip to interface to be shown
VYLO.Client.addInterfaceElement(blip, BlipManager.interfaceHandle, pMapInstance.id + '- blip');
blip.hide();
// Track the blip
BlipManager.track(blip);
// Add a reference to the blip
this.instance = blip;
// Show the interface if it isn't shown
if (!VYLO.Client.checkInterfaceShown(this.interfaceHandle)) {
VYLO.Client.showInterface(this.interfaceHandle);
}
}
/**
* Removes this blip
*/
remove() {
// We check if there is an instance attached to this blip, because some blips can be created when the max blips already exist and they will be useless
if (this.instance) this.instance.hide();
BlipManager.untrack(this.instance);
}
}