Quests are now loaded from a json file generated from quest files.

This commit is contained in:
Daan Vanden Bosch 2019-06-22 00:27:04 +02:00
parent 4a91cb2a29
commit 71feaf7867
184 changed files with 3318 additions and 863 deletions

3032
public/quests.ephinea.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +0,0 @@
episode: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
quest hours Hildebear Hildeblue Rag Rappy Al Rappy Monest Savage Wolf Barbarous Wolf Booma Gobooma Gigobooma Dragon Grass Assassin Poison Lily Nar Lily Nano Dragon Evil Shark Pal Shark Guil Shark Pofuilly Slime Pouilly Slime Pan Arms De Rol Le Dubchic Gilchic Garanz Sinow Beat Sinow Gold Canadine Canane Dubswitch Vol Opt Delsaber Chaos Sorcerer Dark Gunner Chaos Bringer Dark Belra Dimenian La Dimenian So Dimenian Bulclaw Claw Dark Falz Hildebear Hildeblue Rag Rappy Love Rappy Monest Poison Lily Nar Lily Grass Assassin Dimenian La Dimenian So Dimenian Dark Belra Barba Ray Savage Wolf Barbarous Wolf Pan Arms Dubchic Gilchic Garanz Dubswitch Delsaber Chaos Sorcerer Gol Dragon Sinow Berill Sinow Spigell Merillia Meriltas Mericarol Merikle Mericus Ul Gibbon Zol Gibbon Gibbles Gee Gi Gue Gal Gryphon Deldepth Delbiter Dolmolm Dolmdarl Morfos Recobox Epsilon Sinow Zoa Sinow Zele Ill Gill Del Lily Olga Flow Sand Rappy Del Rappy Astark Satellite Lizard Yowie Merissa A Merissa AA Girtablulu Zu Pazuzu Boota Ze Boota Ba Boota Dorphon Dorphon Eclair Goran Pyro Goran Goran Detonator Saint-Milion Shambertin Kondrieu Normal Box Gold Box
Maximum Attack 4th Stage -A- (Ep. I) 0.33 25 63 14 43 21 28 16 20 46 12 17 21 67 3 4 7 31 3 23 37 33 30 8 51
Maximum Attack 4th Stage -B- (Ep. I) 0.33 28 62 22 20 40 32 14 21 21 13 19 16 59 3 4 10 24 5 27 13 18 25 11 52
Maximum Attack 4th Stage -C- (Ep. I) 0.33 28 62 22 20 40 32 14 24 19 16 17 19 48 4 3 7 18 12 9 11 11 20 5 17 1
Principal's Gift 0.5 6 68 2 22 3 20 1
Endless Nightmare #1 0.5 8 19 4 29 4 60 32 23
Endless Nightmare #2 0.5 20 32 6 152 44 28 5
Endless Nightmare #3 0.5 37 108 10 48 14 84 2 8
Endless Nightmare #4 0.5 44 3 21 13 28 49 59 49 6 90
Mop-up Operation #1 0.5 2 10 5 14 23 36
Mop-up Operation #2 0.5 10 9 18 72 21 24
Mop-up Operation #3 0.5 9 59 6 17 7 55 2 2
Mop-up Operation #4 0.5 13 5 6 12 35 27 33 50
Today's Rate 0.5 52 7 1 1 53 5 6 9 47 6 27 7 25 3
Fragment of Memory 0.5 81 68 77 16 38 226 86 237 1
Gallon's Treachery 0.5 22 7 16 13 14 5 1 3 2 3 3 3 10 12
Lost HAVOC VULCAN 0.5 68 28 81 49 54 156 94 89 36 152 1
Lost HEAT SWORD 0.33 12 10 7 26 7 34 38 53 1
Lost ICE SPINNER 1 37 56 26 129 65 125 7 5 1
Lost SOUL BLADE 0.5 17 85 26 70 37 112 4 4 1
Rappy's Holiday 0.5 75 10 15 20 20 11 13
Labyrinthine Trial 0.5 14 15 2 10 38 16 12 6
Towards the Future 0.5 4 1 2 1 12 6 7 1 13 5 13 5 7 5 4 1 1 26 3 12 2 24 1 1 1 2 4 4 17 4 14 1
Dream Messenger 0.5 8 8 1 49
Maximum Attack 2 0.5 18 35 6 20 21 27 14 56 5 10 52 17 36 19 2 76 97 7 1 1 106 78 8 122 9
Maximum Attack 4th Stage -A- (Ep. II) 0.33 20 8 32 21 1 29 41 6 62 12 7 55 51 20 56 12 7
Maximum Attack 4th Stage -B- (Ep. II) 0.33 25 12 52 42 6 1 1 16 27 67 25 2 51 53 38 42 12 18
Maximum Attack 4th Stage -C- (Ep. II) 0.33 22 14 24 20 4 6 6 14 26 14 45 15 10 19 44 38 16 58 4 18 18 21 28
Phantasmal World #1 0.5 25 7 83 41 6 45 20 122 12
Phantasmal World #2 0.5 24 9 54 42 114 26 9 80
Phantasmal World #3 0.5 31 27 103 57 47 27 33 22
Phantasmal World #4 1 11 6 6 20 22 19 20 9 72 57
The East Tower 0.5 2 2 16 5 1 3 6 9 6 8 8 3 14 1 4 7
The West Tower 0.5 6 1 1 5 7 14 10 10 36 1 7 6 8 9
Reach for the Dream 0.5 4 2 6 19 3 1 7
Respective Tomorrow 0.5 5 10 2 19 3 18 9 1 1 1 1 28 1 1 6 1 1 3 19 2 6 1 1 6 3 4 88 5 1 1 2 4 4 62 1 9 1 3 3 1
LOGiN presents 勇場のマッチレース 1 88 19 46 70 84 14 20 59 51 22 4 94 52 22
Maximum Attack 4th Stage -A- (Ep. IV) 0.33 57 13 68 56 58 20 18 13 16 3 30 26 6
Maximum Attack 4th Stage -B- (Ep. IV) 0.33 60 11 58 50 20 3 48 24 15 17 3 40 32 19
Maximum Attack 4th Stage -C- (Ep. IV) 0.33 73 16 69 74 37 6 33 20 22 16 13 37 26 18
New Mop-up Operation #1 0.5 37 22 32 20 8 20 5 5 2
New Mop-up Operation #2 0.5 20 11 41 23 5 31 36 14 5
New Mop-up Operation #3 0.5 38 73 41 4 4 108 55 16
New Mop-up Operation #4 0.5 29 25 38 24 1 46 40 3
New Mop-up Operation #5 0.5 11 38 40 25 4 19 37 30 7
War of Limits 1 0.5 42 42 51 25 12 47 12 10 5
War of Limits 2 0.5 37 11 92 37 5 39 74 25 3
War of Limits 3 0.5 47 37 66 6 85 87 26 4
War of Limits 4 0.5 26 30 26 28 3 1 37 36 16
War of Limits 5 0.5 44 48 41 11 4 15 49 19 7
MAXIMUM ATTACK 3 Ver2 0.5 83 54 122 115 99 4 46 45 39 47 10 101 103 32
1-1:Planet Ragol 0.5 2 4 36 4 40 7 6
1-2:Torrential Woods 0.5 5 17 3 19 7 35 31 16
1-3:Subterranean Den 0.5 5 12 3 33 10 45 32 15 1
2-1:Infernal Cavern 0.75 18 37 17 114 18 3
2-2:Deep Within 0.75 20 59 15 165 31 7 16
2-3:The Mutation 0.75 7 41 12 89 42 24 10 2 13
2-4:Waterway Shadow 0.75 16 36 4 17 165 87 28 4 4 1
3-1:The Facility 0.75 3 115 12 2 47 2 1
3-2:Machines Attack 0.75 22 60 9 14 2 59 2 7
3-3:Central Control 0.75 11 93 4 16 6 90 3 3 1
4-1:The Lost Ruins 0.75 14 42 3 8 4 41 1 4 10 13 18 61 7 3 5 89
4-2:Buried Relics 0.75 31 20 2 28 55 6 5 50
4-3:Hero & Daughter 0.75 26 4 30 7 5 75 50 22 8 122
4-4:The Tomb Stirs 0.75 17 16 10 25 82 35 34 7 55
4-5:Dark Inheritance 0.75 26 11 36 9 12 111 78 44 6 143 1
5-1:Test/VR Temple 1 0.75 5 13 3 29 2 16 17 33
5-2:Test/VR Temple 2 0.75 11 23 6 52 17 21 25 15 7
5-3:Test/VR Temple 3 0.75 9 4 4 35 12 17 17 12 9
5-4:Test/VR Temple 4 0.75 5 29 5 56 5 24 14 47 23 8
5-5:Test/VR Temple 5 0.75 11 26 6 63 16 21 34 27 9 1
6-1:Test/Spaceship 1 0.75 32 24 5 54 7 12
6-2:Test/Spaceship 2 0.75 50 24 9 8 84 5 3 17 5
6-3:Test/Spaceship 3 0.75 36 27 15 16 50 1 5 26 5
6-4:Test/Spaceship 4 0.75 39 36 8 10 87 6 3 25 11
6-5:Test/Spaceship 5 0.75 41 32 10 10 92 5 3 27 11 1
7-1:From the Past 0.75 3 55 17 4 20 3 20
7-2:Seeking Clues 0.75 3 37 19 1 25 43 6 8
7-3:Silent Beach 0.75 4 29 8 30 45 1 94 5
7-4:Central Control 0.75 11 6 22 6 4 17 18 3 34 4
7-5:Isle of Mutants 0.75 11 6 59 42 1 43 56 1 132 1 1
8-1:Below the Waves 0.75 9 3 35 17 25 23 3
8-2:Desire's End 0.75 2 4 34 43 25 28 24 5
8-3:Purple Lamplight 0.75 7 7 23 38 19 41 39 4 1
9-1:Missing Research 0.75 26 10 40 12 67 48 27 1
9-2:Data Retrieval 0.75 41 18 28 18 48 21 24 5
9-3:Reality & Truth 0.75 24 9 31 4 13 21 22 50 1
9-4:Pursuit 0.75 30 17 34 5 17 45 55 29 5
9-5:The Chosen (1/2) 0.75 36 5 46 9 25 10 12 5 3 2 62 20 9
9-6:The Chosen (2/2) 0.75 12 21 19 34 1 2 25 34 4
9-7:Sacred Ground 0.75 28 39 11 29 1 5 54 38 11
9-8:The Final Cycle 0.75 45 41 63 47 1 6 57 61 8 1
Point of Disaster 0.25 50 31 50 31 4 1 20 24 8 6 9 10 4 3 1
Battle Training 0.5 2 2 12 2 14 3
Claiming a Stake 0.5 5 1 1 19 2
Magnitude of Metal 0.5 4 1 16
Journalistic Pursuit 0.5 1 11 3 12 1 18 9 6
The Fake in Yellow 0.5 47
Native Research 0.5 2 13 4 10 4 29 13 8
Forest of Sorrow 0.13 16 15 4 14 7 26 15 3
Gran Squall 0.5 4 15 7 18 4 24 13 7
Addicting Food 0.5 19 62 2 19 195 38 15 13 7
The Lost Bride 0.5 2 20 2 33 6 5 7
Waterfall Tears 0.5 6 18 12 37 6 1
Black Paper 0.5 12 20 2 14 94 31 10 8 3
Secret Delivery 0.5 9 18 5 59 9 5 5
Soul of a Blacksmith 0.5 5 11 5 28 11 31 29 27 1 14 17 6 12 125 44 25 9 2 6
Letter from Lionel 0.5 5 17 2 19 4 34 15 5 4 36 2 74 19 9 11 1
The Grave's Butler 0.5 9 22 4 35 7 2 1
Knowing One's Heart 0.5 22 5 2 29 1
The Retired Hunter 0.5 1 8 2 7 6 6 4 3 3 3 4 17 7 18 1 42
Dr. Osto's Research 0.5 13 27 3 2 15 3
Unsealed Door 0.5 1 13 97 6 2 62 3 3
Soul of Steel 0.5 22 1 2 1 5 11 4 7 14 12 14 4 40
Doc's Secret Plan 0.5 9 11 7 47 7 1 6 29
Seek my Master 0.5 21 3 20 2 9 43 24 4 2 86
From the Depths 0.5 11 3 17 6 4 41 23 11 7 71
Central Dome Fire Swirl 0.5 4 19 19 14 9 19 34
Seat of the Heart 0.5 3 7 2 9 3 12 11 12 2 25 1 16 16 3 2 1 1 2 5 5 12 4 8 4 6 10 5 2 13 1 7 2
Pioneer Spirit 0.5 40 1 14 25 29 31 3 21 8 2 4 4 40 29 5
Warrior's Pride 0.5 61 46 51 53 29 35 10 23 13
The Restless Lion 0.5 8 16 13 14 3 13 10 1
To the End of the Wilderness 0.5 56 14 14 41 13 77 55 47 2
1 episode: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
2 quest hours Hildebear Hildeblue Rag Rappy Al Rappy Monest Savage Wolf Barbarous Wolf Booma Gobooma Gigobooma Dragon Grass Assassin Poison Lily Nar Lily Nano Dragon Evil Shark Pal Shark Guil Shark Pofuilly Slime Pouilly Slime Pan Arms De Rol Le Dubchic Gilchic Garanz Sinow Beat Sinow Gold Canadine Canane Dubswitch Vol Opt Delsaber Chaos Sorcerer Dark Gunner Chaos Bringer Dark Belra Dimenian La Dimenian So Dimenian Bulclaw Claw Dark Falz Hildebear Hildeblue Rag Rappy Love Rappy Monest Poison Lily Nar Lily Grass Assassin Dimenian La Dimenian So Dimenian Dark Belra Barba Ray Savage Wolf Barbarous Wolf Pan Arms Dubchic Gilchic Garanz Dubswitch Delsaber Chaos Sorcerer Gol Dragon Sinow Berill Sinow Spigell Merillia Meriltas Mericarol Merikle Mericus Ul Gibbon Zol Gibbon Gibbles Gee Gi Gue Gal Gryphon Deldepth Delbiter Dolmolm Dolmdarl Morfos Recobox Epsilon Sinow Zoa Sinow Zele Ill Gill Del Lily Olga Flow Sand Rappy Del Rappy Astark Satellite Lizard Yowie Merissa A Merissa AA Girtablulu Zu Pazuzu Boota Ze Boota Ba Boota Dorphon Dorphon Eclair Goran Pyro Goran Goran Detonator Saint-Milion Shambertin Kondrieu Normal Box Gold Box
3 Maximum Attack 4th Stage -A- (Ep. I) 0.33 25 63 14 43 21 28 16 20 46 12 17 21 67 3 4 7 31 3 23 37 33 30 8 51
4 Maximum Attack 4th Stage -B- (Ep. I) 0.33 28 62 22 20 40 32 14 21 21 13 19 16 59 3 4 10 24 5 27 13 18 25 11 52
5 Maximum Attack 4th Stage -C- (Ep. I) 0.33 28 62 22 20 40 32 14 24 19 16 17 19 48 4 3 7 18 12 9 11 11 20 5 17 1
6 Principal's Gift 0.5 6 68 2 22 3 20 1
7 Endless Nightmare #1 0.5 8 19 4 29 4 60 32 23
8 Endless Nightmare #2 0.5 20 32 6 152 44 28 5
9 Endless Nightmare #3 0.5 37 108 10 48 14 84 2 8
10 Endless Nightmare #4 0.5 44 3 21 13 28 49 59 49 6 90
11 Mop-up Operation #1 0.5 2 10 5 14 23 36
12 Mop-up Operation #2 0.5 10 9 18 72 21 24
13 Mop-up Operation #3 0.5 9 59 6 17 7 55 2 2
14 Mop-up Operation #4 0.5 13 5 6 12 35 27 33 50
15 Today's Rate 0.5 52 7 1 1 53 5 6 9 47 6 27 7 25 3
16 Fragment of Memory 0.5 81 68 77 16 38 226 86 237 1
17 Gallon's Treachery 0.5 22 7 16 13 14 5 1 3 2 3 3 3 10 12
18 Lost HAVOC VULCAN 0.5 68 28 81 49 54 156 94 89 36 152 1
19 Lost HEAT SWORD 0.33 12 10 7 26 7 34 38 53 1
20 Lost ICE SPINNER 1 37 56 26 129 65 125 7 5 1
21 Lost SOUL BLADE 0.5 17 85 26 70 37 112 4 4 1
22 Rappy's Holiday 0.5 75 10 15 20 20 11 13
23 Labyrinthine Trial 0.5 14 15 2 10 38 16 12 6
24 Towards the Future 0.5 4 1 2 1 12 6 7 1 13 5 13 5 7 5 4 1 1 26 3 12 2 24 1 1 1 2 4 4 17 4 14 1
25 Dream Messenger 0.5 8 8 1 49
26 Maximum Attack 2 0.5 18 35 6 20 21 27 14 56 5 10 52 17 36 19 2 76 97 7 1 1 106 78 8 122 9
27 Maximum Attack 4th Stage -A- (Ep. II) 0.33 20 8 32 21 1 29 41 6 62 12 7 55 51 20 56 12 7
28 Maximum Attack 4th Stage -B- (Ep. II) 0.33 25 12 52 42 6 1 1 16 27 67 25 2 51 53 38 42 12 18
29 Maximum Attack 4th Stage -C- (Ep. II) 0.33 22 14 24 20 4 6 6 14 26 14 45 15 10 19 44 38 16 58 4 18 18 21 28
30 Phantasmal World #1 0.5 25 7 83 41 6 45 20 122 12
31 Phantasmal World #2 0.5 24 9 54 42 114 26 9 80
32 Phantasmal World #3 0.5 31 27 103 57 47 27 33 22
33 Phantasmal World #4 1 11 6 6 20 22 19 20 9 72 57
34 The East Tower 0.5 2 2 16 5 1 3 6 9 6 8 8 3 14 1 4 7
35 The West Tower 0.5 6 1 1 5 7 14 10 10 36 1 7 6 8 9
36 Reach for the Dream 0.5 4 2 6 19 3 1 7
37 Respective Tomorrow 0.5 5 10 2 19 3 18 9 1 1 1 1 28 1 1 6 1 1 3 19 2 6 1 1 6 3 4 88 5 1 1 2 4 4 62 1 9 1 3 3 1
38 LOGiN presents 勇場のマッチレース 1 88 19 46 70 84 14 20 59 51 22 4 94 52 22
39 Maximum Attack 4th Stage -A- (Ep. IV) 0.33 57 13 68 56 58 20 18 13 16 3 30 26 6
40 Maximum Attack 4th Stage -B- (Ep. IV) 0.33 60 11 58 50 20 3 48 24 15 17 3 40 32 19
41 Maximum Attack 4th Stage -C- (Ep. IV) 0.33 73 16 69 74 37 6 33 20 22 16 13 37 26 18
42 New Mop-up Operation #1 0.5 37 22 32 20 8 20 5 5 2
43 New Mop-up Operation #2 0.5 20 11 41 23 5 31 36 14 5
44 New Mop-up Operation #3 0.5 38 73 41 4 4 108 55 16
45 New Mop-up Operation #4 0.5 29 25 38 24 1 46 40 3
46 New Mop-up Operation #5 0.5 11 38 40 25 4 19 37 30 7
47 War of Limits 1 0.5 42 42 51 25 12 47 12 10 5
48 War of Limits 2 0.5 37 11 92 37 5 39 74 25 3
49 War of Limits 3 0.5 47 37 66 6 85 87 26 4
50 War of Limits 4 0.5 26 30 26 28 3 1 37 36 16
51 War of Limits 5 0.5 44 48 41 11 4 15 49 19 7
52 MAXIMUM ATTACK 3 Ver2 0.5 83 54 122 115 99 4 46 45 39 47 10 101 103 32
53 1-1:Planet Ragol 0.5 2 4 36 4 40 7 6
54 1-2:Torrential Woods 0.5 5 17 3 19 7 35 31 16
55 1-3:Subterranean Den 0.5 5 12 3 33 10 45 32 15 1
56 2-1:Infernal Cavern 0.75 18 37 17 114 18 3
57 2-2:Deep Within 0.75 20 59 15 165 31 7 16
58 2-3:The Mutation 0.75 7 41 12 89 42 24 10 2 13
59 2-4:Waterway Shadow 0.75 16 36 4 17 165 87 28 4 4 1
60 3-1:The Facility 0.75 3 115 12 2 47 2 1
61 3-2:Machines Attack 0.75 22 60 9 14 2 59 2 7
62 3-3:Central Control 0.75 11 93 4 16 6 90 3 3 1
63 4-1:The Lost Ruins 0.75 14 42 3 8 4 41 1 4 10 13 18 61 7 3 5 89
64 4-2:Buried Relics 0.75 31 20 2 28 55 6 5 50
65 4-3:Hero & Daughter 0.75 26 4 30 7 5 75 50 22 8 122
66 4-4:The Tomb Stirs 0.75 17 16 10 25 82 35 34 7 55
67 4-5:Dark Inheritance 0.75 26 11 36 9 12 111 78 44 6 143 1
68 5-1:Test/VR Temple 1 0.75 5 13 3 29 2 16 17 33
69 5-2:Test/VR Temple 2 0.75 11 23 6 52 17 21 25 15 7
70 5-3:Test/VR Temple 3 0.75 9 4 4 35 12 17 17 12 9
71 5-4:Test/VR Temple 4 0.75 5 29 5 56 5 24 14 47 23 8
72 5-5:Test/VR Temple 5 0.75 11 26 6 63 16 21 34 27 9 1
73 6-1:Test/Spaceship 1 0.75 32 24 5 54 7 12
74 6-2:Test/Spaceship 2 0.75 50 24 9 8 84 5 3 17 5
75 6-3:Test/Spaceship 3 0.75 36 27 15 16 50 1 5 26 5
76 6-4:Test/Spaceship 4 0.75 39 36 8 10 87 6 3 25 11
77 6-5:Test/Spaceship 5 0.75 41 32 10 10 92 5 3 27 11 1
78 7-1:From the Past 0.75 3 55 17 4 20 3 20
79 7-2:Seeking Clues 0.75 3 37 19 1 25 43 6 8
80 7-3:Silent Beach 0.75 4 29 8 30 45 1 94 5
81 7-4:Central Control 0.75 11 6 22 6 4 17 18 3 34 4
82 7-5:Isle of Mutants 0.75 11 6 59 42 1 43 56 1 132 1 1
83 8-1:Below the Waves 0.75 9 3 35 17 25 23 3
84 8-2:Desire's End 0.75 2 4 34 43 25 28 24 5
85 8-3:Purple Lamplight 0.75 7 7 23 38 19 41 39 4 1
86 9-1:Missing Research 0.75 26 10 40 12 67 48 27 1
87 9-2:Data Retrieval 0.75 41 18 28 18 48 21 24 5
88 9-3:Reality & Truth 0.75 24 9 31 4 13 21 22 50 1
89 9-4:Pursuit 0.75 30 17 34 5 17 45 55 29 5
90 9-5:The Chosen (1/2) 0.75 36 5 46 9 25 10 12 5 3 2 62 20 9
91 9-6:The Chosen (2/2) 0.75 12 21 19 34 1 2 25 34 4
92 9-7:Sacred Ground 0.75 28 39 11 29 1 5 54 38 11
93 9-8:The Final Cycle 0.75 45 41 63 47 1 6 57 61 8 1
94 Point of Disaster 0.25 50 31 50 31 4 1 20 24 8 6 9 10 4 3 1
95 Battle Training 0.5 2 2 12 2 14 3
96 Claiming a Stake 0.5 5 1 1 19 2
97 Magnitude of Metal 0.5 4 1 16
98 Journalistic Pursuit 0.5 1 11 3 12 1 18 9 6
99 The Fake in Yellow 0.5 47
100 Native Research 0.5 2 13 4 10 4 29 13 8
101 Forest of Sorrow 0.13 16 15 4 14 7 26 15 3
102 Gran Squall 0.5 4 15 7 18 4 24 13 7
103 Addicting Food 0.5 19 62 2 19 195 38 15 13 7
104 The Lost Bride 0.5 2 20 2 33 6 5 7
105 Waterfall Tears 0.5 6 18 12 37 6 1
106 Black Paper 0.5 12 20 2 14 94 31 10 8 3
107 Secret Delivery 0.5 9 18 5 59 9 5 5
108 Soul of a Blacksmith 0.5 5 11 5 28 11 31 29 27 1 14 17 6 12 125 44 25 9 2 6
109 Letter from Lionel 0.5 5 17 2 19 4 34 15 5 4 36 2 74 19 9 11 1
110 The Grave's Butler 0.5 9 22 4 35 7 2 1
111 Knowing One's Heart 0.5 22 5 2 29 1
112 The Retired Hunter 0.5 1 8 2 7 6 6 4 3 3 3 4 17 7 18 1 42
113 Dr. Osto's Research 0.5 13 27 3 2 15 3
114 Unsealed Door 0.5 1 13 97 6 2 62 3 3
115 Soul of Steel 0.5 22 1 2 1 5 11 4 7 14 12 14 4 40
116 Doc's Secret Plan 0.5 9 11 7 47 7 1 6 29
117 Seek my Master 0.5 21 3 20 2 9 43 24 4 2 86
118 From the Depths 0.5 11 3 17 6 4 41 23 11 7 71
119 Central Dome Fire Swirl 0.5 4 19 19 14 9 19 34
120 Seat of the Heart 0.5 3 7 2 9 3 12 11 12 2 25 1 16 16 3 2 1 1 2 5 5 12 4 8 4 6 10 5 2 13 1 7 2
121 Pioneer Spirit 0.5 40 1 14 25 29 31 3 21 8 2 4 4 40 29 5
122 Warrior's Pride 0.5 61 46 51 53 29 35 10 23 13
123 The Restless Lion 0.5 8 16 13 14 3 13 10 1
124 To the End of the Wilderness 0.5 56 14 14 41 13 77 55 47 2

View File

@ -14,7 +14,7 @@ export interface BinFile {
data: ArrayBufferCursor;
}
export function parseBin(cursor: ArrayBufferCursor): BinFile {
export function parseBin(cursor: ArrayBufferCursor, lenient: boolean = false): BinFile {
const objectCodeOffset = cursor.u32();
const functionOffsetTableOffset = cursor.u32(); // Relative offsets
const size = cursor.u32();
@ -40,7 +40,8 @@ export function parseBin(cursor: ArrayBufferCursor): BinFile {
}
const instructions = parseObjectCode(
cursor.seekStart(objectCodeOffset).take(functionOffsetTableOffset - objectCodeOffset)
cursor.seekStart(objectCodeOffset).take(functionOffsetTableOffset - objectCodeOffset),
lenient
);
return {
@ -66,47 +67,55 @@ export interface Instruction {
size: number;
}
function parseObjectCode(cursor: ArrayBufferCursor): Instruction[] {
function parseObjectCode(cursor: ArrayBufferCursor, lenient: boolean): Instruction[] {
const instructions = [];
while (cursor.bytesLeft) {
const mainOpcode = cursor.u8();
let opcode;
let opsize;
let list;
try {
while (cursor.bytesLeft) {
const mainOpcode = cursor.u8();
let opcode;
let opsize;
let list;
switch (mainOpcode) {
case 0xF8:
opcode = cursor.u8();
opsize = 2;
list = F8opcodeList;
break;
case 0xF9:
opcode = cursor.u8();
opsize = 2;
list = F9opcodeList;
break;
default:
opcode = mainOpcode;
opsize = 1;
list = opcodeList;
switch (mainOpcode) {
case 0xF8:
opcode = cursor.u8();
opsize = 2;
list = F8opcodeList;
break;
case 0xF9:
opcode = cursor.u8();
opsize = 2;
list = F9opcodeList;
break;
default:
opcode = mainOpcode;
opsize = 1;
list = opcodeList;
break;
}
const [, mnemonic, mask] = list[opcode];
const opargs = parseInstructionArguments(cursor, mask);
if (!opargs) {
logger.error(`Parameters unknown for opcode 0x${opcode.toString(16).toUpperCase()}.`);
break;
}
instructions.push({
opcode,
mnemonic,
args: opargs.args,
size: opsize + opargs.size
});
}
const [, mnemonic, mask] = list[opcode];
const opargs = parseInstructionArguments(cursor, mask);
if (!opargs) {
logger.error(`Parameters unknown for opcode 0x${opcode.toString(16).toUpperCase()}.`);
break;
} catch (e) {
if (lenient) {
logger.error("Couldn't fully parse object code.", e);
} else {
throw e;
}
instructions.push({
opcode,
mnemonic,
args: opargs.args,
size: opsize + opargs.size
});
}
return instructions;

View File

@ -22,7 +22,7 @@ const logger = Logger.get('bin-data/parsing/quest');
*
* Always delegates to parseQst at the moment.
*/
export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined {
export function parseQuest(cursor: ArrayBufferCursor, lenient: boolean = false): Quest | undefined {
const qst = parseQst(cursor);
if (!qst) {
@ -33,9 +33,11 @@ export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined {
let binFile = null;
for (const file of qst.files) {
if (file.name.endsWith('.dat')) {
const fileName = file.name.trim().toLowerCase();
if (fileName.endsWith('.dat')) {
datFile = file;
} else if (file.name.endsWith('.bin')) {
} else if (fileName.endsWith('.bin')) {
binFile = file;
}
}
@ -53,7 +55,7 @@ export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined {
}
const dat = parseDat(prs.decompress(datFile.data));
const bin = parseBin(prs.decompress(binFile.data));
const bin = parseBin(prs.decompress(binFile.data), lenient);
let episode = 1;
let areaVariants: AreaVariant[] = [];
@ -62,7 +64,7 @@ export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined {
if (func0Ops) {
episode = getEpisode(func0Ops);
areaVariants = getAreaVariants(episode, func0Ops);
areaVariants = getAreaVariants(episode, func0Ops, lenient);
} else {
logger.warn(`Function 0 offset ${bin.functionOffsets[0]} is invalid.`);
}
@ -129,7 +131,11 @@ function getEpisode(func0Ops: Instruction[]): number {
}
}
function getAreaVariants(episode: number, func0Ops: Instruction[]): AreaVariant[] {
function getAreaVariants(
episode: number,
func0Ops: Instruction[],
lenient: boolean
): AreaVariant[] {
const areaVariants = new Map();
const bbMaps = func0Ops.filter(op => op.mnemonic === 'BB_Map_Designate');
@ -139,12 +145,25 @@ function getAreaVariants(episode: number, func0Ops: Instruction[]): AreaVariant[
areaVariants.set(areaId, variantId);
}
const areaVariantsArray = new Array<AreaVariant>();
for (const [areaId, variantId] of areaVariants.entries()) {
try {
areaVariantsArray.push(
areaStore.getVariant(episode, areaId, variantId)
);
} catch (e) {
if (lenient) {
logger.error(`Unknown area variant.`, e);
} else {
throw e;
}
}
}
// Sort by area order and then variant id.
return (
Array.from(areaVariants)
.map(([areaId, variantId]) =>
areaStore.getVariant(episode, areaId, variantId))
.sort((a, b) => a.area.order - b.area.order || a.id - b.id)
return areaVariantsArray.sort((a, b) =>
a.area.order - b.area.order || a.id - b.id
);
}

View File

@ -138,7 +138,7 @@ function parseFiles(cursor: ArrayBufferCursor, expectedSizes: Map<string, number
// Each chunk has a 24 byte header, 1024 byte data segment and an 8 byte trailer.
const files = new Map<string, QstContainedFile>();
while (cursor.bytesLeft) {
while (cursor.bytesLeft >= 1056) {
const startPosition = cursor.position;
// Read meta data.
@ -185,6 +185,10 @@ function parseFiles(cursor: ArrayBufferCursor, expectedSizes: Map<string, number
}
}
if (cursor.bytesLeft) {
logger.warn(`${cursor.bytesLeft} Bytes left in file.`);
}
for (const file of files.values()) {
// Clean up file properties.
file.data.seekStart(0);

View File

@ -1,66 +0,0 @@
import * as fs from 'fs';
import { ArrayBufferCursor } from '../../ArrayBufferCursor';
import { parseQuest, writeQuestQst } from '.
import { ObjectType, Quest } from '../../../domain';
test('parse Towards the Future', () => {
const buffer = fs.readFileSync('test/resources/quest118_e.qst').buffer;
const cursor = new ArrayBufferCursor(buffer, true);
const quest = parseQuest(cursor)!;
expect(quest.name).toBe('Towards the Future');
expect(quest.shortDescription).toBe('Challenge the\nnew simulator.');
expect(quest.longDescription).toBe('Client: Principal\nQuest: Wishes to have\nhunters challenge the\nnew simulator\nReward: ??? Meseta');
expect(quest.episode).toBe(1);
expect(quest.objects.length).toBe(277);
expect(quest.objects[0].type).toBe(ObjectType.MenuActivation);
expect(quest.objects[4].type).toBe(ObjectType.PlayerSet);
expect(quest.npcs.length).toBe(216);
expect(testableAreaVariants(quest)).toEqual([
[0, 0], [2, 0], [11, 0], [5, 4], [12, 0], [7, 4], [13, 0], [8, 4], [10, 4], [14, 0]
]);
});
/**
* Parse a QST file, write the resulting Quest object to QST again, then parse that again.
* Then check whether the two Quest objects are equal.
*/
test('parseQuest and writeQuestQst', () => {
const buffer = fs.readFileSync('test/resources/tethealla_v0.143_quests/solo/ep1/02.qst').buffer;
const cursor = new ArrayBufferCursor(buffer, true);
const origQuest = parseQuest(cursor)!;
const testQuest = parseQuest(writeQuestQst(origQuest, '02.qst'))!;
expect(testQuest.name).toBe(origQuest.name);
expect(testQuest.shortDescription).toBe(origQuest.shortDescription);
expect(testQuest.longDescription).toBe(origQuest.longDescription);
expect(testQuest.episode).toBe(origQuest.episode);
expect(testableObjects(testQuest))
.toEqual(testableObjects(origQuest));
expect(testableNpcs(testQuest))
.toEqual(testableNpcs(origQuest));
expect(testableAreaVariants(testQuest))
.toEqual(testableAreaVariants(origQuest));
});
function testableObjects(quest: Quest) {
return quest.objects.map(object => [
object.areaId,
object.sectionId,
object.position,
object.type
]);
}
function testableNpcs(quest: Quest) {
return quest.npcs.map(npc => [
npc.areaId,
npc.sectionId,
npc.position,
npc.type
]);
}
function testableAreaVariants(quest: Quest) {
return quest.areaVariants.map(av => [av.area.id, av.id]);
}

View File

@ -1,541 +0,0 @@
import { ArrayBufferCursor } from '../../ArrayBufferCursor';
import * as prs from '../../compression/prs';
import { parseDat, writeDat, DatObject, DatNpc } from './dat';
import { parseBin, writeBin, Instruction } from './bin';
import { parseQst, writeQst } from './qst';
import {
Vec3,
AreaVariant,
QuestNpc,
QuestObject,
Quest,
ObjectType,
NpcType
} from '../../../domain';
import { areaStore } from '../../../stores/AreaStore';
import Logger from 'js-logger';
const logger = Logger.get('bin-data/parsing/quest');
/**
* High level parsing function that delegates to lower level parsing functions.
*
* Always delegates to parseQst at the moment.
*/
export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined {
const qst = parseQst(cursor);
if (!qst) {
return;
}
let datFile = null;
let binFile = null;
for (const file of qst.files) {
if (file.name.endsWith('.dat')) {
datFile = file;
} else if (file.name.endsWith('.bin')) {
binFile = file;
}
}
// TODO: deal with missing/multiple DAT or BIN file.
if (!datFile) {
logger.error('File contains no DAT file.');
return;
}
if (!binFile) {
logger.error('File contains no BIN file.');
return;
}
const dat = parseDat(prs.decompress(datFile.data));
const bin = parseBin(prs.decompress(binFile.data));
let episode = 1;
let areaVariants: AreaVariant[] = [];
if (bin.functionOffsets.length) {
const func0Ops = getFuncOperations(bin.instructions, bin.functionOffsets[0]);
if (func0Ops) {
episode = getEpisode(func0Ops);
areaVariants = getAreaVariants(episode, func0Ops);
} else {
logger.warn(`Function 0 offset ${bin.functionOffsets[0]} is invalid.`);
}
} else {
logger.warn('File contains no functions.');
}
return new Quest(
bin.questName,
bin.shortDescription,
bin.longDescription,
datFile.questNo,
episode,
areaVariants,
parseObjData(dat.objs),
parseNpcData(episode, dat.npcs),
dat.unknowns,
bin.data
);
}
export function writeQuestQst(quest: Quest, fileName: string): ArrayBufferCursor {
const dat = writeDat({
objs: objectsToDatData(quest.objects),
npcs: npcsToDatData(quest.npcs),
unknowns: quest.datUnkowns
});
const bin = writeBin({ data: quest.binData });
const extStart = fileName.lastIndexOf('.');
const baseFileName = extStart === -1 ? fileName : fileName.slice(0, extStart);
return writeQst({
files: [
{
name: baseFileName + '.dat',
questNo: quest.questNo,
data: prs.compress(dat)
},
{
name: baseFileName + '.bin',
questNo: quest.questNo,
data: prs.compress(bin)
}
]
});
}
/**
* Defaults to episode I.
*/
function getEpisode(func0Ops: Instruction[]): number {
const setEpisode = func0Ops.find(op => op.mnemonic === 'set_episode');
if (setEpisode) {
switch (setEpisode.args[0]) {
default:
case 0: return 1;
case 1: return 2;
case 2: return 4;
}
} else {
logger.debug('Function 0 has no set_episode instruction.');
return 1;
}
}
function getAreaVariants(episode: number, func0Ops: Instruction[]): AreaVariant[] {
const areaVariants = new Map();
const bbMaps = func0Ops.filter(op => op.mnemonic === 'BB_Map_Designate');
for (const bbMap of bbMaps) {
const areaId = bbMap.args[0];
const variantId = bbMap.args[2];
areaVariants.set(areaId, variantId);
}
// Sort by area order and then variant id.
return (
Array.from(areaVariants)
.map(([areaId, variantId]) =>
areaStore.getVariant(episode, areaId, variantId))
.sort((a, b) => a.area.order - b.area.order || a.id - b.id)
);
}
function getFuncOperations(operations: Instruction[], funcOffset: number) {
let position = 0;
let funcFound = false;
const funcOps: Instruction[] = [];
for (const operation of operations) {
if (position === funcOffset) {
funcFound = true;
}
if (funcFound) {
funcOps.push(operation);
// Break when ret is encountered.
if (operation.opcode === 1) {
break;
}
}
position += operation.size;
}
return funcFound ? funcOps : null;
}
function parseObjData(objs: DatObject[]): QuestObject[] {
return objs.map(objData => {
const { x, y, z } = objData.position;
const rot = objData.rotation;
return new QuestObject(
objData.areaId,
objData.sectionId,
new Vec3(x, y, z),
new Vec3(rot.x, rot.y, rot.z),
ObjectType.fromPsoId(objData.typeId),
objData
);
});
}
function parseNpcData(episode: number, npcs: DatNpc[]): QuestNpc[] {
return npcs.map(npcData => {
const { x, y, z } = npcData.position;
const rot = npcData.rotation;
return new QuestNpc(
npcData.areaId,
npcData.sectionId,
new Vec3(x, y, z),
new Vec3(rot.x, rot.y, rot.z),
getNpcType(episode, npcData),
npcData
);
});
}
// TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon.
function getNpcType(episode: number, { typeId, unknown, skin, areaId }: DatNpc): NpcType {
const regular = (unknown[2][18] & 0x80) === 0;
switch (`${typeId}, ${skin % 3}, ${episode}`) {
case `${0x044}, 0, 1`: return NpcType.Booma;
case `${0x044}, 1, 1`: return NpcType.Gobooma;
case `${0x044}, 2, 1`: return NpcType.Gigobooma;
case `${0x063}, 0, 1`: return NpcType.EvilShark;
case `${0x063}, 1, 1`: return NpcType.PalShark;
case `${0x063}, 2, 1`: return NpcType.GuilShark;
case `${0x0A6}, 0, 1`: return NpcType.Dimenian;
case `${0x0A6}, 0, 2`: return NpcType.Dimenian2;
case `${0x0A6}, 1, 1`: return NpcType.LaDimenian;
case `${0x0A6}, 1, 2`: return NpcType.LaDimenian2;
case `${0x0A6}, 2, 1`: return NpcType.SoDimenian;
case `${0x0A6}, 2, 2`: return NpcType.SoDimenian2;
case `${0x0D6}, 0, 2`: return NpcType.Mericarol;
case `${0x0D6}, 1, 2`: return NpcType.Mericus;
case `${0x0D6}, 2, 2`: return NpcType.Merikle;
case `${0x115}, 0, 4`: return NpcType.Boota;
case `${0x115}, 1, 4`: return NpcType.ZeBoota;
case `${0x115}, 2, 4`: return NpcType.BaBoota;
case `${0x117}, 0, 4`: return NpcType.Goran;
case `${0x117}, 1, 4`: return NpcType.PyroGoran;
case `${0x117}, 2, 4`: return NpcType.GoranDetonator;
}
switch (`${typeId}, ${skin % 2}, ${episode}`) {
case `${0x040}, 0, 1`: return NpcType.Hildebear;
case `${0x040}, 0, 2`: return NpcType.Hildebear2;
case `${0x040}, 1, 1`: return NpcType.Hildeblue;
case `${0x040}, 1, 2`: return NpcType.Hildeblue2;
case `${0x041}, 0, 1`: return NpcType.RagRappy;
case `${0x041}, 0, 2`: return NpcType.RagRappy2;
case `${0x041}, 0, 4`: return NpcType.SandRappy;
case `${0x041}, 1, 1`: return NpcType.AlRappy;
case `${0x041}, 1, 2`: return NpcType.LoveRappy;
case `${0x041}, 1, 4`: return NpcType.DelRappy;
case `${0x061}, 0, 1`: return areaId > 15 ? NpcType.DelLily : NpcType.PoisonLily;
case `${0x061}, 0, 2`: return areaId > 15 ? NpcType.DelLily : NpcType.PoisonLily2;
case `${0x061}, 1, 1`: return areaId > 15 ? NpcType.DelLily : NpcType.NarLily;
case `${0x061}, 1, 2`: return areaId > 15 ? NpcType.DelLily : NpcType.NarLily2;
case `${0x080}, 0, 1`: return NpcType.Dubchic;
case `${0x080}, 0, 2`: return NpcType.Dubchic2;
case `${0x080}, 1, 1`: return NpcType.Gilchic;
case `${0x080}, 1, 2`: return NpcType.Gilchic2;
case `${0x0D4}, 0, 2`: return NpcType.SinowBerill;
case `${0x0D4}, 1, 2`: return NpcType.SinowSpigell;
case `${0x0D5}, 0, 2`: return NpcType.Merillia;
case `${0x0D5}, 1, 2`: return NpcType.Meriltas;
case `${0x0D7}, 0, 2`: return NpcType.UlGibbon;
case `${0x0D7}, 1, 2`: return NpcType.ZolGibbon;
case `${0x0DD}, 0, 2`: return NpcType.Dolmolm;
case `${0x0DD}, 1, 2`: return NpcType.Dolmdarl;
case `${0x0E0}, 0, 2`: return areaId > 15 ? NpcType.Epsilon : NpcType.SinowZoa;
case `${0x0E0}, 1, 2`: return areaId > 15 ? NpcType.Epsilon : NpcType.SinowZele;
case `${0x112}, 0, 4`: return NpcType.MerissaA;
case `${0x112}, 1, 4`: return NpcType.MerissaAA;
case `${0x114}, 0, 4`: return NpcType.Zu;
case `${0x114}, 1, 4`: return NpcType.Pazuzu;
case `${0x116}, 0, 4`: return NpcType.Dorphon;
case `${0x116}, 1, 4`: return NpcType.DorphonEclair;
case `${0x119}, 0, 4`: return regular ? NpcType.SaintMilion : NpcType.Kondrieu;
case `${0x119}, 1, 4`: return regular ? NpcType.Shambertin : NpcType.Kondrieu;
}
switch (`${typeId}, ${episode}`) {
case `${0x042}, 1`: return NpcType.Monest;
case `${0x042}, 2`: return NpcType.Monest2;
case `${0x043}, 1`: return regular ? NpcType.SavageWolf : NpcType.BarbarousWolf;
case `${0x043}, 2`: return regular ? NpcType.SavageWolf2 : NpcType.BarbarousWolf2;
case `${0x060}, 1`: return NpcType.GrassAssassin;
case `${0x060}, 2`: return NpcType.GrassAssassin2;
case `${0x062}, 1`: return NpcType.NanoDragon;
case `${0x064}, 1`: return regular ? NpcType.PofuillySlime : NpcType.PouillySlime;
case `${0x065}, 1`: return NpcType.PanArms;
case `${0x065}, 2`: return NpcType.PanArms2;
case `${0x081}, 1`: return NpcType.Garanz;
case `${0x081}, 2`: return NpcType.Garanz2;
case `${0x082}, 1`: return regular ? NpcType.SinowBeat : NpcType.SinowGold;
case `${0x083}, 1`: return NpcType.Canadine;
case `${0x084}, 1`: return NpcType.Canane;
case `${0x085}, 1`: return NpcType.Dubswitch;
case `${0x085}, 2`: return NpcType.Dubswitch2;
case `${0x0A0}, 1`: return NpcType.Delsaber;
case `${0x0A0}, 2`: return NpcType.Delsaber2;
case `${0x0A1}, 1`: return NpcType.ChaosSorcerer;
case `${0x0A1}, 2`: return NpcType.ChaosSorcerer2;
case `${0x0A2}, 1`: return NpcType.DarkGunner;
case `${0x0A4}, 1`: return NpcType.ChaosBringer;
case `${0x0A5}, 1`: return NpcType.DarkBelra;
case `${0x0A5}, 2`: return NpcType.DarkBelra2;
case `${0x0A7}, 1`: return NpcType.Bulclaw;
case `${0x0A8}, 1`: return NpcType.Claw;
case `${0x0C0}, 1`: return NpcType.Dragon;
case `${0x0C0}, 2`: return NpcType.GalGryphon;
case `${0x0C1}, 1`: return NpcType.DeRolLe;
// TODO:
// case `${0x0C2}, 1`: return NpcType.VolOptPart1;
case `${0x0C5}, 1`: return NpcType.VolOpt;
case `${0x0C8}, 1`: return NpcType.DarkFalz;
case `${0x0CA}, 2`: return NpcType.OlgaFlow;
case `${0x0CB}, 2`: return NpcType.BarbaRay;
case `${0x0CC}, 2`: return NpcType.GolDragon;
case `${0x0D8}, 2`: return NpcType.Gibbles;
case `${0x0D9}, 2`: return NpcType.Gee;
case `${0x0DA}, 2`: return NpcType.GiGue;
case `${0x0DB}, 2`: return NpcType.Deldepth;
case `${0x0DC}, 2`: return NpcType.Delbiter;
case `${0x0DE}, 2`: return NpcType.Morfos;
case `${0x0DF}, 2`: return NpcType.Recobox;
case `${0x0E1}, 2`: return NpcType.IllGill;
case `${0x110}, 4`: return NpcType.Astark;
case `${0x111}, 4`: return regular ? NpcType.SatelliteLizard : NpcType.Yowie;
case `${0x113}, 4`: return NpcType.Girtablulu;
}
switch (typeId) {
case 0x004: return NpcType.FemaleFat;
case 0x005: return NpcType.FemaleMacho;
case 0x007: return NpcType.FemaleTall;
case 0x00A: return NpcType.MaleDwarf;
case 0x00B: return NpcType.MaleFat;
case 0x00C: return NpcType.MaleMacho;
case 0x00D: return NpcType.MaleOld;
case 0x019: return NpcType.BlueSoldier;
case 0x01A: return NpcType.RedSoldier;
case 0x01B: return NpcType.Principal;
case 0x01C: return NpcType.Tekker;
case 0x01D: return NpcType.GuildLady;
case 0x01E: return NpcType.Scientist;
case 0x01F: return NpcType.Nurse;
case 0x020: return NpcType.Irene;
case 0x0F1: return NpcType.ItemShop;
case 0x0FE: return NpcType.Nurse2;
}
return NpcType.Unknown;
}
function objectsToDatData(objects: QuestObject[]): DatObject[] {
return objects.map(object => ({
typeId: object.type.psoId!,
sectionId: object.sectionId,
position: object.sectionPosition,
rotation: object.rotation,
areaId: object.areaId,
unknown: object.dat.unknown
}));
}
function npcsToDatData(npcs: QuestNpc[]): DatNpc[] {
return npcs.map(npc => {
// If the type is unknown, typeData will be null and we use the raw data from the DAT file.
const typeData = npcTypeToDatData(npc.type);
if (typeData) {
npc.dat.unknown[2][18] = (npc.dat.unknown[2][18] & ~0x80) | (typeData.regular ? 0 : 0x80);
}
return {
typeId: typeData ? typeData.typeId : npc.dat.typeId,
sectionId: npc.sectionId,
position: npc.sectionPosition,
rotation: npc.rotation,
skin: typeData ? typeData.skin : npc.dat.skin,
areaId: npc.areaId,
unknown: npc.dat.unknown
};
});
}
function npcTypeToDatData(
type: NpcType
): { typeId: number, skin: number, regular: boolean } | null {
switch (type) {
default: throw new Error(`Unexpected type ${type.code}.`);
case NpcType.Unknown: return null;
case NpcType.FemaleFat: return { typeId: 0x004, skin: 0, regular: true };
case NpcType.FemaleMacho: return { typeId: 0x005, skin: 0, regular: true };
case NpcType.FemaleTall: return { typeId: 0x007, skin: 0, regular: true };
case NpcType.MaleDwarf: return { typeId: 0x00A, skin: 0, regular: true };
case NpcType.MaleFat: return { typeId: 0x00B, skin: 0, regular: true };
case NpcType.MaleMacho: return { typeId: 0x00C, skin: 0, regular: true };
case NpcType.MaleOld: return { typeId: 0x00D, skin: 0, regular: true };
case NpcType.BlueSoldier: return { typeId: 0x019, skin: 0, regular: true };
case NpcType.RedSoldier: return { typeId: 0x01A, skin: 0, regular: true };
case NpcType.Principal: return { typeId: 0x01B, skin: 0, regular: true };
case NpcType.Tekker: return { typeId: 0x01C, skin: 0, regular: true };
case NpcType.GuildLady: return { typeId: 0x01D, skin: 0, regular: true };
case NpcType.Scientist: return { typeId: 0x01E, skin: 0, regular: true };
case NpcType.Nurse: return { typeId: 0x01F, skin: 0, regular: true };
case NpcType.Irene: return { typeId: 0x020, skin: 0, regular: true };
case NpcType.ItemShop: return { typeId: 0x0F1, skin: 0, regular: true };
case NpcType.Nurse2: return { typeId: 0x0FE, skin: 0, regular: true };
case NpcType.Hildebear: return { typeId: 0x040, skin: 0, regular: true };
case NpcType.Hildeblue: return { typeId: 0x040, skin: 1, regular: true };
case NpcType.RagRappy: return { typeId: 0x041, skin: 0, regular: true };
case NpcType.AlRappy: return { typeId: 0x041, skin: 1, regular: true };
case NpcType.Monest: return { typeId: 0x042, skin: 0, regular: true };
case NpcType.SavageWolf: return { typeId: 0x043, skin: 0, regular: true };
case NpcType.BarbarousWolf: return { typeId: 0x043, skin: 0, regular: false };
case NpcType.Booma: return { typeId: 0x044, skin: 0, regular: true };
case NpcType.Gobooma: return { typeId: 0x044, skin: 1, regular: true };
case NpcType.Gigobooma: return { typeId: 0x044, skin: 2, regular: true };
case NpcType.Dragon: return { typeId: 0x0C0, skin: 0, regular: true };
case NpcType.GrassAssassin: return { typeId: 0x060, skin: 0, regular: true };
case NpcType.PoisonLily: return { typeId: 0x061, skin: 0, regular: true };
case NpcType.NarLily: return { typeId: 0x061, skin: 1, regular: true };
case NpcType.NanoDragon: return { typeId: 0x062, skin: 0, regular: true };
case NpcType.EvilShark: return { typeId: 0x063, skin: 0, regular: true };
case NpcType.PalShark: return { typeId: 0x063, skin: 1, regular: true };
case NpcType.GuilShark: return { typeId: 0x063, skin: 2, regular: true };
case NpcType.PofuillySlime: return { typeId: 0x064, skin: 0, regular: true };
case NpcType.PouillySlime: return { typeId: 0x064, skin: 0, regular: false };
case NpcType.PanArms: return { typeId: 0x065, skin: 0, regular: true };
case NpcType.DeRolLe: return { typeId: 0x0C1, skin: 0, regular: true };
case NpcType.Dubchic: return { typeId: 0x080, skin: 0, regular: true };
case NpcType.Gilchic: return { typeId: 0x080, skin: 1, regular: true };
case NpcType.Garanz: return { typeId: 0x081, skin: 0, regular: true };
case NpcType.SinowBeat: return { typeId: 0x082, skin: 0, regular: true };
case NpcType.SinowGold: return { typeId: 0x082, skin: 0, regular: false };
case NpcType.Canadine: return { typeId: 0x083, skin: 0, regular: true };
case NpcType.Canane: return { typeId: 0x084, skin: 0, regular: true };
case NpcType.Dubswitch: return { typeId: 0x085, skin: 0, regular: true };
case NpcType.VolOpt: return { typeId: 0x0C5, skin: 0, regular: true };
case NpcType.Delsaber: return { typeId: 0x0A0, skin: 0, regular: true };
case NpcType.ChaosSorcerer: return { typeId: 0x0A1, skin: 0, regular: true };
case NpcType.DarkGunner: return { typeId: 0x0A2, skin: 0, regular: true };
case NpcType.ChaosBringer: return { typeId: 0x0A4, skin: 0, regular: true };
case NpcType.DarkBelra: return { typeId: 0x0A5, skin: 0, regular: true };
case NpcType.Dimenian: return { typeId: 0x0A6, skin: 0, regular: true };
case NpcType.LaDimenian: return { typeId: 0x0A6, skin: 1, regular: true };
case NpcType.SoDimenian: return { typeId: 0x0A6, skin: 2, regular: true };
case NpcType.Bulclaw: return { typeId: 0x0A7, skin: 0, regular: true };
case NpcType.Claw: return { typeId: 0x0A8, skin: 0, regular: true };
case NpcType.DarkFalz: return { typeId: 0x0C8, skin: 0, regular: true };
case NpcType.Hildebear2: return { typeId: 0x040, skin: 0, regular: true };
case NpcType.Hildeblue2: return { typeId: 0x040, skin: 1, regular: true };
case NpcType.RagRappy2: return { typeId: 0x041, skin: 0, regular: true };
case NpcType.LoveRappy: return { typeId: 0x041, skin: 1, regular: true };
case NpcType.Monest2: return { typeId: 0x042, skin: 0, regular: true };
case NpcType.PoisonLily2: return { typeId: 0x061, skin: 0, regular: true };
case NpcType.NarLily2: return { typeId: 0x061, skin: 1, regular: true };
case NpcType.GrassAssassin2: return { typeId: 0x060, skin: 0, regular: true };
case NpcType.Dimenian2: return { typeId: 0x0A6, skin: 0, regular: true };
case NpcType.LaDimenian2: return { typeId: 0x0A6, skin: 1, regular: true };
case NpcType.SoDimenian2: return { typeId: 0x0A6, skin: 2, regular: true };
case NpcType.DarkBelra2: return { typeId: 0x0A5, skin: 0, regular: true };
case NpcType.BarbaRay: return { typeId: 0x0CB, skin: 0, regular: true };
case NpcType.SavageWolf2: return { typeId: 0x043, skin: 0, regular: true };
case NpcType.BarbarousWolf2: return { typeId: 0x043, skin: 0, regular: false };
case NpcType.PanArms2: return { typeId: 0x065, skin: 0, regular: true };
case NpcType.Dubchic2: return { typeId: 0x080, skin: 0, regular: true };
case NpcType.Gilchic2: return { typeId: 0x080, skin: 1, regular: true };
case NpcType.Garanz2: return { typeId: 0x081, skin: 0, regular: true };
case NpcType.Dubswitch2: return { typeId: 0x085, skin: 0, regular: true };
case NpcType.Delsaber2: return { typeId: 0x0A0, skin: 0, regular: true };
case NpcType.ChaosSorcerer2: return { typeId: 0x0A1, skin: 0, regular: true };
case NpcType.GolDragon: return { typeId: 0x0CC, skin: 0, regular: true };
case NpcType.SinowBerill: return { typeId: 0x0D4, skin: 0, regular: true };
case NpcType.SinowSpigell: return { typeId: 0x0D4, skin: 1, regular: true };
case NpcType.Merillia: return { typeId: 0x0D5, skin: 0, regular: true };
case NpcType.Meriltas: return { typeId: 0x0D5, skin: 1, regular: true };
case NpcType.Mericarol: return { typeId: 0x0D6, skin: 0, regular: true };
case NpcType.Mericus: return { typeId: 0x0D6, skin: 1, regular: true };
case NpcType.Merikle: return { typeId: 0x0D6, skin: 2, regular: true };
case NpcType.UlGibbon: return { typeId: 0x0D7, skin: 0, regular: true };
case NpcType.ZolGibbon: return { typeId: 0x0D7, skin: 1, regular: true };
case NpcType.Gibbles: return { typeId: 0x0D8, skin: 0, regular: true };
case NpcType.Gee: return { typeId: 0x0D9, skin: 0, regular: true };
case NpcType.GiGue: return { typeId: 0x0DA, skin: 0, regular: true };
case NpcType.GalGryphon: return { typeId: 0x0C0, skin: 0, regular: true };
case NpcType.Deldepth: return { typeId: 0x0DB, skin: 0, regular: true };
case NpcType.Delbiter: return { typeId: 0x0DC, skin: 0, regular: true };
case NpcType.Dolmolm: return { typeId: 0x0DD, skin: 0, regular: true };
case NpcType.Dolmdarl: return { typeId: 0x0DD, skin: 1, regular: true };
case NpcType.Morfos: return { typeId: 0x0DE, skin: 0, regular: true };
case NpcType.Recobox: return { typeId: 0x0DF, skin: 0, regular: true };
case NpcType.Epsilon: return { typeId: 0x0E0, skin: 0, regular: true };
case NpcType.SinowZoa: return { typeId: 0x0E0, skin: 0, regular: true };
case NpcType.SinowZele: return { typeId: 0x0E0, skin: 1, regular: true };
case NpcType.IllGill: return { typeId: 0x0E1, skin: 0, regular: true };
case NpcType.DelLily: return { typeId: 0x061, skin: 0, regular: true };
case NpcType.OlgaFlow: return { typeId: 0x0CA, skin: 0, regular: true };
case NpcType.SandRappy: return { typeId: 0x041, skin: 0, regular: true };
case NpcType.DelRappy: return { typeId: 0x041, skin: 1, regular: true };
case NpcType.Astark: return { typeId: 0x110, skin: 0, regular: true };
case NpcType.SatelliteLizard: return { typeId: 0x111, skin: 0, regular: true };
case NpcType.Yowie: return { typeId: 0x111, skin: 0, regular: false };
case NpcType.MerissaA: return { typeId: 0x112, skin: 0, regular: true };
case NpcType.MerissaAA: return { typeId: 0x112, skin: 1, regular: true };
case NpcType.Girtablulu: return { typeId: 0x113, skin: 0, regular: true };
case NpcType.Zu: return { typeId: 0x114, skin: 0, regular: true };
case NpcType.Pazuzu: return { typeId: 0x114, skin: 1, regular: true };
case NpcType.Boota: return { typeId: 0x115, skin: 0, regular: true };
case NpcType.ZeBoota: return { typeId: 0x115, skin: 1, regular: true };
case NpcType.BaBoota: return { typeId: 0x115, skin: 2, regular: true };
case NpcType.Dorphon: return { typeId: 0x116, skin: 0, regular: true };
case NpcType.DorphonEclair: return { typeId: 0x116, skin: 1, regular: true };
case NpcType.Goran: return { typeId: 0x117, skin: 0, regular: true };
case NpcType.PyroGoran: return { typeId: 0x117, skin: 1, regular: true };
case NpcType.GoranDetonator: return { typeId: 0x117, skin: 2, regular: true };
case NpcType.SaintMilion: return { typeId: 0x119, skin: 0, regular: true };
case NpcType.Shambertin: return { typeId: 0x119, skin: 1, regular: true };
case NpcType.Kondrieu: return { typeId: 0x119, skin: 0, regular: false };
}
}

View File

@ -340,9 +340,8 @@ export class EnemyDrop implements ItemDrop {
export class HuntMethod {
readonly id: string;
readonly name: string;
readonly episode: Episode;
readonly quest: SimpleQuest;
readonly npcs: Array<SimpleNpc>;
readonly enemies: Array<SimpleNpc>;
readonly enemyCounts: Map<NpcType, number>;
/**
* The time it takes to complete the quest in hours.
@ -370,15 +369,9 @@ export class HuntMethod {
this.id = id;
this.name = name;
this.episode = quest.episode;
this.quest = quest;
this.npcs = this.quest.npcs;
this.enemies = this.npcs.filter(npc => npc.type.enemy);
this.enemyCounts = new Map();
for (const npc of this.enemies) {
this.enemyCounts.set(npc.type, (this.enemyCounts.get(npc.type) || 0) + 1);
}
this.enemyCounts = quest.enemyCounts;
this.defaultTime = defaultTime;
}
}
@ -387,18 +380,11 @@ export class SimpleQuest {
constructor(
public readonly id: number,
public readonly name: string,
public readonly npcs: SimpleNpc[]
public readonly episode: Episode,
public readonly enemyCounts: Map<NpcType, number>
) {
if (!id) throw new Error('id is required.');
if (!name) throw new Error('name is required.');
if (!npcs) throw new Error('npcs is required.');
}
}
export class SimpleNpc {
constructor(
public type: NpcType
) {
if (!type) throw new Error('type is required.');
if (!enemyCounts) throw new Error('enemyCounts is required.');
}
}

View File

@ -75,3 +75,10 @@ export type BoxDropDto = {
itemTypeId: number,
dropRate: number,
}
export type QuestDto = {
id: number,
name: string,
episode: 1 | 2 | 4,
enemyCounts: { [npcTypeCode: string]: number },
}

View File

@ -29,7 +29,10 @@ class AreaStore {
area(8, 'Ruins 1', order++, 5),
area(9, 'Ruins 2', order++, 5),
area(10, 'Ruins 3', order++, 5),
area(14, 'Dark Falz', order++, 1)
area(14, 'Dark Falz', order++, 1),
area(15, 'BA Ruins', order++, 3),
area(16, 'BA Spaceship', order++, 3),
area(17, 'Lobby', order++, 15),
];
order = 0;
this.areas[2] = [

View File

@ -1,8 +1,9 @@
import Logger from 'js-logger';
import { observable } from "mobx";
import { HuntMethod, NpcType, Server, SimpleNpc, SimpleQuest } from "../domain";
import { HuntMethod, NpcType, Server, SimpleQuest } from "../domain";
import { QuestDto } from "../dto";
import { Loadable } from "../Loadable";
import { ServerMap } from "./ServerMap";
import Logger from 'js-logger';
const logger = Logger.get('stores/HuntMethodStore');
@ -13,46 +14,35 @@ class HuntMethodStore {
private async loadHuntMethods(server: Server): Promise<HuntMethod[]> {
const response = await fetch(
`${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.tsv`
`${process.env.PUBLIC_URL}/quests.${Server[server].toLowerCase()}.json`
);
const data = await response.text();
const rows = data.split('\n').map(line => line.split('\t'));
const quests = await response.json() as QuestDto[];
const npcTypeByIndex = rows[0].slice(2, -2).map((episode, i) => {
const enemy = rows[1][i + 2];
return NpcType.byNameAndEpisode(enemy, parseInt(episode, 10))!;
});
return quests.map(quest => {
let totalCount = 0;
const enemyCounts = new Map<NpcType, number>();
return rows.slice(2).map((row, i) => {
const questId = i + 1;
const questName = row[0];
const time = parseFloat(row[1]);
for (const [code, count] of Object.entries(quest.enemyCounts)) {
const npcType = NpcType.byCode(code);
const npcs = row.slice(2, -2).flatMap((cell, cellI) => {
const amount = parseInt(cell, 10);
const type = npcTypeByIndex[cellI];
const enemies = [];
if (type) {
for (let i = 0; i < amount; i++) {
enemies.push(new SimpleNpc(type));
}
if (!npcType) {
logger.error(`No NpcType found for code ${code}.`);
} else {
logger.error(`Couldn't get type for cellI ${cellI}.`);
enemyCounts.set(npcType, count);
totalCount += count;
}
return enemies;
});
}
return new HuntMethod(
`q${questId}`,
questName,
`q${quest.id}`,
quest.name,
new SimpleQuest(
questId,
questName,
npcs
quest.id,
quest.name,
quest.episode,
enemyCounts
),
time
/^\d-\d.*/.test(quest.name) ? 0.75 : (totalCount > 400 ? 0.75 : 0.5)
);
});
}

View File

@ -1,6 +1,6 @@
import solver from 'javascript-lp-solver';
import { autorun, IObservableArray, observable, computed } from "mobx";
import { Difficulties, Difficulty, HuntMethod, ItemType, KONDRIEU_PROB, NpcType, RARE_ENEMY_PROB, SectionId, SectionIds, Server } from "../domain";
import { Difficulties, Difficulty, HuntMethod, ItemType, KONDRIEU_PROB, NpcType, RARE_ENEMY_PROB, SectionId, SectionIds, Server, Episode } from "../domain";
import { applicationStore } from './ApplicationStore';
import { huntMethodStore } from "./HuntMethodStore";
import { itemDropStores } from './ItemDropStore';
@ -33,6 +33,7 @@ export class OptimalMethod {
readonly difficulty: Difficulty,
readonly sectionIds: Array<SectionId>,
readonly methodName: string,
readonly methodEpisode: Episode,
readonly methodTime: number,
readonly runs: number,
readonly itemCounts: Map<ItemType, number>
@ -154,15 +155,15 @@ class HuntOptimizerStore {
// Counts include rare enemies, so they are fractional.
const counts = new Map<NpcType, number>();
for (const enemy of method.enemies) {
const count = counts.get(enemy.type);
for (const [enemy, count] of method.enemyCounts.entries()) {
const oldCount = counts.get(enemy) || 0;
if (enemy.type.rareType == null) {
counts.set(enemy.type, (count || 0) + 1);
if (enemy.rareType == null) {
counts.set(enemy, oldCount + count);
} else {
let rate, rareRate;
if (enemy.type.rareType === NpcType.Kondrieu) {
if (enemy.rareType === NpcType.Kondrieu) {
rate = 1 - KONDRIEU_PROB;
rareRate = KONDRIEU_PROB;
} else {
@ -170,10 +171,11 @@ class HuntOptimizerStore {
rareRate = RARE_ENEMY_PROB;
}
counts.set(enemy.type, (count || 0) + rate);
const rareCount = counts.get(enemy.type.rareType);
counts.set(enemy.type.rareType, (rareCount || 0) + rareRate);
counts.set(enemy, oldCount + count * rate);
counts.set(
enemy.rareType,
(counts.get(enemy.rareType) || 0) + count * rareRate
);
}
}
@ -321,6 +323,7 @@ class HuntOptimizerStore {
difficulty,
sectionIds,
method.name + (splitPanArms ? ' (Split Pan Arms)' : ''),
method.episode,
method.time,
runs,
items

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import moment, { Moment } from "moment";
import React from "react";
import { AutoSizer, Index } from "react-virtualized";
import { HuntMethod } from "../../domain";
import { HuntMethod, Episode } from "../../domain";
import { EnemyNpcTypes } from "../../domain/NpcType";
import { huntMethodStore } from "../../stores/HuntMethodStore";
import { Column, BigTable } from "../BigTable";
@ -20,6 +20,11 @@ export class MethodsComponent extends React.Component {
width: 250,
cellRenderer: (method) => method.name,
},
{
name: 'Ep.',
width: 34,
cellRenderer: (method) => Episode[method.episode],
},
{
name: 'Time',
width: 50,

View File

@ -2,9 +2,9 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { AutoSizer, Index } from "react-virtualized";
import { Difficulty, SectionId } from "../../domain";
import { Difficulty, Episode, SectionId } from "../../domain";
import { huntOptimizerStore, OptimalMethod } from "../../stores/HuntOptimizerStore";
import { Column, BigTable } from "../BigTable";
import { BigTable, Column } from "../BigTable";
import { SectionIdIcon } from "../SectionIdIcon";
import { hoursToString } from "../time";
import "./OptimizationResultComponent.less";
@ -36,6 +36,11 @@ export class OptimizationResultComponent extends React.Component {
cellRenderer: (result) => result.methodName,
tooltip: (result) => result.methodName,
},
{
name: 'Ep.',
width: 34,
cellRenderer: (result) => Episode[result.methodEpisode],
},
{
name: 'Section ID',
width: 80,

Some files were not shown because too many files have changed in this diff Show More