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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All@@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand DownExpand Up@@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand DownExpand Up@@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All@@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All@@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand DownExpand Up@@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All@@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand DownExpand Up@@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand DownExpand Up@@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand DownExpand Up@@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading