UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

281 lines (239 loc) 8.63 kB
/** * LittleJS Medal System * - Achievement/trophy system for games * - Medal class with name, description, icon, and unlock tracking * - Automatic saving to local storage * - Visual display queue with slide-in notifications * - Newgrounds API integration for online achievements * - Debug mode to unlock/reset medals during development * @namespace Medals */ 'use strict'; let debugMedals = false; /////////////////////////////////////////////////////////////////////////////// // Medals settings /** How long to show medals for in seconds * @type {number} * @default * @memberof Settings */ let medalDisplayTime = 5; /** How quickly to slide on/off medals in seconds * @type {number} * @default * @memberof Settings */ let medalDisplaySlideTime = .5; /** Size of medal display * @type {Vector2} * @default Vector2(640,80) * @memberof Settings */ let medalDisplaySize = vec2(640, 80); /** Set to stop medals from being unlockable (like if cheats are enabled) * @type {boolean} * @default * @memberof Settings */ let medalsPreventUnlock = false; /** List of all medals * @type {Object} * @memberof Medals */ const medals = {}; // Engine internal variables not exposed to documentation let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast; /////////////////////////////////////////////////////////////////////////////// /** Initialize medals with a save name used for storage * - Call this after creating all medals * - Checks if medals are unlocked * @param {string} saveName * @memberof Medals */ function medalsInit(saveName) { // check if medals are unlocked medalsSaveName = saveName; if (!debugMedals) { let saved = {}; try { saved = JSON.parse(localStorage[saveName] || '{}'); } catch (e) { saved = {}; } medalsForEach(medal => { medal.unlocked = !!(saved[medal.id] && saved[medal.id].unlocked); }); medalsSave(); } // engine automatically renders medals engineAddPlugin(undefined, medalsRender); // plugin functions function medalsRender() { if (!medalsDisplayQueue.length) return; // update first medal in queue const medal = medalsDisplayQueue[0]; const time = timeReal - medalsDisplayTimeLast; if (!medalsDisplayTimeLast) medalsDisplayTimeLast = timeReal; else if (time > medalDisplayTime) { medalsDisplayTimeLast = 0; medalsDisplayQueue.shift(); } else { // slide on/off medals const slideOffTime = medalDisplayTime - medalDisplaySlideTime; const hidePercent = time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime : time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0; medal.render(hidePercent); } } } /** * @callback MedalCallbackFunction - Function that processes a medal * @param {Medal} medal * @memberof Medals */ /** Calls a function for each medal * @param {MedalCallbackFunction} callback * @memberof Medals */ function medalsForEach(callback) { Object.values(medals).forEach(medal=>callback(medal)); } /** Reset all medals to locked and persist the cleared catalog * @memberof Medals */ function medalsReset() { medalsForEach(medal => medal.unlocked = false); medalsSave(); } function medalsSave() { if (!medalsSaveName) return; const data = {}; medalsForEach(medal => { const entry = { name: medal.name, description: medal.description, icon: medal.icon, unlocked: medal.unlocked, }; if (medal.image) entry.src = medal.image.src; data[medal.id] = entry; }); localStorage[medalsSaveName] = JSON.stringify(data); } /////////////////////////////////////////////////////////////////////////////// /** * Medal - Tracks an unlockable medal * @memberof Medals * @example * // create a medal * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️'); * * // initialize medals * medalsInit('Example Game'); * * // unlock the medal * medal_example.unlock(); */ class Medal { /** Create a medal object and adds it to the list of medals * @param {number} id - The unique identifier of the medal * @param {string} name - Name of the medal * @param {string} [description] - Description of the medal * @param {string} [icon] - Icon for the medal * @param {string} [src] - Image location for the medal */ constructor(id, name, description='', icon='🏆', src) { ASSERT(id >= 0 && !medals[id]); /** @property {number} - The unique identifier of the medal */ this.id = id; /** @property {string} - Name of the medal */ this.name = name; /** @property {string} - Description of the medal */ this.description = description; /** @property {string} - Icon for the medal */ this.icon = icon; /** @property {boolean} - Is the medal unlocked? */ this.unlocked = false; /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */ this.image = undefined; if (src) (this.image = new Image).src = src; // add this to list of medals medals[id] = this; } /** Unlocks a medal if not already unlocked */ unlock() { if (medalsPreventUnlock || this.unlocked) return; ASSERT(medalsSaveName, 'save name must be set'); this.unlocked = true; medalsSave(); medalsDisplayQueue.push(this); } /** Render a medal * @param {number} [hidePercent] - How much to slide the medal off screen */ render(hidePercent=0) { const context = mainContext; const width = min(medalDisplaySize.x, mainCanvas.width); const height = medalDisplaySize.y; const x = mainCanvas.width - width; const y = -height*hidePercent; const backgroundColor = hsl(0,0,.9); // draw containing rect and clip to that region context.save(); context.beginPath(); context.fillStyle = backgroundColor.toString(); context.strokeStyle = BLACK.toString(); context.lineWidth = 3; context.rect(x, y, width, height); context.fill(); context.stroke(); context.clip(); // draw the icon const gap = vec2(.1, .05).scale(height); const medalDisplayIconSize = height - 2*gap.x; this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize); // draw the name const nameSize = height*.5; const descriptionSize = height*.3; const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2); const textWidth = width - medalDisplayIconSize - 3*gap.x; drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth); // draw the description pos.y = y + height - gap.y*2 - descriptionSize/2; drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth); context.restore(); } /** Render the icon for a medal * @param {Vector2} pos - Screen space position * @param {number} size - Screen space size */ renderIcon(pos, size) { // draw the image or icon if (this.image) mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size); else drawTextScreen(this.icon, pos, size*.7, BLACK); } } /////////////////////////////////////////////////////////////////////////////// // Medals setting setters /** Set how long to show medals for in seconds * @param {number} time * @memberof Settings */ function setMedalDisplayTime(time) { medalDisplayTime = time; } /** Set how quickly to slide on/off medals in seconds * @param {number} time * @memberof Settings */ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; } /** Set size of medal display * @param {Vector2} size * @memberof Settings */ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); } /** Set to stop medals from being unlockable * @param {boolean} preventUnlock * @memberof Settings */ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }