// Hintcard.js
(function () {
/* global adventurejs A */
/**
* @class adventurejs.Hintcard
* @ajsinternal
* @param {Game} game A reference to the game instance.
* @ajsnavheading FrameworkClasses
* @summary Manages hints for a {@link adventurejs.Game|Game} instance.
* @classdesc
* <p>
* <strong>Hintcard</strong> is a repository for game hint
* options. Hintcard is created automatically
* by {@link adventurejs.Game|Game}. This is an internal class
* that authors should not need to construct. However,
* authors can set hint options from their game file as
* shown below.
* </p>
* <h3 class="examples">Example:</h3>
* <pre class="display"><code class="language-javascript">var MyGame = new adventurejs.Game( "MyGame", "GameDisplay" );
* MyGame.Hintcard.set({
*
* });
* </code></pre>
*/
class Hintcard {
constructor(game) {
this.game = game;
this.game.world._hintcard = {};
// this.key = 0;
this.introduction = "";
this.hints = {};
this.active = true;
}
/**
* Activate a hint, with optional recurse.
* @method adventurejs.Hintcard#activateHint
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
activateHint(key, recurse = true) {
return this.toggleHint(key, "active", true, recurse);
}
/**
* Get a count of the number of hints in each group.
* @method adventurejs.Hintcard#countHints
*/
countHints(node = this) {
if (!node.active) return [0, 0];
if (
"undefined" === typeof node.hints ||
Object.keys(node.hints).length === 0
) {
return [node.revealed ? 0 : 1, 1];
}
let count = 0;
let unrevealed = 0;
for (const key in node.hints) {
let [u, t] = this.countHints(node.hints[key]);
unrevealed += u;
count += t;
}
node.unrevealed = unrevealed;
node.count = count;
return [unrevealed, count];
}
/**
* Create a new hint.
* @method adventurejs.Hintcard#createHint
* @returns {String}
*/
createHint() {
// console.warn(`createHint`);
let hint = {
key: "",
text: "",
description: "",
name: "",
hints: {},
revealed: false,
active: true,
};
// return JSON.stringify(hint);
return hint;
}
/**
* Deactivate a hint, with optional recurse.
* @method adventurejs.Hintcard#deactivateHint
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to deactivate recursively.
* @returns {}
*/
deactivateHint(key, recurse = true) {
return this.toggleHint(key, "active", false, recurse);
}
/**
* Find hint by key. Looks recursively.
* @method adventurejs.Hintcard#findHintByKey
* @param {String} key An object key to search for.
* @param {Object} root The root object level to begin search.
* @returns {Object|null}
*/
findHintByKey(key, root = this) {
if (!root || typeof root !== "object") return null;
if (root.hints && Object.prototype.hasOwnProperty.call(root.hints, key)) {
return root.hints[key];
}
if (root.hints) {
for (const k of Object.keys(root.hints)) {
const found = this.findHintByKey(key, root.hints[k]);
if (found) return found;
}
}
return null;
}
/**
* Find hint by name. Looks recursively.
* @method adventurejs.Hintcard#findHintByName
* @param {String} key An object key to search for.
* @param {Object} root The root object level to begin search.
* @returns {Object|null}
*/
findHintByName(name, root = this) {
if (
!name ||
!root ||
typeof root !== "object" ||
!root.hints ||
typeof root.hints !== "object"
)
return null;
const comparator = this.game.settings.obfuscate_hints
? A.obfuscate(name)
: name;
for (let key in root.hints) {
let hint = root.hints[key];
if (comparator === hint.name) {
return hint;
}
if (hint.hints) {
let subhint = this.findHintByName(name, hint);
if (subhint) return subhint;
}
}
return null;
}
/**
* Hide a hint, with optional recurse.
* @method adventurejs.Hintcard#hideHint
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
hideHint(key, recurse = true) {
return this.toggleHint(key, "revealed", false, recurse);
}
/**
* Reveal a hint, with optional recurse.
* @method adventurejs.Hintcard#activateHint
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
revealHint(key, recurse = true) {
return this.toggleHint(key, "revealed", true, recurse);
}
/**
* Obfuscate hint data.
* @method adventurejs.Hintcard#obfuscateHint
*/
obfuscateHint(hint) {
if (!this.game.settings.obfuscate_hints) return hint;
hint.name = A.obfuscate(hint.name);
hint.text = A.obfuscate(hint.text);
// hint.description = A.obfuscate(hint.description);
return hint;
}
/**
* Process hint data in longhand format.
* @method adventurejs.Hintcard#processLonghand
*/
processLonghand(source, dest = this.hints, prefix = "$") {
let count = 0;
for (let srckey in source) {
count++;
const srchint = source[srckey];
// create a new empty point
const destkey = `${prefix}${count}`;
dest[destkey] = dest[destkey] || this.createHint();
const hint = dest[destkey];
hint.key = destkey;
if ("string" === typeof srchint) {
hint.text = srchint;
// hint.description = srchint;
} else if ("object" === typeof srchint && Object.keys(srchint).length) {
for (let prop in srchint) {
if (prop !== "hints") {
hint[prop] = srchint[prop];
}
}
if (srchint.hints && Object.keys(srchint.hints).length) {
hint.name = srckey;
this.processLonghand(srchint.hints, hint.hints, destkey + "|");
}
}
if (this.game.settings.obfuscate_hints) {
hint.name = A.obfuscate(hint.name);
hint.text = A.obfuscate(hint.text);
// hint.description = A.obfuscate(hint.description);
}
} // for key in hints
return count;
}
/**
* Process hint data in shorthand format.
* @method adventurejs.Hintcard#processShorthand
*/
processShorthand(hints) {
const lines = String(hints)
.trim()
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
// ancestors[level] is the most recent group node for that header level.
// ancestors[0] is the root container.
const ancestors = [];
ancestors[0] = this;
let count = 0;
for (let line of lines) {
let id = count;
let active = false;
count++;
let re = /\s*\|\s*\+$/;
if (re.test(line)) {
line = line.replace(re, "");
active = true;
}
re = /\s*\|\s*x$/;
if (re.test(line)) {
line = line.replace(re, "");
active = false;
}
// separate # indicators from text
// console.warn(line);
// console.warn(line.match(/^(#{1,6})?\s*(.*)$/));
const line_content = line.match(/^(#{1,6})?\s*(.*)$/);
if (!line_content) continue;
// get level // 1 => "#", 2 => "##", 3 => "###", ...
const level = line_content[1] ? line_content[1].length : 1;
// get text
let line_body = line_content[2].trim();
// remove leading dash
const is_hint = line_body.startsWith("-");
if (is_hint) line_body = line_body.replace(/^-\s*/, "").trim();
let name,
text = "";
if (line_body.includes("|")) {
[name, text] = line_body.split("|").map((s) => s.trim());
}
let hint = null;
// find nearest group at level-1 or above (should exist at ancestors[level-1])
const node = ancestors[level - 1] || ancestors[0];
node.hints = node.hints || {};
if (is_hint) {
// Attach item under the node at this exact header level if it exists;
// otherwise attach to the node (level-1 (ancestors[level]
// will exist if a node line with this level appeared)
const groupAtLevel = ancestors[level];
const attachTo =
groupAtLevel && groupAtLevel.hints ? groupAtLevel : node;
attachTo.hints = attachTo.hints || {};
// use the count of the hint as its key
let nodecount = Object.keys(attachTo.hints).length + 1;
let id = `${attachTo.key ? attachTo.key + "|" : "$"}${nodecount}`;
attachTo.hints[id] = attachTo.hints[id] || this.createHint();
hint = attachTo.hints[id];
// hint.description = description ? description : line_body;
hint.name = name;
hint.text = text || "";
hint.key = id;
} else {
// create or reuse group node at this level
let nodecount = Object.keys(node.hints).length + 1;
let id = `${node.key ? node.key + "|" : "$"}${nodecount}`;
node.hints[id] = node.hints[id] || this.createHint();
hint = node.hints[id];
hint.text = text || "";
hint.name = name ? name : line_body;
// hint.description = description;
hint.key = id;
ancestors[level] = hint;
// clear any deeper-level ancestors (so later siblings won't incorrectly attach)
for (let k = level + 1; k < ancestors.length; k++)
ancestors[k] = undefined;
}
hint.active = active;
if (this.game.settings.obfuscate_hints) {
hint.name = A.obfuscate(hint.name);
hint.text = A.obfuscate(hint.text);
// hint.description = A.obfuscate(hint.description);
}
}
}
/**
* Provides a chainable shortcut method for setting a number of properties on the instance.
* @method adventurejs.Hintcard#set
* @param {Object} props A generic object containing properties to copy to the instance.
* @returns {adventurejs.Hintcard} Returns the instance the method is called on (useful for chaining calls.)
* @chainable
*/
set(hintcard) {
if (hintcard.hints) {
// handle shortcut format
if ("string" === typeof hintcard.hints) {
this.processShorthand(hintcard.hints);
}
// handle full format
else {
this.processLonghand(hintcard.hints);
}
delete hintcard.hints;
this.countHints();
// pass on any other properties - expected properties are
for (let prop in hintcard) {
this[prop] = hintcard[prop];
}
return this;
}
}
test(path, delimiter = ",") {
console.warn({ path });
if (typeof path !== "string") return undefined;
const parts = path
.split(delimiter)
.map((s) => A.serialize(s.trim()))
.filter(Boolean);
let current = this.hints;
for (const part of parts) {
if (!current) {
console.warn("hint path not found");
return undefined;
}
// Prefer nested "hints" object if it exists and contains the key
if (
current.hints &&
Object.prototype.hasOwnProperty.call(current.hints, part)
) {
current = current.hints[part];
} else if (Object.prototype.hasOwnProperty.call(current, part)) {
current = current[part];
} else {
// not found
console.warn("hint path not found");
return undefined;
}
}
console.warn(current);
return current;
}
/**
* Toggle a hint property, with optional recurse.
* @method adventurejs.Hintcard#toggleHint
* @param {String} key A name or id to search for.
* @param {String} property A property to toggle.
* @param {Boolean} bool Activate or deactivate.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
toggleHint(key, property, bool, recurse = true) {
const hint = this.findHintByKey(key) || this.findHintByName(key);
if (!hint) return false;
hint[property] = bool;
if (
recurse &&
"object" === typeof hint.hints &&
Object.keys(hint.hints).length
) {
for (let subkey in hint.hints) {
this.toggleHint(subkey, property, bool, recurse);
}
}
this.countHints();
return true;
}
}
adventurejs.Hintcard = Hintcard;
})();