mirror of https://git.nuv.sh/nuv/condition_overload
1import argv
2import gleam/fetch
3import gleam/http/request
4import gleam/int
5import gleam/io
6import gleam/javascript/promise
7import gleam/list
8import gleam/pair
9import gleam/result
10import gleam/string
11import splitter
12
13pub fn main() {
14 let search =
15 argv.load().arguments
16 |> list.reduce(fn(acc, x) { acc <> " " <> x })
17 |> result.map(string.lowercase)
18
19 case search {
20 Error(_) -> io.println("Please provide a search term")
21
22 Ok(search) -> {
23 do_search(search)
24 }
25 }
26}
27
28/// do search and print result if any
29///
30fn do_search(search: String) {
31 [
32 "https://wiki.warframe.com/w/Condition_Overload_%28Mechanic%29?action=edit§ion=7",
33 "https://wiki.warframe.com/w/Condition_Overload_%28Mechanic%29?action=edit§ion=8",
34 ]
35 |> list.map(get_gun_page_data)
36 |> promise.await_list()
37 |> promise.map(result.values)
38 |> promise.map(list.flatten)
39 |> promise.map(
40 list.filter(_, fn(item) {
41 item.names
42 |> list.any(fn(name) {
43 let lower = string.lowercase(name)
44 string.contains(lower, search)
45 })
46 }),
47 )
48 |> promise.map(fn(rows) {
49 case list.length(rows) {
50 0 -> io.println("\"" <> search <> "\" could not be found")
51 _ -> {
52 rows
53 |> list.take(4)
54 |> list.map(format_row)
55 |> list.reduce(fn(acc, x) { acc <> " || " <> x })
56 |> result.map(io.println)
57 |> result.unwrap_both()
58 }
59 }
60 })
61 Nil
62}
63
64/// format row into human readable string
65///
66fn format_row(row: Row) -> String {
67 let rating = case row {
68 Row(math_behavior: "Multiplying", ..) -> "very good"
69 Row(math_behavior: "Adding", co_bonus_rel_base:, ..) -> {
70 let co_bonus_rel_base =
71 co_bonus_rel_base
72 |> string.split_once("%")
73 |> result.map(pair.first)
74 |> result.try(int.parse)
75 |> result.unwrap(0)
76
77 case co_bonus_rel_base {
78 bonus if bonus > 100 -> "good"
79 bonus if bonus == 100 -> "normal"
80 bonus if bonus < 100 -> "poor"
81 _ -> panic as "unreachable(some secret fourth option)"
82 }
83 }
84
85 Row(math_behavior: "N/A", ..) | Row(math_behavior: "", ..) -> "bad"
86
87 _ -> "some secret third option"
88 }
89
90 let Row(
91 names:,
92 attack:,
93 projectile:,
94 base_damage: _,
95 co_bonus_at_100: _,
96 co_bonus_rel_base: _,
97 math_behavior: _,
98 notes: _,
99 ) = row
100
101 let name =
102 list.reduce(names, fn(acc, x) { acc <> " / " <> x })
103 |> result.unwrap("")
104
105 "The '"
106 <> name
107 <> "' '"
108 <> attack
109 <> " "
110 <> projectile
111 <> "' has a "
112 <> rating
113 <> " interaction with GunCO."
114}
115
116/// do request and return Row if successful
117///
118fn get_gun_page_data(
119 url: String,
120) -> promise.Promise(Result(List(Row), fetch.FetchError)) {
121 let assert Ok(req) = request.to(url)
122
123 // Send the HTTP request to the server
124 use resp <- promise.try_await(fetch.send(req))
125 use resp <- promise.try_await(fetch.read_text_body(resp))
126
127 let rows =
128 resp.body
129 |> get_text_area()
130 |> result.map(parse_text_area)
131 |> result.unwrap([])
132
133 promise.resolve(Ok(rows))
134}
135
136/// discards the html and returns only the raw text from the textarea
137///
138fn get_text_area(html_text: String) -> Result(String, Nil) {
139 html_text
140 // trim to start of textarea
141 |> string.split_once("<textarea")
142 |> result.map(pair.second)
143 // remove rest of tag
144 |> result.try(string.split_once(_, "\">"))
145 |> result.map(pair.second)
146 // remove stuff after
147 |> result.try(string.split_once(_, "</textarea>"))
148 |> result.map(pair.first)
149}
150
151fn parse_text_area(textarea: String) {
152 textarea
153 |> string.split("\n")
154 |> list.map(string.trim)
155 |> process_lines([])
156}
157
158pub type Row {
159 Row(
160 names: List(String),
161 attack: String,
162 projectile: String,
163 base_damage: String,
164 co_bonus_at_100: String,
165 co_bonus_rel_base: String,
166 math_behavior: String,
167 notes: String,
168 )
169}
170
171/// Parse data line by line from the following formats:
172///
173/// !Weapon!!Attack Name!!Projectile Type!!Attack Unmodded Damage!!Actual CO Damage Bonus at +100%!!CO Damage Bonus Relative To Base Damage!!Math/Behavior Type!!Notes
174///
175/// single name - one line
176///
177/// |{{Weapon|Ambassador}}||Alt-fire Hitscan AoE||AoE||800||600||75%||Adding||Radial hit only receives CO bonus on target directly hit by laser. CO-bonus scales off hitscan damage. AoE does not scale off multishot.
178/// |-
179///
180/// multi name - one line
181///
182/// |{{Weapon|Braton}}/{{Weapon|MK1-Braton|MK1}}/{{Weapon|Braton Prime|Prime}}/{{Weapon|Braton Vandal|Vandal}}||Incarnon Form AoE||AoE||74||70||95%||Adding||Listed values for Braton Prime with inactive Daring Reverie. Radial hit only receives CO bonus on target directly hit by bullet. AoE does not scale off multishot.
183/// |-
184///
185/// multi line - single & multi name
186///
187/// |{{Weapon|Evensong}}
188/// |Charged Radial Attack
189/// |AoE
190/// |150
191/// |0
192/// |0%
193/// |N/A
194/// |Does not apply
195/// |-
196///
197/// as well as both mixed, into row type
198///
199///
200fn process_lines(lines: List(String), acc: List(Row)) -> List(Row) {
201 case lines {
202 ["|{{Weapon|" <> name_line, ..rest] -> {
203 let #(row, rest) = parse_row(name_line, rest)
204
205 process_lines(rest, [row, ..acc])
206 }
207 [_, ..rest] -> process_lines(rest, acc)
208 [] -> acc
209 }
210}
211
212/// parses 1 'Row' type worth of data from the supplied lines
213/// handles single line, multi line and mixed data
214///
215fn parse_row(name_line: String, lines: List(String)) -> #(Row, List(String)) {
216 let #(names, line_rest) = parse_names(name_line, [])
217
218 let #(attack, line_rest, rest) = parse_next_value(line_rest, lines)
219 let #(projectile, line_rest, rest) = parse_next_value(line_rest, rest)
220 let #(base_damage, line_rest, rest) = parse_next_value(line_rest, rest)
221 let #(co_bonus_at_100, line_rest, rest) = parse_next_value(line_rest, rest)
222 let #(co_bonus_rel_base, line_rest, rest) = parse_next_value(line_rest, rest)
223 let #(math_behavior, line_rest, rest) = parse_next_value(line_rest, rest)
224 let #(notes, _, rest) = parse_next_value(line_rest, rest)
225
226 #(
227 Row(
228 names:,
229 attack:,
230 projectile:,
231 base_damage:,
232 co_bonus_at_100:,
233 co_bonus_rel_base:,
234 math_behavior:,
235 notes:,
236 ),
237 rest,
238 )
239}
240
241/// parses the next value from the data
242///
243fn parse_next_value(
244 line_rest: String,
245 lines: List(String),
246) -> #(String, String, List(String)) {
247 let sep = splitter.new(["||"])
248
249 case splitter.split(sep, line_rest) {
250 #("", _, _) -> handle_empty_line(sep, lines)
251 #(value, "||", line_rest) -> #(value, line_rest, lines)
252 #(value, "", _) -> #(value, "", lines)
253 #(_, _, _) -> handle_empty_line(sep, lines)
254 }
255}
256
257//
258//
259fn handle_empty_line(sep, lines) {
260 case lines {
261 ["|-", ..] -> #("", "", lines)
262 ["|" <> value, ..rest] -> {
263 case splitter.split(sep, value) {
264 #(value, "||", line_rest) -> #(value, line_rest, rest)
265 #(value, _, _) -> #(value, "", rest)
266 }
267 }
268 [] | [_] | [_, _, ..] -> #("", "", lines)
269 }
270}
271
272/// parse the weapon names
273///
274fn parse_names(line: String, acc) {
275 let sep = splitter.new(["}}/{{Weapon|", "}}"])
276
277 let #(name, split_by, rest) = splitter.split(sep, line)
278
279 let name =
280 string.split_once(name, "|")
281 |> result.map(pair.first)
282 |> result.unwrap(name)
283
284 case split_by {
285 "}}/{{Weapon|" | "}}/{{Weapon" -> parse_names(rest, [name, ..acc])
286 _ -> {
287 // handle special cases where extra text is included after the weapon names
288 let rest =
289 string.split_once(rest, "||")
290 |> result.map(pair.second)
291 |> result.unwrap(rest)
292
293 #([name, ..acc], rest)
294 }
295 }
296}