Skip to content

Commit

Permalink
Merge pull request #553 from hymccord/school-of-sorcery
Browse files Browse the repository at this point in the history
Add School of Sorcery stages
  • Loading branch information
AardWolf authored Apr 23, 2024
2 parents 7b25e2c + 60e622f commit e319d2c
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 5 deletions.
67 changes: 67 additions & 0 deletions src/scripts/hunt-filter/exemptions/environments/schoolOfSorcery.ts
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

0 comments on commit e319d2c

Please sign in to comment.