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 for (let t_id in game.things) {
829 let t = game.things[t_id];
830 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
831 let symbol = game.thing_types[t.type_];
832 info += "THING: " + t.type_ + " / " + symbol;
834 info += t.player_char;
837 info += " (" + t.name_ + ")";
842 if (this.position in game.portals) {
843 info += "PORTAL: " + game.portals[this.position] + "\n";
845 if (this.position in this.info_db) {
846 info += "ANNOTATIONS: " + this.info_db[this.position];
852 annotate: function(msg) {
853 if (msg.length == 0) {
854 msg = " "; // triggers annotation deletion
856 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
858 set_portal: function(msg) {
859 if (msg.length == 0) {
860 msg = " "; // triggers portal deletion
862 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
866 tui.inputEl.addEventListener('input', (event) => {
867 if (tui.mode.has_input_prompt) {
868 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
869 if (tui.inputEl.value.length > max_length) {
870 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
872 tui.recalc_input_lines();
873 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
874 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
875 tui.switch_mode(mode_play);
879 tui.inputEl.addEventListener('keydown', (event) => {
880 tui.show_help = false;
881 if (event.key == 'Enter') {
882 event.preventDefault();
884 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
885 tui.show_help = true;
887 tui.restore_input_values();
888 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
889 tui.show_help = true;
890 } else if (tui.mode == mode_login && event.key == 'Enter') {
891 tui.login_name = tui.inputEl.value;
892 server.send(['LOGIN', tui.inputEl.value]);
894 } else if (tui.mode == mode_portal && event.key == 'Enter') {
895 explorer.set_portal(tui.inputEl.value);
896 tui.switch_mode(mode_play);
897 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
898 explorer.annotate(tui.inputEl.value);
899 tui.switch_mode(mode_play);
900 } else if (tui.mode == mode_password && event.key == 'Enter') {
901 if (tui.inputEl.value.length == 0) {
902 tui.inputEl.value = " ";
904 tui.password = tui.inputEl.value
905 tui.switch_mode(mode_play);
906 } else if (tui.mode == mode_chat && event.key == 'Enter') {
907 let tokens = parser.tokenize(tui.inputEl.value);
908 if (tokens.length > 0 && tokens[0].length > 0) {
909 if (tui.inputEl.value[0][0] == '/') {
910 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
911 tui.switch_mode(mode_play);
912 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
913 tui.switch_mode(mode_study);
914 } else if (tokens[0].slice(1) == 'nick') {
915 if (tokens.length > 1) {
916 server.send(['NICK', tokens[1]]);
918 tui.log_msg('? need new name');
921 tui.log_msg('? unknown command');
924 server.send(['ALL', tui.inputEl.value]);
926 } else if (tui.inputEl.valuelength > 0) {
927 server.send(['ALL', tui.inputEl.value]);
930 } else if (tui.mode == mode_play) {
931 if (event.key === tui.keys.switch_to_chat) {
932 event.preventDefault();
933 tui.switch_mode(mode_chat);
934 } else if (event.key === tui.keys.switch_to_edit
935 && game.tasks.includes('WRITE')) {
936 event.preventDefault();
937 tui.switch_mode(mode_edit);
938 } else if (event.key === tui.keys.switch_to_study) {
939 tui.switch_mode(mode_study);
940 } else if (event.key === tui.keys.switch_to_password) {
941 event.preventDefault();
942 tui.switch_mode(mode_password);
943 } else if (event.key === tui.keys.flatten
944 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
945 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
946 } else if (event.key === tui.keys.take_thing
947 && game.tasks.includes('PICK_UP')) {
948 server.send(["TASK:PICK_UP"]);
949 } else if (event.key === tui.keys.drop_thing
950 && game.tasks.includes('DROP')) {
951 server.send(["TASK:DROP"]);
952 } else if (event.key in tui.movement_keys
953 && game.tasks.includes('MOVE')) {
954 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
955 } else if (event.key === tui.keys.teleport) {
957 } else if (event.key === tui.keys.switch_to_portal) {
958 event.preventDefault();
959 tui.switch_mode(mode_portal);
960 } else if (event.key === tui.keys.switch_to_annotate) {
961 event.preventDefault();
962 tui.switch_mode(mode_annotate);
964 } else if (tui.mode == mode_study) {
965 if (event.key === tui.keys.switch_to_chat) {
966 event.preventDefault();
967 tui.switch_mode(mode_chat);
968 } else if (event.key == tui.keys.switch_to_play) {
969 tui.switch_mode(mode_play);
970 } else if (event.key in tui.movement_keys) {
971 explorer.move(tui.movement_keys[event.key]);
972 } else if (event.key == tui.keys.toggle_map_mode) {
973 if (tui.map_mode == 'terrain') {
974 tui.map_mode = 'annotations';
975 } else if (tui.map_mode == 'annotations') {
976 tui.map_mode = 'control';
978 tui.map_mode = 'terrain';
985 rows_selector.addEventListener('input', function() {
986 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
989 window.localStorage.setItem(rows_selector.id, rows_selector.value);
990 terminal.initialize();
993 cols_selector.addEventListener('input', function() {
994 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
997 window.localStorage.setItem(cols_selector.id, cols_selector.value);
998 terminal.initialize();
999 tui.window_width = terminal.cols / 2,
1002 for (let key_selector of key_selectors) {
1003 key_selector.addEventListener('input', function() {
1004 window.localStorage.setItem(key_selector.id, key_selector.value);
1008 window.setInterval(function() {
1009 if (server.connected) {
1010 server.send(['PING']);
1012 server.reconnect_to(server.url);
1013 tui.log_msg('@ attempting reconnect …')
1016 document.getElementById("terminal").onclick = function() {
1017 tui.inputEl.focus();
1019 document.getElementById("help").onclick = function() {
1020 tui.show_help = true;
1023 document.getElementById("switch_to_play").onclick = function() {
1024 tui.switch_mode(mode_play);
1027 document.getElementById("switch_to_study").onclick = function() {
1028 tui.switch_mode(mode_study);
1031 document.getElementById("switch_to_chat").onclick = function() {
1032 tui.switch_mode(mode_chat);
1035 document.getElementById("switch_to_password").onclick = function() {
1036 tui.switch_mode(mode_password);
1039 document.getElementById("switch_to_edit").onclick = function() {
1040 tui.switch_mode(mode_edit);
1043 document.getElementById("switch_to_annotate").onclick = function() {
1044 tui.switch_mode(mode_annotate);
1047 document.getElementById("switch_to_portal").onclick = function() {
1048 tui.switch_mode(mode_portal);
1051 document.getElementById("toggle_map_mode").onclick = function() {
1052 if (tui.map_mode == 'terrain') {
1053 tui.map_mode = 'annotations';
1054 } else if (tui.map_mode == 'annotations') {
1055 tui.map_mode = 'control';
1057 tui.map_mode = 'terrain';
1061 document.getElementById("take_thing").onclick = function() {
1062 server.send(['TASK:PICK_UP']);
1064 document.getElementById("drop_thing").onclick = function() {
1065 server.send(['TASK:DROP']);
1067 document.getElementById("flatten").onclick = function() {
1068 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1070 document.getElementById("teleport").onclick = function() {
1073 document.getElementById("move_upleft").onclick = function() {
1074 if (tui.mode == mode_play) {
1075 server.send(['TASK:MOVE', 'UPLEFT']);
1077 explorer.move('UPLEFT');
1080 document.getElementById("move_left").onclick = function() {
1081 if (tui.mode == mode_play) {
1082 server.send(['TASK:MOVE', 'LEFT']);
1084 explorer.move('LEFT');
1087 document.getElementById("move_downleft").onclick = function() {
1088 if (tui.mode == mode_play) {
1089 server.send(['TASK:MOVE', 'DOWNLEFT']);
1091 explorer.move('DOWNLEFT');
1094 document.getElementById("move_down").onclick = function() {
1095 if (tui.mode == mode_play) {
1096 server.send(['TASK:MOVE', 'DOWN']);
1098 explorer.move('DOWN');
1101 document.getElementById("move_up").onclick = function() {
1102 if (tui.mode == mode_play) {
1103 server.send(['TASK:MOVE', 'UP']);
1105 explorer.move('UP');
1108 document.getElementById("move_upright").onclick = function() {
1109 if (tui.mode == mode_play) {
1110 server.send(['TASK:MOVE', 'UPRIGHT']);
1112 explorer.move('UPRIGHT');
1115 document.getElementById("move_right").onclick = function() {
1116 if (tui.mode == mode_play) {
1117 server.send(['TASK:MOVE', 'RIGHT']);
1119 explorer.move('RIGHT');
1122 document.getElementById("move_downright").onclick = function() {
1123 if (tui.mode == mode_play) {
1124 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1126 explorer.move('DOWNRIGHT');