Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add School of Sorcery stages #553

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type {IMessageExemption} from "@scripts/hunt-filter/interfaces";
import type {IntakeMessage} from "@scripts/types/mhct";

/**
* Allow transitions from Boss stages to course stages or hallway
*/
class CourseBossExemption implements IMessageExemption {
readonly description = "School of Sorcery Boss Encounter";
readonly property = "stage";

readonly CourseBossStagesToMouse: Record<string, string | undefined> = {
'Arcane Arts Boss': 'Arcane Master Sorcerer' ,
'Shadow Sciences Boss': 'Shadow Master Sorcerer' ,
'Final Exam Boss': 'Mythical Master Sorcerer',
};

getExemptions(
pre: IntakeMessage,
post: IntakeMessage
): (keyof IntakeMessage)[] | null {

// Only allow transitions from boss to outside for now
if (this.isTransitionFromBossToHallway(pre.stage, post.stage, pre.mouse)) {
return ['stage', 'cheese'];
}

if (this.isTransitionFromBossToCourse(pre.stage, post.stage, pre.mouse)) {
return ['stage'];
}

return null;
}

private isTransitionFromBossToHallway(preStage: unknown, postStage: unknown, mouse: unknown) {
return this.isCourseStageAndBoss(preStage, mouse)
&& this.isHallwayStage(postStage);
}

private isTransitionFromBossToCourse(preStage: unknown, postStage: unknown, mouse: unknown) {
return this.isCourseStageAndBoss(preStage, mouse)
&& this.isCourseStage(postStage);
}

/** Check if stage is a known boss stage that matches associated mouse */
private isCourseStageAndBoss(stage: unknown, mouse: unknown) {
if (typeof stage === 'string') {
return this.CourseBossStagesToMouse[stage] === mouse;
}

return false;
}

private isCourseStage(stage: unknown) {
return stage === 'Arcane Arts'
|| stage === 'Shadow Sciences'
|| stage === 'Final Exam - Arcane'
|| stage === 'Final Exam - Shadow';
}

private isHallwayStage(stage: unknown) {
return stage === 'Hallway';
}
}

export const schoolOfSorceryExemptions = [
new CourseBossExemption(),
];
45 changes: 45 additions & 0 deletions src/scripts/hunt-filter/exemptions/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type {IMessageExemption} from "@scripts/hunt-filter/interfaces";
import type {IntakeMessage} from "@scripts/types/mhct";

/**
* Provides an exemption on the 'cheese' difference when cheese runs out.
*/
class OutOfCheeseExemption implements IMessageExemption {
readonly description = "Ran out of cheese";
readonly property = "cheese";

getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null {
if (
pre.cheese.id > 0 &&
post.cheese.id == 0
) {
return ["cheese"];
}

return null;
}
}

/**
* Provides an exemption on the 'charm' difference when charms run out.
*/
class OutOfCharmsExemption implements IMessageExemption {
readonly description = "Ran out of cheese";
readonly property = "charm";

getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null {
if (
(pre.charm?.id ?? 0) > 0 &&
post.charm?.id === 0
) {
return ["charm"];
}

return null;
}
}

export const globalExemptions = [
new OutOfCheeseExemption(),
new OutOfCharmsExemption(),
];
5 changes: 5 additions & 0 deletions src/scripts/hunt-filter/exemptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import type {IMessageExemption} from '../interfaces';

import {globalExemptions} from './global';
import {acolyteRealmExemptions} from './environments/acolyteRealm';
import {bountifulBeanstalkExemptions} from './environments/bountifulBeanstalk';
import {clawShotCityExemptions} from './environments/clawShotCity';
import {floatingIslandsExemptions} from './environments/floatingIslands';
import {icebergExemptions} from './environments/iceberg';
import {iceFortressExemptions} from './environments/iceFortress';
import {schoolOfSorceryExemptions} from './environments/schoolOfSorcery';
import {superBrieFactoryExemptions} from './environments/superBrieFactory';
import {valourRiftExemptions} from './environments/valourRift';

export const MessageExemptions: IMessageExemption[] = [
...globalExemptions,

...acolyteRealmExemptions,
...bountifulBeanstalkExemptions,
...clawShotCityExemptions,
...icebergExemptions,
...iceFortressExemptions,
...floatingIslandsExemptions,
...schoolOfSorceryExemptions,
...superBrieFactoryExemptions,
...valourRiftExemptions,
];
10 changes: 10 additions & 0 deletions src/scripts/hunt-filter/messageRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ class IntakeMessageSameWeapon implements IPropertyRule<IntakeMessage> {
}
}

class IntakeMessageSameCharm implements IPropertyRule<IntakeMessage> {
readonly description = "Charm should not change";
readonly property = "charm";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
return (pre.charm != null && post.charm !== null)
&& pre.charm.name === post.charm.name;
}
}

class IntakeMessageSameBase implements IPropertyRule<IntakeMessage> {
readonly description = "Base should not change";
readonly property = "base";
Expand Down Expand Up @@ -51,6 +60,7 @@ class IntakeMessageSameStage implements IPropertyRule<IntakeMessage> {
export const MessageRules: IPropertyRule<IntakeMessage>[] = [
new IntakeMessageSameCheese,
new IntakeMessageSameWeapon,
new IntakeMessageSameCharm,
new IntakeMessageSameBase,
new IntakeMessageSameLocation,
new IntakeMessageSameStage,
Expand Down
13 changes: 9 additions & 4 deletions src/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1081,10 +1081,15 @@ import * as detailingFuncs from './modules/details/legacy';
const prop_name = `${component.prop}_name`;
const prop_id = `${component.prop}_item_id`;
const item_name = user[prop_name];
message[component.message_field] = (!item_name) ? {} : {
id: user[prop_id],
name: item_name.replace(component.replacer, ''),
};
message[component.message_field] = (!item_name)
? {
id: 0,
name: '',
}
: {
id: user[prop_id],
name: item_name.replace(component.replacer, ''),
};

if (item_name !== user_post[prop_name]) {
debug_logs.push(`User ${component.message_field} changed: Was '${item_name}' and is now '${user_post[prop_name] || "None"}'`);
Expand Down
44 changes: 44 additions & 0 deletions src/scripts/modules/stages/environments/schoolOfSorcery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {User} from '@scripts/types/hg';
import type {IntakeMessage} from '@scripts/types/mhct';
import type {IStager} from '../stages.types';
import type {CourseSelection, CourseType} from '@scripts/types/quests/schoolOfSorcery';


export class SchoolOfSorceryStager implements IStager {
readonly environment: string = 'School of Sorcery';

addStage(message: IntakeMessage, userPre: User, userPost: User, journal: unknown): void {
const quest = userPre.quests.QuestSchoolOfSorcery;

if (!quest) {
throw new Error('QuestSchoolOfSorcery is undefined');
}

if (!quest.in_course) {
message.stage = 'Hallway';
} else {
const currentCourse = quest.current_course;
message.stage = this.getCourseName(quest.course_selections, currentCourse.course_type);

if (currentCourse.is_boss_encounter) {
message.stage += " Boss";
} else if (currentCourse.course_type === 'exam_course') {
// Final Exam gets current powertype appended

let powerType: string = currentCourse.power_type;
// Capitalize first letter
powerType = powerType.charAt(0).toUpperCase() + powerType.slice(1);
message.stage += ` - ${powerType}`;
}
}
}

private getCourseName(courseSelections: CourseSelection[], currentCourseType: CourseType): string {
const course = courseSelections.find(c => c.type === currentCourseType);
if (!course) {
throw new Error(`The course type '${currentCourseType}' was not found in the course_selections array`);
}

return course.name;
}
}
1 change: 1 addition & 0 deletions src/scripts/types/hg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface Quests {
QuestRiftWhiskerWoods?: quests.QuestRiftWhiskerWoods
QuestRiftValour?: quests.QuestRiftValour
QuestSandDunes?: quests.QuestSandDunes
QuestSchoolOfSorcery?: quests.QuestSchoolOfSorcery
QuestSunkenCity?: unknown
QuestSuperBrieFactory?: quests.QuestSuperBrieFactory
QuestSpringHunt?: quests.QuestSpringHunt
Expand Down
1 change: 1 addition & 0 deletions src/scripts/types/quests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from '@scripts/types/quests/quesoGeyser';
export * from '@scripts/types/quests/sandDunes';
export * from '@scripts/types/quests/superBrieFactory';
export * from '@scripts/types/quests/tableOfContents';
export * from '@scripts/types/quests/schoolOfSorcery';
export * from '@scripts/types/quests/springHunt';
export * from '@scripts/types/quests/toxicSpill';
export * from '@scripts/types/quests/valourRift';
Expand Down
25 changes: 25 additions & 0 deletions src/scripts/types/quests/schoolOfSorcery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type QuestSchoolOfSorcery = {
course_selections: CourseSelection[];
} & (RunningInHallway | TakingACourse);

interface RunningInHallway {
in_course: false;
}

interface TakingACourse {
current_course: CurrentCourse;
in_course: true;
}

export interface CourseSelection {
type: CourseType;
name: string;
}

export interface CurrentCourse {
course_type: CourseType;
power_type: 'shadow' | 'arcane'
is_boss_encounter: boolean | null
}

export type CourseType = 'arcane_101_course' | 'shadow_101_course' | 'exam_course';
2 changes: 1 addition & 1 deletion src/scripts/util/HornHud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class HornHud {
* Check if the horn can be sounded if it's ready and not currently animated
*/
public static canSoundHorn() {
return this.isHornReady() && !this.isHornSounding();
return !this.isMessageActive() && this.isHornReady() && !this.isHornSounding();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {IntakeRejectionEngine} from "@scripts/hunt-filter/engine";
import {SchoolOfSorceryStager} from "@scripts/modules/stages/environments/schoolOfSorcery";
import {User} from "@scripts/types/hg";
import {ComponentEntry, IntakeMessage} from "@scripts/types/mhct";
import {CourseType} from "@scripts/types/quests/schoolOfSorcery";
import {LoggerService} from "@scripts/util/logger";
import {
getDefaultIntakeMessage,
getDefaultUser
} from "@tests/scripts/hunt-filter/common";
import * as stageTest from "@tests/scripts/modules/stages/environments/schoolOfSorcery.spec";

describe("School of Sorcery exemptions", () => {
let logger: LoggerService;
let stager: SchoolOfSorceryStager;
let target: IntakeRejectionEngine;

beforeEach(() => {
logger = {} as LoggerService;
stager = new SchoolOfSorceryStager();
target = new IntakeRejectionEngine(logger);

logger.debug = jest.fn();
});

describe("validateMessage", () => {
let preUser: User;
let postUser: User;
let preMessage: IntakeMessage;
let postMessage: IntakeMessage;

beforeEach(() => {
preUser = {...getDefaultUser(), ...getSchoolOfSorceryUser()};
postUser = {...getDefaultUser(), ...getSchoolOfSorceryUser()};
preMessage = {...getDefaultIntakeMessage()};
postMessage = {...getDefaultIntakeMessage()};
});

describe("In Course", () => {
it("should reject on transition to boss", () => {
preUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType: 'arcane_101_course', powerType: 'arcane'}, false);
postUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType: 'arcane_101_course', powerType: 'arcane'}, true);
preMessage.mouse = postMessage.mouse = "Some Random Mouse";

calculateStage();

const valid = target.validateMessage(preMessage, postMessage);

expect(valid).toBe(false);
});

describe.each<{courseType: CourseType, powerType: 'arcane' | 'shadow', boss: string}>([
{courseType: 'arcane_101_course', powerType: 'arcane', boss: 'Arcane Master Sorcerer'},
{courseType: 'shadow_101_course', powerType: 'shadow', boss: 'Shadow Master Sorcerer'},
{courseType: 'exam_course', powerType: 'arcane', boss: 'Mythical Master Sorcerer'},
{courseType: 'exam_course', powerType: 'shadow', boss: 'Mythical Master Sorcerer'},
])("Boss catching", ({courseType, powerType, boss}) => {
it(`should accept on transition from catching ${boss} in ${courseType} to hallway`, () => {
preUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType, powerType}, true);
postUser.quests.QuestSchoolOfSorcery = stageTest.createHallwayAttributes();
preMessage.mouse = postMessage.mouse = boss;
// Going back changes cheese
preMessage.cheese = {id: 1, name: 'Some'};
postMessage.cheese = {id: 2, name: 'Another'};

calculateStage();

const valid = target.validateMessage(preMessage, postMessage);
expect(valid).toBe(true);
});

it(`should accept on transition from catching ${boss} in ${courseType} to hallway and cheese disarming`, () => {
preUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType, powerType}, true);
postUser.quests.QuestSchoolOfSorcery = stageTest.createHallwayAttributes();
preMessage.mouse = postMessage.mouse = boss;
// Going back disarms cheese (sets user.bait_item_id: 0, user.bait_name: 0). See createMessageFromHunt
preMessage.cheese = {id: 1, name: 'Some Cheese'};
postMessage.cheese = {} as ComponentEntry;

calculateStage();

const valid = target.validateMessage(preMessage, postMessage);
expect(valid).toBe(true);
});

it(`should accept on transition from catching ${boss} in ${courseType} and continuing course`, () => {
let postPowerType = powerType;
if (courseType === 'exam_course') {
postPowerType = postPowerType === 'arcane' ? 'shadow' : 'arcane';
}
preUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType, powerType}, true);
postUser.quests.QuestSchoolOfSorcery = stageTest.createCourseAttributes({courseType, powerType: postPowerType}, false);
preMessage.mouse = postMessage.mouse = boss;

calculateStage();

const valid = target.validateMessage(preMessage, postMessage);
expect(valid).toBe(true);
});
});
});

/** Sets the pre and post message stage based on current pre and post user */
function calculateStage() {
stager.addStage(preMessage, preUser, {} as User, {});
stager.addStage(postMessage, postUser, {} as User, {});
}
});

function getSchoolOfSorceryUser(): User {
return {
environment_name: "School of Sorcery",
quests: {
QuestSchoolOfSorcery: stageTest.getDefaultQuest(),
},
} as User;
}
});
Loading