// HintManager.js
(function () {
/* global adventurejs A */
/**
*
* @class adventurejs.HintManager
* @ajsnavheading FrameworkClasses
* @param {Game} game A reference to the game instance.
* @summary Manages the display of the hint system.
* @classdesc
* <p>
* <strong>HintManager</strong> manages the job of displaying hints.
* It contains all the methods needed to create the
* Hint pop-up screen.
* </p>
* <p>
* HintManager is created automatically
* by {@link adventurejs.Game|Game}. This is an internal class that
* authors should not need to construct or modify. However, if
* you'd like to try, you can find styles for the Hint
* pop-ups in <a href="/css/adventurejs.css">adventurejs.css</a>.
* All relevant styles are prefixed with '.hint_'.
* </p>
*/
class HintManager {
constructor(game) {
/**
* A reference back to the main {@link adventurejs.Game|Game} object.
* @var {Object} adventurejs.HintManager#game
* @default {}
*/
this.game = game;
/**
* Used to manage the drawing of the hint dialog.
* @var {Object} adventurejs.HintManager#initialized
* @default false
*/
this.initialized = false;
/**
* Div element to contain the Hint dialog.
* @var {HTMLElement} adventurejs.HintManager#hint_dialog
* @default {}
*/
// MODAL VERSION
// this.hint_dialog = document.createElement("dialog");
// NON-MODAL VERSION
this.hint_dialog = document.createElement("div");
this.hint_dialog.setAttribute("aria-hidden", true);
this.hint_dialog.setAttribute("aria-modal", true);
this.hint_dialog.role = "dialog";
this.hint_dialog.style.display = "none";
this.hint_dialog.tabIndex = "1";
this.hint_dialog.classList.add("ajs-hint-dialog", "ajs-dialog");
this.hint_dialog.setAttribute(
"aria-labelledby",
`${this.game.game_name}-hint-dialog-title`
);
this.game.display.dialogsEl.appendChild(this.hint_dialog);
// Source HTML is stored in hintManager.html
this.hint_dialog.innerHTML = `
<div class="ajs-hint-dialog-outer ajs-dialog-outer">
<div class="ajs-hint-dialog-inner ajs-dialog-inner">
<h2 id="${this.game.game_name}-hint-dialog-title" class="ajs-hint-dialog-title ajs-dialog-title">HINTS</h2>
<div class="ajs-hint-dialog-content ajs-dialog-content">
<!-- put content here -->
<div class="ajs-hint-list-container">
<div id="${this.game.game_name}-hint-list">
</div>
</div>
<div class="ajs-hint-dialog-buttons ajs-dialog-button-container">
<button
id="${this.game.game_name}-hint-dialog-cancel-button"
class="ajs-hint-dialog-cancel-button ajs-cancel-button ajs-button ajs-dialog-button button-secondary"
type="button"
value="Cancel"
name="hintCancel"
>
Exit
</button>
</div>
<!-- /ajs-hint-dialog-buttons -->
</div>
<!-- /ajs-hint-dialog-content -->
</div>
<!-- /ajs-hint-dialog-inner -->
</div>
<!-- /ajs-hint-dialog-outer -->
`;
// hint_list is where we'll put our text
this.hint_list = this.hint_dialog.querySelector(
`#${this.game.game_name}-hint-list`
);
// --------------------------------------------------
// EXIT BUTTON
// --------------------------------------------------
/**
* Button element to dispel the Hint dialog.
* @var {HTMLElement} adventurejs.HintManager#hintCancel
* @default {}
*/
this.hintCancel = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-hint-dialog-cancel-button`
);
this.hintCancel.manager = this;
this.hintCancel.addEventListener("click", function () {
this.manager.clickClose();
});
this.handleEscape = this.handleEscape.bind(this);
} // constructor
/**
* Function that gets called by the Close button.
* @memberOf adventurejs.HintManager
* @method adventurejs.HintManager#clickClose
* @kind function
*/
clickClose() {
this.closeDialog();
var msg = "Hints closed.";
this.game.print(msg);
return;
}
/**
* Close the Hint modal dialog.
* @memberOf adventurejs.HintManager
* @method adventurejs.HintManager#closeDialog
* @kind function
*/
closeDialog() {
document.removeEventListener("keyup", this.handleEscape);
this.hint_dialog.classList.remove("active");
// MODAL VERSION
// this.hint_dialog.close();
// NON-MODAL VERSION
document.activeElement.blur();
this.hint_dialog.setAttribute("aria-hidden", true);
setTimeout(() => {
this.hint_dialog.style.display = "none";
this.game.display.contentEl.setAttribute("aria-hidden", false);
this.game.display.contentEl.removeAttribute("inert");
// set focus on input
this.game.display.inputEl.focus();
}, 250);
}
/**
* Prep hint data for display in the hints modal.
* @memberOf adventurejs.HintManager
* @method adventurejs.HintManager#drawHints
* @kind function
*/
drawHints(hints = this.game.hintcard.hints, html = "") {
let singles = 0;
let revealed = 0;
let o = this.game.settings.obfuscate_hints;
let count = 0;
for (let item in hints) {
let hint = hints[item];
count++;
if (!hint.active) continue;
let is_group = Object.keys(hint.hints).length > 0 ? true : false;
if (is_group) {
html += `
<details class="ajs-hint-group" data-unrevealed="${hint.unrevealed}" data-key="${hint.key}">
<summary>${o ? A.deobfuscate(hint.name) : hint.name}`;
if (hint.unrevealed) {
html += `<span class="ajs-hint-unrevealed" data-unrevealed="${hint.unrevealed}">${hint.unrevealed}</span>`;
}
html += `</summary>`;
if (hint.text) {
html += `
<div class="ajs-hint-description">${o ? A.deobfuscate(hint.text) : hint.text}</div>
`;
}
html += `<div class="ajs-nested-hints">`;
let [nested_html, nested_count, nested_revealed] = this.drawHints(
hint.hints
);
html += nested_html;
if (nested_count && nested_count > nested_revealed) {
html += `
<div class="ajs-hinteractive">
<button data-total="${nested_count}" class="ajs-button ajs-hint-button button-primary" onclick="window.${this.game.game_name}.hintManager.revealHint(event)">Show Hint <span class="bold next">${nested_revealed + 1}</span> of <span class="bold">${nested_count}</span></button>
</div>
`;
}
html += `</div>`;
html += `</details>`;
} // is_group
if (!is_group) {
singles++;
if (hint.revealed) revealed++;
html += `
<div class="ajs-hint ${hint.revealed ? "revealed" : ""}" data-key="${hint.key}">${count}. ${o ? A.deobfuscate(hint.text) : hint.text}</div>
`;
} // !is_group
}
return [html, singles, revealed];
}
/**
* Function that closes the dialog on escape keyup.
* @param {*} event
*/
handleEscape(event) {
var key = event.which || event.keyCode;
if (key === 27) {
event.stopPropagation();
this.game.hintManager.clickClose(this);
}
}
/**
* Open the Hint modal dialog.
* @memberOf adventurejs.HintManager
* @method adventurejs.HintManager#openDialog
* @kind function
*/
openDialog() {
// if (!this.initialized) {
if (this.game.hintcard.introduction) {
this.hint_list.innerHTML = `
<div class="ajs-hint-introduction">
${this.game.hintcard.introduction}
</div>
`;
}
this.hint_list.innerHTML = this.hint_list.innerHTML + this.drawHints()[0];
this.placeButtons();
this.initialized = true;
// }
document.addEventListener("keyup", this.handleEscape);
// activate dialog
this.hint_dialog.classList.add("active");
// MODAL VERSION
// this.hint_dialog.showModal();
// NON-MODAL VERSION
// deactivate main content
document.activeElement.blur();
this.game.display.contentEl.blur();
this.game.display.contentEl.setAttribute("aria-hidden", true);
this.game.display.contentEl.setAttribute("inert", "");
// activate dialog
this.hint_dialog.style.display = "block";
this.hint_dialog.classList.add("active");
this.hint_dialog.setAttribute("aria-hidden", false);
this.hint_dialog.focus();
}
/**
* In the case of mixed hints / hint groups, we'll have placed a reveal
* button in an awkward place and we want to reposition it.
* @memberOf adventurejs.HintManager
* @method adventurejs.HintManager#placeButtons
* @kind function
*/
placeButtons() {
const containers = this.hint_list.querySelectorAll(".ajs-nested-hints");
containers.forEach((container) => {
// snapshot of direct children and their original indices
const children = Array.from(container.children);
const indexOf = new Map(children.map((n, i) => [n, i]));
// find the last direct child that is an .ajs-hinteractive
let lastInteractiveIndex = -1;
for (let i = children.length - 1; i >= 0; i--) {
if (children[i].classList.contains("ajs-hinteractive")) {
lastInteractiveIndex = i;
break;
}
}
// nothing to do if there are no interactive items
if (lastInteractiveIndex === -1) return;
// find all direct .ajs-hint-group children in original order
const groups = children.filter((n) =>
n.classList.contains("ajs-hint-group")
);
// move groups that were originally before the last interactive
// we insert each moved group *after* the current lastInteractiveIndex,
// then bump lastInteractiveIndex so subsequent moves append after the previous one.
groups.forEach((group) => {
const origIndex = indexOf.get(group);
if (origIndex < lastInteractiveIndex) {
// where to insert: after the current lastInteractiveIndex
const insertBeforeNode =
container.children[lastInteractiveIndex + 1] || null;
container.insertBefore(group, insertBeforeNode);
lastInteractiveIndex += 1; // moved group now sits after the interactive(s)
}
});
});
}
/**
* Reveal a hint.
* @method adventurejs.HintManager#revealHint
*/
revealHint(event) {
const button = event.currentTarget;
const group = button.closest(".ajs-hint-group");
const nest = button.closest(".ajs-nested-hints");
const reveal = nest.querySelector(":scope > .ajs-hint:not(.revealed)");
if (reveal === null) {
button.classList.add("hidden");
return;
}
const hintnode = this.game.hintcard.findHintByKey(reveal.dataset.key);
hintnode.revealed = true;
reveal.classList.add("revealed");
this.game.hintcard.countHints();
this.updateCounts();
const count = nest.querySelectorAll(":scope > .revealed").length;
const next = count + 1;
if (next > button.dataset.total) {
button.classList.add("hidden");
} else {
button.querySelector(".next").innerHTML = next;
}
// const groupnode = this.game.hintcard.findHintByKey(group.dataset.key);
// if (groupnode.unrevealed === 0) {
// button.classList.add("hidden");
// }
}
/**
* Provides a chainable shortcut method for setting a number of properties on the instance.
* @method adventurejs.HintManager#set
* @param {Object} props A generic object containing properties to copy to the DisplayObject instance.
* @returns {adventurejs.HintManager} Returns the instance the method is called on (useful for chaining calls.)
* @chainable
*/
set(props) {
return A.deepSet.call(this.game, props, this);
}
/**
* Update hint counts.
* @method adventurejs.HintManager#updateCounts
*/
updateCounts() {
const groups = this.hint_dialog.querySelectorAll(
".ajs-hint-group[data-unrevealed]"
);
groups.forEach((group) => {
const unrevealed = this.game.hintcard.findHintByKey(
group.dataset.key
).unrevealed;
group.dataset.unrevealed = unrevealed;
const groupEl = group.querySelector(".ajs-hint-unrevealed");
groupEl.innerHTML = unrevealed;
groupEl.dataset.unrevealed = unrevealed;
});
}
}
adventurejs.HintManager = HintManager;
})();