// Goalcard.js
(function () {
/* global adventurejs A */
/**
* @class adventurejs.Goalcard
* @ajsinternal
* @param {Game} game A reference to the game instance.
* @ajsnavheading FrameworkClasses
* @summary Manages goals for a {@link adventurejs.Game|Game} instance.
* @classdesc
* <p>
* <strong>Goalcard</strong> is a repository for player goals.
* Goalcard is created automatically
* by {@link adventurejs.Game|Game}. This is an internal class
* that authors should not need to construct. However,
* authors can set goal 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.Goalcard.set({
*
* });
* </code></pre>
*/
class Goalcard {
constructor(game) {
this.game = game;
this.game.world._goalcard = {};
this.introduction = "";
this.goals = {};
this.show_satisfied_goals = true;
}
/**
* Activate a goal, with optional recurse.
* @method adventurejs.Goalcard#activateGoal
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
activateGoal(key, recurse = true) {
return this.toggleGoal(key, true, recurse);
}
/**
* Complete a goal, with optional recurse.
* @method adventurejs.Goalcard#completeGoal
* @param {String} key A name or id to search for.
* @param {Boolean} recurse Whether to complete recursively.
* @returns {}
*/
completeGoal(key, recurse = true) {
let goal = this.findGoal(key);
if (!goal) return;
goal.complete = true;
if (
recurse &&
"object" === typeof goal.goals &&
Object.keys(goal.goals).length
) {
for (let subkey in goal.goals) {
this.completeGoal(subkey, recurse);
}
}
}
/**
* Get a count of the number of goals in each group.
* @method adventurejs.Goalcard#countGoals
*/
countGoals(node = this) {
if (
"undefined" === typeof node.goals ||
Object.keys(node.goals).length === 0
) {
return [node.active ? 0 : 1, 1];
}
let count = 0;
let unrevealed = 0;
for (const key in node.goals) {
// total += this.countGoals(node.goals[key]);
let [u, t] = this.countGoals(node.goals[key]);
unrevealed += u;
count += t;
}
node.unrevealed = unrevealed;
node.count = count;
return [unrevealed, count];
}
/**
* Create a new goal.
* @method adventurejs.Goalcard#createGoal
* @returns {String}
*/
createGoal() {
let goal = {
key: "",
text: "",
name: "",
active: true,
complete: false,
complete_text: "",
parent: null,
goals: {},
};
return goal;
}
/**
* Deactivate a goal, with optional recurse.
* @method adventurejs.Goalcard#deactivateGoal
* @param {String} key A name or id to search for.
* @returns {}
*/
deactivateGoal(key, recurse = true) {
return this.toggleGoal(key, false, recurse);
}
/**
* Recursively find goal by name or key.
* @method adventurejs.Scorecard#findGoal
* @param {String} key A name or id to search for.
* @param {Object} root The root object level to begin search.
* @returns {Object|null}
*/
findGoal(key) {
return this.findGoalByName(key) || this.findGoalByKey(key);
}
/**
* Find goal by key. Looks recursively.
* @method adventurejs.Goalcard#findGoalByKey
* @param {String} key An object key to search for.
* @param {Object} root The root object level to begin search.
* @returns {Object|null}
*/
findGoalByKey(key, root = this) {
if (
!key ||
!root ||
typeof root !== "object" ||
!root.goals ||
typeof root.goals !== "object"
)
return null;
if (root.goals && Object.prototype.hasOwnProperty.call(root.goals, key)) {
return root.goals[key];
}
if (root.goals) {
for (const k of Object.keys(root.goals)) {
const found = this.findGoalByKey(key, root.goals[k]);
if (found) return found;
}
}
return null;
}
/**
* Recursively find goal by its name.
* @method adventurejs.Scorecard#findGoalByName
* @param {String} key An object key to search for.
* @param {Object} root The root object level to begin search.
* @returns {Object|null}
*/
findGoalByName(name, root = this) {
if (
!name ||
!root ||
typeof root !== "object" ||
!root.goals ||
typeof root.goals !== "object"
)
return null;
const comparator = this.game.settings.obfuscate_goals
? A.obfuscate(name)
: name;
for (let key in root.goals) {
let goal = root.goals[key];
if (goal.name === comparator) {
return goal;
}
if (goal.goals) {
let subgoal = this.findGoalByName(name, goal);
if (subgoal) return subgoal;
}
}
return null;
}
/**
* Get a printable list of goals.
* @method adventurejs.Scorecard#listGoals
*/
listGoals(goals = this.goals) {
let msg = "";
for (let key in goals) {
const goal = goals[key];
if (!goal.active) continue;
let li = "";
let complete = "";
if (goal.complete) {
if (this.game.settings.show_completed_goals) {
li = goal.complete_text || goal.text;
complete = "ajs-completed-goal";
}
} else {
li = goal.text;
}
if ("object" === typeof goal.goals && Object.keys(goal.goals).length) {
li += this.listGoals(goal.goals);
}
if (li) msg += `<li class="ajs-li ajs-goal ${complete}">${li}</li>`;
}
if (msg) msg = `<ul class="ajs-ul">${msg}</ul>`;
return msg;
}
/**
* Process goal data in longhand format.
* @method adventurejs.Goalcard#processLonghand
*/
processLonghand(source, dest = this.goals, prefix = "$") {
let count = 0;
for (let srckey in source) {
count++;
const srcgoal = source[srckey];
// create a new empty point
const destkey = `${prefix}${count}`;
dest[destkey] = dest[destkey] || this.createGoal();
const goal = dest[destkey];
goal.key = destkey;
if ("string" === typeof srcgoal) {
goal.text = srcgoal;
} else if ("object" === typeof srcgoal && Object.keys(srcgoal).length) {
for (let prop in srcgoal) {
if (prop !== "goals") {
goal[prop] = srcgoal[prop];
}
}
goal.name = srckey;
if (srcgoal.goals && Object.keys(srcgoal.goals).length) {
this.processLonghand(srcgoal.goals, goal.goals, destkey + "|");
}
}
} // for key in goals
return count;
}
/**
* Process goal data in shorthand format.
* @method adventurejs.Goalcard#processShorthand
*/
processShorthand(goals) {
// break shortcut format into lines
const lines = String(goals)
.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) {
count++;
let active = false;
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
let line_content = line.match(/^(#{1,6})?\s*(.*)$/);
if (!line_content) continue;
// get level // 1 => "#", 2 => "##", 3 => "###", ...
// if none set, default to 1
const level = line_content[1] ? line_content[1].length : 1;
// get body of line
let line_body = line_content[2].trim();
// remove leading dash
const is_sub = line_body.startsWith("-");
if (is_sub) line_body = line_body.replace(/^-\s*/, "").trim();
// find nearest group at level-1 or above
// (should exist at ancestors[level-1])
const node = ancestors[level - 1] || ancestors[0];
node.goals = node.goals || {};
let [name, text, complete_text] = line_body.split(/\s*\|\s*/);
if (!name) continue;
if (!text) text = name;
let target, parent, id;
if (is_sub) {
// 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.goals ? groupAtLevel : node;
parent = attachTo;
attachTo.goals = attachTo.goals || {};
// attachTo.goals[count] = this.createGoal();
// target = attachTo.goals[count];
// use the count of the hint as its key
let nodecount = Object.keys(attachTo.goals).length + 1;
id = `${attachTo.key ? attachTo.key + "|" : "$"}${nodecount}`;
attachTo.goals[id] = this.createGoal();
target = attachTo.goals[id];
} else {
// create or reuse group node at this level
let nodecount = Object.keys(node.goals).length + 1;
id = `${node.key ? node.key + "|" : "$"}${nodecount}`;
// node.goals[count] = this.createGoal();
// target = node.goals[count];
node.goals[id] = this.createGoal();
target = node.goals[id];
ancestors[level] = target;
// clear any deeper-level ancestors
// (so later siblings won't incorrectly attach)
for (let k = level + 1; k < ancestors.length; k++)
ancestors[k] = undefined;
}
target.active = active;
target.name = this.game.settings.obfuscate_goals
? A.obfuscate(name)
: name;
target.text = this.game.settings.obfuscate_goals
? A.obfuscate(text)
: text;
if (complete_text) {
target.complete_text = this.game.settings.obfuscate_goals
? A.obfuscate(complete_text)
: complete_text;
}
// target.key = count;
target.key = id;
if (parent) target.parent = parent;
// count++;
}
}
/**
* Provides a chainable shortcut method for setting a number of properties on the instance.
* @method adventurejs.Goalcard#set
* @param {Object} props A generic object containing properties to copy to the instance.
* @returns {adventurejs.Goalcard} Returns the instance the method is called on (useful for chaining calls.)
* @chainable
*/
set(goalcard) {
if (goalcard.goals) {
// handle shortcut format
if ("string" === typeof goalcard.goals) {
this.processShorthand(goalcard.goals);
}
// handle full format
else {
this.processLonghand(goalcard.goals);
}
delete goalcard.goals;
this.countGoals();
// pass on any other properties - expected properties are
for (let prop in goalcard) {
this[prop] = goalcard[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.goals;
for (const part of parts) {
if (!current) {
console.warn("goal path not found");
return undefined;
}
// Prefer nested "goals" object if it exists and contains the key
if (
current.goals &&
Object.prototype.hasOwnProperty.call(current.goals, part)
) {
current = current.goals[part];
} else if (Object.prototype.hasOwnProperty.call(current, part)) {
current = current[part];
} else {
// not found
console.warn("goal path not found");
return undefined;
}
}
console.warn(current);
return current;
}
/**
* Toggle a goal's active state, with optional recurse.
* @method adventurejs.Goalcard#toggleGoal
* @param {String} key A name or id to search for.
* @param {Boolean} bool Activate or deactivate.
* @param {Boolean} recurse Whether to activate recursively.
* @returns {}
*/
toggleGoal(key, bool, recurse = true) {
let goal = this.findGoal(key);
if (!goal) return false;
goal.active = bool;
if (
recurse &&
"object" === typeof goal.goals &&
Object.keys(goal.goals).length
) {
for (let subkey in goal.goals) {
this.toggleGoal(subkey, bool, recurse);
}
}
return true;
}
}
adventurejs.Goalcard = Goalcard;
})();