7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
34 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
36 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
37 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
38 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
39 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
40 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
41 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
42 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
43 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
44 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
45 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
46 <li>help: <input id="key_help" type="text" value="h" />
47 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
48 <li>teleport: <input id="key_teleport" type="text" value="p" />
49 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
50 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
51 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
52 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
53 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
54 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
55 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
56 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
57 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
58 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
63 let websocket_location = "wss://plomlompom.com/rogue_chat/";
64 //let websocket_location = "ws://localhost:8000/";
66 let rows_selector = document.getElementById("n_rows");
67 let cols_selector = document.getElementById("n_cols");
68 let key_selectors = document.querySelectorAll('[id^="key_"]');
70 function restore_selector_value(selector) {
71 let stored_selection = window.localStorage.getItem(selector.id);
72 if (stored_selection) {
73 selector.value = stored_selection;
76 restore_selector_value(rows_selector);
77 restore_selector_value(cols_selector);
78 for (let key_selector of key_selectors) {
79 restore_selector_value(key_selector);
85 initialize: function() {
86 this.rows = rows_selector.value;
87 this.cols = cols_selector.value;
88 this.pre_el = document.getElementById("terminal");
89 this.pre_el.style.color = this.foreground;
90 this.pre_el.style.backgroundColor = this.background;
93 for (let y = 0, x = 0; y <= this.rows; x++) {
97 this.content.push(line);
106 blink_screen: function() {
107 this.pre_el.style.color = this.background;
108 this.pre_el.style.backgroundColor = this.foreground;
110 this.pre_el.style.color = this.foreground;
111 this.pre_el.style.backgroundColor = this.background;
114 refresh: function() {
116 for (let y = 0; y < this.rows; y++) {
117 let line = this.content[y].join('');
118 pre_string += line + '\n';
120 this.pre_el.textContent = pre_string;
122 write: function(start_y, start_x, msg) {
123 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
124 this.content[start_y][x] = msg[i];
127 drawBox: function(start_y, start_x, height, width) {
128 let end_y = start_y + height;
129 let end_x = start_x + width;
130 for (let y = start_y, x = start_x; y < this.rows; x++) {
138 this.content[y][x] = ' ';
142 terminal.initialize();
145 tokenize: function(str) {
150 for (let i = 0; i < str.length; i++) {
156 } else if (c == '\\') {
158 } else if (c == '"') {
163 } else if (c == '"') {
165 } else if (c === ' ') {
166 if (token.length > 0) {
174 if (token.length > 0) {
179 parse_yx: function(position_string) {
180 let coordinate_strings = position_string.split(',')
181 let position = [0, 0];
182 position[0] = parseInt(coordinate_strings[0].slice(2));
183 position[1] = parseInt(coordinate_strings[1].slice(2));
195 init: function(url) {
197 this.websocket = new WebSocket(this.url);
198 this.websocket.onopen = function(event) {
199 server.connected = true;
200 game.thing_types = {};
202 server.send(['TASKS']);
203 server.send(['TERRAINS']);
204 server.send(['THING_TYPES']);
205 tui.log_msg("@ server connected! :)");
206 tui.switch_mode(mode_login);
208 this.websocket.onclose = function(event) {
209 server.connected = false;
210 tui.switch_mode(mode_waiting_for_server);
211 tui.log_msg("@ server disconnected :(");
213 this.websocket.onmessage = this.handle_event;
215 reconnect_to: function(url) {
216 this.websocket.close();
219 send: function(tokens) {
220 this.websocket.send(unparser.untokenize(tokens));
222 handle_event: function(event) {
223 let tokens = parser.tokenize(event.data);
224 if (tokens[0] === 'TURN') {
225 game.turn_complete = false;
226 explorer.empty_info_db();
229 game.turn = parseInt(tokens[1]);
230 } else if (tokens[0] === 'THING') {
231 let t = game.get_thing(tokens[3], true);
232 t.position = parser.parse_yx(tokens[1]);
234 } else if (tokens[0] === 'THING_NAME') {
235 let t = game.get_thing(tokens[1], false);
239 } else if (tokens[0] === 'THING_CHAR') {
240 let t = game.get_thing(tokens[1], false);
242 t.player_char = tokens[2];
244 } else if (tokens[0] === 'TASKS') {
245 game.tasks = tokens[1].split(',')
246 } else if (tokens[0] === 'THING_TYPE') {
247 game.thing_types[tokens[1]] = tokens[2]
248 } else if (tokens[0] === 'TERRAIN') {
249 game.terrains[tokens[1]] = tokens[2]
250 } else if (tokens[0] === 'MAP') {
251 game.map_geometry = tokens[1];
253 game.map_size = parser.parse_yx(tokens[2]);
255 } else if (tokens[0] === 'FOV') {
257 } else if (tokens[0] === 'MAP_CONTROL') {
258 game.map_control = tokens[1]
259 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
260 game.turn_complete = true;
261 if (tui.mode == mode_post_login_wait) {
262 tui.switch_mode(mode_play);
263 } else if (tui.mode == mode_study) {
264 explorer.query_info();
267 } else if (tokens[0] === 'CHAT') {
268 tui.log_msg('# ' + tokens[1], 1);
269 } else if (tokens[0] === 'PLAYER_ID') {
270 game.player_id = parseInt(tokens[1]);
271 } else if (tokens[0] === 'LOGIN_OK') {
272 this.send(['GET_GAMESTATE']);
273 tui.switch_mode(mode_post_login_wait);
274 } else if (tokens[0] === 'PORTAL') {
275 let position = parser.parse_yx(tokens[1]);
276 game.portals[position] = tokens[2];
277 } else if (tokens[0] === 'ANNOTATION_HINT') {
278 let position = parser.parse_yx(tokens[1]);
279 explorer.info_hints = explorer.info_hints.concat([position]);
280 } else if (tokens[0] === 'ANNOTATION') {
281 let position = parser.parse_yx(tokens[1]);
282 explorer.update_info_db(position, tokens[2]);
283 tui.restore_input_values();
285 } else if (tokens[0] === 'UNHANDLED_INPUT') {
286 tui.log_msg('? unknown command');
287 } else if (tokens[0] === 'PLAY_ERROR') {
288 tui.log_msg('? ' + tokens[1]);
289 terminal.blink_screen();
290 } else if (tokens[0] === 'ARGUMENT_ERROR') {
291 tui.log_msg('? syntax error: ' + tokens[1]);
292 } else if (tokens[0] === 'GAME_ERROR') {
293 tui.log_msg('? game error: ' + tokens[1]);
294 } else if (tokens[0] === 'PONG') {
297 tui.log_msg('? unhandled input: ' + event.data);
303 quote: function(str) {
305 for (let i = 0; i < str.length; i++) {
307 if (['"', '\\'].includes(c)) {
313 return quoted.join('');
315 to_yx: function(yx_coordinate) {
316 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
318 untokenize: function(tokens) {
319 let quoted_tokens = [];
320 for (let token of tokens) {
321 quoted_tokens.push(this.quote(token));
323 return quoted_tokens.join(" ");
328 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
330 this.has_input_prompt = has_input_prompt;
331 this.shows_info= shows_info;
332 this.is_intro = is_intro;
333 this.help_intro = help_intro;
336 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
337 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
338 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
339 let mode_chat = new Mode('chat', 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:', true, false);
340 let mode_annotate = new Mode('annotate', 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.', true, true);
341 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
342 let mode_study = new Mode('study', 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it.', false, true);
343 let mode_edit = new Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.', false, false);
344 let mode_portal = new Mode('portal', 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.', true, true);
345 let mode_password = new Mode('password', 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.', true, false, false);
348 mode: mode_waiting_for_server,
352 window_width: terminal.cols / 2,
359 this.inputEl = document.getElementById("input");
360 this.inputEl.focus();
361 this.recalc_input_lines();
362 this.height_header = this.height_turn_line + this.height_mode_line;
363 this.log_msg("@ waiting for server connection ...");
366 init_keys: function() {
368 for (let key_selector of key_selectors) {
369 this.keys[key_selector.id.slice(4)] = key_selector.value;
371 this.movement_keys = {
372 [this.keys.square_move_up]: 'UP',
373 [this.keys.square_move_left]: 'LEFT',
374 [this.keys.square_move_down]: 'DOWN',
375 [this.keys.square_move_right]: 'RIGHT'
377 if (game.map_geometry == 'Hex') {
378 this.movement_keys = {
379 [this.keys.hex_move_upleft]: 'UPLEFT',
380 [this.keys.hex_move_upright]: 'UPRIGHT',
381 [this.keys.hex_move_right]: 'RIGHT',
382 [this.keys.hex_move_downright]: 'DOWNRIGHT',
383 [this.keys.hex_move_downleft]: 'DOWNLEFT',
384 [this.keys.hex_move_left]: 'LEFT'
388 switch_mode: function(mode) {
389 this.inputEl.focus();
390 this.show_help = false;
391 this.map_mode = 'terrain';
392 if (mode.shows_info && game.player_id in game.things) {
393 explorer.position = game.things[game.player_id].position;
394 explorer.query_info();
398 this.restore_input_values();
399 document.getElementById("take_thing").disabled = true;
400 document.getElementById("drop_thing").disabled = true;
401 document.getElementById("flatten").disabled = true;
402 document.getElementById("teleport").disabled = true;
403 document.getElementById("toggle_map_mode").disabled = true;
404 document.getElementById("switch_to_chat").disabled = true;
405 document.getElementById("switch_to_play").disabled = true;
406 document.getElementById("switch_to_study").disabled = true;
407 document.getElementById("switch_to_edit").disabled = true;
408 document.getElementById("switch_to_portal").disabled = true;
409 document.getElementById("switch_to_annotate").disabled = true;
410 document.getElementById("switch_to_password").disabled = true;
411 document.getElementById("move_left").disabled = true;
412 document.getElementById("move_upleft").disabled = true;
413 document.getElementById("move_up").disabled = true;
414 document.getElementById("move_upright").disabled = true;
415 document.getElementById("move_downleft").disabled = true;
416 document.getElementById("move_down").disabled = true;
417 document.getElementById("move_downright").disabled = true;
418 document.getElementById("move_right").disabled = true;
419 if (mode == mode_play || mode == mode_study) {
420 document.getElementById("move_left").disabled = false;
421 document.getElementById("move_right").disabled = false;
422 if (game.map_geometry == 'Hex') {
423 document.getElementById("move_upleft").disabled = false;
424 document.getElementById("move_upright").disabled = false;
425 document.getElementById("move_downleft").disabled = false;
426 document.getElementById("move_downright").disabled = false;
428 document.getElementById("move_up").disabled = false;
429 document.getElementById("move_down").disabled = false;
432 if (!mode.is_intro && mode != mode_play) {
433 document.getElementById("switch_to_play").disabled = false;
435 if (!mode.is_intro && mode != mode_study) {
436 document.getElementById("switch_to_study").disabled = false;
438 if (!mode.is_intro && mode != mode_chat) {
439 document.getElementById("switch_to_chat").disabled = false;
441 if (mode == mode_login) {
442 if (this.login_name) {
443 server.send(['LOGIN', this.login_name]);
445 this.log_msg("? need login name");
447 } else if (mode == mode_play) {
448 if (game.tasks.includes('PICK_UP')) {
449 document.getElementById("take_thing").disabled = false;
451 if (game.tasks.includes('DROP')) {
452 document.getElementById("drop_thing").disabled = false;
454 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
455 document.getElementById("flatten").disabled = false;
457 if (game.tasks.includes('MOVE')) {
459 document.getElementById("teleport").disabled = false;
460 document.getElementById("switch_to_annotate").disabled = false;
461 document.getElementById("switch_to_edit").disabled = false;
462 document.getElementById("switch_to_portal").disabled = false;
463 document.getElementById("switch_to_password").disabled = false;
464 } else if (mode == mode_study) {
465 document.getElementById("toggle_map_mode").disabled = false;
466 } else if (mode == mode_edit) {
467 this.show_help = true;
471 restore_input_values: function() {
472 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
473 let info = explorer.info_db[explorer.position];
474 if (info != "(none)") {
475 this.inputEl.value = info;
476 this.recalc_input_lines();
478 } else if (this.mode == mode_portal && explorer.position in game.portals) {
479 let portal = game.portals[explorer.position]
480 this.inputEl.value = portal;
481 this.recalc_input_lines();
482 } else if (this.mode == mode_password) {
483 this.inputEl.value = this.password;
484 this.recalc_input_lines();
487 empty_input: function(str) {
488 this.inputEl.value = "";
489 if (this.mode.has_input_prompt) {
490 this.recalc_input_lines();
492 this.height_input = 0;
495 recalc_input_lines: function() {
496 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
497 this.height_input = this.input_lines.length;
499 msg_into_lines_of_width: function(msg, width) {
502 for (let i = 0, x = 0; i < msg.length; i++, x++) {
503 if (x >= width || msg[i] == "\n") {
508 if (msg[i] != "\n") {
515 log_msg: function(msg) {
517 while (this.log.length > 100) {
522 draw_map: function() {
523 let map_lines_split = [];
525 let map_content = game.map;
526 if (this.map_mode == 'control') {
527 map_content = game.map_control;
529 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
530 if (j == game.map_size[1]) {
531 map_lines_split.push(line);
535 line.push(map_content[i] + ' ');
537 map_lines_split.push(line);
538 if (this.map_mode == 'annotations') {
539 for (const coordinate of explorer.info_hints) {
540 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
542 } else if (this.map_mode == 'terrain') {
543 for (const p in game.portals) {
544 let coordinate = p.split(',')
545 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
547 let used_positions = [];
548 for (const thing_id in game.things) {
549 let t = game.things[thing_id];
550 let symbol = game.thing_types[t.type_];
553 meta_char = t.player_char;
555 if (used_positions.includes(t.position.toString())) {
558 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
559 used_positions.push(t.position.toString());
562 if (tui.mode.shows_info) {
563 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
566 if (game.map_geometry == 'Square') {
567 for (let line_split of map_lines_split) {
568 map_lines.push(line_split.join(''));
570 } else if (game.map_geometry == 'Hex') {
572 for (let line_split of map_lines_split) {
573 map_lines.push(' '.repeat(indent) + line_split.join(''));
581 let window_center = [terminal.rows / 2, this.window_width / 2];
582 let player = game.things[game.player_id];
583 let center_position = [player.position[0], player.position[1]];
584 if (tui.mode.shows_info) {
585 center_position = [explorer.position[0], explorer.position[1]];
587 center_position[1] = center_position[1] * 2;
588 let offset = [center_position[0] - window_center[0],
589 center_position[1] - window_center[1]]
590 if (game.map_geometry == 'Hex' && offset[0] % 2) {
593 let term_y = Math.max(0, -offset[0]);
594 let term_x = Math.max(0, -offset[1]);
595 let map_y = Math.max(0, offset[0]);
596 let map_x = Math.max(0, offset[1]);
597 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
598 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
599 terminal.write(term_y, term_x, to_draw);
602 draw_mode_line: function() {
603 let help = 'hit [' + this.keys.help + '] for help';
604 if (this.mode.has_input_prompt) {
605 help = 'enter /help for help';
607 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
609 draw_turn_line: function(n) {
610 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
612 draw_history: function() {
613 let log_display_lines = [];
614 for (let line of this.log) {
615 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
617 for (let y = terminal.rows - 1 - this.height_input,
618 i = log_display_lines.length - 1;
619 y >= this.height_header && i >= 0;
621 terminal.write(y, this.window_width, log_display_lines[i]);
624 draw_info: function() {
625 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
626 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
627 terminal.write(y, this.window_width, lines[i]);
630 draw_input: function() {
631 if (this.mode.has_input_prompt) {
632 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
633 terminal.write(y, this.window_width, this.input_lines[i]);
637 draw_help: function() {
638 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
639 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
640 if (this.mode == mode_play) {
641 content += "Available actions:\n";
642 if (game.tasks.includes('MOVE')) {
643 content += "[" + movement_keys_desc + "] – move player\n";
645 if (game.tasks.includes('PICK_UP')) {
646 content += "[" + this.keys.take_thing + "] – take thing under player\n";
648 if (game.tasks.includes('DROP')) {
649 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
651 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
652 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
654 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
655 content += '\nOther modes available from here:\n';
656 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
657 content += '[' + this.keys.switch_to_study + '] – study mode\n';
658 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
659 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
660 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
661 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
662 } else if (this.mode == mode_study) {
663 content += "Available actions:\n";
664 content += '[' + movement_keys_desc + '] – move question mark\n';
665 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
666 content += '\nOther modes available from here:\n';
667 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
668 content += '[' + this.keys.switch_to_play + '] – play mode\n';
669 } else if (this.mode == mode_chat) {
670 content += '/nick NAME – re-name yourself to NAME\n';
671 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
672 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
675 if (!this.mode.has_input_prompt) {
676 start_x = this.window_width
678 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
679 let lines = this.msg_into_lines_of_width(content, this.window_width);
680 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
681 terminal.write(y, start_x, lines[i]);
684 full_refresh: function() {
685 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
686 if (this.mode.is_intro) {
690 if (game.turn_complete) {
692 this.draw_turn_line();
694 this.draw_mode_line();
695 if (this.mode.shows_info) {
702 if (this.show_help) {
714 this.map_control = "";
715 this.map_size = [0,0];
720 get_thing: function(id_, create_if_not_found=false) {
721 if (id_ in game.things) {
722 return game.things[id_];
723 } else if (create_if_not_found) {
724 let t = new Thing([0,0]);
725 game.things[id_] = t;
729 move: function(start_position, direction) {
730 let target = [start_position[0], start_position[1]];
731 if (direction == 'LEFT') {
733 } else if (direction == 'RIGHT') {
735 } else if (game.map_geometry == 'Square') {
736 if (direction == 'UP') {
738 } else if (direction == 'DOWN') {
741 } else if (game.map_geometry == 'Hex') {
742 let start_indented = start_position[0] % 2;
743 if (direction == 'UPLEFT') {
745 if (!start_indented) {
748 } else if (direction == 'UPRIGHT') {
750 if (start_indented) {
753 } else if (direction == 'DOWNLEFT') {
755 if (!start_indented) {
758 } else if (direction == 'DOWNRIGHT') {
760 if (start_indented) {
765 if (target[0] < 0 || target[1] < 0 ||
766 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
771 teleport: function() {
772 let player = this.get_thing(game.player_id);
773 if (player.position in this.portals) {
774 server.reconnect_to(this.portals[player.position]);
776 terminal.blink_screen();
777 tui.log_msg('? not standing on portal')
785 server.init(websocket_location);
791 move: function(direction) {
792 let target = game.move(this.position, direction);
794 this.position = target
797 terminal.blink_screen();
800 update_info_db: function(yx, str) {
801 this.info_db[yx] = str;
802 if (tui.mode == mode_study) {
806 empty_info_db: function() {
808 this.info_hints = [];
809 if (tui.mode == mode_study) {
813 query_info: function() {
814 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
816 get_info: function() {
817 let position_i = this.position[0] * game.map_size[1] + this.position[1];
818 if (game.fov[position_i] != '.') {
819 return 'outside field of view';
822 let terrain_char = game.map[position_i]
823 let terrain_desc = '?'
824 if (game.terrains[terrain_char]) {
825 terrain_desc = game.terrains[terrain_char];
827 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
828 let protection = game.map_control[position_i];
829 if (protection == '.') {
830 protection = 'unprotected';
832 info += 'PROTECTION: ' + protection + '\n';
833 for (let t_id in game.things) {
834 let t = game.things[t_id];
835 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
836 let symbol = game.thing_types[t.type_];
837 info += "THING: " + t.type_ + " / " + symbol;
839 info += t.player_char;
842 info += " (" + t.name_ + ")";
847 if (this.position in game.portals) {
848 info += "PORTAL: " + game.portals[this.position] + "\n";
850 if (this.position in this.info_db) {
851 info += "ANNOTATIONS: " + this.info_db[this.position];
857 annotate: function(msg) {
858 if (msg.length == 0) {
859 msg = " "; // triggers annotation deletion
861 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
863 set_portal: function(msg) {
864 if (msg.length == 0) {
865 msg = " "; // triggers portal deletion
867 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
871 tui.inputEl.addEventListener('input', (event) => {
872 if (tui.mode.has_input_prompt) {
873 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
874 if (tui.inputEl.value.length > max_length) {
875 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
877 tui.recalc_input_lines();
878 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
879 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
880 tui.switch_mode(mode_play);
884 tui.inputEl.addEventListener('keydown', (event) => {
885 tui.show_help = false;
886 if (event.key == 'Enter') {
887 event.preventDefault();
889 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
890 tui.show_help = true;
892 tui.restore_input_values();
893 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
894 tui.show_help = true;
895 } else if (tui.mode == mode_login && event.key == 'Enter') {
896 tui.login_name = tui.inputEl.value;
897 server.send(['LOGIN', tui.inputEl.value]);
899 } else if (tui.mode == mode_portal && event.key == 'Enter') {
900 explorer.set_portal(tui.inputEl.value);
901 tui.switch_mode(mode_play);
902 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
903 explorer.annotate(tui.inputEl.value);
904 tui.switch_mode(mode_play);
905 } else if (tui.mode == mode_password && event.key == 'Enter') {
906 if (tui.inputEl.value.length == 0) {
907 tui.inputEl.value = " ";
909 tui.password = tui.inputEl.value
910 tui.switch_mode(mode_play);
911 } else if (tui.mode == mode_chat && event.key == 'Enter') {
912 let tokens = parser.tokenize(tui.inputEl.value);
913 if (tokens.length > 0 && tokens[0].length > 0) {
914 if (tui.inputEl.value[0][0] == '/') {
915 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
916 tui.switch_mode(mode_play);
917 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
918 tui.switch_mode(mode_study);
919 } else if (tokens[0].slice(1) == 'nick') {
920 if (tokens.length > 1) {
921 server.send(['NICK', tokens[1]]);
923 tui.log_msg('? need new name');
926 tui.log_msg('? unknown command');
929 server.send(['ALL', tui.inputEl.value]);
931 } else if (tui.inputEl.valuelength > 0) {
932 server.send(['ALL', tui.inputEl.value]);
935 } else if (tui.mode == mode_play) {
936 if (event.key === tui.keys.switch_to_chat) {
937 event.preventDefault();
938 tui.switch_mode(mode_chat);
939 } else if (event.key === tui.keys.switch_to_edit
940 && game.tasks.includes('WRITE')) {
941 event.preventDefault();
942 tui.switch_mode(mode_edit);
943 } else if (event.key === tui.keys.switch_to_study) {
944 tui.switch_mode(mode_study);
945 } else if (event.key === tui.keys.switch_to_password) {
946 event.preventDefault();
947 tui.switch_mode(mode_password);
948 } else if (event.key === tui.keys.flatten
949 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
950 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
951 } else if (event.key === tui.keys.take_thing
952 && game.tasks.includes('PICK_UP')) {
953 server.send(["TASK:PICK_UP"]);
954 } else if (event.key === tui.keys.drop_thing
955 && game.tasks.includes('DROP')) {
956 server.send(["TASK:DROP"]);
957 } else if (event.key in tui.movement_keys
958 && game.tasks.includes('MOVE')) {
959 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
960 } else if (event.key === tui.keys.teleport) {
962 } else if (event.key === tui.keys.switch_to_portal) {
963 event.preventDefault();
964 tui.switch_mode(mode_portal);
965 } else if (event.key === tui.keys.switch_to_annotate) {
966 event.preventDefault();
967 tui.switch_mode(mode_annotate);
969 } else if (tui.mode == mode_study) {
970 if (event.key === tui.keys.switch_to_chat) {
971 event.preventDefault();
972 tui.switch_mode(mode_chat);
973 } else if (event.key == tui.keys.switch_to_play) {
974 tui.switch_mode(mode_play);
975 } else if (event.key in tui.movement_keys) {
976 explorer.move(tui.movement_keys[event.key]);
977 } else if (event.key == tui.keys.toggle_map_mode) {
978 if (tui.map_mode == 'terrain') {
979 tui.map_mode = 'annotations';
980 } else if (tui.map_mode == 'annotations') {
981 tui.map_mode = 'control';
983 tui.map_mode = 'terrain';
990 rows_selector.addEventListener('input', function() {
991 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
994 window.localStorage.setItem(rows_selector.id, rows_selector.value);
995 terminal.initialize();
998 cols_selector.addEventListener('input', function() {
999 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1002 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1003 terminal.initialize();
1004 tui.window_width = terminal.cols / 2,
1007 for (let key_selector of key_selectors) {
1008 key_selector.addEventListener('input', function() {
1009 window.localStorage.setItem(key_selector.id, key_selector.value);
1013 window.setInterval(function() {
1014 if (server.connected) {
1015 server.send(['PING']);
1017 server.reconnect_to(server.url);
1018 tui.log_msg('@ attempting reconnect …')
1021 document.getElementById("terminal").onclick = function() {
1022 tui.inputEl.focus();
1024 document.getElementById("help").onclick = function() {
1025 tui.show_help = true;
1028 document.getElementById("switch_to_play").onclick = function() {
1029 tui.switch_mode(mode_play);
1032 document.getElementById("switch_to_study").onclick = function() {
1033 tui.switch_mode(mode_study);
1036 document.getElementById("switch_to_chat").onclick = function() {
1037 tui.switch_mode(mode_chat);
1040 document.getElementById("switch_to_password").onclick = function() {
1041 tui.switch_mode(mode_password);
1044 document.getElementById("switch_to_edit").onclick = function() {
1045 tui.switch_mode(mode_edit);
1048 document.getElementById("switch_to_annotate").onclick = function() {
1049 tui.switch_mode(mode_annotate);
1052 document.getElementById("switch_to_portal").onclick = function() {
1053 tui.switch_mode(mode_portal);
1056 document.getElementById("toggle_map_mode").onclick = function() {
1057 if (tui.map_mode == 'terrain') {
1058 tui.map_mode = 'annotations';
1059 } else if (tui.map_mode == 'annotations') {
1060 tui.map_mode = 'control';
1062 tui.map_mode = 'terrain';
1066 document.getElementById("take_thing").onclick = function() {
1067 server.send(['TASK:PICK_UP']);
1069 document.getElementById("drop_thing").onclick = function() {
1070 server.send(['TASK:DROP']);
1072 document.getElementById("flatten").onclick = function() {
1073 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1075 document.getElementById("teleport").onclick = function() {
1078 document.getElementById("move_upleft").onclick = function() {
1079 if (tui.mode == mode_play) {
1080 server.send(['TASK:MOVE', 'UPLEFT']);
1082 explorer.move('UPLEFT');
1085 document.getElementById("move_left").onclick = function() {
1086 if (tui.mode == mode_play) {
1087 server.send(['TASK:MOVE', 'LEFT']);
1089 explorer.move('LEFT');
1092 document.getElementById("move_downleft").onclick = function() {
1093 if (tui.mode == mode_play) {
1094 server.send(['TASK:MOVE', 'DOWNLEFT']);
1096 explorer.move('DOWNLEFT');
1099 document.getElementById("move_down").onclick = function() {
1100 if (tui.mode == mode_play) {
1101 server.send(['TASK:MOVE', 'DOWN']);
1103 explorer.move('DOWN');
1106 document.getElementById("move_up").onclick = function() {
1107 if (tui.mode == mode_play) {
1108 server.send(['TASK:MOVE', 'UP']);
1110 explorer.move('UP');
1113 document.getElementById("move_upright").onclick = function() {
1114 if (tui.mode == mode_play) {
1115 server.send(['TASK:MOVE', 'UPRIGHT']);
1117 explorer.move('UPRIGHT');
1120 document.getElementById("move_right").onclick = function() {
1121 if (tui.mode == mode_play) {
1122 server.send(['TASK:MOVE', 'RIGHT']);
1124 explorer.move('RIGHT');
1127 document.getElementById("move_downright").onclick = function() {
1128 if (tui.mode == mode_play) {
1129 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1131 explorer.move('DOWNRIGHT');