Skip to content

Commit

Permalink
Merge pull request #4 from MythicPalette/sub-objectives
Browse files Browse the repository at this point in the history
Done
  • Loading branch information
MythicPalette authored Sep 30, 2024
2 parents 23572df + b226951 commit a85158d
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 72 deletions.
45 changes: 38 additions & 7 deletions scripts/data/database.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { objectiveState } from "../helpers/constants.js";
import { Settings } from "../helpers/settings.js";
import { newId } from "../helpers/constants.js";

export class QuestDatabase extends Collection {
static #quests;
static get quests() {
// Collect all quests.
let q = this.#quests.map((q) => {
// Get all of the objectives with index.
let objs = q.objectives.map((o, i) => {
return {
...o,
index: i,
};
});
let objs = q.objectives.map((o, i) =>
QuestDatabase.nestedObjectives(o, i, 0)
);

// If the user is not a GM, completely remove
// all secret objectives from the list.
if (!game.user.isGM) {
objs = objs.filter((o) => !o.secret);
function filterObjectives(arr) {
return arr.filter((o) => {
// If the current object is a secret, filter it out.
if (o.secret) return false;

// If the object is not a secret, check each sub objective.
if (o.subs) o.subs = filterObjectives(o.subs);
return true;
});
}
filterObjectives(objs);

// If the view mode is next only, strip extra objectives
if (q.viewStyle === "next") {
Expand Down Expand Up @@ -104,7 +113,7 @@ export class QuestDatabase extends Collection {
let index = -1;
do {
// Create a new id
data.id = "id" + Math.random().toString(16).slice(2);
data.id = QuestDatabase.newId();

// Try to find any quests with the id
// index will be -1 if none were found.
Expand Down Expand Up @@ -140,6 +149,28 @@ export class QuestDatabase extends Collection {

static refresh() {
this.#quests = Settings.get(Settings.NAMES.QUEST_DB).quests;

// Verify that all quest objectives have an id
this.#quests.forEach((q, i) => {
this.#quests[i] = {
...q,
objectives: q.objectives.map((o) =>
QuestDatabase.nestedObjectives(o, 0)
),
};
});
}

static nestedObjectives(objective) {
return {
...objective,
id: objective.id || newId(),
subs: objective.subs
? objective.subs.map((o) => QuestDatabase.nestedObjectives(o))
: null,
};
// Append the index to the objective
// Iterate through the nested objects
console.log("Loaded " + this.#quests.length + " quests.");
}
}
74 changes: 67 additions & 7 deletions scripts/data/objective.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { objectiveState } from "../helpers/constants.js";
import { newId, objectiveState } from "../helpers/constants.js";

export class Objective {
constructor(data = {}) {
Expand All @@ -14,32 +14,36 @@ export class Objective {

// Prepare the basic data.
let data = {
id: newId(),
text: "",
secret: false,
state: objectiveState.INCOMPLETE,
sublevel: 0,
subs: [],
};

// Check for headers.
while (
str.startsWith("-") ||
str.startsWith("+") ||
str.startsWith("/")
str.startsWith("/") ||
str.startsWith("*")
) {
// Check for State flag
if (str.startsWith("-")) {
data.state = objectiveState.FAILED;
str = str.substring(1);
} else if (str.startsWith("+")) {
data.state = objectiveState.COMPLETE;
str = str.substring(1);
} else if (str.startsWith("/")) {
// Mark as secret
data.secret = true;
str = str.substring(1);
} else if (str.startsWith("*")) {
data.sublevel++;
}

// Trim any whitespace that may be created.
// This is to fix lines like "+ Objective"
str = str.substring(1);
str = str.trim();
}

Expand All @@ -66,17 +70,73 @@ export class Objective {
// Try to parse the line.
let o = Objective.parse(l);

// Skip any lines that failed to parse.
if (o) result.push(o);
// If the line fails to parse then skip it.
if (!o) return;

// If there are no sublevels, the objective is not a subobjective
if (!o.sublevel) {
result.push(o);
return;
}

// If the sublevel is greater than zero
// Get the last item.
let parent = result[result.length - 1];

// For each sub level, move a layer deeper
for (let i = 1; i < o.sublevel; i++) {
// While there are more sublevels, iterate through the subobjectives
parent = parent.subs[parent.subs.length - 1];
}

// Parent should now be the parent of the current objective.
parent.subs.push(o);
});

// Return the list of objectives.
return result;
}

loadData(data = {}) {
this.id = data.id || newId();
this.text = data.text || "";
this.secret = data.secret || false;
this.state = data.state || objectiveState.INCOMPLETE;
this.sublevel = data.sublevel || 0;
this.subs = data.subs || [];
}

static stringify(objective) {
let line = "";
// First, start by marking the quest as complete or failed if necessary.
if (objective.state === objectiveState.COMPLETE) line += "+";
else if (objective.state === objectiveState.FAILED) line += "-";

// Mark the quest as a secret if needed.
if (objective.secret) line += "/";

// Add any subquest indentation
line += "*".repeat(objective.sublevel);

// Add the text.
line += objective.text;

// For each subobjective
// stringify the subobjective
// Add it to the line as a new line
if (objective.subs)
objective.subs.forEach((sub) => {
line += "\n" + Objective.stringify(sub);
});

// Push the completed line to the stack.
return line;
}

static getNextState(state) {
if (state === objectiveState.INCOMPLETE) return objectiveState.COMPLETE;
else if (state === objectiveState.COMPLETE)
return objectiveState.FAILED;
else return objectiveState.INCOMPLETE;
}
}
8 changes: 8 additions & 0 deletions scripts/data/quest.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ export class Quest {
});
}
}

static stringify(quest) {
let strings = [];
quest.objectives.forEach((o) => {
strings.push(Objective.stringify(o));
});
return strings.join("\n");
}
}
4 changes: 4 additions & 0 deletions scripts/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export const questViewStyle = Object.freeze({
VIEW_NEXT: 1,
VIEW_COMPLETE: 2,
});

export function newId() {
return "id" + Math.random().toString(16).slice(2);
}
6 changes: 6 additions & 0 deletions scripts/helpers/handlebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ export class HandlebarHelper {
return o1 === o2;
});

Handlebars.registerHelper("simplerQuestsLastObjective", (list, id) => {
return list[list.length - 1].id === id;
});

await loadTemplates({
trackerBody: "modules/simpler-quests/templates/tracker-body.hbs",
trackerObjective:
"modules/simpler-quests/templates/tracker-objective.hbs",
});
}
}
26 changes: 1 addition & 25 deletions scripts/ui/questeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,38 +99,14 @@ export class QuestEditor extends Application {
}

async getData(options = {}) {
// Create an empty collection of strings.
// This is to convert the objectives back into
// a text body for the user to interact with.
let strings = [];

// Iterate through each objective to convert individual
// objectives into lines.
this.quest.objectives.forEach((o) => {
let line = "";
// First, start by marking the quest as complete or failed if necessary.
if (o.state === objectiveState.COMPLETE) line += "+";
else if (o.state === objectiveState.FAILED) line += "-";

// Mark the quest as a secret if needed.
if (o.secret) line += "/";
line += o.text;

// Push the completed line to the stack.
strings.push(line);
});

// Create the final text for the user to interact with.
const objectiveString = strings.join("\n");

const viewStyle =
this.quest.viewStyle ||
Settings.get(Settings.NAMES.QUEST_VIEW_STYLE);

return foundry.utils.mergeObject(super.getData(), {
title: "Quest Editor Test",
questTitle: this.quest.title,
objectives: objectiveString,
objectives: Quest.stringify(this.quest),
visible: this.quest.visible,
viewStyle: viewStyle,
viewStyleHint: this.#viewStyles[viewStyle],
Expand Down
61 changes: 40 additions & 21 deletions scripts/ui/questtracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QuestDatabase } from "../data/database.js";
import { QuestEditor } from "./questeditor.js";
import { objectiveState } from "../helpers/constants.js";
import { Settings } from "../helpers/settings.js";
import { Objective } from "../data/objective.js";

export class QuestTracker extends Application {
#collapsed;
Expand Down Expand Up @@ -154,33 +155,37 @@ export class QuestTracker extends Application {
// Get the quest and objective ID
const questId =
evt.target.closest("[data-quest-id]").dataset.questId;
const objIndex = evt.target.dataset.index;

// Get the objective index
const objId = evt.target.dataset.id;

// Get the index of the quest objective.
if (!questId || objIndex === undefined) return;
if (!questId || objId === undefined) return;

// Get the quest.
let quest = QuestDatabase.getQuest(questId);

// Ensure the quest is a valid object.
if (!quest) return;

// Progress the quest to the next state. Progression is cyclical.
if (quest.objectives.length > objIndex) {
if (
quest.objectives[objIndex].state ===
objectiveState.INCOMPLETE
)
quest.objectives[objIndex].state = objectiveState.COMPLETE;
else if (
quest.objectives[objIndex].state === objectiveState.COMPLETE
)
quest.objectives[objIndex].state = objectiveState.FAILED;
else
quest.objectives[objIndex].state =
objectiveState.INCOMPLETE;
// This function will be used to recursively search for and update
// the selected quest objective.
function findAndUpdateState(obj, id) {
// If the current objective is the correct one, update it.
if (obj.id === id) {
obj.state = Objective.getNextState(obj.state);
} else if (obj.subs) {
// Iterate through subobjectives and attempt to update.
obj.subs.forEach((o) => {
findAndUpdateState(o, id);
});
}
}

quest.objectives.forEach((o) => {
findAndUpdateState(o, objId);
});

// Update the quest and save.
QuestDatabase.update(quest);
QuestDatabase.save();
Expand All @@ -194,17 +199,31 @@ export class QuestTracker extends Application {
// Get the quest and objective ID
const questId =
evt.target.closest("[data-quest-id]").dataset.questId;
const objIndex = evt.target.dataset.index;
const objId = evt.target.dataset.id;

// If either ID is invalid, abort.
if (!questId || objIndex === undefined) return;
if (!questId || objId === undefined) return;

// Get the quest
let quest = QuestDatabase.getQuest(questId);

// Update the objective's secret status.
quest.objectives[objIndex].secret =
!quest.objectives[objIndex].secret;
// This function will be used to recursively search for and update
// the selected quest objective.
function findAndUpdateSecret(obj, id) {
// If the current objective is the correct one, update it.
if (obj.id === id) {
obj.secret = !obj.secret;
} else if (obj.subs) {
// Iterate through subobjectives and attempt to update.
obj.subs.forEach((o) => {
findAndUpdateSecret(o, id);
});
}
}

quest.objectives.forEach((o) => {
findAndUpdateSecret(o, objId);
});

// Update the database and save.
QuestDatabase.update(quest);
Expand Down
Loading

0 comments on commit a85158d

Please sign in to comment.