mirror of https://git.nuv.sh/nuv/condition_overload
at main 7.6 kB view raw
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&section=7", 33 "https://wiki.warframe.com/w/Condition_Overload_%28Mechanic%29?action=edit&section=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}